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 @@
|
||||
export * from "./temp-tree";
|
||||
@@ -0,0 +1,60 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { existsSync, statSync } from "node:fs";
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
export interface TempTree {
|
||||
root: string;
|
||||
/** 根目录下的绝对路径 */
|
||||
path(rel: string): string;
|
||||
/** 写文件(自动建父目录);content 为数字时写入该长度的随机字节 */
|
||||
write(rel: string, content: string | Buffer | number): Promise<string>;
|
||||
cleanup(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 临时目录夹具。spec 的 key 为相对路径(以 / 结尾表示目录),value 同 write。
|
||||
* sentinel 为真时写入库根哨兵 .uhd-root。
|
||||
*/
|
||||
export async function createTempTree(
|
||||
spec: Record<string, string | Buffer | number | null> = {},
|
||||
opts: { base?: string; sentinel?: boolean | string; prefix?: string } = {},
|
||||
): Promise<TempTree> {
|
||||
const root = await mkdtemp(join(opts.base ?? tmpdir(), opts.prefix ?? "umt-fs-"));
|
||||
const tree: TempTree = {
|
||||
root,
|
||||
path: (rel) => join(root, rel),
|
||||
write: async (rel, content) => {
|
||||
const p = join(root, rel);
|
||||
await mkdir(dirname(p), { recursive: true });
|
||||
await writeFile(p, typeof content === "number" ? randomBytes(content) : content);
|
||||
return p;
|
||||
},
|
||||
cleanup: () => rm(root, { recursive: true, force: true }),
|
||||
};
|
||||
if (opts.sentinel) {
|
||||
await writeFile(join(root, typeof opts.sentinel === "string" ? opts.sentinel : ".uhd-root"), "sentinel\n");
|
||||
}
|
||||
for (const [rel, v] of Object.entries(spec)) {
|
||||
if (rel.endsWith("/") || v === null) await mkdir(join(root, rel), { recursive: true });
|
||||
else await tree.write(rel, v);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 找一个与 os.tmpdir() 不在同一设备上的可写目录(优先 /dev/shm),用于 EXDEV 测试。
|
||||
* 找不到返回 null,测试应 skip 并注明。
|
||||
*/
|
||||
export function findCrossDeviceBase(candidates = ["/dev/shm", "/run/user", "/var/tmp"]): string | null {
|
||||
const baseDev = statSync(tmpdir()).dev;
|
||||
for (const c of candidates) {
|
||||
try {
|
||||
if (existsSync(c) && statSync(c).dev !== baseDev) return c;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in new issue
Block a user