diff --git a/api/src/controllers/videos.ts b/api/src/controllers/videos.ts index c151cec9..49a1d6a5 100644 --- a/api/src/controllers/videos.ts +++ b/api/src/controllers/videos.ts @@ -7,6 +7,7 @@ import { lt, max, min, + ne, notExists, or, sql, @@ -100,10 +101,23 @@ async function linkVideos( .innerJoin(shows, eq(entries.showPk, shows.pk)) .as("entriesQ"); - const hasRenderingQ = tx - .select() - .from(entryVideoJoin) - .where(eq(entryVideoJoin.entryPk, entriesQ.pk)); + const renderVid = alias(videos, "renderVid"); + const hasRenderingQ = or( + gt( + sql`dense_rank() over (partition by ${entriesQ.pk} order by ${videos.rendering})`, + 1, + ), + sql`exists(${tx + .select() + .from(entryVideoJoin) + .innerJoin(renderVid, eq(renderVid.pk, entryVideoJoin.videoPk)) + .where( + and( + eq(entryVideoJoin.entryPk, entriesQ.pk), + ne(renderVid.rendering, videos.rendering), + ), + )})`, + )!; const ret = await tx .insert(entryVideoJoin) @@ -112,7 +126,7 @@ async function linkVideos( .selectDistinctOn([entriesQ.pk, videos.pk], { entryPk: entriesQ.pk, videoPk: videos.pk, - slug: computeVideoSlug(entriesQ.slug, sql`exists(${hasRenderingQ})`), + slug: computeVideoSlug(entriesQ.slug, hasRenderingQ), }) .from( values(links, { diff --git a/api/tests/videos/scanner.test.ts b/api/tests/videos/scanner.test.ts index 9e0760eb..3867ae44 100644 --- a/api/tests/videos/scanner.test.ts +++ b/api/tests/videos/scanner.test.ts @@ -591,4 +591,127 @@ describe("Video seeding", () => { expect(vid!.evj[1].slug).toBe("made-in-abyss-s2e1"); expect(vid!.evj[1].entry.slug).toBe("made-in-abyss-s2e1"); }); + + it("work with duplicated episodes", async () => { + await db.delete(videos); + const [resp, body] = await createVideo({ + guess: { + title: "mia", + episodes: [ + { season: 1, episode: 13 }, + { season: 1, episode: 13 }, + ], + from: "test", + history: [], + }, + part: null, + path: "/video/mia s1e13.mkv", + rendering: "duptest", + version: 1, + for: [ + { serie: madeInAbyss.slug, season: 1, episode: 13 }, + { serie: madeInAbyss.slug, season: 1, episode: 13 }, + ], + }); + + expectStatus(resp, body).toBe(201); + expect(body).toBeArrayOfSize(1); + expect(body[0].id).toBeString(); + + const vid = await db.query.videos.findFirst({ + where: eq(videos.id, body[0].id), + with: { + evj: { with: { entry: true } }, + }, + }); + + expect(vid).not.toBeNil(); + expect(vid!.path).toBe("/video/mia s1e13.mkv"); + expect(vid!.guess).toMatchObject({ title: "mia", from: "test" }); + + expect(body[0].entries).toBeArrayOfSize(1); + expect(vid!.evj).toBeArrayOfSize(1); + + expect(vid!.evj[0].slug).toBe("made-in-abyss-s1e13"); + expect(vid!.evj[0].entry.slug).toBe("made-in-abyss-s1e13"); + }); + + it("work with duplicated two episodes", async () => { + await db.delete(videos); + const [resp, body] = await createVideo([ + { + guess: { + title: "mia", + episodes: [ + { season: 1, episode: 13 }, + { season: 1, episode: 13 }, + ], + from: "test", + history: [], + }, + part: null, + path: "/video/mia s1e13.mkv", + rendering: "duptest-two", + version: 1, + for: [ + { serie: madeInAbyss.slug, season: 1, episode: 13 }, + { serie: madeInAbyss.slug, season: 1, episode: 13 }, + ], + }, + { + guess: { + title: "mia", + episodes: [ + { season: 1, episode: 13 }, + { season: 1, episode: 13 }, + ], + from: "test", + history: [], + }, + part: null, + path: "/video/mia s1e13 bis.mkv", + rendering: "duptest-two-bis", + version: 1, + for: [ + { serie: madeInAbyss.slug, season: 1, episode: 13 }, + { serie: madeInAbyss.slug, season: 1, episode: 13 }, + ], + }, + ]); + + expectStatus(resp, body).toBe(201); + expect(body).toBeArrayOfSize(2); + expect(body[0].id).toBeString(); + expect(body[1].id).toBeString(); + expect(body[0].entries).toBeArrayOfSize(1); + expect(body[0].entries[0].slug).toBe("made-in-abyss-s1e13"); + expect(body[1].entries).toBeArrayOfSize(1); + expect(body[1].entries[0].slug).toBe("made-in-abyss-s1e13-duptest-two-bis"); + + let vid = await db.query.videos.findFirst({ + where: eq(videos.id, body[0].id), + with: { + evj: { with: { entry: true } }, + }, + }); + + expect(vid).not.toBeNil(); + expect(vid!.path).toBe("/video/mia s1e13.mkv"); + expect(vid!.guess).toMatchObject({ title: "mia", from: "test" }); + expect(vid!.evj).toBeArrayOfSize(1); + expect(vid!.evj[0].slug).toBe("made-in-abyss-s1e13"); + + vid = await db.query.videos.findFirst({ + where: eq(videos.id, body[1].id), + with: { + evj: { with: { entry: true } }, + }, + }); + + expect(vid).not.toBeNil(); + expect(vid!.path).toBe("/video/mia s1e13 bis.mkv"); + expect(vid!.guess).toMatchObject({ title: "mia", from: "test" }); + expect(vid!.evj).toBeArrayOfSize(1); + expect(vid!.evj[0].slug).toBe("made-in-abyss-s1e13-duptest-two-bis"); + }); }); diff --git a/scanner/scanner/identifiers/identify.py b/scanner/scanner/identifiers/identify.py index 393efcc7..90e61f1a 100644 --- a/scanner/scanner/identifiers/identify.py +++ b/scanner/scanner/identifiers/identify.py @@ -86,3 +86,4 @@ if __name__ == "__main__": print(ret.model_dump_json(indent=4, by_alias=True)) asyncio.run(main()) + # use this with `uv run python3 -m scanner.identifiers.identify "path"`