/** * Component: Library Recently Added Check Processor * Documentation: documentation/backend/services/scheduler.md * * Lightweight polling for new library items (Plex or Audiobookshelf) */ import { prisma } from '../db'; import { createJobLogger } from '../utils/job-logger'; import { getLibraryService } from '../services/library'; export interface PlexRecentlyAddedPayload { jobId?: string; scheduledJobId?: string; } export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPayload): Promise { const { jobId, scheduledJobId } = payload; const logger = jobId ? createJobLogger(jobId, 'RecentlyAdded') : null; const { getConfigService } = await import('../services/config.service'); const configService = getConfigService(); // Get backend mode const backendMode = await configService.getBackendMode(); await logger?.info(`Backend mode: ${backendMode}`); // Validate configuration based on backend mode if (backendMode === 'audiobookshelf') { const absConfig = await configService.getMany([ 'audiobookshelf.server_url', 'audiobookshelf.api_token', 'audiobookshelf.library_id', ]); const missingFields: string[] = []; if (!absConfig['audiobookshelf.server_url']) missingFields.push('Audiobookshelf server URL'); if (!absConfig['audiobookshelf.api_token']) missingFields.push('Audiobookshelf API token'); if (!absConfig['audiobookshelf.library_id']) missingFields.push('Audiobookshelf library ID'); if (missingFields.length > 0) { const errorMsg = `Audiobookshelf is not configured. Missing: ${missingFields.join(', ')}`; await logger?.warn(errorMsg); return { success: false, message: errorMsg, skipped: true }; } } else { const plexConfig = await configService.getMany([ 'plex_url', 'plex_token', 'plex_audiobook_library_id', ]); const missingFields: string[] = []; if (!plexConfig.plex_url) missingFields.push('Plex server URL'); if (!plexConfig.plex_token) missingFields.push('Plex auth token'); if (!plexConfig.plex_audiobook_library_id) missingFields.push('Plex audiobook library ID'); if (missingFields.length > 0) { const errorMsg = `Plex is not configured. Missing: ${missingFields.join(', ')}`; await logger?.warn(errorMsg); return { success: false, message: errorMsg, skipped: true }; } } await logger?.info(`Starting recently added check...`); // Get library service (automatically selects Plex or Audiobookshelf) const libraryService = await getLibraryService(); try { // Get configured library ID const libraryId = backendMode === 'audiobookshelf' ? await configService.get('audiobookshelf.library_id') : await configService.get('plex_audiobook_library_id'); // Fetch top 10 recently added items using abstraction layer const recentItems = await libraryService.getRecentlyAdded(libraryId!, 10); await logger?.info(`Found ${recentItems.length} recently added items`); if (recentItems.length === 0) { return { success: true, message: 'No recent items', newCount: 0, updatedCount: 0, matchedDownloads: 0 }; } // Check for new items not in database let newCount = 0; let updatedCount = 0; let matchedDownloads = 0; for (const item of recentItems) { const existing = await prisma.plexLibrary.findUnique({ where: { plexGuid: item.externalId }, }); if (!existing) { await prisma.plexLibrary.create({ data: { plexGuid: item.externalId, plexRatingKey: item.id, title: item.title, author: item.author || 'Unknown Author', narrator: item.narrator, summary: item.description, duration: item.duration ? item.duration * 1000 : null, // Convert seconds to milliseconds year: item.year, asin: item.asin, // Store ASIN from library backend isbn: item.isbn, // Store ISBN from library backend thumbUrl: item.coverUrl, plexLibraryId: libraryId!, addedAt: item.addedAt, lastScannedAt: new Date(), }, }); newCount++; await logger?.info(`New item added: ${item.title} by ${item.author}`); } else { await prisma.plexLibrary.update({ where: { plexGuid: item.externalId }, data: { title: item.title, author: item.author || existing.author, narrator: item.narrator || existing.narrator, summary: item.description || existing.summary, duration: item.duration ? item.duration * 1000 : existing.duration, year: item.year || existing.year, asin: item.asin || existing.asin, // Update ASIN if available isbn: item.isbn || existing.isbn, // Update ISBN if available thumbUrl: item.coverUrl || existing.thumbUrl, lastScannedAt: new Date(), }, }); updatedCount++; } } // Check for downloaded requests to match const downloadedRequests = await prisma.request.findMany({ where: { status: 'downloaded', deletedAt: null, }, include: { audiobook: true }, take: 50, }); if (downloadedRequests.length > 0) { await logger?.info(`Checking ${downloadedRequests.length} downloaded requests for matches`); const { findPlexMatch } = await import('../utils/audiobook-matcher'); for (const request of downloadedRequests) { try { const audiobook = request.audiobook; const match = await findPlexMatch({ asin: audiobook.audibleAsin || '', title: audiobook.title, author: audiobook.author, narrator: audiobook.narrator || undefined, }); if (match) { await logger?.info(`Match found: "${audiobook.title}" → "${match.title}"`); // Update audiobook with matched library item ID const updateData: any = { updatedAt: new Date() }; if (backendMode === 'audiobookshelf') { updateData.absItemId = match.plexGuid; // plexGuid field stores the externalId from either backend } else { updateData.plexGuid = match.plexGuid; } await prisma.audiobook.update({ where: { id: audiobook.id }, data: updateData, }); await prisma.request.update({ where: { id: request.id }, data: { status: 'available', completedAt: new Date(), updatedAt: new Date() }, }); matchedDownloads++; // Trigger metadata match for Audiobookshelf items (only for our downloaded requests) if (backendMode === 'audiobookshelf') { const itemId = match.plexGuid; // plexGuid contains the Audiobookshelf item ID const asin = audiobook.audibleAsin || undefined; const matchInfo = asin ? ` with ASIN ${asin}` : ''; await logger?.info(`Triggering metadata match for matched item: ${itemId}${matchInfo}`); const { triggerABSItemMatch } = await import('../services/audiobookshelf/api'); await triggerABSItemMatch(itemId, asin); } } } catch (error) { await logger?.error(`Failed to match request ${request.id}: ${error instanceof Error ? error.message : 'Unknown error'}`); } } } await logger?.info(`Complete: ${newCount} new, ${updatedCount} updated, ${matchedDownloads} matched downloads`); return { success: true, message: `Recently added check completed (${backendMode})`, backendMode, newCount, updatedCount, matchedDownloads, }; } catch (error) { await logger?.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`); throw error; } }