184 lines
7.3 KiB
TypeScript
184 lines
7.3 KiB
TypeScript
import type { SiteRelease } from "@umt/adapters";
|
|
import { createMockRssServer, type MockFeedItem, type MockRssServer } from "@umt/testkit";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import { ingestFeed } from "../../src/modules/sources-identity/service/feeds.ts";
|
|
import { upsertSiteRelease } from "../../src/modules/sources-identity/service/snapshot.ts";
|
|
import { listSiteReleases } from "../../src/modules/sources-identity/service/workbench.ts";
|
|
import { createHarness, type Harness, pgAvailable, seedFeed, seedSite } from "./harness.ts";
|
|
|
|
// HH-2:憨憨 RSS 条目规范化(`[分类]种子名[副标题][大小]`)、与列表快照合并、旧行补规范化、RSS 请求计数
|
|
const up = await pgAvailable();
|
|
const BASE = "https://hhanclub.example";
|
|
|
|
const item = (id: string, title: string, pubDate: string, extra: Partial<MockFeedItem> = {}): MockFeedItem => ({
|
|
guid: `hh-${id}`,
|
|
title,
|
|
link: `${BASE}/details.php?id=${id}`,
|
|
enclosureUrl: `${BASE}/download.php?id=${id}&passkey=PK`,
|
|
size: 731525231,
|
|
pubDate: new Date(pubDate),
|
|
categories: [],
|
|
...extra,
|
|
});
|
|
|
|
const snapshotRelease = (id: string, title: string, uploadedAt: string): SiteRelease => ({
|
|
siteId: "hhanclub",
|
|
siteReleaseId: id,
|
|
title,
|
|
subtitle: "快照副标题",
|
|
category: { id: "402", name: "电视剧" },
|
|
sizeBytes: 1024,
|
|
seeders: 22,
|
|
leechers: 66,
|
|
snatched: 5,
|
|
promotion: { type: "free", downloadFactor: 0, uploadFactor: 1, expiresAt: null },
|
|
hr: false,
|
|
uploadedAt,
|
|
detailUrl: `${BASE}/details.php?id=${id}`,
|
|
tags: [],
|
|
raw: {},
|
|
observedAt: new Date().toISOString(),
|
|
});
|
|
|
|
describe.skipIf(!up)("站点 RSS 条目规范化(HH-2)", () => {
|
|
let h: Harness;
|
|
let siteId: string;
|
|
const servers: MockRssServer[] = [];
|
|
|
|
beforeAll(async () => {
|
|
h = await createHarness("rssnorm");
|
|
siteId = (await seedSite(h, { key: "hhanclub", baseUrl: `${BASE}/`, cookie: "c=1" })).site.id;
|
|
});
|
|
afterAll(async () => {
|
|
for (const s of servers) await s.close();
|
|
await h?.close();
|
|
});
|
|
|
|
const feed = async (items: MockFeedItem[]) => {
|
|
const s = await createMockRssServer({ items, passkey: "PK" });
|
|
servers.push(s);
|
|
return { server: s, row: await seedFeed(h, { siteId, url: s.feedUrl, name: "憨憨 RSS" }) };
|
|
};
|
|
const rel = (id: string) =>
|
|
h.db
|
|
.selectFrom("source_release")
|
|
.selectAll()
|
|
.where("site_id", "=", siteId)
|
|
.where("site_release_id", "=", id)
|
|
.executeTakeFirstOrThrow();
|
|
|
|
it("拆出分类 / 副标题 / 干净标题;大小取 enclosure 精确值;原始标题留在 raw.rss;请求计入站点计数", async () => {
|
|
const { row } = await feed([
|
|
item(
|
|
"219390",
|
|
"[音乐]Travis - L.A. Times (2024) - WEB-DL - 24bit ALAC-HHWEB[Travis - L.A. Times | ALAC | 苹果音乐][697.64 MB]",
|
|
"2026-09-29T08:51:01Z",
|
|
),
|
|
item("219391", "[纪录片]Some Doc 2026 2160p WEB-DL-HHWEB[新][某纪录片][1.5 GB]", "2026-09-29T08:10:00Z", {
|
|
size: 0,
|
|
}),
|
|
]);
|
|
expect((await ingestFeed(h.rt, row.id)).status).toBe("ok");
|
|
const r = await rel("219390");
|
|
expect(r).toMatchObject({
|
|
title: "Travis - L.A. Times (2024) - WEB-DL - 24bit ALAC-HHWEB",
|
|
subtitle: "Travis - L.A. Times | ALAC | 苹果音乐",
|
|
category_raw: "410",
|
|
});
|
|
expect(Number(r.size_bytes)).toBe(731525231);
|
|
expect((r.raw as { rss: Record<string, unknown> }).rss).toMatchObject({
|
|
categoryName: "音乐",
|
|
mediaType: "music",
|
|
rawTitle: expect.stringContaining("[697.64 MB]"),
|
|
v: 1,
|
|
});
|
|
const doc = await rel("219391");
|
|
expect(doc).toMatchObject({ title: "Some Doc 2026 2160p WEB-DL-HHWEB", subtitle: "某纪录片", category_raw: "404" });
|
|
expect(Number(doc.size_bytes)).toBe(Math.round(1.5 * 1024 ** 3));
|
|
|
|
// 计数是异步写的,等一下
|
|
let n = 0;
|
|
for (let i = 0; i < 50 && n === 0; i++) {
|
|
n = await h.rt.siteRequestCount(siteId);
|
|
if (!n) await new Promise((res) => setTimeout(res, 20));
|
|
}
|
|
expect(n).toBe(1);
|
|
});
|
|
|
|
it("与列表快照按站点资源 ID 合并:快照补做种 / 促销 / H&R,之后 RSS 更新不再覆盖快照标题", async () => {
|
|
const { server, row } = await feed([
|
|
item("219380", "[电视剧]Show S01 2026 1080p WEB-DL-HHWEB[示例剧 | 第1集][1.2 GB]", "2026-09-29T07:00:00Z"),
|
|
]);
|
|
await ingestFeed(h.rt, row.id);
|
|
await h.db
|
|
.transaction()
|
|
.execute((trx) =>
|
|
upsertSiteRelease(
|
|
h.rt,
|
|
trx,
|
|
{ id: siteId },
|
|
snapshotRelease("219380", "Show S01 2026 1080p WEB-DL-HHWEB", "2026-09-29T07:00:00Z"),
|
|
),
|
|
);
|
|
let r = await rel("219380");
|
|
expect(r).toMatchObject({ seeders: 22, leechers: 66, subtitle: "快照副标题", category_raw: "402" });
|
|
expect((r.raw as { _site: { hr: boolean } })._site.hr).toBe(false);
|
|
expect((r.raw as { rss: { categoryName: string } }).rss.categoryName).toBe("电视剧");
|
|
|
|
server.updateItem("hh-219380", { title: "[电视剧]Show S01 2026 1080p WEB-DL-HHWEB[示例剧 | 第1-2集][2.4 GB]" });
|
|
await ingestFeed(h.rt, row.id, { manual: true });
|
|
r = await rel("219380");
|
|
expect(r.title).toBe("Show S01 2026 1080p WEB-DL-HHWEB");
|
|
expect(r.seeders).toBe(22);
|
|
});
|
|
|
|
it("规范化前写进来的旧 RSS 行:下次抓取时按原标题补规范化并重新触发识别", async () => {
|
|
const dirty = "[音乐]Old Album (2020) - WEB-DL - 16bit ALAC-HHWEB[Old Album | ALAC][120.5 MB]";
|
|
const old = await h.db
|
|
.insertInto("source_release")
|
|
.values({
|
|
site_id: siteId,
|
|
site_release_id: "219001",
|
|
title: dirty,
|
|
size_bytes: 126353408,
|
|
raw: JSON.stringify({ rss: { link: `${BASE}/details.php?id=219001`, categories: [] } }),
|
|
// 旧行:固定首见时间,别让"现在"影响下一个用例的发布时间排序(没有 source_published_at 时按首见时间排)
|
|
first_seen_at: new Date("2026-09-28T00:00:00Z"),
|
|
})
|
|
.returning(["id"])
|
|
.executeTakeFirstOrThrow();
|
|
const { row } = await feed([
|
|
item("219002", "[电影]New Movie 2026 1080p-HHWEB[新片][4 GB]", "2026-09-29T09:00:00Z"),
|
|
]);
|
|
await ingestFeed(h.rt, row.id);
|
|
const r = await rel("219001");
|
|
expect(r).toMatchObject({
|
|
title: "Old Album (2020) - WEB-DL - 16bit ALAC-HHWEB",
|
|
subtitle: "Old Album | ALAC",
|
|
category_raw: "410",
|
|
});
|
|
expect((r.raw as { rss: { rawTitle: string } }).rss.rawTitle).toBe(dirty);
|
|
const events = (await h.outbox("release.observed")).filter((e) => e.payload.sourceReleaseId === old.id);
|
|
expect(events.map((e) => e.payload.isNew)).toEqual([false]);
|
|
});
|
|
|
|
it("资源列表按发布时间排序:缺做种 / H&R 的 RSS 条目不会因此排到前面", async () => {
|
|
await h.db
|
|
.transaction()
|
|
.execute((trx) =>
|
|
upsertSiteRelease(
|
|
h.rt,
|
|
trx,
|
|
{ id: siteId },
|
|
snapshotRelease("219500", "Newest Snapshot 2026-HHWEB", "2026-09-29T10:00:00Z"),
|
|
),
|
|
);
|
|
const list = await listSiteReleases(h.rt, siteId, { limit: 50, live: "false" });
|
|
const ids = list.items.map((i) => i.siteReleaseId);
|
|
expect(ids[0]).toBe("219500");
|
|
expect(ids.indexOf("219390")).toBeLessThan(ids.indexOf("219391"));
|
|
const times = list.items.map((i) => Date.parse(i.sourcePublishedAt ?? i.firstSeenAt));
|
|
expect([...times].sort((a, b) => b - a)).toEqual(times);
|
|
});
|
|
});
|