Files
UHD_Media_Tool/packages/adapters/src/qbittorrent/state.ts
T
rootandClaude Fable 5.1 ef2e504a94 feat(adapters): qBittorrent, filesystem, TMDB and Emby adapters with testkit mocks (W1-3a)
- 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]>
2026-09-27 18:07:48 +08:00

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;
}