Files
blueslmjandClaude Opus 5 0e85433a5d feat: 多站点磁力搜索工具(网页版 + 命令行)
聚合 TPB(apibay) / therarbg / knaben / EZTV 四个站点的 JSON API,
按 infohash 跨站去重,seeders 取各站最大值,0 种子明确标出。

绕过的几个实测问题:
- 站点关键词搜索按整词匹配,"s03" 搜不到(标题里是 S03E07),
  裸季号自动展开成 s03e01..e30 逐集补搜(TPB 实测 0 条 -> 193 条)
- 放宽查询会撞分页天花板,所以三条路互补:knaben 用原查询、
  TPB 用逐集补搜、EZTV 用 IMDb id 列出全剧
- IMDb id 必须从未过滤的结果里推断,只看命中项的话季号一过滤就没了
- therarbg 的 keywords: 必须用 %20 编码,用 + 多词查询返回 0 条

匹配支持智能/完全两种模式,完全匹配按「从标题开头连续对上」判定
(发布名规范是 剧名.SxxExx.质量.小组,剧名永远在开头)。
输入可直接粘发布名,pawn.stars.s24 和 pawn stars s24 等价。

网页版走 SSE 实时推进度,Windows 下双击 .bat 即可启动。
零运行时依赖,48 个测试全部不联网。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 22:42:42 +08:00

166 lines
6.6 KiB
JavaScript

'use strict';
const test = require('node:test');
const assert = require('node:assert');
const M = require('../cli/match.js');
const F = require('../cli/format.js');
const S = require('../cli/sources.js');
test('关键词按子串匹配,分隔符无关', () => {
const tokens = M.tokenize('pawn stars s03');
assert.deepStrictEqual(tokens, ['pawn', 'stars', 's03']);
assert.ok(M.matchesTokens('Pawn.Stars.S03E07.HDTV.XviD-MOMENTUM', tokens));
assert.ok(M.matchesTokens('Pawn Stars S03E26 Wise Guys 480p x264', tokens));
assert.ok(!M.matchesTokens('Pawn.Stars.S04E01.720p', tokens));
assert.ok(!M.matchesTokens('Cajun.Stars.S03E01', tokens)); // 少了 pawn
});
test('放宽查询只留片名,季集号和规格词摘掉', () => {
assert.strictEqual(M.broadQuery('pawn stars s03 1080p x264'), 'pawn stars');
assert.strictEqual(M.broadQuery('Pawn.Stars.S03E07.HDTV'), 'pawn stars');
// 全是规格词时保留原样,不然就没得搜了
assert.strictEqual(M.broadQuery('s03'), 's03');
});
test('裸季号才展开逐集查询', () => {
assert.strictEqual(M.bareSeason(M.tokenize('pawn stars s03')), 's03');
assert.strictEqual(M.bareSeason(M.tokenize('pawn stars s03e01')), '');
assert.deepStrictEqual(M.episodeQueries('pawn stars s03', 's03', 3), [
'pawn stars s03e01',
'pawn stars s03e02',
'pawn stars s03e03',
]);
});
test('同一 infohash 跨站合并:种子数取最大,来源都记下', () => {
const merged = M.mergeResults([
{ title: 'Show S03E01 720p', hash: 'AABB', seeders: 3, leechers: 1, size: 100, source: 'TPB' },
{
title: 'Show S03E01 720p x264-GRP',
hash: 'aabb',
seeders: 46,
leechers: 29,
size: 100,
magnet: 'magnet:?xt=urn:btih:aabb',
source: 'EZTV',
},
]);
assert.strictEqual(merged.length, 1);
assert.strictEqual(merged[0].seeders, 46);
assert.strictEqual(merged[0].leechers, 29);
assert.strictEqual(merged[0].seedFrom, 'EZTV');
assert.deepStrictEqual(merged[0].sources, ['TPB', 'EZTV']);
assert.strictEqual(merged[0].magnet, 'magnet:?xt=urn:btih:aabb');
assert.strictEqual(merged[0].title, 'Show S03E01 720p x264-GRP'); // 取更长的标题
});
test('没有 infohash 时按归一化标题去重', () => {
const merged = M.mergeResults([
{ title: 'Show.S03E01.720p', seeders: 1, source: 'a' },
{ title: 'show s03e01 720p', seeders: 5, source: 'b' },
]);
assert.strictEqual(merged.length, 1);
assert.strictEqual(merged[0].seeders, 5);
});
test('默认按种子数从多到少排', () => {
const list = M.sortResults(
[
{ title: 'a', seeders: 1, leechers: 0, size: 1 },
{ title: 'b', seeders: 46, leechers: 0, size: 1 },
{ title: 'c', seeders: 0, leechers: 9, size: 1 },
],
'seeds'
);
assert.deepStrictEqual(list.map((x) => x.title), ['b', 'a', 'c']);
});
test('IMDb id 取出现最多的那个,tt 前缀去掉', () => {
assert.strictEqual(
M.guessImdb([
{ imdb: 'tt1492088' },
{ imdb: 'tt1492088' },
{ imdb: 'tt0000001' },
{ imdb: '' },
{ imdb: 'nope' },
]),
'1492088'
);
assert.strictEqual(M.guessImdb([{ imdb: '' }]), '');
});
test('裸 infohash 能拼出带 tracker 的磁力', () => {
const mag = M.magnetFrom('AABBCCDD', 'Some Show S03E01');
assert.ok(mag.startsWith('magnet:?xt=urn:btih:aabbccdd'));
assert.ok(mag.includes('&dn=Some%20Show%20S03E01'));
assert.ok(mag.includes('tracker.opentrackr.org'));
});
test('体积格式化与解析能对上', () => {
assert.strictEqual(F.humanSize(0), '-');
assert.strictEqual(F.humanSize(1024), '1.0 KB');
assert.strictEqual(F.humanSize(146 * 1024 * 1024), '146 MB');
assert.strictEqual(S.parseSize('188.4 MiB'), Math.round(188.4 * 1024 * 1024));
assert.strictEqual(S.parseSize('15.9 GB'), Math.round(15.9 * 1024 ** 3));
assert.strictEqual(S.parseSize('不是体积'), 0);
});
test('中文按两格宽算,表格才不会歪', () => {
assert.strictEqual(F.dispWidth('中文'), 4);
assert.strictEqual(F.dispWidth('abc'), 3);
assert.strictEqual(F.clip('abcdefgh', 5), 'abcd…');
});
test('表格输出含表头和数据', () => {
const out = F.renderTable([
{ seeders: 46, leechers: 29, size: 146 * 1024 * 1024, sources: ['therarbg', 'EZTV'], title: 'Pawn Stars S03E06' },
]);
const lines = out.split('\n');
assert.ok(lines[0].includes('种子'));
assert.ok(lines[2].includes('Pawn Stars S03E06'));
assert.ok(lines[2].includes('therarbg+EZTV'));
});
test('CSV 带 BOM 和全部字段;txt 可选带标题', () => {
const list = [
{ seeders: 5, leechers: 1, size: 1024, sources: ['TPB'], title: 'X S03E01', magnet: 'magnet:?xt=urn:btih:ab', hash: 'ab' },
];
const csv = F.toCsv(list);
assert.ok(csv.charCodeAt(0) === 0xfeff);
assert.ok(csv.includes('"种子"'));
assert.ok(csv.includes('"magnet:?xt=urn:btih:ab"'));
assert.strictEqual(F.toTxt(list, true).trim(), 'X S03E01\tmagnet:?xt=urn:btih:ab');
assert.strictEqual(F.toTxt(list, false).trim(), 'magnet:?xt=urn:btih:ab');
// 没有磁力的不导出
assert.strictEqual(F.toTxt([{ title: 'no magnet' }], true).trim(), '');
});
test('完全匹配:查询从标题开头连续对上,不误伤中间夹关键词的剧', () => {
const t = 'Your.Friends.and.Neighbors.S01E39.1080p.x265-ELiTE';
// 智能匹配会命中(friends 是子串),完全匹配必须排除
assert.ok(M.matchesTokens(t, M.tokenize("friends s01")));
assert.strictEqual(M.matchesExact(t, M.tokenize("friends s01")), false);
});
test('完全匹配:带 the 的剧名和裸季号都能对上', () => {
assert.ok(M.matchesExact('The.Big.Bang.Theory.S01E01.1080p', M.tokenize('big bang theory')));
assert.ok(M.matchesExact('The.Walking.Dead.S01E02', M.tokenize('walking dead s01')));
assert.ok(M.matchesExact('Pawn.Stars.S03E07.HDTV', M.tokenize('pawn stars')));
});
test('完全匹配:顺序错、少词、词不连贯都该排除', () => {
assert.strictEqual(M.matchesExact('Stars.Pawn.S03E07', M.tokenize('pawn stars')), false);
assert.strictEqual(M.matchesExact('Friends.S01', M.tokenize('friends s02')), false);
assert.strictEqual(M.matchesExact('The.Big.Bang.Theory', M.tokenize('big theory')), false);
});
test('matchesAny 三种模式:smart 子串 / exact 开头连续 / loose 只要首词', () => {
const title = 'Your.Friends.and.Neighbors.S01E39';
const tokens = M.tokenize("friends s01");
assert.ok(M.matchesAny(title, tokens, "smart"));
assert.strictEqual(M.matchesAny(title, tokens, "exact"), false);
assert.ok(M.matchesAny(title, tokens, "loose"));
assert.strictEqual(M.matchesAny('Nothing.To.Do.With.Friends', M.tokenize('nothing s01'), 'loose'), true);
assert.strictEqual(M.matchesAny('Nothing', M.tokenize("'friends"), "loose"), false);
});