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

@@ -0,0 +1,43 @@
/**
* CHDBits(彩虹岛)列表页 fixture:按 MovieClaw v0.27.0 chdbits.yaml 选择器描述的标准 NexusPHP 结构自行编写
* (tr > td > table.torrentname、font.subtitle、img.pro_*、td.embedded > span[title] 促销截止),**未对照真实页面**。
*/
export const CHDBITS_LIST_PAGE = `<!DOCTYPE html><html><head><title>CHDBits :: 种子</title></head><body>
<table id="info_block"><tr><td><a href="userdetails.php?id=42" class="VIP_Name">carol</a> <a href="logout.php">退出</a>
<font class="color_uploaded">上传量:</font> 25.052 TB <font class="color_downloaded">下载量:</font> 1.2 TB</td></tr></table>
<table class="torrents" cellspacing="0" cellpadding="5" width="100%">
<tr><td class="colhead">类型</td><td class="colhead">标题</td><td class="colhead">评论</td><td class="colhead">时间</td><td class="colhead">大小</td><td class="colhead">种子</td><td class="colhead">下载</td><td class="colhead">完成</td><td class="colhead">发布者</td></tr>
<tr>
<td class="rowfollow nowrap"><a href="?cat=402"><img class="c_tvseries" src="pic/cattrans.gif" alt="TV Series"/></a></td>
<td class="rowfollow"><table class="torrentname" width="100%"><tr>
<td class="embedded"><a title="The.Last.of.Us.S02.2025.1080p.BluRay.x264.DTS-HD.MA.5.1-CHD" href="details.php?id=555001&amp;hit=1"><b>The.Last.of.Us.S02.2025.1080p.BluRay...</b></a>
<img class="pro_free2up" src="pic/trans.gif" alt="2X Free"/> <span title="2026-09-30 12:00:00">[限时]</span><br/><font class="subtitle">最后生还者 第二季 <span class="tags">中字</span></font></td>
<td class="embedded" width="20"><a href="download.php?id=555001"><img class="download" alt="download"/></a></td>
</tr></table></td>
<td class="rowfollow">3</td>
<td class="rowfollow nowrap"><span title="2026-09-25 08:00:00">2天</span></td>
<td class="rowfollow">45.20<br/>GB</td>
<td class="rowfollow"><b><a href="details.php?id=555001#peers">42</a></b></td>
<td class="rowfollow">3</td>
<td class="rowfollow"><a href="viewsnatches.php?id=555001"><b>100</b></a></td>
<td class="rowfollow"><a href="userdetails.php?id=1">uploader</a></td>
</tr>
<tr>
<td class="rowfollow nowrap"><a href="?cat=401"><img class="c_movie" src="pic/cattrans.gif" alt="Movies"/></a></td>
<td class="rowfollow"><table class="torrentname" width="100%"><tr>
<td class="embedded"><a title="Oppenheimer.2023.2160p.UHD.BluRay.REMUX-CHD" href="details.php?id=555002&amp;hit=1"><b>Oppenheimer.2023...</b></a><br/><font class="subtitle">奥本海默</font></td>
<td class="embedded" width="20"><a href="download.php?id=555002"><img class="download" alt="download"/></a></td>
</tr></table></td>
<td class="rowfollow">0</td>
<td class="rowfollow nowrap"><span title="2026-09-01 10:00:00">26天</span></td>
<td class="rowfollow">80.00<br/>GB</td>
<td class="rowfollow">7</td>
<td class="rowfollow">0</td>
<td class="rowfollow">55</td>
<td class="rowfollow"><a href="userdetails.php?id=1">uploader</a></td>
</tr>
</table></body></html>`;
export const CHDBITS_LOGIN_PAGE = `<!DOCTYPE html><html><head><title>CHDBits :: 登录</title></head><body>
<form method="post" action="takelogin.php"><input name="username"/><input type="password" name="password"/></form>
<p>未登录!</p></body></html>`;
@@ -0,0 +1,47 @@
/**
* HHClub(憨憨)列表/详情页 fixture:按 MovieClaw v0.27.0 hhanclub.yaml 选择器所描述的结构自行编写
* (div 卡片模板 + promotion-tag-* + torrent-info-hr + Tailwind 详情页),**未对照真实页面**。
*/
export const HHANCLUB_LIST_PAGE = `<!DOCTYPE html><html><head><title>HHCLUB :: 种子</title></head><body>
<div id="user-info-panel"><a href="userdetails.php?id=7" class="EliteUser_Name">bob</a> <a href="logout.php">退出</a></div>
<div class="torrent-table">
<div class="torrent-table-sub-info">
<div class="torrent-cat"><a href="?cat[]=403"><img src="pic/cattrans.gif" alt="综艺"/></a></div>
<div class="torrent-title"><a class="torrent-info-text-name" href="details.php?id=90001&amp;hit=1">Go.Fighting.S10E01.2026.2160p.WEB-DL.H265.AAC-HHWEB</a>
<span class="promotion-tag promotion-tag-free">免费</span></div>
<div class="torrent-info-text-small_name">极限挑战 第十季 第1期</div>
<div class="torrent-manage"><a href="download.php?id=90001">下载</a></div>
<div class="torrent-info-text-added"><span title="2026-09-20 20:30:00">6天23时</span></div>
<div class="torrent-info-text-size">6.12 GB</div>
<div class="torrent-info-text-seeders">88</div>
<div class="torrent-info-text-leechers">5</div>
<div class="torrent-info-text-finished">1,024</div>
<div class="torrent-info-text-author">anon</div>
<div class="torrent-info-hr">30</div>
</div>
<div class="torrent-table-sub-info">
<div class="torrent-cat"><a href="?cat[]=401"><img src="pic/cattrans.gif" alt="电影"/></a></div>
<div class="torrent-title"><a class="torrent-info-text-name" href="details.php?id=90002&amp;hit=1">Dune.Part.Two.2024.2160p.UHD.BluRay.REMUX.DV.HDR.HEVC.TrueHD.7.1.Atmos-HHWEB</a>
<span class="promotion-tag promotion-tag-50pctdown2up">50%</span></div>
<div class="torrent-info-text-small_name">沙丘2</div>
<div class="torrent-manage"><a href="download.php?id=90002">下载</a></div>
<div class="torrent-info-text-added">2026-09-01 08:00:00</div>
<div class="torrent-info-text-size">78.3 GB</div>
<div class="torrent-info-text-seeders">12</div>
<div class="torrent-info-text-leechers">0</div>
<div class="torrent-info-text-finished">300</div>
<div class="torrent-info-text-author">anon</div>
</div>
</div>
<div class="pagination"><a href="?page=0">1-100</a><a href="?page=3">301-400</a></div>
</body></html>`;
export const HHANCLUB_DETAIL_PAGE = `<!DOCTYPE html><html><head><title>HHCLUB :: 种子详情</title></head><body>
<a href="logout.php">退出</a>
<div class="grid">
<div class="font-bold leading-6">标题</div><div class="font-bold leading-6">Dune.Part.Two.2024.2160p.UHD.BluRay.REMUX-HHWEB</div>
<div class="font-bold leading-6">副标题</div><div class="font-bold leading-6">沙丘2 | 中字</div>
<div><span class="font-bold">大小:</span><span>78.30 GB</span></div>
<div><a href="download.php?id=90002">下载种子</a></div>
<div id="kdescr"><a href="https://www.imdb.com/title/tt15239678/">IMDb</a> <a href="https://movie.douban.com/subject/35575567/">豆瓣</a> 简介……</div>
</div></body></html>`;
@@ -0,0 +1,2 @@
export * from "./chdbits.ts";
export * from "./hhanclub.ts";
@@ -0,0 +1,48 @@
/** 海胆 `torrents.php?isapi=1` 响应样例(自行编写,字段名按 MP haidan.py 读取的键;不代表真实数据)。 */
export const HAIDAN_SEARCH_OK = {
code: 0,
msg: "success",
data: {
"30001": {
group_id: 9001,
category: "401",
name: "Dune.Part.Two.2024.2160p.UHD.BluRay.REMUX.HDR.HEVC.Atmos-HDH",
small_descr: "沙丘2 | 4K 原盘 REMUX",
url: "https://www.haidan.video/download.php?id=30001&passkey=HDPASSKEY123",
added: 1726790400,
size: "76543210987",
seeders: "88",
leechers: "3",
times_completed: "412",
sp_state: "2",
},
"30002": {
group_id: 9002,
category: "402",
name: "Shogun.2024.S01.1080p.WEB-DL.DDP5.1.H.264-HDH",
small_descr: "幕府将军 第一季",
url: "https://www.haidan.video/download.php?id=30002&passkey=HDPASSKEY123",
added: "2026-09-20 10:00:00",
size: 21474836480,
seeders: 12,
leechers: 0,
times_completed: 30,
sp_state: "6",
},
"30003": {
group_id: 9003,
category: "406",
name: "Some.Album.2024.FLAC",
small_descr: "",
url: "https://www.haidan.video/download.php?id=30003&passkey=HDPASSKEY123",
added: 1726790400,
size: 1073741824,
seeders: 5,
leechers: 1,
times_completed: 2,
sp_state: "1",
},
},
};
export const HAIDAN_SEARCH_ERROR = { code: 1, msg: "请先登录" };
@@ -0,0 +1,53 @@
/** HDDolby `/api/v1/torrent/search` 响应样例(自行编写,结构按 MP hddolby.py 注释示例)。 */
export const HDDOLBY_SEARCH_OK = {
data: [
{
id: 120202,
promotion_time_type: 2,
promotion_until: "2026-09-30 12:00:00",
category: 402,
leechers: 0,
seeders: 1,
name: "[DBY] Lost S06 2010 Complete 1080p Netflix WEB-DL AVC DDP5.1-DBTV",
small_descr: "lost ",
times_completed: 0,
size: 33665425886,
added: "2025-02-18 19:47:56",
url: 0,
hr: 1,
downhash: "DOWNHASHSECRET",
tags: "gf;zz;xx",
},
{
id: 120203,
promotion_time_type: 6,
promotion_until: "0000-00-00 00:00:00",
category: 401,
leechers: 2,
seeders: 40,
name: "Movie.2024.2160p.WEB-DL.DV.HDR.H.265-DBTV",
small_descr: "电影",
times_completed: 99,
size: 12345678901,
added: "2026-09-01 08:00:00",
hr: 0,
downhash: "DOWNHASHSECRET2",
tags: "",
},
{
id: 120204,
promotion_time_type: 0,
category: 406,
name: "Concert.2023.1080p",
size: 1000,
seeders: 1,
leechers: 0,
times_completed: 0,
added: "2026-09-01 08:00:00",
downhash: "X",
tags: null,
},
],
};
export const HDDOLBY_SEARCH_ERROR = { error: { message: "invalid api key" } };
@@ -0,0 +1,7 @@
export * from "./haidan.ts";
export * from "./hddolby.ts";
export * from "./rousi.ts";
export * from "./sunnypt.ts";
export * from "./tnode.ts";
export * from "./torrentleech.ts";
export * from "./yema.ts";
@@ -0,0 +1,45 @@
/** Rousi.pro `/api/v1/torrents` 响应样例(自行编写,字段名按 MP rousi.py)。 */
export const ROUSI_SEARCH_OK = {
code: 0,
data: {
torrents: [
{
id: 501,
title: "The.Bear.S03.2160p.DSNP.WEB-DL.DDP5.1.HDR.H.265",
subtitle: "熊家餐馆 第三季",
category: { slug: "tv", name: "TV" },
size: "50000000000",
seeders: 20,
leechers: 2,
downloads: 77,
created_at: "2026-09-20T02:00:00Z",
promotion: { is_active: true, down_multiplier: 0, up_multiplier: 2, until: "2026-09-28T00:00:00+08:00" },
},
{
id: 502,
title: "Oppenheimer.2023.1080p.BluRay.x264",
subtitle: "奥本海默",
category: "movie",
size: 15000000000,
seeders: 50,
leechers: 0,
downloads: 300,
created_at: "2026-09-19 10:00:00",
promotion: { is_active: false, down_multiplier: 0.5 },
},
{
id: 503,
title: "Variety.Show.E01",
category: { slug: "variety" },
size: 1,
seeders: 1,
leechers: 0,
downloads: 0,
created_at: "2026-09-19 10:00:00",
},
],
},
};
export const ROUSI_SEARCH_ERROR = { code: 403, message: "permission denied: torrent:read" };
export const ROUSI_DETAIL_OK = { code: 0, data: { download_url: "https://dl.rousi.invalid/t/501?cap=CAPSECRET" } };
@@ -0,0 +1,55 @@
/** SunnyPT MoviePilot API 响应样例(自行编写,字段名按 MP sunnypt.py)。 */
export const SUNNYPT_CATEGORIES_OK = {
code: 0,
data: [
{ id: 401, name: "电影", media_types: ["movie"] },
{ id: 402, name: "剧集", media_types: ["tv"] },
{ id: 405, name: "动漫", media_types: ["movie", "tv"] },
{ id: 406, name: "音乐", media_types: ["music"] },
],
};
export const SUNNYPT_SEARCH_OK = {
code: "0",
data: {
items: [
{
id: 7001,
title: "Arcane.S02.2160p.NF.WEB-DL.DDP5.1.Atmos.DV.H.265",
subtitle: "英雄联盟:双城之战 第二季",
media_type: "tv",
category_id: 402,
promotion: { is_active: true, down_multiplier: 0, up_multiplier: 1, until: "2026-09-30 23:59:59" },
created_at: "2026-09-18 12:00:00",
size: 60000000000,
seeders: 30,
leechers: 1,
completed: 150,
details_url: "https://sunnypt.top/details.php?id=7001",
imdb_id: "tt11126994",
tags: ["官方", "中字"],
hit_and_run: true,
},
{
id: 7002,
title: "Inception.2010.1080p.BluRay",
media_type: "movie",
category_id: 401,
promotion: { is_active: false, down_multiplier: 0 },
created_at: "2026-09-18 12:00:00",
size: "9000000000",
seeders: 3,
leechers: 0,
completed: 10,
details_url: "https://sunnypt.top/details.php?id=7002",
hit_and_run: false,
},
],
},
};
export const SUNNYPT_SEARCH_ERROR = { code: 40101, msg: "API Key 无效" };
export const SUNNYPT_DOWNLOAD_TOKEN_OK = {
code: "0",
data: { download_url: "/api/v1/mp/torrents/7001/download?token=TOK" },
};
@@ -0,0 +1,37 @@
/** TNode 首页(含 CSRF meta)与 `api/torrent/advancedSearch` 响应样例(自行编写)。 */
export const TNODE_HOME_HTML = `<!DOCTYPE html><html><head><meta name="x-csrf-token" content="CSRF-TOKEN-XYZ"><title>TNode</title></head><body>ok</body></html>`;
export const TNODE_HOME_NO_TOKEN = `<!DOCTYPE html><html><head><title>登录</title></head><body><form><input type="password"></form></body></html>`;
export const TNODE_SEARCH_OK = {
status: 200,
data: {
total: 2,
torrents: [
{
id: 3301,
title: "Evangelion.1.11.2007.1080p.BluRay.x264.FLAC",
subtitle: "新世纪福音战士新剧场版:序",
upload_time: 1726790400,
size: 9876543210,
seeding: 15,
leeching: 1,
complete: 60,
downloadRate: 0,
uploadRate: 1,
imdb: "tt0923811",
},
{
id: 3302,
title: "Mushishi.S01.1080p.BluRay",
subtitle: "虫师",
upload_time: 1726794000,
size: 40000000000,
seeding: 8,
leeching: 0,
complete: 20,
downloadRate: 0.5,
uploadRate: 2,
},
],
},
};
@@ -0,0 +1,31 @@
/** TorrentLeech `torrents/browse/list/...` 响应样例(自行编写,字段名按 MP torrentleech.py)。 */
export const TORRENTLEECH_SEARCH_OK = {
numFound: 2,
torrentList: [
{
fid: 240001,
filename: "The.Matrix.1999.2160p.UHD.BluRay.x265-TL.torrent",
name: "The.Matrix.1999.2160p.UHD.BluRay.x265-TL",
addedTimestamp: "2026-09-20 08:00:00",
size: 25000000000,
seeders: 120,
leechers: 4,
completed: 999,
download_multiplier: 0,
imdbID: "tt0133093",
categoryID: 47,
},
{
fid: 240002,
filename: "Show.S01E01.1080p.WEB.h264.torrent",
name: "Show.S01E01.1080p.WEB.h264",
addedTimestamp: "2026-09-20 09:00:00",
size: 2000000000,
seeders: 30,
leechers: 1,
completed: 100,
download_multiplier: 1,
categoryID: 32,
},
],
};
@@ -0,0 +1,46 @@
/** YemaPT 开放 API 响应样例(自行编写,字段名按 MP yema.py)。 */
export const YEMA_SEARCH_MOVIE = {
success: true,
data: [
{
id: 88001,
showName: "Past.Lives.2023.1080p.WEB-DL.DDP5.1.H.264",
shortDesc: "过往人生",
categoryId: 4,
tagList: ["3", "6", "99"],
listingTime: "2026-09-21 09:30:00",
fileSize: 8000000000,
seedNum: 25,
leechNum: 2,
completedNum: 80,
downloadPromotion: "free",
uploadPromotion: "double_upload",
downloadPromotionEndTime: "2026-09-29 00:00:00",
hrPunishEnable: true,
},
{ id: "bad-id", showName: "非整数 ID 被跳过" },
],
};
export const YEMA_SEARCH_TV_A = {
success: true,
data: [
{
id: 88002,
showName: "Show.S01.1080p",
categoryId: 5,
tagList: [],
listingTime: "2026-09-21 09:30:00",
fileSize: 1,
seedNum: 1,
leechNum: 0,
completedNum: 0,
downloadPromotion: "half",
uploadPromotion: "one_half",
hrPunishEnable: false,
},
],
};
export const YEMA_SEARCH_ERROR = { success: false, errorMessage: "AuthKey 无效" };
export const YEMA_DOWNLOAD_KEY_OK = { success: true, data: "DLKEY-123" };
@@ -0,0 +1,24 @@
/**
* 极速之星(BitPT)用户信息 fixture(自行编写,不代表真实页面)。
* 对应解析器:adapters/src/sites/user-parsers/bitpt.ts(MP bitpt.py)。
*/
export const BITPT_USER_INDEX = `<html><body><div id="userinfotop"><a href="userdetails.php?uid=55">gina</a></div></body></html>`;
export const BITPT_USER_DETAILS = `<html><body><table class="frmtable">
<tr><td>UID</td><td>55</td></tr>
<tr><td>用户名</td><td>gina (在线)</td></tr>
<tr><td>用户级别</td><td>Elite</td></tr>
<tr><td>注册时间</td><td>2021-06-07 08:09:10</td></tr>
<tr><td>上传流量</td><td>3.00 TB</td></tr>
<tr><td>下载流量</td><td>1.00 TB</td></tr>
<tr><td>共享率</td><td>3.000</td></tr>
<tr><td>星辰</td><td>当前100 累计2048.5</td></tr>
</table></body></html>`;
export const BITPT_USER_SEEDING_P1 = `<table class="torrenttable"><tr><td>标题</td><td class="r">大小</td></tr>
<tr class="btr"><td>A</td><td class="r"><a>5.00 GB</a></td></tr>
<tr class="btr"><td>B</td><td class="r">1.00 GB</td></tr>
<tr class="other"><td>C</td><td class="r">9.00 GB</td></tr>
</table>`;
export const BITPT_USER_SEEDING_EMPTY = `<table class="torrenttable"><tr><td>标题</td></tr></table>`;
@@ -0,0 +1,21 @@
/**
* Discuz!X PT 插件用户信息 fixture(自行编写,不代表真实页面)。
* 对应解析器:adapters/src/sites/user-parsers/discuz.ts(MP discuz.py)。
*/
export const DISCUZ_USER_INDEX = `<html><body>
<a href="home.php?mod=space&amp;uid=42">frank<img src="a.png"/></a>
<a href="member.php?mod=logging&amp;action=logout">退出</a>
</body></html>`;
export const DISCUZ_USER_SPACE = `<html><body><ul>
<li><em>注册时间</em>2018-02-03 04:05</li>
<li>分享率 (1.5)</li>
<li><em>积分</em>520</li>
<li><em>上传量</em>1 TB / 2.00 TB</li>
<li><em>下载量</em>10 GB / 500.00 GB</li>
</ul>
<a href="home.php?mod=spacecp&amp;ac=usergroup">用户组</a> <a href="home.php?mod=spacecp&amp;ac=usergroup&amp;gid=3">Lv3</a>
</body></html>`;
export const DISCUZ_USER_SEEDING = `<table><tr><td>标题</td><td>分类</td><td><img class="size" alt="size"/></td><td><img class="seeders" alt="seeders"/></td></tr>
<tr><td>A</td><td>电影</td><td>8.00 GB</td><td>6</td></tr></table>`;
@@ -0,0 +1,16 @@
/** FileList 用户信息 fixture(自行编写的最小结构,不代表真实站点;对应 adapters/src/sites/user-parsers/file-list.ts)。 */
export const FILELIST_USER_INDEX = `<html><body><a href="userdetails.php?id=555">erin</a> <a href="logout.php">Logout</a></body></html>`;
export const FILELIST_USER_DETAIL = `<html><body><table>
<tr><td>Uploaded</td><td>3.00 TB</td></tr>
<tr><td>Downloaded</td><td>1.00 TB</td></tr>
<tr><td>Share ratio</td><td>3.00</td></tr>
<tr><td>Seed bonus</td><td><b>Seeding:</b><span>42</span><b>Size:</b><span>2.50 TB</span></td></tr>
<tr><td>Class</td><td> Elite User </td></tr>
<tr><td>Join date</td><td>2017-07-08 09:10:11 (8 years ago)</td></tr>
</table><a href="shop.php">1,000.5</a></body></html>`;
export const FILELIST_USER_SEEDING = `<html><body><table>
<tr><td>c1</td><td>c2</td><td>c3</td><td>c4</td><td>c5</td><td>Size</td><td>Seeders</td></tr>
<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>1.00 GB</td><td>4</td></tr>
</table></body></html>`;
@@ -0,0 +1,21 @@
/** Gazelle 用户信息 fixture(自行编写的最小结构,不代表真实站点;对应 adapters/src/sites/user-parsers/gazelle.ts)。 */
export const GAZELLE_USER_INDEX = `<html><body><div id="userinfo"><ul>
<li id="nav_userinfo"><a href="user.php?id=4242" class="username">bob</a></li>
<li><a href="logout.php?auth=x">Logout</a></li>
<li id="stats_seeding">Up: <span>120.50 GB</span></li>
<li id="stats_leeching">Down: <span>40.25 GB</span></li>
<li><a href="bonus.php" data-tooltip="Bonus points: 1,234.5">Bonus</a></li>
</ul></div></body></html>`;
export const GAZELLE_USER_DETAIL = `<html><body><div class="box_userinfo_stats"><ul>
<li>用户等级: Power User</li>
<li>加入时间 <span>2019-05-06 07:08:09</span></li>
</ul></div></body></html>`;
export const GAZELLE_USER_SEEDING = `<html><body><table id="torrent_table">
<tr><td>Name</td><td>Files</td><td>Time</td><td>Size</td><td>Snatches</td><td>Seeders</td><td>Leechers</td></tr>
<tr><td>A</td><td>1</td><td>1d</td><td>10.00 GB</td><td>3</td><td>7</td><td>0</td></tr>
<tr><td>B</td><td>2</td><td>2d</td><td>1.00 GB</td><td>1</td><td>2</td><td>0</td></tr>
</table></body></html>`;
export const GAZELLE_LOGIN_PAGE = `<html><body><form action="login.php"><input type="password" name="password"/></form></body></html>`;
@@ -0,0 +1,26 @@
/** HDDolby 用户信息 fixture(自行编写的最小结构,不代表真实站点;对应 adapters/src/sites/user-parsers/hddolby.ts)。 */
export const HDDOLBY_USER_DATA = {
status: 0,
data: [
{
id: "1",
username: "ivy",
added: "2019-03-03 15:30:36",
class: "6",
uploaded: "852071699418",
downloaded: "1885536536",
seedbonus: "99774.5",
unread_messages: "2",
},
],
};
export const HDDOLBY_USER_PEERS = {
status: 0,
data: [
{ size: 1000, seeders: 4 },
{ size: 500, seeders: 0 },
],
};
export const HDDOLBY_USER_ERROR = { status: 1, error: { message: "invalid api key" } };
@@ -0,0 +1,20 @@
export * from "./bitpt.ts";
export * from "./discuz.ts";
export * from "./file-list.ts";
export * from "./gazelle.ts";
export * from "./hddolby.ts";
export * from "./ipt.ts";
export * from "./mtorrent.ts";
export * from "./nexus-audiences.ts";
export * from "./nexus-hhanclub.ts";
export * from "./nexus-php.ts";
export * from "./nexus-project.ts";
export * from "./nexus-rabbit.ts";
export * from "./rousi.ts";
export * from "./small-horse.ts";
export * from "./sunnypt.ts";
export * from "./tnode.ts";
export * from "./torrent-leech.ts";
export * from "./unit3d.ts";
export * from "./yema.ts";
export * from "./zhixing.ts";
@@ -0,0 +1,12 @@
/** IPTorrents 用户信息 fixture(自行编写的最小结构,不代表真实站点;对应 adapters/src/sites/user-parsers/ipt.ts)。 */
export const IPT_USER_INDEX = `<html><body><a href="/logout.php">Logout</a>
<a href="/u/777"><b>dave</b></a>
<div class="stats"><div><div>
<span>2.500</span><span>1.20 TB</span><span>480.00 GB</span>
<a href="/a0">x</a><a href="/a1">y</a><a href="/peers?u=777">15<i></i>3</a><a href="/bonus">4321.5</a>
</div></div></div></body></html>`;
export const IPT_USER_DETAIL = `<html><body><table>
<tr><th>Class</th><td>Power User</td></tr>
<tr><th>Join date</th><td>2018-01-02 03:04:05 (7 years ago)</td></tr>
</table></body></html>`;
@@ -0,0 +1,34 @@
/** M-Team 用户信息 fixture(自行编写的最小结构,不代表真实站点;对应 adapters/src/sites/user-parsers/mtorrent.ts)。 */
export const MTORRENT_USER_PROFILE = {
code: "0",
message: "SUCCESS",
data: {
id: "31337",
username: "henry",
role: "6",
memberStatus: { createdDate: "2020-10-11 12:13:14" },
memberCount: { uploaded: "3000", downloaded: "1000", shareRate: "3.000", bonus: "4567.8" },
},
};
export const MTORRENT_USER_SEEDING_PAGE1 = {
code: "0",
data: { data: [{ torrent: { size: "100", source: "5" } }, { torrent: { size: "200", source: "2" } }] },
};
export const MTORRENT_USER_SEEDING_PAGE2 = {
code: "0",
data: { data: [{ torrent: { size: "300", source: "1" } }] },
};
export const MTORRENT_USER_PEER_STATUS = { code: "0", data: { seeder: "3", leecher: "0" } };
export const MTORRENT_USER_MESSAGES = {
code: "0",
data: {
data: [
{ id: "m1", unread: true, title: "欢迎", createdDate: "2026-09-01 10:00:00", context: "hello" },
{ id: "m2", unread: false, title: "旧消息", createdDate: "2026-08-01 10:00:00", context: "old" },
],
},
};
export const MTORRENT_USER_KEY_INVALID = { code: "1", message: "key無效" };
@@ -0,0 +1,31 @@
/**
* 观众(Audiences)新版 userbar 用户信息 fixture(自行编写的最小结构,不代表真实页面)。
* 对应解析器:adapters/src/sites/user-parsers/nexus-audiences.ts(MP nexus_audiences.py)。
*/
export const AUDIENCES_USER_INDEX = `<html><body><div class="site-userbar">
<a class="site-userbar__user" href="userdetails.php?id=233" data-uploader-url="userdetails.php?id=233" data-uploader-label="bob" data-uploader-badge="Elite User" data-uploader-stats='[{"label":"上传量:","tone":"uploaded","value":"2.00 TB"},{"label":"下载量","tone":"downloaded","value":"100.00 GB"},{"label":"爆米花","tone":"bonus","value":"8,888.5"},{"label":"活跃","tone":"active","value":"↑ 30 / ↓ 1"}]'>bob</a>
<span class="site-userbar__compact-metric site-userbar__compact-metric--ratio"><span>分享率</span><span>20.48</span></span>
<a class="site-userbar__compact-tool site-userbar__compact-tool--has-unread" href="messages.php" title="收件箱 1749/2">信</a>
<a href="logout.php">退出</a>
</div></body></html>`;
export const AUDIENCES_USER_DETAILS = `<html><body><table>
<tr><td class="rowhead">加入日期</td><td class="rowfollow">2020-05-06 07:08:09 (5年前)</td></tr>
</table></body></html>`;
export const AUDIENCES_USER_SEEDING = `<table class="table table-bordered"><tr><td>类型</td><td>数量</td><td>体积</td></tr>
<tr><td>电影</td><td>20</td><td>2.00 TB</td></tr>
<tr><td>Total</td><td>31</td><td>3.50 TB</td></tr></table>`;
export const AUDIENCES_USER_MAILBOX = `<html><body><div class="pm-list">
<div class="pm-item-row is-unread"><a href="messageviewer.php?viewmessage=1&id=901">看</a>
<span class="pm-item__subject">种子被删除</span><span class="pm-item__time">2026-09-01 10:00:00</span><span class="pm-item__preview">你的种子 X 已被删除</span></div>
<div class="pm-item-row is-unread"><a href="messageviewer.php?viewmessage=1&id=902">看</a>
<span class="pm-item__subject">欢迎</span><span class="pm-item__time">2026-09-02 11:00:00</span><span class="pm-item__preview">欢迎加入观众</span></div>
<div class="pm-item-row"><a href="messageviewer.php?viewmessage=1&id=800">已读</a></div>
</div><a href="logout.php">退出</a></body></html>`;
export const AUDIENCES_USER_MESSAGE_901 = `<html><body><div class="pm-view">
<h2 class="pm-hero__title">种子被删除</h2>
<div class="pm-view__meta"><span class="pm-view__label">日期</span><span class="pm-view__value">2026-09-01 10:00:00</span></div>
<div class="pm-view__body">你的种子 X 因重复已被删除。</div></div></body></html>`;
@@ -0,0 +1,23 @@
/**
* 憨憨(HHClub)用户信息 fixture(自行编写的最小结构,仿 #user-info-panel 布局;不代表真实页面)。
* 对应解析器:adapters/src/sites/user-parsers/nexus-hhanclub.ts(MP nexus_hhanclub.py)。
*/
export const HHCLUB_USER_INDEX = `<html><body>
<a href="userdetails.php?id=4567"><b class="Power_Name">hana</b></a> <a href="logout.php">登出</a>
<div id="user-info-panel">
<div>hana</div>
<div>
<div><div><div>[分享率]: 3.5</div></div></div>
<div><div>a</div><div>b</div><div>c</div><div>[上传量]: 1.20 TB</div><div>[下载量]: 300.00 GB</div></div>
</div>
</div>
<a href="mybonus.php">魔力值: 6,789.0</a>
</body></html>`;
export const HHCLUB_USER_DETAILS = `<html><body>
<div><span>加入日期</span><span><span title="2022-02-02 02:02:02">3 年前</span></span></div>
<b class="Power_Name">Power User</b>
</body></html>`;
/** 已登录但 #user-info-panel 缺失(改版):MP 在 xpath(...)[0] 抛 IndexError */
export const HHCLUB_USER_INDEX_NO_PANEL = `<html><body><a href="userdetails.php?id=4567">hana</a> <a href="logout.php">登出</a></body></html>`;
@@ -0,0 +1,29 @@
/**
* NexusPHP 用户信息页 fixture(自行编写的最小结构,仿 NexusPHP 1.x 默认模板;不代表任何真实站点)。
* 对应解析器:adapters/src/sites/user-parsers/nexus-php.ts(MP nexus_php.py)。
*/
export const NEXUS_PHP_INDEX = `<!DOCTYPE html><html><head><title>Demo NexusPHP</title></head><body>
<table id="info_block"><tr><td><span class="medium">欢迎回来, <a href="userdetails.php?id=10086" class="User_Name"><b>alice</b></a>
[<a href="logout.php">退出</a>] <a href="mybonus.php">[使用]</a>: 12,345.6
<font class="color_ratio">分享率:</font> 3.217
<font class="color_uploaded">上传量:</font> 1.50 TB
<font class="color_downloaded">下载量:</font> 477.30 GB
<font class="color_active">当前活动:</font><img class="arrowup" alt="Torrents seeding" />12 <img class="arrowdown" alt="Torrents leeching" />2
</span></td>
<td><a href="messages.php"><img alt="inbox" /></a> 信息箱 3</td></tr></table>
</body></html>`;
export const NEXUS_PHP_USERDETAILS = `<html><body><table>
<tr><td class="rowhead">加入日期</td><td class="rowfollow">2021-03-04 05:06:07 (4年前)</td></tr>
<tr><td class="rowhead">等级</td><td class="rowfollow"><img alt="Veteran User" title="Veteran User" src="pic/veteran.gif"/></td></tr>
<tr><td class="rowhead">魔力值</td><td class="rowfollow">12345.6</td></tr>
</table></body></html>`;
export const NEXUS_PHP_SEEDING = `<table class="torrents" border="1"><tr><td class="colhead">类型</td><td class="colhead">标题</td><td class="colhead"><img class="size" src="pic/trans.gif" alt="size" /></td><td class="colhead"><img class="seeders" src="pic/trans.gif" alt="seeders" /></td></tr>
<tr><td>电影</td><td><a href="details.php?id=1">A</a></td><td>10.00 GB</td><td><b><a href="#">5</a></b></td></tr>
<tr><td>剧集</td><td><a href="details.php?id=2">B</a></td><td>1.5 TB</td><td><b><a href="#">12</a></b></td></tr>
</table>`;
export const NEXUS_PHP_LOGIN_PAGE = `<html><body><form action="takelogin.php" method="post"><input name="username"/><input type="password" name="password"/></form></body></html>`;
export const NEXUS_PHP_CF_CHALLENGE = `<html><head><title>Just a moment...</title></head><body><div id="cf-please-wait"></div></body></html>`;
@@ -0,0 +1,8 @@
/**
* NexusProject 变种(star-space 风格 p_user/user_detail.php?uid=)用户信息 fixture(自行编写,不代表真实页面)。
* 对应解析器:adapters/src/sites/user-parsers/nexus-project.ts(MP nexus_project.py)。
*/
export const NEXUS_PROJECT_USER_INDEX = `<html><body>
<a href="p_user/user_detail.php?uid=77"><b>carol</b></a> <a href="logout.php">退出</a>
<span id="user_info">上传:445.11 G</span><span id="user_info_no_hover">下载:29.61 G</span>
</body></html>`;
@@ -0,0 +1,23 @@
/**
* NexusRabbit(layui-hares 模板)用户信息 fixture(自行编写,不代表真实页面)。
* 对应解析器:adapters/src/sites/user-parsers/nexus-rabbit.ts(MP nexus_rabbit.py)。
*/
export const RABBIT_USER_INDEX = `<html><body>
<a href="user.php?id=5">dave</a> <a href="logout.php">退出</a>
<div class="bonus"><div>奶糖余额</div><div> 1,024.5 </div></div>
</body></html>`;
export const RABBIT_USER_DETAILS = `<html><body><div class="layui-hares-user-info-right">
<span>用户名:<a href="user.php?id=5"><span> dave </span></a></span>
<span>等级:<b>Power User</b></span>
<span>注册日期:2022-01-02 03:04:05</span>
<span>上传量:1.50 TB</span><span>下载量:500.00 GB</span>
<span>分享率:<em>3.072</em></span>
</div></body></html>`;
export const RABBIT_USER_SEEDING_JSON = {
data: [
{ seeders: "3", size: "1.00 GB" },
{ seeders: 7, size: "2.50 GB" },
],
};
@@ -0,0 +1,23 @@
/** Rousi.pro 用户信息 fixture(自行编写的最小结构,不代表真实站点;对应 adapters/src/sites/user-parsers/rousi.ts)。 */
export const ROUSI_USER_PROFILE = {
code: 0,
message: "success",
data: {
id: 1,
username: "jack",
level_text: "Lv.5",
registered_at: "2024-01-01T00:00:00Z",
uploaded: 1073741824,
downloaded: 536870912,
ratio: 2.0,
karma: 1000.5,
seeding_leeching_data: {
seeding_count: 10,
seeding_size: 10737418240,
leeching_count: 2,
leeching_size: 2147483648,
},
},
};
export const ROUSI_USER_ERROR = { code: 401, message: "API Key 无效" };
@@ -0,0 +1,29 @@
/**
* Small Horse(Gazelle 变种)用户信息 fixture(自行编写,不代表真实页面)。
* 对应解析器:adapters/src/sites/user-parsers/small-horse.ts(MP small_horse.py)。
*/
export const SMALL_HORSE_USER_INDEX = `<html><body><a href="user.php?id=9">eve</a> <a href="logout.php?auth=x">Logout</a></body></html>`;
export const SMALL_HORSE_USER_PAGE = `<html><body>
<ul class="stats nobullet"><li>Profile</li></ul>
<ul class="stats nobullet">
<li>Joined: <span title="x">2019-01-01 00:00:00</span></li>
<li>Last seen: now</li>
<li>Uploaded: 1.00 TB</li>
<li>Downloaded: 200.00 GB</li>
<li>Ratio: <span class="r20">5.12</span></li>
<li>Bonus: 1234.5</li>
</ul>
<ul class="stats nobullet"><li>Percentile</li></ul>
<ul class="stats nobullet"><li>Class: Power User</li></ul>
<ul class="stats nobullet"><li>a</li><li>b</li><li>c</li><li>d</li><li>e</li><li>f</li><li>Leeching: 3</li></ul>
</body></html>`;
const row = (size: string, seeders: number) =>
`<tr><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>${size}</td><td>7</td><td>${seeders}</td></tr>`;
export const SMALL_HORSE_USER_SEEDING_P1 = `<table id="torrent_table"><tr><td>h</td></tr>${row("10.00 GB", 4)}${row("5.00 GB", 2)}</table>
<ul class="pagination"><li class="active">1</li><li>2</li><li>Next</li></ul>`;
export const SMALL_HORSE_USER_SEEDING_P2 = `<table id="torrent_table"><tr><td>h</td></tr>${row("1.00 GB", 9)}</table>
<ul class="pagination"><li>1</li><li class="active">2</li></ul>`;
@@ -0,0 +1,31 @@
/** SunnyPT 用户信息 fixture(自行编写的最小结构,不代表真实站点;对应 adapters/src/sites/user-parsers/sunnypt.ts)。 */
export const SUNNYPT_USER_PROFILE = {
code: 0,
msg: "ok",
data: {
id: 12,
username: "kate",
level: "Elite User",
registered_at: "2023-04-05 06:07:08",
uploaded: 4000,
downloaded: 2000,
ratio: 2,
bonus: 88.8,
seeding_count: 7,
seeding_size: 7000,
leeching_count: 1,
leeching_size: 100,
unread_messages: 1,
},
};
export const SUNNYPT_USER_MESSAGES = {
code: 0,
data: {
unread_count: 1,
has_more: false,
items: [{ id: 5, unread: true, title: "通知", content: "内容", created_at: "2026-09-20 08:00:00" }],
},
};
export const SUNNYPT_USER_ERROR = { code: 403, msg: "API Key 无效" };
@@ -0,0 +1,28 @@
/** TNode 用户信息 fixture(自行编写的最小结构,不代表真实站点;对应 adapters/src/sites/user-parsers/tnode.ts)。 */
export const TNODE_USER_INDEX = `<html><head><meta name="x-csrf-token" content="csrf-abc"></head><body></body></html>`;
export const TNODE_USER_MAIN_INFO = {
status: 200,
data: {
id: 99,
username: "gina",
class: { name: "Elite" },
regTime: "2022-01-02 03:04:05",
upload: 2000,
download: 1000,
bonus: 12.5,
unreadAdmin: 1,
unreadInbox: 2,
unreadSystem: 0,
},
};
export const TNODE_USER_SEEDING = {
status: 200,
data: {
torrents: [
{ size: 100, seeding: 3 },
{ size: 50, seeding: 1 },
],
},
};
@@ -0,0 +1,14 @@
/** TorrentLeech 用户信息 fixture(自行编写的最小结构,不代表真实站点;对应 adapters/src/sites/user-parsers/torrent-leech.ts)。 */
export const TORRENTLEECH_USER_INDEX = `<html><body><span class="centerTopBar"><a onclick="location.href='/profile/frank/view'">frank</a></span>
<a href="/user/account/logout">Logout</a></body></html>`;
export const TORRENTLEECH_USER_PROFILE = `<html><body><div class="profile-username">frank</div>
<div class="profile-uploaded"><span>5.00 TB</span></div>
<div class="profile-downloaded"><span>1.00 TB</span></div>
<div class="profile-ratio"><span>5.00</span></div>
<table class="profileViewTable"><tr><td>Class</td><td>VIP</td></tr><tr><td>Registration date</td><td>2016-06-07 08:09:10</td></tr></table>
<span class="total-TL-points">98765.4</span></body></html>`;
export const TORRENTLEECH_USER_SEEDING = `<html><body><table><tbody>
<tr><td>A</td><td>2.00 GB</td><td>3</td><td>4</td><td>5</td><td>6</td><td>11</td></tr>
</tbody></table></body></html>`;
@@ -0,0 +1,13 @@
/** UNIT3D 用户信息 fixture(自行编写的最小结构,不代表真实站点;对应 adapters/src/sites/user-parsers/unit3d.ts)。 */
export const UNIT3D_USER_INDEX = `<html><body><nav>
<a href="https://pt.example.com/users/carol/settings">Settings</a>
<form action="https://pt.example.com/logout" method="POST"><button>Logout</button></form>
<a href="https://pt.example.com/users/carol/bonus/earnings"><i></i> 5,678.9</a>
<ul class="ratio-bar"><li>上传量: 2.00 TiB</li><li>下载量: 500.00 GiB</li><li>分享率: 4.1</li></ul>
</nav></body></html>`;
export const UNIT3D_USER_DETAIL = `<html><body><div class="content"><span class="badge-user">Elite</span>
<h4>Registration date 2020-02-03 04:05:06</h4></div></body></html>`;
export const UNIT3D_USER_SEEDING = `<html><body><table><thead><tr><th class="name">Name</th><th class="seeders">Seeders</th><th class="size">Size</th></tr></thead>
<tbody><tr><td>X</td><td>9</td><td>3.00 GiB</td></tr><tr><td>Y</td><td>1</td><td>1.00 GiB</td></tr></tbody></table></body></html>`;
@@ -0,0 +1,15 @@
/** YemaPT 用户信息 fixture(自行编写的最小结构,不代表真实站点;对应 adapters/src/sites/user-parsers/yema.ts)。 */
export const YEMA_USER_BASIC = {
success: true,
data: {
id: 321,
name: "leo",
level: 3,
registerTime: "2024-05-06 07:08:09",
promotionUploadSize: 3000,
promotionDownloadSize: 1000,
bonus: 12.3,
},
};
export const YEMA_USER_ERROR = { success: false, errorMessage: "AuthKey 无效" };
@@ -0,0 +1,22 @@
/**
* 知行(Zhixing)用户信息 fixture(自行编写,不代表真实页面)。
* 对应解析器:adapters/src/sites/user-parsers/zhixing.ts(MP zhixing.py)。
*/
export const ZHIXING_USER_INDEX = `<html><body><div id="um"><a href="/user/321/">zed</a></div></body></html>`;
export const ZHIXING_USER_BASIC = `<html><body><div class="user-details-tabs">
<p>UID:321</p><p>用户名:zed</p><p>用户组:Lv2 (查看权限)</p>
<p>注册时间:2023-01-01 10:00:00 (2年前)</p>
<p>上传流量:1.5 TB</p><p>下载流量:123456</p>
<p>共享率:12.5</p><p>保种积分:88.8</p>
<p>当前保种数量:7</p><p>当前保种容量:70 GB</p>
</div></body></html>`;
export const ZHIXING_USER_SEEDING_P1 = `<table>
<tr id="t1"><td>A</td><td class="r"><a>10 GB</a></td></tr>
<tr id="t2"><td>B</td><td class="r">20 GB</td></tr>
</table>`;
export const ZHIXING_USER_SEEDING_EMPTY = `<table><tr><td>没有种子</td></tr></table>`;
export const ZHIXING_LOGIN_PAGE = `<html><body><form><input type="password"/></form></body></html>`;
+15 -5
View File
@@ -1,10 +1,20 @@
export * from "./fixtures/definitions/index.ts";
export * from "./fixtures/spiders/index.ts";
export * from "./fixtures/user-parsers/index.ts";
export {
createMockFetch,
type MockFetch,
type MockFetchRequest,
type MockRouteHandler,
type MockRouteResponse,
} from "./mock-fetch.ts";
export {
createMockMteamApi,
type MockMteamApi,
type MockMteamFailMode,
type MockMteamOptions,
} from "./mock-mteam";
export { createMockNexusPhpSite, type MockNexusPhpOptions, type MockNexusPhpSite } from "./mock-nexusphp";
export { type MockMteamTorrent, MTEAM_FIXTURE_TORRENTS } from "./mteam-fixtures";
export { AUDIENCES_FIXTURE_RELEASES, type MockNexusPromo, type MockNexusRelease } from "./nexusphp-fixtures";
export { buildFakeTorrent, type FakeTorrent } from "./torrent";
} from "./mock-mteam.ts";
export { createMockNexusPhpSite, type MockNexusPhpOptions, type MockNexusPhpSite } from "./mock-nexusphp.ts";
export { type MockMteamTorrent, MTEAM_FIXTURE_TORRENTS } from "./mteam-fixtures.ts";
export { AUDIENCES_FIXTURE_RELEASES, type MockNexusPromo, type MockNexusRelease } from "./nexusphp-fixtures.ts";
export { buildFakeTorrent, type FakeTorrent } from "./torrent.ts";
+97
View File
@@ -0,0 +1,97 @@
/**
* 路由表式 fetch 替身(站点用户信息解析器 / API spider 测试用)。
* 路由键:`METHOD path?query`(query 可省略,省略时匹配任意 query;METHOD 省略为任意方法);
* 值为响应描述或函数。未命中返回 404。记录全部请求(含头与 body)供断言。
*/
export interface MockRouteResponse {
status?: number;
body?: string | object;
contentType?: string;
headers?: Record<string, string>;
}
export type MockRouteHandler = MockRouteResponse | ((req: MockFetchRequest) => MockRouteResponse);
export interface MockFetchRequest {
method: string;
url: URL;
headers: Record<string, string>;
body: string | null;
}
export interface MockFetch {
fetch: typeof globalThis.fetch;
requests: MockFetchRequest[];
/** 追加/覆盖路由 */
route(key: string, handler: MockRouteHandler): void;
}
function match(key: string, req: MockFetchRequest): boolean {
let method: string | null = null;
let target = key;
const sp = key.indexOf(" ");
if (sp > 0 && /^[A-Z]+$/.test(key.slice(0, sp))) {
method = key.slice(0, sp);
target = key.slice(sp + 1);
}
if (method && method !== req.method) return false;
// 可带主机:"https://api.x.com/path"
let host: string | null = null;
if (/^https?:\/\//.test(target)) {
const u = new URL(target);
host = u.host;
target = u.pathname + u.search;
}
if (host && host !== req.url.host) return false;
const [path, query] = target.split("?");
if (path !== req.url.pathname) return false;
if (query === undefined) return true;
const want = new URLSearchParams(query);
for (const [k, v] of want) if (req.url.searchParams.get(k) !== v) return false;
return true;
}
export function createMockFetch(routes: Record<string, MockRouteHandler> = {}): MockFetch {
const table = new Map<string, MockRouteHandler>(Object.entries(routes));
const requests: MockFetchRequest[] = [];
const fetchImpl = async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
const headers: Record<string, string> = {};
new Headers(init?.headers).forEach((v, k) => {
headers[k] = v;
});
const req: MockFetchRequest = {
method: (init?.method ?? "GET").toUpperCase(),
url,
headers,
body: typeof init?.body === "string" ? init.body : null,
};
requests.push(req);
// 后加入的路由优先,便于测试覆盖默认路由;同时更具体(带 query)的键优先
const keys = [...table.keys()].reverse().sort((a, b) => Number(b.includes("?")) - Number(a.includes("?")));
for (const key of keys) {
if (!match(key, req)) continue;
const h = table.get(key)!;
const r = typeof h === "function" ? h(req) : h;
const isJson = r.body !== undefined && typeof r.body !== "string";
const body = r.body === undefined ? "" : isJson ? JSON.stringify(r.body) : (r.body as string);
return new Response(body, {
status: r.status ?? 200,
headers: {
"content-type": r.contentType ?? (isJson ? "application/json" : "text/html; charset=utf-8"),
...(r.headers ?? {}),
},
});
}
return new Response("not found", { status: 404 });
};
return {
fetch: fetchImpl as typeof globalThis.fetch,
requests,
route(key, handler) {
table.delete(key);
table.set(key, handler);
},
};
}
+3 -3
View File
@@ -1,8 +1,8 @@
import { randomBytes } from "node:crypto";
import type { ServerResponse } from "node:http";
import { type StartedServer, startHttpServer } from "../rss/server";
import { type MockMteamTorrent, MTEAM_FIXTURE_TORRENTS } from "./mteam-fixtures";
import { buildFakeTorrent } from "./torrent";
import { type StartedServer, startHttpServer } from "../rss/server.ts";
import { type MockMteamTorrent, MTEAM_FIXTURE_TORRENTS } from "./mteam-fixtures.ts";
import { buildFakeTorrent } from "./torrent.ts";
export type MockMteamFailMode = "banned_key" | "rate_limit" | "http_429" | "non_json" | "no_dl_token";
+3 -3
View File
@@ -1,7 +1,7 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { type StartedServer, startHttpServer } from "../rss/server";
import { AUDIENCES_FIXTURE_RELEASES, type MockNexusRelease } from "./nexusphp-fixtures";
import { buildFakeTorrent } from "./torrent";
import { type StartedServer, startHttpServer } from "../rss/server.ts";
import { AUDIENCES_FIXTURE_RELEASES, type MockNexusRelease } from "./nexusphp-fixtures.ts";
import { buildFakeTorrent } from "./torrent.ts";
export interface MockNexusPhpOptions {
/** 登录态 Cookie;请求 Cookie 头需包含它 */