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:
rootandClaude Opus 5.5 committed 2026-09-27 18:39:27 +08:00
1 parent cf85481d95
commit d9f2af2f3c
166 files changed
+16577 -186

No files matched your search

+11 -11
View File
@@ -9,12 +9,12 @@
* - `app/modules/indexer/parser/mtorrent.py`(api/member/profile)→ `healthCheck`
* 详情 `/api/torrent/detail` 为本项目扩展(MP spider 无详情调用,字段结构参考 MovieClaw mteam.py,data-ref)。
*/
import { redactUrl } from "../../rss/redact";
import { buildHttpContext, type SiteAdapterDeps } from "../deps";
import { readBody, siteFetch } from "../http";
import { healthFromError } from "../spider/adapter";
import { promotionFromFactors } from "../spider/map";
import type { SpiderTorrent } from "../spider/spider";
import { redactUrl } from "../../rss/redact.ts";
import { buildHttpContext, type SiteAdapterDeps } from "../deps.ts";
import { readBody, siteFetch } from "../http.ts";
import { healthFromError } from "../spider/adapter.ts";
import { promotionFromFactors } from "../spider/map.ts";
import type { SpiderTorrent } from "../spider/spider.ts";
import {
type DownloadTarget,
type SiteAdapter,
@@ -24,11 +24,11 @@ import {
type SitePage,
type SiteRelease,
type SiteReleaseDetail,
} from "../types";
import { extractDoubanId, inspectTorrent, parseSiteTime } from "../util";
import { mteamPost } from "./client";
import { type MteamDefinition, mteamApiBase } from "./definition";
import { type MteamTorrentJson, mteamSearchParams, parseMteamResult } from "./spider";
} from "../types.ts";
import { extractDoubanId, inspectTorrent, parseSiteTime } from "../util.ts";
import { mteamPost } from "./client.ts";
import { type MteamDefinition, mteamApiBase } from "./definition.ts";
import { type MteamTorrentJson, mteamSearchParams, parseMteamResult } from "./spider.ts";
interface MteamSearchData {
data?: MteamTorrentJson[] | null;
+3 -3
View File
@@ -2,9 +2,9 @@
* M-Team API 调用。对应 MP `MTorrentSpider.search/__process_response`(MP 只看 HTTP 200);
* 业务 code/message 检查与凭据/解析错误分层参考 MovieClaw `_post_api`(behavior-ref,迁移账本 MT2)。
*/
import { redactText } from "../../rss/redact";
import { readText, type SiteHttpContext, siteFetch } from "../http";
import { SiteError } from "../types";
import { redactText } from "../../rss/redact.ts";
import { readText, type SiteHttpContext, siteFetch } from "../http.ts";
import { SiteError } from "../types.ts";
export type MteamApiPath =
| "/api/torrent/search"
+15 -33
View File
@@ -1,4 +1,7 @@
import type { SiteCapabilities } from "../types";
import mteamJson from "../definitions/mteam.json" with { type: "json" };
import { parseMteamDefinition } from "../definitions/schema.ts";
import type { DefinitionProvenance } from "../spider/schema.ts";
import type { SiteCapabilities } from "../types.ts";
/**
* 馒头站点定义:键名沿用 MP indexer dict(id/name/domain/parser/timeout),
@@ -7,6 +10,10 @@ import type { SiteCapabilities } from "../types";
export interface MteamDefinition {
id: string;
name: string;
enabled?: boolean;
/** MP 用户信息解析器 schema(MTorrent) */
schema?: string;
provenance?: DefinitionProvenance;
/** 网页域(MP `_url`,详情页 = `${domain}detail/${id}`) */
domain: string;
parser: "mTorrent";
@@ -22,41 +29,16 @@ export interface MteamDefinition {
capabilities: SiteCapabilities;
/** 原始分类 ID → 名称(自行编制,待核验) */
categoryNames: Record<string, string>;
authSupported?: string[];
customClass?: string;
};
}
export const mteamDefinition: MteamDefinition = {
id: "mteam",
name: "馒头",
domain: "https://tp.m-team.cc/",
parser: "mTorrent",
timeout: 15,
umt: {
version: "[email protected]",
kind: "mteam",
displayName: "馒头",
timezone: "+08:00",
rateLimit: { minIntervalMs: 1000, burst: 1 },
capabilities: { list: true, search: true, detail: true, download: true, rss: false },
categoryNames: {
"401": "电影",
"419": "电影",
"420": "电影",
"421": "电影",
"439": "电影",
"402": "剧集",
"403": "综艺",
"435": "剧集",
"438": "剧集",
"404": "纪录",
"405": "动画",
"407": "体育",
"406": "音乐",
"408": "音乐",
"434": "音乐",
},
},
};
/**
* 馒头定义:definitions/mteam.json(由 scripts/mp-sync/movieclaw-import.mjs 从 MovieClaw v0.27.0 mteam.yaml 转换,
* W1-3b 原值经 movieclaw-overrides/mteam.json 覆盖),加载时 zod 校验。
*/
export const mteamDefinition: MteamDefinition = parseMteamDefinition(mteamJson);
/** MP `site_rules.extract_domain` 的简化:取主机名最后两级。 */
export function rootDomain(url: string): string {
@@ -9,7 +9,7 @@ import {
SiteError,
type SiteRateLimiter,
staticSecretResolver,
} from "../index";
} from "../index.ts";
let api: MockMteamApi | undefined;
afterEach(async () => {
+12 -4
View File
@@ -14,7 +14,7 @@
* 2. 额外输出 `category_id`(原始分类 ID),`category` 仍是 MP 的媒体类型投影。
* 3. 额外保留 `status` 原文到 raw,便于核验。
*/
import type { MediaKind, MpMediaType, SpiderTorrent } from "../spider/spider";
import type { MediaKind, MpMediaType, SpiderTorrent } from "../spider/spider.ts";
export const MOVIE_CATEGORY = ["401", "419", "420", "421", "439", "405", "404"];
export const TV_CATEGORY = ["403", "402", "435", "438", "404", "405"];
@@ -87,10 +87,18 @@ export function getUploadVolumeFactor(discount: string | null | undefined): numb
return discount ? (map[discount] ?? 1) : 1;
}
/** MP `time_tools.format_timestamp`:非数字字符串原样返回,Unix 秒转本地时间(此处按站点时区交给调用方)。 */
function formatTimestamp(v: string | null | undefined): string | undefined {
/**
* MP `time_tools.format_timestamp`:非纯数字字符串原样返回;Unix 秒 → 本地墙钟 "YYYY-MM-DD HH:MM:SS"。
* MP 用进程本地时区(容器默认 Asia/Shanghai);这里固定 +08:00,与 adapter 按 `umt.timezone`(+08:00)转 UTC 配套。
*/
function formatTimestamp(v: string | number | null | undefined): string | undefined {
if (v == null) return undefined;
return String(v);
if (typeof v === "string" && !/^\d+$/.test(v)) return v;
const n = Number(v);
if (!Number.isFinite(n)) return String(v);
const d = new Date(Math.trunc(n) * 1000 + 8 * 3600_000);
const p = (x: number) => String(x).padStart(2, "0");
return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}`;
}
function toInt(v: unknown): number {