Files
UHD_Media_Tool/packages/testkit/src/qbittorrent/http-util.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

47 lines
1.6 KiB
TypeScript

import type { IncomingMessage } from "node:http";
export async function readBody(req: IncomingMessage): Promise<Buffer> {
const chunks: Buffer[] = [];
for await (const c of req) chunks.push(c as Buffer);
return Buffer.concat(chunks);
}
export interface MultipartPart {
name: string;
filename?: string;
data: Buffer;
}
/** 极简 multipart/form-data 解析,仅供 mock 使用。 */
export function parseMultipart(body: Buffer, contentType: string): MultipartPart[] {
const m = /boundary=(?:"([^"]+)"|([^;]+))/i.exec(contentType);
if (!m) return [];
const boundary = Buffer.from(`--${m[1] ?? m[2]}`);
const parts: MultipartPart[] = [];
let pos = body.indexOf(boundary);
while (pos >= 0) {
const start = pos + boundary.length;
if (body.subarray(start, start + 2).toString() === "--") break;
const next = body.indexOf(boundary, start);
if (next < 0) break;
const chunk = body.subarray(start + 2, next - 2); // 去掉起始 CRLF 与结尾 CRLF
const sep = chunk.indexOf("\r\n\r\n");
const head = chunk.subarray(0, sep).toString();
const data = chunk.subarray(sep + 4);
const name = /name="([^"]*)"/.exec(head)?.[1] ?? "";
const filename = /filename="([^"]*)"/.exec(head)?.[1];
parts.push({ name, ...(filename !== undefined ? { filename } : {}), data });
pos = next;
}
return parts;
}
export function parseCookies(header: string | undefined): Record<string, string> {
const out: Record<string, string> = {};
for (const p of (header ?? "").split(";")) {
const i = p.indexOf("=");
if (i > 0) out[p.slice(0, i).trim()] = p.slice(i + 1).trim();
}
return out;
}