feat(adapters): RSS and PT site adapters ported from MoviePilot v3 + mocks (W1-3b)
- rss: port of MP RssHelper._parse_impl/get_rss_link semantics with safe XML (entities off, DTD/ENTITY rejected, size/item caps), ETag/Last-Modified, backoff + Retry-After, circuit breaker with manual reset, passkey redaction, overlap-window GUID+digest dedupe - sites/spider: TS port of MP SiteSpider (indexer schema, search URL builder, field DSL, filters, Jinja templates via nunjucks, login-page detection); audiences definition hand-written as JSON; raw category id preserved; shared per-site token bucket - sites/mteam: port of MP MTorrentSpider (params, result mapping, promo factors, genDlToken flow) without embedding the API key in enclosures; business code/message checks - testkit: mock RSS server, mock NexusPHP site, mock M-Team API - docs: W1-3b migration mapping appended to porting ledger Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
ef2e504a94
commit
a6d049db81
47 files changed
+5414
No files matched your search
@@ -0,0 +1,82 @@
|
||||
import { redactUrl } from "../../rss/redact";
|
||||
import type { PromotionType, SitePromotion, SiteRelease } from "../types";
|
||||
import { parseSiteTime } from "../util";
|
||||
import type { IndexerDefinition } from "./schema";
|
||||
import type { SpiderTorrent } from "./spider";
|
||||
|
||||
/**
|
||||
* MP 系数 → 统一促销类型。MP 只有 downloadvolumefactor/uploadvolumefactor/freedate 三个字段,
|
||||
* 类型名由系数组合推出;字段未配置(undefined)时为 unknown。
|
||||
*/
|
||||
export function promotionFromFactors(
|
||||
dl: number | undefined,
|
||||
ul: number | undefined,
|
||||
freedate: string | null,
|
||||
): SitePromotion {
|
||||
if (dl === undefined && ul === undefined)
|
||||
return { type: "unknown", downloadFactor: null, uploadFactor: null, expiresAt: null };
|
||||
const d = dl ?? 1;
|
||||
const u = ul ?? 1;
|
||||
let type: PromotionType;
|
||||
if (d === 0) type = u >= 2 ? "2xfree" : "free";
|
||||
else if (d === 0.5) type = u >= 2 ? "2x50%" : "50%";
|
||||
else if (d === 0.3) type = "30%";
|
||||
else if (d === 1 && u >= 2) type = "2x";
|
||||
else if (d === 1 && u === 1) type = "none";
|
||||
else type = "unknown";
|
||||
return { type, downloadFactor: dl ?? null, uploadFactor: ul ?? null, expiresAt: type === "none" ? null : freedate };
|
||||
}
|
||||
|
||||
/** 从详情链接取站点资源 ID(NexusPHP `details.php?id=`)。 */
|
||||
export function releaseIdFromUrl(url: string | undefined): string | null {
|
||||
if (!url) return null;
|
||||
return /[?&]id=(\d+)/.exec(url)?.[1] ?? null;
|
||||
}
|
||||
|
||||
export function categoryName(def: IndexerDefinition, id: string | undefined): string | null {
|
||||
if (!id) return null;
|
||||
const named = def.umt.categoryNames?.[id];
|
||||
if (named) return named;
|
||||
for (const k of ["movie", "tv", "music"] as const) {
|
||||
const hit = (def.category?.[k] ?? []).find((c) => String(c.id) === id);
|
||||
if (hit?.desc) return hit.desc;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** SpiderTorrent(MP TorrentInfo 字段)→ SiteRelease。raw 保存完整 MP 字段(enclosure 脱敏)。 */
|
||||
export function spiderTorrentToRelease(
|
||||
t: SpiderTorrent,
|
||||
def: IndexerDefinition,
|
||||
observedAt: string,
|
||||
): SiteRelease | null {
|
||||
const id = releaseIdFromUrl(t.page_url) ?? releaseIdFromUrl(t.enclosure);
|
||||
if (!id || !t.title) return null;
|
||||
const tz = def.umt.timezone;
|
||||
const freedate = t.freedate ? parseSiteTime(t.freedate, tz) : null;
|
||||
const raw: Record<string, unknown> = { ...t };
|
||||
if (t.enclosure) raw.enclosure = redactUrl(t.enclosure);
|
||||
const release: SiteRelease = {
|
||||
siteId: def.id,
|
||||
siteReleaseId: id,
|
||||
title: t.title,
|
||||
category: { id: t.category_id ?? null, name: categoryName(def, t.category_id) },
|
||||
sizeBytes: t.size ?? null,
|
||||
seeders: t.seeders ?? null,
|
||||
leechers: t.peers ?? null,
|
||||
snatched: t.grabs ?? null,
|
||||
promotion: promotionFromFactors(t.downloadvolumefactor, t.uploadvolumefactor, freedate),
|
||||
hr: t.hit_and_run === undefined ? "unknown" : t.hit_and_run,
|
||||
uploadedAt: parseSiteTime(t.pubdate, tz),
|
||||
detailUrl: t.page_url ?? new URL(`${def.umt.detailPath}?id=${id}`, def.domain).toString(),
|
||||
downloadUrl: t.enclosure
|
||||
? redactUrl(t.enclosure)
|
||||
: new URL(`${def.umt.downloadPath}?id=${id}`, def.domain).toString(),
|
||||
tags: t.labels ?? [],
|
||||
raw,
|
||||
observedAt,
|
||||
};
|
||||
if (t.description) release.subtitle = t.description;
|
||||
if (t.imdbid) release.imdbId = t.imdbid;
|
||||
return release;
|
||||
}
|
||||
Reference in new issue
Block a user