diff --git a/apps/server/src/modules/acquisition-organize/services/organize-finalize.ts b/apps/server/src/modules/acquisition-organize/services/organize-finalize.ts index ebf781cf..3c92e0d3 100644 --- a/apps/server/src/modules/acquisition-organize/services/organize-finalize.ts +++ b/apps/server/src/modules/acquisition-organize/services/organize-finalize.ts @@ -145,16 +145,22 @@ export async function finalizeRecord(d: AoDeps, recordId: string): Promise a.files.some((old) => old.locationId === hit!.id)) + ((pf?.action === "replace" && + (!frozen.replacement || + frozen.replacement.assets.some((a) => a.files.some((old) => old.locationId === hit!.id)))) || + hit.media_entity_id !== frozen.entityId) ) { await trx.updateTable("asset_location").set({ status: "removed" }).where("id", "=", hit.id).execute(); await sql`update media_asset set status='retired' where id=${hit.media_asset_id} and not exists(select 1 from asset_location where media_asset_id=${hit.media_asset_id} and status <> 'removed')`.execute( @@ -166,46 +172,80 @@ export async function finalizeRecord(d: AoDeps, recordId: string): Promise { + let h: Harness; + let lib: TempTree; + let src: TempTree; + let entity = ""; + + beforeAll(async () => { + lib = await createTempTree({}, { sentinel: true, prefix: "umt-aflib-" }); + src = await createTempTree({ [OLD]: 200_000, [NEW]: 300_000 }, { prefix: "umt-afsrc-" }); + h = await createHarness("asset_facts"); + await seedBase(h.db, { libraryPath: lib.root, cachePath: src.root, qbUrl: "http://127.0.0.1:9" }); + await h.setSettings("organize", { scrapeAfterImport: false }); + entity = await seedEntity(h.db, { kind: "movie", title: "替换片", year: 2020 }); + }, 60_000); + afterAll(async () => { + await h?.close(); + for (const t of [lib, src]) await t?.cleanup(); + }); + + async function organize(file: string, extra: Record = {}) { + const plan = await planImport(h.d, { + sourcePaths: [src.path(file)], + mediaEntityId: entity, + action: "copy", + trigger: "manual", + actorType: "user", + ...extra, + }); + expect(plan.status).toBe("running"); + expect((await executeRecord(h.d, plan.recordId!)).status).toBe("succeeded"); + return h.db + .selectFrom("organization_file_result") + .select(["media_asset_id", "target_path"]) + .where("organization_record_id", "=", plan.recordId!) + .where("role", "=", "main") + .executeTakeFirstOrThrow(); + } + const asset = (id: string) => + h.db.selectFrom("media_asset").selectAll().where("id", "=", id).executeTakeFirstOrThrow(); + const locations = (id: string) => + h.db.selectFrom("asset_location").select(["status", "size_bytes"]).where("media_asset_id", "=", id).execute(); + + let newAsset = ""; + let target = ""; + + it("非洗版替换:退役旧位置与旧资产,新建资产记新文件的大小与版本", async () => { + const first = await organize(OLD); + const second = await organize(NEW, { onExisting: "replace_with_verify", grantReplace: true }); + expect(second.target_path).toBe(first.target_path); + expect(second.media_asset_id).not.toBe(first.media_asset_id); + newAsset = second.media_asset_id!; + target = second.target_path!; + + expect((await asset(first.media_asset_id!)).status).toBe("retired"); + expect(await locations(first.media_asset_id!)).toEqual([{ status: "removed", size_bytes: expect.anything() }]); + const a = await asset(newAsset); + expect(a).toMatchObject({ status: "active", media_entity_id: entity }); + expect(Number(a.size_bytes)).toBe(300_000); + expect(JSON.stringify(a.delivery)).toContain("BBB"); + expect((await locations(newAsset)).map((l) => l.status)).toEqual(["available"]); + }); + + it("复用判为缺失的位置:位置恢复可用,资产恢复 active 且事实换成新文件", async () => { + await h.db + .updateTable("asset_location") + .set({ status: "confirmed_missing" }) + .where("media_asset_id", "=", newAsset) + .execute(); + await h.db.updateTable("media_asset").set({ status: "missing" }).where("id", "=", newAsset).execute(); + await rm(lib.path(target)); + const again = await organize(OLD); + expect(again.media_asset_id).toBe(newAsset); + const a = await asset(newAsset); + expect(a.status).toBe("active"); + expect(Number(a.size_bytes)).toBe(200_000); + expect(JSON.stringify(a.delivery)).toContain("AAA"); + const [loc] = await locations(newAsset); + expect(loc).toMatchObject({ status: "available" }); + expect(Number(loc!.size_bytes)).toBe(200_000); + }); +});