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:
kikootwo
2026-02-26 12:45:24 -05:00
parent d38f03b8f4
commit 1b0a80052d
6 changed files with 262 additions and 227 deletions
+7 -14
View File
@@ -1091,20 +1091,12 @@ export class QBittorrentService implements IDownloadClient {
protected mapTorrentToDownloadInfo(torrent: TorrentInfo): DownloadInfo {
const status = this.mapStateToDownloadStatus(torrent.state);
// For completed/seeding torrents, combine save_path with the content folder basename.
// Two problems are solved simultaneously:
// 1. TempPathEnabled race — content_path may still reference the temp/incomplete directory
// after qBittorrent marks the torrent as seeding but before the file move finishes.
// 2. Name mismatch — torrent.name (display name) can differ from the actual folder name
// on disk (the root folder inside the torrent archive). content_path always reflects
// the real filesystem name, so we extract its basename for the join.
const isFinished = status === 'seeding' || status === 'completed';
const contentBasename = torrent.content_path
? path.basename(torrent.content_path)
: torrent.name;
const downloadPath = isFinished
? path.join(torrent.save_path, contentBasename)
: (torrent.content_path || path.join(torrent.save_path, torrent.name));
// content_path is the canonical path from qBittorrent — always use it directly.
// It correctly handles all torrent structures (multi-file folders, single files,
// single files in wrapper folders, name mismatches).
// For TempPathEnabled race detection, we expose save_path so the monitor can
// compare and wait for files to relocate before triggering file organization.
const downloadPath = torrent.content_path || path.join(torrent.save_path, torrent.name);
return {
id: torrent.hash,
@@ -1117,6 +1109,7 @@ export class QBittorrentService implements IDownloadClient {
eta: torrent.eta,
category: torrent.category,
downloadPath,
savePath: torrent.save_path,
completedAt: torrent.completion_on > 0 ? new Date(torrent.completion_on * 1000) : undefined,
seedingTime: torrent.seeding_time,
ratio: torrent.ratio,