mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
174e9f05b6
Implements admin ability to delete requests with soft delete, media file cleanup, and seeding-aware torrent management. Adds new API endpoint, frontend confirmation dialog, and request actions dropdown. Updates database schema with deletedAt and deletedBy fields, and ensures all queries filter out deleted requests. Documentation added for feature and user flow.
206 lines
7.2 KiB
TypeScript
206 lines
7.2 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++;
|
|
}
|
|
} 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;
|
|
}
|
|
}
|