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]>
This commit is contained in:
1 parent
9c881e737f
commit
ef2e504a94
45 files changed
+5766
-1
No files matched your search
@@ -0,0 +1,71 @@
|
||||
// 迁移来源: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;
|
||||
}
|
||||
Reference in new issue
Block a user