- qbittorrent: WebUI API v2 client (cookie/API-key auth, add/info/files/filePrio, pause/resume with 5.x stop/start fallback, tags, delete with explicit deleteFiles, maindata), error classes, idempotent submit with local infohash (own bencode) and post-timeout hash/probe-tag lookup; state mapping per MoviePilot - fs: copyWithVerify (tmp + fsync + size/sha256/sample + no-clobber commit), moveSafe (EXDEV surfaced), resumable crossDeviceMove, statSafe/checkRoot with root sentinel, budgeted incremental scan with cursor, ffprobe, statfs usage - tmdb: multi-language search/details/seasons, LRU+TTL cache with coalescing, rate-limit backoff, MoviePilot match chain (name normalization, year +-1) - emby: Emby/Jellyfin items/seasons/episodes/MediaSources, failures -> unknown - testkit: mock qB / TMDB / Emby servers, torrent builder, temp-tree fixtures - docs: W1-3a porting table appended to downloader-and-sites ledger Co-Authored-By: Claude Fable 5.1 <[email protected]>
72 lines
1.8 KiB
TypeScript
72 lines
1.8 KiB
TypeScript
// 迁移来源:MoviePilot app/modules/qbittorrent/qbittorrent.py get_completed_torrents(Q6)与 __init__.py __normalize_torrent_state(Q23),e6820a8。
|
|
/**
|
|
* qB 原始 state → 观测态。与 MoviePilot 不同:未识别/error/missingFiles 绝不兜底为完成
|
|
* (见 docs/porting/moviepilot-downloader-and-sites.md §1.4、QA5)。
|
|
*/
|
|
export type QbObservedState =
|
|
| "downloading"
|
|
| "stalled"
|
|
| "paused"
|
|
| "completed_paused"
|
|
| "seeding"
|
|
| "checking"
|
|
| "moving"
|
|
| "error"
|
|
| "missing_files"
|
|
| "queued"
|
|
| "metadata"
|
|
| "unknown";
|
|
|
|
const MAP: Record<string, QbObservedState> = {
|
|
downloading: "downloading",
|
|
forceddl: "downloading",
|
|
allocating: "downloading",
|
|
checkingresumedata: "checking",
|
|
checkingdl: "checking",
|
|
checkingup: "checking",
|
|
metadl: "metadata",
|
|
forcedmetadl: "metadata",
|
|
queueddl: "queued",
|
|
stalleddl: "stalled",
|
|
pauseddl: "paused",
|
|
stoppeddl: "paused",
|
|
pausedup: "completed_paused",
|
|
stoppedup: "completed_paused",
|
|
uploading: "seeding",
|
|
stalledup: "seeding",
|
|
forcedup: "seeding",
|
|
queuedup: "seeding",
|
|
moving: "moving",
|
|
error: "error",
|
|
missingfiles: "missing_files",
|
|
};
|
|
|
|
export function mapQbState(raw: string | null | undefined): QbObservedState {
|
|
if (!raw) return "unknown";
|
|
return MAP[raw.toLowerCase()] ?? "unknown";
|
|
}
|
|
|
|
const NOT_DONE = new Set([
|
|
"allocating",
|
|
"checkingdl",
|
|
"checkingup",
|
|
"checkingresumedata",
|
|
"downloading",
|
|
"error",
|
|
"forceddl",
|
|
"missingfiles",
|
|
"metadl",
|
|
"forcedmetadl",
|
|
"moving",
|
|
"queueddl",
|
|
"stalleddl",
|
|
"unknown",
|
|
]);
|
|
|
|
/** 严格完成判定(QK4):progress>=1 && amountLeft<=0 && state 属于已知终态。 */
|
|
export function isDownloadComplete(t: { progress: number; amountLeft: number; state: string }): boolean {
|
|
const s = t.state.toLowerCase();
|
|
if (!(s in MAP) || NOT_DONE.has(s)) return false;
|
|
return t.progress >= 1 && t.amountLeft <= 0;
|
|
}
|