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,110 @@
|
||||
{
|
||||
"_provenance": "按 MoviePilot SiteSpider indexer schema 手写;字段结构参考 MovieClaw audiences.yaml(data-ref,未复制);选择器值按 NexusPHP 公开页面惯例编写,仅对 testkit mock 验证,待真实页面核验。见 ../README.md",
|
||||
"id": "audiences",
|
||||
"name": "观众",
|
||||
"domain": "https://audiences.me/",
|
||||
"encoding": "UTF-8",
|
||||
"public": false,
|
||||
"proxy": false,
|
||||
"language": "zh",
|
||||
"result_num": 100,
|
||||
"timeout": 30,
|
||||
"search": {
|
||||
"paths": [{ "path": "torrents.php", "type": "all" }],
|
||||
"params": { "search": "{keyword}", "search_area": 4 }
|
||||
},
|
||||
"batch": { "delimiter": " ", "space_replace": "_" },
|
||||
"browse": { "path": "torrents.php?page={page}", "start": 0 },
|
||||
"category": {
|
||||
"movie": [{ "id": "401", "cat": "Movies", "desc": "电影" }],
|
||||
"tv": [{ "id": "402", "cat": "TV Series", "desc": "剧集" }, { "id": "403", "cat": "TV Shows", "desc": "综艺" }],
|
||||
"music": []
|
||||
},
|
||||
"torrents": {
|
||||
"list": {
|
||||
"selector": "table.torrents > tbody > tr:has(table.torrentname), table.torrents > tr:has(table.torrentname)"
|
||||
},
|
||||
"fields": {
|
||||
"title_default": { "selector": "a[href*=\"details.php?id=\"]" },
|
||||
"title_optional": { "selector": "a[title][href*=\"details.php?id=\"]", "attribute": "title" },
|
||||
"title": {
|
||||
"text": "{% if fields['title_optional'] %}{{ fields['title_optional'] }}{% else %}{{ fields['title_default'] }}{% endif %}"
|
||||
},
|
||||
"details": { "selector": "a[href*=\"details.php?id=\"]", "attribute": "href" },
|
||||
"download": { "selector": "a[href*=\"download.php?id=\"]", "attribute": "href" },
|
||||
"imdbid": {
|
||||
"selector": "a[href*=\"imdb.com/title/tt\"]",
|
||||
"attribute": "href",
|
||||
"filters": [{ "name": "re_search", "args": ["tt\\d+", 0] }]
|
||||
},
|
||||
"date_elapsed": { "selector": "td:nth-child(4) > span" },
|
||||
"date_added": { "selector": "td:nth-child(4) > span", "attribute": "title" },
|
||||
"date": {
|
||||
"text": "{% if fields['date_elapsed'] or fields['date_added'] %}{{ fields['date_added'] if fields['date_added'] else fields['date_elapsed'] }}{% else %}now{% endif %}",
|
||||
"filters": [{ "name": "dateparse", "args": "%Y-%m-%d %H:%M:%S" }]
|
||||
},
|
||||
"size": { "selector": "td:nth-child(5)" },
|
||||
"seeders": { "selector": "td:nth-child(6)" },
|
||||
"leechers": { "selector": "td:nth-child(7)" },
|
||||
"grabs": { "selector": "td:nth-child(8)" },
|
||||
"downloadvolumefactor": {
|
||||
"case": {
|
||||
"img.pro_free": 0,
|
||||
"img.pro_free2up": 0,
|
||||
"img.pro_50pctdown": 0.5,
|
||||
"img.pro_50pctdown2up": 0.5,
|
||||
"img.pro_30pctdown": 0.3,
|
||||
"*": 1
|
||||
}
|
||||
},
|
||||
"uploadvolumefactor": {
|
||||
"case": {
|
||||
"img.pro_50pctdown2up": 2,
|
||||
"img.pro_free2up": 2,
|
||||
"img.pro_2up": 2,
|
||||
"*": 1
|
||||
}
|
||||
},
|
||||
"freedate": {
|
||||
"selector": "img[class^=\"pro_\"]",
|
||||
"attribute": "title",
|
||||
"filters": [{ "name": "re_search", "args": ["\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}", 0] }]
|
||||
},
|
||||
"description": { "selector": "span.torrent-subtitle-line" },
|
||||
"labels": { "selector": "span.tags" },
|
||||
"category": {
|
||||
"selector": "a[href*=\"?cat=\"]",
|
||||
"attribute": "href",
|
||||
"filters": [{ "name": "querystring", "args": "cat" }]
|
||||
}
|
||||
}
|
||||
},
|
||||
"umt": {
|
||||
"version": "[email protected]",
|
||||
"kind": "nexusphp",
|
||||
"displayName": "观众",
|
||||
"timezone": "+08:00",
|
||||
"rateLimit": { "minIntervalMs": 3000, "burst": 1 },
|
||||
"capabilities": { "list": true, "search": true, "detail": true, "download": true, "rss": true },
|
||||
"categoryNames": { "401": "电影", "402": "剧集", "403": "综艺", "406": "纪录片" },
|
||||
"loginSuccess": "a[href*=\"logout\"]",
|
||||
"emptyMarkers": ["没有种子", "没有结果", "Nothing found"],
|
||||
"pagination": "a[href*=\"page=\"]",
|
||||
"detailPath": "details.php",
|
||||
"downloadPath": "download.php",
|
||||
"detail": {
|
||||
"path": "details.php",
|
||||
"title": "h1#top",
|
||||
"rowLabel": "td.rowhead",
|
||||
"rowValue": "td.rowfollow",
|
||||
"labels": {
|
||||
"subtitle": ["副标题", "Small Description"],
|
||||
"basicInfo": ["基本信息", "Basic Info"],
|
||||
"peers": ["同伴", "Peers"]
|
||||
},
|
||||
"description": "#kdescr",
|
||||
"headerScope": "h1#top",
|
||||
"notFoundMarkers": ["没有该ID的种子", "No torrent with ID"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { IndexerDefinition } from "../spider/schema";
|
||||
import audiencesJson from "./audiences.json";
|
||||
|
||||
/** 站点定义最小校验(随产品发布的定义文件;加站只需新增 JSON 并在此登记)。 */
|
||||
export function loadIndexerDefinition(json: unknown): IndexerDefinition {
|
||||
const d = json as Partial<IndexerDefinition>;
|
||||
if (!d || typeof d !== "object") throw new Error("站点定义必须是对象");
|
||||
for (const k of ["id", "name", "domain"] as const) {
|
||||
if (typeof d[k] !== "string" || !d[k]) throw new Error(`站点定义缺少 ${k}`);
|
||||
}
|
||||
if (!d.torrents?.list?.selector || !d.torrents.fields) throw new Error(`站点定义 ${d.id} 缺少 torrents.list/fields`);
|
||||
if (!d.umt?.version || !d.umt.timezone || !d.umt.rateLimit) throw new Error(`站点定义 ${d.id} 缺少 umt 扩展`);
|
||||
return d as IndexerDefinition;
|
||||
}
|
||||
|
||||
export const audiencesDefinition: IndexerDefinition = loadIndexerDefinition(audiencesJson);
|
||||
Reference in new issue
Block a user