mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
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.
This commit is contained in:
@@ -82,7 +82,22 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent
|
||||
try {
|
||||
const downloadHistory = request.downloadHistory[0];
|
||||
|
||||
if (!downloadHistory || !downloadHistory.downloadClientId || !downloadHistory.indexerName) {
|
||||
if (!downloadHistory || !downloadHistory.indexerName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip SABnzbd downloads - Usenet doesn't have seeding concept
|
||||
if (downloadHistory.nzbId && !downloadHistory.torrentHash) {
|
||||
// For soft-deleted SABnzbd requests, hard delete immediately (no seeding needed)
|
||||
if (request.deletedAt) {
|
||||
await prisma.request.delete({ where: { id: request.id } });
|
||||
await logger?.info(`Hard-deleted orphaned SABnzbd request ${request.id}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only process torrent downloads
|
||||
if (!downloadHistory.torrentHash) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -111,7 +126,7 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent
|
||||
|
||||
let torrent;
|
||||
try {
|
||||
torrent = await qbt.getTorrent(downloadHistory.downloadClientId);
|
||||
torrent = await qbt.getTorrent(downloadHistory.torrentHash);
|
||||
} catch (error) {
|
||||
// Torrent might already be deleted, skip
|
||||
continue;
|
||||
@@ -130,7 +145,7 @@ export async function processCleanupSeededTorrents(payload: CleanupSeededTorrent
|
||||
await logger?.info(`Torrent ${torrent.name} (${indexerName}) has met seeding requirement (${Math.floor(actualSeedingTime / 60)}/${seedingConfig.seedingTimeMinutes} minutes)`);
|
||||
|
||||
// Delete torrent and files from qBittorrent
|
||||
await qbt.deleteTorrent(downloadHistory.downloadClientId, true); // true = delete files
|
||||
await qbt.deleteTorrent(downloadHistory.torrentHash, true); // true = delete files
|
||||
|
||||
// If this is a soft-deleted request (orphaned download), hard delete it now
|
||||
if (request.deletedAt) {
|
||||
|
||||
@@ -181,6 +181,16 @@ export async function processPlexRecentlyAddedCheck(payload: PlexRecentlyAddedPa
|
||||
});
|
||||
|
||||
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'}`);
|
||||
|
||||
@@ -82,12 +82,13 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo
|
||||
|
||||
let downloadPath: string;
|
||||
|
||||
// Try to get download path from qBittorrent if we have the torrent
|
||||
if (downloadHistory.downloadClientId) {
|
||||
// Try to get download path from the appropriate download client
|
||||
if (downloadHistory.torrentHash) {
|
||||
// qBittorrent download
|
||||
try {
|
||||
const { getQBittorrentService } = await import('../integrations/qbittorrent.service');
|
||||
const qbt = await getQBittorrentService();
|
||||
const torrent = await qbt.getTorrent(downloadHistory.downloadClientId);
|
||||
const torrent = await qbt.getTorrent(downloadHistory.torrentHash);
|
||||
const qbPath = `${torrent.save_path}/${torrent.name}`;
|
||||
downloadPath = PathMapper.transform(qbPath, mappingConfig);
|
||||
await logger?.info(
|
||||
@@ -119,10 +120,51 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo
|
||||
(downloadPath !== fallbackPath ? ` → ${downloadPath} (mapped)` : '')
|
||||
);
|
||||
}
|
||||
} else if (downloadHistory.nzbId) {
|
||||
// SABnzbd download
|
||||
try {
|
||||
const { getSABnzbdService } = await import('../integrations/sabnzbd.service');
|
||||
const sabnzbd = await getSABnzbdService();
|
||||
const nzbInfo = await sabnzbd.getNZB(downloadHistory.nzbId);
|
||||
if (nzbInfo && nzbInfo.downloadPath) {
|
||||
downloadPath = PathMapper.transform(nzbInfo.downloadPath, mappingConfig);
|
||||
await logger?.info(
|
||||
`Got download path from SABnzbd for request ${request.id}: ${nzbInfo.downloadPath}` +
|
||||
(downloadPath !== nzbInfo.downloadPath ? ` → ${downloadPath} (mapped)` : '')
|
||||
);
|
||||
} else {
|
||||
await logger?.warn(`NZB ${downloadHistory.nzbId} not found or has no download path for request ${request.id}, falling back to configured path`);
|
||||
|
||||
if (!downloadHistory.torrentName) {
|
||||
await logger?.warn(`No name stored for request ${request.id}, cannot construct fallback path, skipping`);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const downloadDir = await configService.get('download_dir');
|
||||
|
||||
if (!downloadDir) {
|
||||
await logger?.error(`download_dir not configured, cannot retry request ${request.id}, skipping`);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const fallbackPath = `${downloadDir}/${downloadHistory.torrentName}`;
|
||||
downloadPath = PathMapper.transform(fallbackPath, mappingConfig);
|
||||
await logger?.info(
|
||||
`Using fallback download path for request ${request.id}: ${fallbackPath}` +
|
||||
(downloadPath !== fallbackPath ? ` → ${downloadPath} (mapped)` : '')
|
||||
);
|
||||
}
|
||||
} catch (sabnzbdError) {
|
||||
await logger?.warn(`SABnzbd error for request ${request.id}: ${sabnzbdError instanceof Error ? sabnzbdError.message : 'Unknown error'}, skipping`);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// No download client ID - use fallback path
|
||||
if (!downloadHistory.torrentName) {
|
||||
await logger?.warn(`No download client ID or torrent name for request ${request.id}, skipping`);
|
||||
await logger?.warn(`No download client ID or name for request ${request.id}, skipping`);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -373,6 +373,16 @@ export async function processScanPlex(payload: ScanPlexPayload): Promise<any> {
|
||||
});
|
||||
|
||||
matchedCount++;
|
||||
|
||||
// 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'}`);
|
||||
|
||||
Reference in New Issue
Block a user