Files
UHD_Media_Tool/packages/adapters/src/sites/spider/map.ts
T
rootandClaude Fable 5.1 a6d049db81 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]>
2026-09-27 18:10:34 +08:00

83 lines
3.2 KiB
TypeScript

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;
}