import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { evaluateAcquisition } from "../../src/modules/acquisition-organize/services/acquisition.ts"; import { ensureSeasonUnit } from "../../src/modules/acquisition-organize/services/subscriptions.ts"; import { createHarness, fakeDisk, type Harness, pgAvailable, seedBase, seedEntity, seedRelease, } from "../acquisition-organize/harness.ts"; // 获取正确性(T3):订阅总有自己的收录版本(H1)、接续后续季的季下界(H4)、重评择优(M4)、 // 重评不止最近 50 条(M5)、手动下载中的集计入在途(M6)。只到建计划为止(假引擎,不提交下载器)。 const hasPg = await pgAvailable(); const SIZE = 300_000; describe.skipIf(!hasPg)("订阅范围与重评(真实 PG)", () => { let h: Harness; const disk = fakeDisk(10_000_000_000); let base: Awaited>; beforeAll(async () => { disk.setPct(10); h = await createHarness("subscope", { diskUsage: disk.fn }); base = await seedBase(h.db, { libraryPath: "/tmp/nonexistent-lib", cachePath: "/tmp", qbUrl: "http://127.0.0.1:9", }); // 计划不提交,预留会一直占并发名额:放宽并发上限 await h.setSettings("budget", { maxConcurrentDownloads: 100 }); }, 60_000); afterAll(async () => { await h?.close(); }); const db = () => h.db; let seq = 0; const hash = () => (++seq).toString(16).padStart(40, "a"); async function release(entityId: string, title: string, firstSeen?: Date) { const id = await seedRelease(db(), { siteId: base.siteId, entityId, title, size: SIZE, hash: hash() }); if (firstSeen) await db().updateTable("source_release").set({ first_seen_at: firstSeen }).where("id", "=", id).execute(); return id; } const slotsOf = (subId: string) => db().selectFrom("acquisition_slot").selectAll().where("subscription_id", "=", subId).orderBy("position").execute(); const planRow = (id: string) => db().selectFrom("acquisition_plan").selectAll().where("id", "=", id).executeTakeFirstOrThrow(); async function seasons(entityId: string, n: number) { const ids: string[] = []; for (let s = 1; s <= n; s++) ids.push( await db() .transaction() .execute((trx) => ensureSeasonUnit(trx, entityId, s)), ); return ids; } async function createSub(body: Record) { const r = await h.api("POST", "/subscriptions", body); expect(r.status, JSON.stringify(r.body)).toBe(201); return r.body.data.id as string; } it("H1:无策略模板、没填质量预设的订阅也有自己的默认收录版本,能下载", async () => { const e = await seedEntity(db(), { kind: "series", title: "无预设剧", year: 2025 }); const subId = await createSub({ mediaEntityId: e, mediaKind: "series" }); const slots = await slotsOf(subId); expect(slots).toHaveLength(1); expect(slots[0]).toMatchObject({ name: "默认版本", hard_conditions: {} }); const r = await release(e, "No.Preset.S01E01.1080p.WEB-DL.x264-GRP"); const ev = await evaluateAcquisition(h.d, { sourceReleaseId: r }); expect(ev.plans, JSON.stringify(ev)).toHaveLength(1); expect((await planRow(ev.plans[0]!)).slot_id).toBe(slots[0]!.id); }); it("H1:S1 旧订阅已有收录版本时再订 S2,S2 订阅建自己的收录版本(重名加序号)并能下载", async () => { const e = await seedEntity(db(), { kind: "series", title: "两季剧", year: 2024 }); const [s1, s2] = await seasons(e, 2); const sub1 = await createSub({ mediaEntityId: e, mediaKind: "series", seasonUnitId: s1, seasonScope: "current_season", params: { resolution: "1080p" }, }); expect(await slotsOf(sub1)).toHaveLength(1); await db().updateTable("subscription").set({ status: "completed" }).where("id", "=", sub1).execute(); const sub2 = await createSub({ mediaEntityId: e, mediaKind: "series", seasonUnitId: s2, seasonScope: "current_season", params: { resolution: "1080p" }, }); const own = await slotsOf(sub2); expect(own).toHaveLength(1); expect(own[0]!.name).toBe("默认版本 2"); // 质量预设写进了 S2 订阅自己的收录版本 expect(own[0]!.hard_conditions).toMatchObject({ resolutions: ["1080p", "1080i"] }); const r = await release(e, "Two.Seasons.S02E01.1080p.WEB-DL.x264-GRP"); const ev = await evaluateAcquisition(h.d, { sourceReleaseId: r }); expect(ev.plans, JSON.stringify(ev)).toHaveLength(1); expect((await planRow(ev.plans[0]!)).slot_id).toBe(own[0]!.id); }); it("H1:已有的 0 收录版本订阅首次评估时补建;编辑时也补建", async () => { const e = await seedEntity(db(), { kind: "movie", title: "Legacy Film", year: 2023 }); const sub = await db() .insertInto("subscription") .values({ media_entity_id: e, media_kind: "movie", mode: "once", season_scope: "current_season" }) .returning("id") .executeTakeFirstOrThrow(); expect(await slotsOf(sub.id)).toHaveLength(0); const r = await release(e, "Legacy.Film.2023.1080p.WEB-DL.x264-GRP"); const ev = await evaluateAcquisition(h.d, { sourceReleaseId: r }); expect(ev.plans, JSON.stringify(ev)).toHaveLength(1); expect(await slotsOf(sub.id)).toHaveLength(1); const e2 = await seedEntity(db(), { kind: "movie", title: "Legacy Film 2", year: 2023 }); const sub2 = await db() .insertInto("subscription") .values({ media_entity_id: e2, media_kind: "movie", mode: "once", season_scope: "current_season" }) .returning("id") .executeTakeFirstOrThrow(); const p = await h.api("PATCH", `/subscriptions/${sub2.id}`, { params: { resolution: "2160p" } }); expect(p.status, JSON.stringify(p.body)).toBe(200); const [slot] = await slotsOf(sub2.id); expect(slot?.hard_conditions).toMatchObject({ resolutions: ["2160p"] }); }); it("H4:从 S3 订「接续后续季」不下 S1 / S2,S3 及以后照常", async () => { const e = await seedEntity(db(), { kind: "series", title: "三季剧", year: 2020 }); const [, , s3] = await seasons(e, 4); const subId = await createSub({ mediaEntityId: e, mediaKind: "series", seasonUnitId: s3, seasonScope: "following_seasons", params: { resolution: "1080p" }, }); const planned = async (title: string) => (await evaluateAcquisition(h.d, { sourceReleaseId: await release(e, title) })).plans.length; expect(await planned("Three.Seasons.S01E01.1080p.WEB-DL.x264-GRP")).toBe(0); expect(await planned("Three.Seasons.S02E05.1080p.WEB-DL.x264-GRP")).toBe(0); expect(await planned("Three.Seasons.S02.1080p.WEB-DL.x264-GRP")).toBe(0); expect(await planned("Three.Seasons.S03E01.1080p.WEB-DL.x264-GRP")).toBe(1); expect(await planned("Three.Seasons.S04E01.1080p.WEB-DL.x264-GRP")).toBe(1); // 订阅重评同口径 const re = await evaluateAcquisition(h.d, { subscriptionId: subId }); expect(re.plans).toHaveLength(0); }); it("M4:订阅重评时同一集多个合格资源选 pri 最高的,不是最新的", async () => { const e = await seedEntity(db(), { kind: "series", title: "择优剧", year: 2024 }); const g = await h.api("POST", "/rules", { name: "择优阶梯", ruleString: "REMUX > WEBDL" }); expect(g.status, JSON.stringify(g.body)).toBe(201); const t0 = Date.now(); const remux = await release(e, "Pick.Best.S01E01.1080p.BluRay.REMUX.AVC-GRP", new Date(t0 - 3_600_000)); await release(e, "Pick.Best.S01E01.1080p.WEB-DL.x264-NEW", new Date(t0 - 60_000)); // 订阅建好后 kickEvaluate 只入队(假引擎),这里直接跑订阅重评 const subId = await createSub({ mediaEntityId: e, mediaKind: "series", params: { filter_groups: [g.body.data.id], resolution: "1080p" }, }); const ev = await evaluateAcquisition(h.d, { subscriptionId: subId }); expect(ev.plans, JSON.stringify(ev)).toHaveLength(1); expect(await planRow(ev.plans[0]!)).toMatchObject({ source_release_id: remux, unit_keys: ["s1e1"] }); expect(ev.deferred.map((x) => x.reason)).toEqual(["units_inflight"]); }); it("M5:超过 50 条较新的资源之后,更早的集仍能被重评选中", async () => { const e = await seedEntity(db(), { kind: "series", title: "长剧", year: 2018 }); const subId = await createSub({ mediaEntityId: e, mediaKind: "series", params: { resolution: "1080p" } }); const t0 = Date.now(); const old = await release(e, "Long.Show.S01E01.1080p.WEB-DL.x264-GRP", new Date(t0 - 86_400_000)); // 55 条更新的 720p 资源(不合格)把最近 50 条占满 for (let i = 2; i <= 56; i++) await release(e, `Long.Show.S01E${String(i).padStart(2, "0")}.720p.WEB-DL.x264-GRP`, new Date(t0 - i * 1000)); const ev = await evaluateAcquisition(h.d, { subscriptionId: subId }); expect(ev.plans, JSON.stringify(ev.skipped.slice(0, 3))).toHaveLength(1); expect(await planRow(ev.plans[0]!)).toMatchObject({ source_release_id: old, unit_keys: ["s1e1"] }); }, 60_000); it("M6:手动下载中的集,订阅不再用别的资源重复下;没在手动下载的集照常", async () => { const e = await seedEntity(db(), { kind: "series", title: "手动剧", year: 2024 }); const subId = await createSub({ mediaEntityId: e, mediaKind: "series", params: { resolution: "1080p" } }); const [slot] = await slotsOf(subId); const manualRel = await release(e, "Manual.Show.S01E01.1080p.WEB-DL.x264-USER"); await db() .insertInto("acquisition_plan") .values({ slot_id: null, source_release_id: manualRel, status: "submitted", priority: "immediate", evaluation: JSON.stringify({ manual: true, mediaEntityId: e }), payload_hash: "manual-test", idempotency_key: `manual:test:${manualRel}`, status_reason: "manual_selection", }) .execute(); const other = await release(e, "Manual.Show.S01E01.1080p.WEB-DL.x265-OTHER"); const ev = await evaluateAcquisition(h.d, { sourceReleaseId: other }); expect(ev.plans).toHaveLength(0); expect(ev.deferred).toEqual([{ sourceReleaseId: other, slotId: slot!.id, reason: "units_inflight" }]); // 手动计划结束不会触发重评:延后时排了退避重评 const s = await db() .selectFrom("acquisition_slot") .select("next_evaluate_at") .where("id", "=", slot!.id) .executeTakeFirstOrThrow(); expect(s.next_evaluate_at).not.toBeNull(); const e2 = await release(e, "Manual.Show.S01E02.1080p.WEB-DL.x264-OTHER"); expect((await evaluateAcquisition(h.d, { sourceReleaseId: e2 })).plans).toHaveLength(1); }); });