mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-07 10:14:13 -04:00
Add entriesCount
, availableCount
& isAvailable
(#831)
This commit is contained in:
commit
74a509ec03
2
api/drizzle/0012_available_count.sql
Normal file
2
api/drizzle/0012_available_count.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "kyoo"."shows" ADD COLUMN "entries_count" integer NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "kyoo"."shows" ADD COLUMN "available_count" integer DEFAULT 0 NOT NULL;
|
1278
api/drizzle/meta/0012_snapshot.json
Normal file
1278
api/drizzle/meta/0012_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -85,6 +85,13 @@
|
|||||||
"when": 1741014917375,
|
"when": 1741014917375,
|
||||||
"tag": "0011_join_rename",
|
"tag": "0011_join_rename",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 12,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1741360992371,
|
||||||
|
"tag": "0012_available_count",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ export const insertCollection = async (
|
|||||||
startAir: show.kind === "movie" ? show.airDate : show.startAir,
|
startAir: show.kind === "movie" ? show.airDate : show.startAir,
|
||||||
endAir: show.kind === "movie" ? show.airDate : show.endAir,
|
endAir: show.kind === "movie" ? show.airDate : show.endAir,
|
||||||
nextRefresh: show.nextRefresh,
|
nextRefresh: show.nextRefresh,
|
||||||
|
entriesCount: 0,
|
||||||
...col,
|
...col,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { eq, sql } from "drizzle-orm";
|
import { and, count, eq, exists, ne, sql } from "drizzle-orm";
|
||||||
import { db } from "~/db";
|
import { db } from "~/db";
|
||||||
import { showTranslations, shows } from "~/db/schema";
|
import { entries, entryVideoJoin, showTranslations, shows } from "~/db/schema";
|
||||||
import { conflictUpdateAllExcept } from "~/db/utils";
|
import { conflictUpdateAllExcept, sqlarr } from "~/db/utils";
|
||||||
import type { SeedCollection } from "~/models/collections";
|
import type { SeedCollection } from "~/models/collections";
|
||||||
import type { SeedMovie } from "~/models/movie";
|
import type { SeedMovie } from "~/models/movie";
|
||||||
import type { SeedSerie } from "~/models/serie";
|
import type { SeedSerie } from "~/models/serie";
|
||||||
@ -93,3 +93,37 @@ async function insertBaseShow(
|
|||||||
slug: show.slug,
|
slug: show.slug,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateAvailableCount(
|
||||||
|
showPks: number[],
|
||||||
|
updateEntryCount = true,
|
||||||
|
) {
|
||||||
|
return await db
|
||||||
|
.update(shows)
|
||||||
|
.set({
|
||||||
|
availableCount: sql`${db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(entries)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(entries.showPk, shows.pk),
|
||||||
|
ne(entries.kind, "extra"),
|
||||||
|
exists(
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(entryVideoJoin)
|
||||||
|
.where(eq(entryVideoJoin.entryPk, entries.pk)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)}`,
|
||||||
|
...(updateEntryCount && {
|
||||||
|
entriesCount: sql`${db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(entries)
|
||||||
|
.where(
|
||||||
|
and(eq(entries.showPk, shows.pk), ne(entries.kind, "extra")),
|
||||||
|
)}`,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.where(eq(shows.pk, sql`any(${sqlarr(showPks)})`));
|
||||||
|
}
|
||||||
|
@ -3,7 +3,7 @@ import type { SeedMovie } from "~/models/movie";
|
|||||||
import { getYear } from "~/utils";
|
import { getYear } from "~/utils";
|
||||||
import { insertCollection } from "./insert/collection";
|
import { insertCollection } from "./insert/collection";
|
||||||
import { insertEntries } from "./insert/entries";
|
import { insertEntries } from "./insert/entries";
|
||||||
import { insertShow } from "./insert/shows";
|
import { insertShow, updateAvailableCount } from "./insert/shows";
|
||||||
import { insertStudios } from "./insert/studios";
|
import { insertStudios } from "./insert/studios";
|
||||||
import { guessNextRefresh } from "./refresh";
|
import { guessNextRefresh } from "./refresh";
|
||||||
|
|
||||||
@ -60,6 +60,7 @@ export const seedMovie = async (
|
|||||||
startAir: bMovie.airDate,
|
startAir: bMovie.airDate,
|
||||||
nextRefresh,
|
nextRefresh,
|
||||||
collectionPk: col?.pk,
|
collectionPk: col?.pk,
|
||||||
|
entriesCount: 1,
|
||||||
...bMovie,
|
...bMovie,
|
||||||
},
|
},
|
||||||
translations,
|
translations,
|
||||||
@ -80,6 +81,7 @@ export const seedMovie = async (
|
|||||||
videos,
|
videos,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
await updateAvailableCount([show.pk], false);
|
||||||
|
|
||||||
const retStudios = await insertStudios(studios, show.pk);
|
const retStudios = await insertStudios(studios, show.pk);
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import { getYear } from "~/utils";
|
|||||||
import { insertCollection } from "./insert/collection";
|
import { insertCollection } from "./insert/collection";
|
||||||
import { insertEntries } from "./insert/entries";
|
import { insertEntries } from "./insert/entries";
|
||||||
import { insertSeasons } from "./insert/seasons";
|
import { insertSeasons } from "./insert/seasons";
|
||||||
import { insertShow } from "./insert/shows";
|
import { insertShow, updateAvailableCount } from "./insert/shows";
|
||||||
import { insertStudios } from "./insert/studios";
|
import { insertStudios } from "./insert/studios";
|
||||||
import { guessNextRefresh } from "./refresh";
|
import { guessNextRefresh } from "./refresh";
|
||||||
|
|
||||||
@ -94,6 +94,7 @@ export const seedSerie = async (
|
|||||||
kind: "serie",
|
kind: "serie",
|
||||||
nextRefresh,
|
nextRefresh,
|
||||||
collectionPk: col?.pk,
|
collectionPk: col?.pk,
|
||||||
|
entriesCount: entries.length,
|
||||||
...serie,
|
...serie,
|
||||||
},
|
},
|
||||||
translations,
|
translations,
|
||||||
@ -106,6 +107,7 @@ export const seedSerie = async (
|
|||||||
show,
|
show,
|
||||||
(extras ?? []).map((x) => ({ ...x, kind: "extra", extraKind: x.kind })),
|
(extras ?? []).map((x) => ({ ...x, kind: "extra", extraKind: x.kind })),
|
||||||
);
|
);
|
||||||
|
await updateAvailableCount([show.pk]);
|
||||||
|
|
||||||
const retStudios = await insertStudios(studios, show.pk);
|
const retStudios = await insertStudios(studios, show.pk);
|
||||||
|
|
||||||
|
@ -88,6 +88,7 @@ export async function getShows({
|
|||||||
status: sql<MovieStatus>`${shows.status}`,
|
status: sql<MovieStatus>`${shows.status}`,
|
||||||
airDate: shows.startAir,
|
airDate: shows.startAir,
|
||||||
kind: sql<any>`${shows.kind}`,
|
kind: sql<any>`${shows.kind}`,
|
||||||
|
isAvailable: sql<boolean>`${shows.availableCount} != 0`,
|
||||||
|
|
||||||
poster: sql<Image>`coalesce(${showTranslations.poster}, ${poster})`,
|
poster: sql<Image>`coalesce(${showTranslations.poster}, ${poster})`,
|
||||||
thumbnail: sql<Image>`coalesce(${showTranslations.thumbnail}, ${thumbnail})`,
|
thumbnail: sql<Image>`coalesce(${showTranslations.thumbnail}, ${thumbnail})`,
|
||||||
@ -99,10 +100,9 @@ export async function getShows({
|
|||||||
.leftJoin(
|
.leftJoin(
|
||||||
showTranslations,
|
showTranslations,
|
||||||
and(
|
and(
|
||||||
|
sql`${preferOriginal ?? false}`,
|
||||||
eq(shows.pk, showTranslations.pk),
|
eq(shows.pk, showTranslations.pk),
|
||||||
eq(showTranslations.language, shows.originalLanguage),
|
eq(showTranslations.language, shows.originalLanguage),
|
||||||
// TODO: check user's settings before fallbacking to false.
|
|
||||||
sql`coalesce(${preferOriginal ?? null}::boolean, false)`,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
@ -139,25 +139,21 @@ export async function getShow(
|
|||||||
extras: {
|
extras: {
|
||||||
airDate: sql<string>`${shows.startAir}`.as("airDate"),
|
airDate: sql<string>`${shows.startAir}`.as("airDate"),
|
||||||
status: sql<MovieStatus>`${shows.status}`.as("status"),
|
status: sql<MovieStatus>`${shows.status}`.as("status"),
|
||||||
|
isAvailable: sql<boolean>`${shows.availableCount} != 0`.as("isAvailable"),
|
||||||
},
|
},
|
||||||
where: and(isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id), filters),
|
where: and(isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id), filters),
|
||||||
with: {
|
with: {
|
||||||
selectedTranslation: selectTranslationQuery(showTranslations, languages),
|
selectedTranslation: selectTranslationQuery(showTranslations, languages),
|
||||||
originalTranslation: {
|
...(preferOriginal && {
|
||||||
columns: {
|
originalTranslation: {
|
||||||
poster: true,
|
columns: {
|
||||||
thumbnail: true,
|
poster: true,
|
||||||
banner: true,
|
thumbnail: true,
|
||||||
logo: true,
|
banner: true,
|
||||||
|
logo: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extras: {
|
}),
|
||||||
// TODO: also fallback on user settings (that's why i made a select here)
|
|
||||||
preferOriginal:
|
|
||||||
sql<boolean>`(select coalesce(${preferOriginal ?? null}::boolean, false))`.as(
|
|
||||||
"preferOriginal",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...(relations.includes("translations") && {
|
...(relations.includes("translations") && {
|
||||||
translations: {
|
translations: {
|
||||||
columns: {
|
columns: {
|
||||||
@ -192,7 +188,7 @@ export async function getShow(
|
|||||||
...ret,
|
...ret,
|
||||||
...translation,
|
...translation,
|
||||||
kind: ret.kind as any,
|
kind: ret.kind as any,
|
||||||
...(ot?.preferOriginal && {
|
...(ot && {
|
||||||
...(ot.poster && { poster: ot.poster }),
|
...(ot.poster && { poster: ot.poster }),
|
||||||
...(ot.thumbnail && { thumbnail: ot.thumbnail }),
|
...(ot.thumbnail && { thumbnail: ot.thumbnail }),
|
||||||
...(ot.banner && { banner: ot.banner }),
|
...(ot.banner && { banner: ot.banner }),
|
||||||
|
@ -72,6 +72,8 @@ export const shows = schema.table(
|
|||||||
collectionPk: integer().references((): AnyPgColumn => shows.pk, {
|
collectionPk: integer().references((): AnyPgColumn => shows.pk, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
|
entriesCount: integer().notNull(),
|
||||||
|
availableCount: integer().notNull().default(0),
|
||||||
|
|
||||||
externalId: externalid(),
|
externalId: externalid(),
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ export const madeInAbyssVideo: Video = {
|
|||||||
title: "Made in abyss",
|
title: "Made in abyss",
|
||||||
season: [1],
|
season: [1],
|
||||||
episode: [13],
|
episode: [13],
|
||||||
type: "episode",
|
kind: "episode",
|
||||||
from: "guessit",
|
from: "guessit",
|
||||||
},
|
},
|
||||||
createdAt: "2024-11-23T15:01:24.968Z",
|
createdAt: "2024-11-23T15:01:24.968Z",
|
||||||
|
@ -59,7 +59,9 @@ export const Movie = t.Intersect([
|
|||||||
MovieTranslation,
|
MovieTranslation,
|
||||||
BaseMovie,
|
BaseMovie,
|
||||||
DbMetadata,
|
DbMetadata,
|
||||||
// t.Object({ isAvailable: t.Boolean() }),
|
t.Object({
|
||||||
|
isAvailable: t.Boolean(),
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
export type Movie = Prettify<typeof Movie.static>;
|
export type Movie = Prettify<typeof Movie.static>;
|
||||||
|
|
||||||
|
@ -45,6 +45,12 @@ const BaseSerie = t.Object({
|
|||||||
),
|
),
|
||||||
|
|
||||||
nextRefresh: t.String({ format: "date-time" }),
|
nextRefresh: t.String({ format: "date-time" }),
|
||||||
|
entriesCount: t.Integer({
|
||||||
|
description: "The number of episodes in this serie",
|
||||||
|
}),
|
||||||
|
availableCount: t.Integer({
|
||||||
|
description: "The number of episodes that can be played right away",
|
||||||
|
}),
|
||||||
|
|
||||||
externalId: ExternalId(),
|
externalId: ExternalId(),
|
||||||
});
|
});
|
||||||
@ -82,7 +88,7 @@ export const FullSerie = t.Intersect([
|
|||||||
export type FullMovie = Prettify<typeof FullSerie.static>;
|
export type FullMovie = Prettify<typeof FullSerie.static>;
|
||||||
|
|
||||||
export const SeedSerie = t.Intersect([
|
export const SeedSerie = t.Intersect([
|
||||||
t.Omit(BaseSerie, ["kind", "nextRefresh"]),
|
t.Omit(BaseSerie, ["kind", "nextRefresh", "entriesCount", "availableCount"]),
|
||||||
t.Object({
|
t.Object({
|
||||||
slug: t.String({ format: "slug" }),
|
slug: t.String({ format: "slug" }),
|
||||||
translations: TranslationRecord(
|
translations: TranslationRecord(
|
||||||
|
@ -2,14 +2,15 @@ import { beforeAll, describe, expect, it } from "bun:test";
|
|||||||
import { expectStatus } from "tests/utils";
|
import { expectStatus } from "tests/utils";
|
||||||
import { seedMovie } from "~/controllers/seed/movies";
|
import { seedMovie } from "~/controllers/seed/movies";
|
||||||
import { db } from "~/db";
|
import { db } from "~/db";
|
||||||
import { shows } from "~/db/schema";
|
import { shows, videos } from "~/db/schema";
|
||||||
import { bubble } from "~/models/examples";
|
import { bubble, bubbleVideo } from "~/models/examples";
|
||||||
import { getMovie } from "../helpers";
|
import { getMovie } from "../helpers";
|
||||||
|
|
||||||
let bubbleId = "";
|
let bubbleId = "";
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await db.delete(shows);
|
await db.delete(shows);
|
||||||
|
await db.insert(videos).values(bubbleVideo);
|
||||||
const ret = await seedMovie(bubble);
|
const ret = await seedMovie(bubble);
|
||||||
if (!("status" in ret)) bubbleId = ret.id;
|
if (!("status" in ret)) bubbleId = ret.id;
|
||||||
});
|
});
|
||||||
@ -116,4 +117,21 @@ describe("Get movie", () => {
|
|||||||
});
|
});
|
||||||
expect(resp.headers.get("Content-Language")).toBe("en");
|
expect(resp.headers.get("Content-Language")).toBe("en");
|
||||||
});
|
});
|
||||||
|
it("With isAvailable", async () => {
|
||||||
|
const [resp, body] = await getMovie(bubble.slug, {});
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body.isAvailable).toBe(true);
|
||||||
|
});
|
||||||
|
it("With isAvailable=false", async () => {
|
||||||
|
await seedMovie({
|
||||||
|
...bubble,
|
||||||
|
slug: "no-video",
|
||||||
|
videos: [],
|
||||||
|
});
|
||||||
|
const [resp, body] = await getMovie("no-video", {});
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body.isAvailable).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
32
api/tests/series/get-series.test.ts
Normal file
32
api/tests/series/get-series.test.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { beforeAll, describe, expect, it } from "bun:test";
|
||||||
|
import { createSerie, getSerie } from "tests/helpers";
|
||||||
|
import { expectStatus } from "tests/utils";
|
||||||
|
import { db } from "~/db";
|
||||||
|
import { shows, videos } from "~/db/schema";
|
||||||
|
import { madeInAbyss, madeInAbyssVideo } from "~/models/examples";
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await db.delete(videos);
|
||||||
|
await db.delete(shows);
|
||||||
|
await db.insert(videos).values(madeInAbyssVideo);
|
||||||
|
await createSerie(madeInAbyss);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("aet series", () => {
|
||||||
|
it("Invalid slug", async () => {
|
||||||
|
const [resp, body] = await getSerie("sotneuhn", { langs: "en" });
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(404);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
status: 404,
|
||||||
|
message: expect.any(String),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("With a valid entryCount/availableCount", async () => {
|
||||||
|
const [resp, body] = await getSerie(madeInAbyss.slug, { langs: "en" });
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body.entriesCount).toBe(madeInAbyss.entries.length);
|
||||||
|
expect(body.availableCount).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user