mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 12:50:09 +00:00
ca7cac0c88
Implements remote-to-local path mapping for qBittorrent downloads, allowing the app to handle differing filesystem paths between qBittorrent and the local environment (e.g., remote seedboxes, Docker). Adds UI controls in admin settings and setup wizard, validates mapping configuration, and applies path transformation in download and import processors. Updates documentation, API routes, and data models to support the new feature. Also improves library scan logic to remove stale records and reset orphaned audiobooks and requests. Increases minimum torrent score threshold from 30 to 50 in search and ranking logic, and exposes torrent source URLs in the admin UI.
173 lines
6.2 KiB
TypeScript
173 lines
6.2 KiB
TypeScript
/**
|
|
* Component: Retry Failed Imports Processor
|
|
* Documentation: documentation/backend/services/scheduler.md
|
|
*
|
|
* Retries file organization for requests that are awaiting import
|
|
*/
|
|
|
|
import { prisma } from '../db';
|
|
import { createJobLogger } from '../utils/job-logger';
|
|
import { getJobQueueService } from '../services/job-queue.service';
|
|
import { getConfigService } from '../services/config.service';
|
|
import { PathMapper } from '../utils/path-mapper';
|
|
|
|
export interface RetryFailedImportsPayload {
|
|
jobId?: string;
|
|
scheduledJobId?: string;
|
|
}
|
|
|
|
export async function processRetryFailedImports(payload: RetryFailedImportsPayload): Promise<any> {
|
|
const { jobId, scheduledJobId } = payload;
|
|
const logger = jobId ? createJobLogger(jobId, 'RetryFailedImports') : null;
|
|
|
|
await logger?.info('Starting retry job for requests awaiting import...');
|
|
|
|
try {
|
|
// Load path mapping configuration once
|
|
const configService = getConfigService();
|
|
const pathMappingConfig = await configService.getMany([
|
|
'download_client_remote_path_mapping_enabled',
|
|
'download_client_remote_path',
|
|
'download_client_local_path',
|
|
]);
|
|
|
|
const mappingConfig = {
|
|
enabled: pathMappingConfig.download_client_remote_path_mapping_enabled === 'true',
|
|
remotePath: pathMappingConfig.download_client_remote_path || '',
|
|
localPath: pathMappingConfig.download_client_local_path || '',
|
|
};
|
|
|
|
// Find all active requests in awaiting_import status
|
|
const requests = await prisma.request.findMany({
|
|
where: {
|
|
status: 'awaiting_import',
|
|
deletedAt: null,
|
|
},
|
|
include: {
|
|
audiobook: true,
|
|
downloadHistory: {
|
|
where: { selected: true },
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 1,
|
|
},
|
|
},
|
|
take: 50, // Limit to 50 requests per run
|
|
});
|
|
|
|
await logger?.info(`Found ${requests.length} requests awaiting import`);
|
|
|
|
if (requests.length === 0) {
|
|
return {
|
|
success: true,
|
|
message: 'No requests awaiting import',
|
|
triggered: 0,
|
|
};
|
|
}
|
|
|
|
// Trigger organize job for each request
|
|
const jobQueue = getJobQueueService();
|
|
let triggered = 0;
|
|
let skipped = 0;
|
|
|
|
for (const request of requests) {
|
|
try {
|
|
// Get the download path from the most recent download history
|
|
const downloadHistory = request.downloadHistory[0];
|
|
|
|
if (!downloadHistory) {
|
|
await logger?.warn(`No download history found for request ${request.id}, skipping`);
|
|
skipped++;
|
|
continue;
|
|
}
|
|
|
|
let downloadPath: string;
|
|
|
|
// Try to get download path from qBittorrent if we have the torrent
|
|
if (downloadHistory.downloadClientId) {
|
|
try {
|
|
const { getQBittorrentService } = await import('../integrations/qbittorrent.service');
|
|
const qbt = await getQBittorrentService();
|
|
const torrent = await qbt.getTorrent(downloadHistory.downloadClientId);
|
|
const qbPath = `${torrent.save_path}/${torrent.name}`;
|
|
downloadPath = PathMapper.transform(qbPath, mappingConfig);
|
|
await logger?.info(
|
|
`Got download path from qBittorrent for request ${request.id}: ${qbPath}` +
|
|
(downloadPath !== qbPath ? ` → ${downloadPath} (mapped)` : '')
|
|
);
|
|
} catch (qbtError) {
|
|
// Torrent not found in qBittorrent - try to construct path from config
|
|
await logger?.warn(`Torrent not found in qBittorrent for request ${request.id}, falling back to configured path`);
|
|
|
|
if (!downloadHistory.torrentName) {
|
|
await logger?.warn(`No torrent 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)` : '')
|
|
);
|
|
}
|
|
} 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`);
|
|
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 configuredPath = `${downloadDir}/${downloadHistory.torrentName}`;
|
|
downloadPath = PathMapper.transform(configuredPath, mappingConfig);
|
|
await logger?.info(
|
|
`Using configured download path for request ${request.id}: ${configuredPath}` +
|
|
(downloadPath !== configuredPath ? ` → ${downloadPath} (mapped)` : '')
|
|
);
|
|
}
|
|
|
|
await jobQueue.addOrganizeJob(
|
|
request.id,
|
|
request.audiobook.id,
|
|
downloadPath
|
|
);
|
|
triggered++;
|
|
await logger?.info(`Triggered organize job for request ${request.id}: ${request.audiobook.title}`);
|
|
} catch (error) {
|
|
await logger?.error(`Failed to trigger organize for request ${request.id}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
skipped++;
|
|
}
|
|
}
|
|
|
|
await logger?.info(`Triggered ${triggered}/${requests.length} organize jobs (${skipped} skipped)`);
|
|
|
|
return {
|
|
success: true,
|
|
message: 'Retry failed imports completed',
|
|
totalRequests: requests.length,
|
|
triggered,
|
|
skipped,
|
|
};
|
|
} catch (error) {
|
|
await logger?.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
throw error;
|
|
}
|
|
}
|