mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 12:50:09 +00:00
Use content_path and add savePath/path-wait
Always use qBittorrent's content_path as the canonical downloadPath and expose savePath on DownloadInfo instead of reconstructing paths from save_path + basename. Add path-waiting logic to the monitor: track consecutive pathWaitCount polls, re-queue the monitor with exponential-ish backoff while content_path remains outside save_path (to handle TempPathEnabled races), and give up after a configurable max attempts. Extend the MonitorDownload payload and JobQueue APIs to carry pathWaitCount. Organize-files processor now attempts to refresh the stored downloadPath from the download client and updates downloadHistory if the client reports a different path (applying path mapping). Update tests to reflect the new behavior and expectations.
This commit is contained in:
@@ -32,7 +32,7 @@ function getBackoffDelay(stallCount: number): number {
|
||||
|
||||
export async function processMonitorDownload(payload: MonitorDownloadPayload): Promise<any> {
|
||||
const { requestId, downloadHistoryId, downloadClientId, downloadClient, jobId,
|
||||
lastProgress: prevProgress, stallCount: prevStallCount } = payload;
|
||||
lastProgress: prevProgress, stallCount: prevStallCount, pathWaitCount: prevPathWaitCount } = payload;
|
||||
|
||||
const logger = RMABLogger.forJob(jobId, 'MonitorDownload');
|
||||
|
||||
@@ -95,6 +95,32 @@ export async function processMonitorDownload(payload: MonitorDownloadPayload): P
|
||||
throw new Error('Download path not available from download client');
|
||||
}
|
||||
|
||||
// Detect TempPathEnabled race: content_path hasn't been relocated to save_path yet
|
||||
if (info.savePath && downloadPath) {
|
||||
const normalizedSave = info.savePath.endsWith('/') ? info.savePath : info.savePath + '/';
|
||||
if (!downloadPath.startsWith(normalizedSave)) {
|
||||
const waitCount = (prevPathWaitCount ?? 0) + 1;
|
||||
const MAX_PATH_WAIT = 30; // Give up after ~5 minutes
|
||||
|
||||
if (waitCount < MAX_PATH_WAIT) {
|
||||
const delay = Math.min(10, waitCount * 2); // 2s, 4s, 6s... up to 10s
|
||||
logger.info(`Download path still in temp location, waiting for relocation (${waitCount}/${MAX_PATH_WAIT})`, {
|
||||
downloadPath, savePath: info.savePath,
|
||||
});
|
||||
|
||||
const jobQueue = getJobQueueService();
|
||||
await jobQueue.addMonitorJob(
|
||||
requestId, downloadHistoryId, downloadClientId, downloadClient,
|
||||
delay, 100, 0, waitCount
|
||||
);
|
||||
|
||||
return { success: true, completed: false, message: 'Waiting for file relocation', pathWaitCount: waitCount };
|
||||
}
|
||||
|
||||
logger.warn(`Download path still in temp location after ${waitCount} checks, proceeding with organization`);
|
||||
}
|
||||
}
|
||||
|
||||
// Get path mapping configuration from the specific download client
|
||||
const clientConfig = await manager.getClientForProtocol(protocol);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getLibraryService } from '../services/library';
|
||||
import { getConfigService } from '../services/config.service';
|
||||
import { getDownloadClientManager } from '../services/download-client-manager.service';
|
||||
import { CLIENT_PROTOCOL_MAP, DownloadClientType } from '../interfaces/download-client.interface';
|
||||
import { PathMapper, PathMappingConfig } from '../utils/path-mapper';
|
||||
import { generateFilesHash } from '../utils/files-hash';
|
||||
import { fixEpubForKindle, cleanupFixedEpub } from '../utils/epub-fixer';
|
||||
import { removeEmptyParentDirectories } from '../utils/cleanup-helpers';
|
||||
@@ -309,6 +310,43 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi
|
||||
// Still have retries left - queue for re-import
|
||||
logger.warn(`Retryable error for request ${requestId}, queueing for retry (attempt ${newAttempts}/${currentRequest.maxImportRetries})`);
|
||||
|
||||
// Re-query download client for fresh path (content_path may have been updated since handoff)
|
||||
try {
|
||||
const downloadHistory = await prisma.downloadHistory.findFirst({
|
||||
where: { requestId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
if (downloadHistory?.downloadClientId && downloadHistory?.downloadClient && downloadHistory.downloadClient !== 'direct') {
|
||||
const configService = getConfigService();
|
||||
const dlManager = getDownloadClientManager(configService);
|
||||
const dlProtocol = CLIENT_PROTOCOL_MAP[downloadHistory.downloadClient as DownloadClientType];
|
||||
|
||||
if (dlProtocol) {
|
||||
const dlClient = await dlManager.getClientServiceForProtocol(dlProtocol);
|
||||
if (dlClient) {
|
||||
const freshInfo = await dlClient.getDownload(downloadHistory.downloadClientId);
|
||||
if (freshInfo?.downloadPath && freshInfo.downloadPath !== downloadPath) {
|
||||
// Apply path mapping and update stored path
|
||||
const clientConfig = await dlManager.getClientForProtocol(dlProtocol);
|
||||
const pathMappingConfig: PathMappingConfig = clientConfig?.remotePathMappingEnabled
|
||||
? { enabled: true, remotePath: clientConfig.remotePath || '', localPath: clientConfig.localPath || '' }
|
||||
: { enabled: false, remotePath: '', localPath: '' };
|
||||
const freshPath = PathMapper.transform(freshInfo.downloadPath, pathMappingConfig);
|
||||
|
||||
logger.info(`Download client returned updated path: ${freshPath} (was: ${downloadPath})`);
|
||||
await prisma.downloadHistory.update({
|
||||
where: { id: downloadHistory.id },
|
||||
data: { downloadPath: freshPath },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (refreshError) {
|
||||
logger.warn(`Failed to refresh download path: ${refreshError instanceof Error ? refreshError.message : String(refreshError)}`);
|
||||
}
|
||||
|
||||
await prisma.request.update({
|
||||
where: { id: requestId },
|
||||
data: {
|
||||
|
||||
Reference in New Issue
Block a user