Files
ReadMeABook/src/lib/processors/plex-recently-added.processor.ts
T
kikootwo 95c25ff73a Add e-book sidecar integration and improve request handling
Introduces optional e-book sidecar downloads from Anna's Archive, including admin UI, settings API, FlareSolverr integration, and documentation. Enhances request creation logic to prevent duplicate downloads by checking for 'downloaded' and 'available' statuses, updates UI to reflect processing state, and adds SABnzbd support to download and cleanup flows. Also updates ranking algorithm documentation and improves cache invalidation for recent requests.
2026-01-28 11:41:58 -05:00

216 lines
7.8 KiB
TypeScript

/**
* 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<any> {
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;
}
}