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,50 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createSiteRateLimiter } from "./rate-limit";
|
||||
import { parseSizeMp as parseSize } from "./spider/spider";
|
||||
import { parseCount, parseSiteTime } from "./util";
|
||||
|
||||
describe("站点令牌桶", () => {
|
||||
it("burst 内不等待,之后按间隔补充;penalize 顺延;并发按顺序排队", async () => {
|
||||
let clock = 0;
|
||||
const sleeps: number[] = [];
|
||||
const l = createSiteRateLimiter({
|
||||
minIntervalMs: 1000,
|
||||
burst: 2,
|
||||
now: () => clock,
|
||||
sleep: async (ms) => {
|
||||
sleeps.push(ms);
|
||||
clock += ms;
|
||||
},
|
||||
});
|
||||
await Promise.all([l.acquire(), l.acquire(), l.acquire()]);
|
||||
expect(sleeps).toEqual([1000]);
|
||||
l.penalize(5000);
|
||||
expect(l.tryAcquire()).toBe(false);
|
||||
await l.acquire();
|
||||
expect(sleeps.at(-1)).toBe(5000);
|
||||
});
|
||||
|
||||
it("取消等待", async () => {
|
||||
const l = createSiteRateLimiter({ minIntervalMs: 60_000 });
|
||||
await l.acquire();
|
||||
const ac = new AbortController();
|
||||
const p = l.acquire(ac.signal);
|
||||
ac.abort(new Error("cancelled"));
|
||||
await expect(p).rejects.toThrow("cancelled");
|
||||
});
|
||||
});
|
||||
|
||||
describe("解析辅助", () => {
|
||||
it("parseSize/parseCount/parseSiteTime", () => {
|
||||
expect(parseSize("78.42 GB")).toBe(84202833838);
|
||||
expect(parseSize("850 KB")).toBe(870400);
|
||||
expect(parseSize("n/a")).toBe(0);
|
||||
expect(parseSize("1,234.56 MiB")).toBe(1294529987);
|
||||
expect(parseSize("2.1 TB")).toBe(2308974418330);
|
||||
expect(parseCount("1,204")).toBe(1204);
|
||||
expect(parseCount("532 / 3")).toBe(532);
|
||||
expect(parseCount("--")).toBeNull();
|
||||
expect(parseSiteTime("2026-09-26 21:14:03", "+08:00")).toBe("2026-09-26T13:14:03.000Z");
|
||||
expect(parseSiteTime("3小时前", "+08:00")).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user