- 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]>
47 lines
1.6 KiB
TypeScript
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;
|
|
}
|