- 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]>
83 lines
3.2 KiB
TypeScript
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;
|
|
}
|