feat(adapters): port MoviePilot open site framework (7 spiders, 20 user parsers) + MovieClaw v0.27.0 site definitions + mp-sync tools (W1-3c)
- sites/spiders: Haidan/HDDolby/RousiPro/SunnyPT/TNode/TorrentLeech/Yema (+mTorrent wrapper), specialized SiteAdapter, indirect download resolved at download time - sites/user-parsers: SiteParserBase + all 20 MP SiteSchema parsers (lxml XPath via xpath+xmldom), refreshSiteUserData with schema fallback - sites/definitions: zod SiteDefinition schemas, 23 MovieClaw v0.27.0 (MIT) definitions via scripts/mp-sync/movieclaw-import.mjs, registry getSiteDefinition/listSiteDefinitions, NOTICE - scripts/mp-sync: ledger.json (upstream sha256), check.mjs, build-ledger.mjs; docs/porting/mp-sync-ledger.md - license-and-provenance: add copied-mit kind, re-anchor MovieClaw rows to v0.27.0 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
cf85481d95
commit
d9f2af2f3c
166 files changed
+16577
-186
No files matched your search
@@ -0,0 +1,74 @@
|
||||
/** 测试辅助(非公开 API):构造适配器、在 mock fetch 前挂二进制 .torrent 路由。 */
|
||||
import { buildFakeTorrent } from "@umt/testkit";
|
||||
import { createSiteRateLimiter } from "../rate-limit.ts";
|
||||
import { type SiteAdapter, type SiteCredentialKind, SiteError, staticSecretResolver } from "../types.ts";
|
||||
import { createSpecializedSiteAdapter } from "./adapter.ts";
|
||||
import type { SpecializedSiteDefinition, SpecializedSpider } from "./types.ts";
|
||||
|
||||
export function testDef(
|
||||
parser: SpecializedSiteDefinition["parser"],
|
||||
over: Partial<SpecializedSiteDefinition> = {},
|
||||
): SpecializedSiteDefinition {
|
||||
return {
|
||||
id: "demo",
|
||||
name: "Demo",
|
||||
domain: "https://pt.example.com/",
|
||||
parser,
|
||||
...over,
|
||||
umt: {
|
||||
version: "[email protected]",
|
||||
kind: "api",
|
||||
displayName: "Demo",
|
||||
timezone: "+08:00",
|
||||
rateLimit: { minIntervalMs: 0, burst: 10 },
|
||||
capabilities: { list: true, search: true, detail: false, download: true, rss: false },
|
||||
...(over.umt ?? {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function adapterWith(
|
||||
def: SpecializedSiteDefinition,
|
||||
spider: SpecializedSpider,
|
||||
fetchImpl: typeof globalThis.fetch,
|
||||
secrets: Partial<Record<SiteCredentialKind, string>> = { cookie: "c=COOKIESECRET", apiKey: "APIKEYSECRET" },
|
||||
): SiteAdapter {
|
||||
return createSpecializedSiteAdapter(def, spider, {
|
||||
fetch: fetchImpl,
|
||||
secrets: staticSecretResolver({ [def.id]: secrets }),
|
||||
limiter: createSiteRateLimiter({ minIntervalMs: 0 }),
|
||||
});
|
||||
}
|
||||
|
||||
/** 包一层 fetch:命中 predicate 的请求返回合法 .torrent 字节 */
|
||||
export function withTorrent(
|
||||
inner: typeof globalThis.fetch,
|
||||
predicate: (url: URL) => boolean,
|
||||
name = "demo",
|
||||
): { fetch: typeof globalThis.fetch; infoHash: string; hits: Array<{ url: URL; headers: Headers }> } {
|
||||
const t = buildFakeTorrent({ name });
|
||||
const hits: Array<{ url: URL; headers: Headers }> = [];
|
||||
const f = (async (input: string | URL | Request, init?: RequestInit) => {
|
||||
const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
|
||||
if (predicate(url)) {
|
||||
hits.push({ url, headers: new Headers(init?.headers) });
|
||||
return new Response(t.bytes as Uint8Array<ArrayBuffer>, {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/x-bittorrent" },
|
||||
});
|
||||
}
|
||||
return inner(input, init);
|
||||
}) as typeof globalThis.fetch;
|
||||
return { fetch: f, infoHash: t.infoHash, hits };
|
||||
}
|
||||
|
||||
export async function expectSiteError(p: Promise<unknown>, kind: string): Promise<SiteError> {
|
||||
try {
|
||||
await p;
|
||||
} catch (e) {
|
||||
if (!(e instanceof SiteError)) throw e;
|
||||
if (e.kind !== kind) throw new Error(`expected SiteError ${kind}, got ${e.kind}: ${e.message}`);
|
||||
return e;
|
||||
}
|
||||
throw new Error(`expected SiteError ${kind}`);
|
||||
}
|
||||
Reference in new issue
Block a user