mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-23 15:30:34 -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,
|
||||
"tag": "0011_join_rename",
|
||||
"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,
|
||||
endAir: show.kind === "movie" ? show.airDate : show.endAir,
|
||||
nextRefresh: show.nextRefresh,
|
||||
entriesCount: 0,
|
||||
...col,
|
||||
})
|
||||
.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 { showTranslations, shows } from "~/db/schema";
|
||||
import { conflictUpdateAllExcept } from "~/db/utils";
|
||||
import { entries, entryVideoJoin, showTranslations, shows } from "~/db/schema";
|
||||
import { conflictUpdateAllExcept, sqlarr } from "~/db/utils";
|
||||
import type { SeedCollection } from "~/models/collections";
|
||||
import type { SeedMovie } from "~/models/movie";
|
||||
import type { SeedSerie } from "~/models/serie";
|
||||
@ -93,3 +93,37 @@ async function insertBaseShow(
|
||||
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 { insertCollection } from "./insert/collection";
|
||||
import { insertEntries } from "./insert/entries";
|
||||
import { insertShow } from "./insert/shows";
|
||||
import { insertShow, updateAvailableCount } from "./insert/shows";
|
||||
import { insertStudios } from "./insert/studios";
|
||||
import { guessNextRefresh } from "./refresh";
|
||||
|
||||
@ -60,6 +60,7 @@ export const seedMovie = async (
|
||||
startAir: bMovie.airDate,
|
||||
nextRefresh,
|
||||
collectionPk: col?.pk,
|
||||
entriesCount: 1,
|
||||
...bMovie,
|
||||
},
|
||||
translations,
|
||||
@ -80,6 +81,7 @@ export const seedMovie = async (
|
||||
videos,
|
||||
},
|
||||
]);
|
||||
await updateAvailableCount([show.pk], false);
|
||||
|
||||
const retStudios = await insertStudios(studios, show.pk);
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { getYear } from "~/utils";
|
||||
import { insertCollection } from "./insert/collection";
|
||||
import { insertEntries } from "./insert/entries";
|
||||
import { insertSeasons } from "./insert/seasons";
|
||||
import { insertShow } from "./insert/shows";
|
||||
import { insertShow, updateAvailableCount } from "./insert/shows";
|
||||
import { insertStudios } from "./insert/studios";
|
||||
import { guessNextRefresh } from "./refresh";
|
||||
|
||||
@ -94,6 +94,7 @@ export const seedSerie = async (
|
||||
kind: "serie",
|
||||
nextRefresh,
|
||||
collectionPk: col?.pk,
|
||||
entriesCount: entries.length,
|
||||
...serie,
|
||||
},
|
||||
translations,
|
||||
@ -106,6 +107,7 @@ export const seedSerie = async (
|
||||
show,
|
||||
(extras ?? []).map((x) => ({ ...x, kind: "extra", extraKind: x.kind })),
|
||||
);
|
||||
await updateAvailableCount([show.pk]);
|
||||
|
||||
const retStudios = await insertStudios(studios, show.pk);
|
||||
|
||||
|
@ -88,6 +88,7 @@ export async function getShows({
|
||||
status: sql<MovieStatus>`${shows.status}`,
|
||||
airDate: shows.startAir,
|
||||
kind: sql<any>`${shows.kind}`,
|
||||
isAvailable: sql<boolean>`${shows.availableCount} != 0`,
|
||||
|
||||
poster: sql<Image>`coalesce(${showTranslations.poster}, ${poster})`,
|
||||
thumbnail: sql<Image>`coalesce(${showTranslations.thumbnail}, ${thumbnail})`,
|
||||
@ -99,10 +100,9 @@ export async function getShows({
|
||||
.leftJoin(
|
||||
showTranslations,
|
||||
and(
|
||||
sql`${preferOriginal ?? false}`,
|
||||
eq(shows.pk, showTranslations.pk),
|
||||
eq(showTranslations.language, shows.originalLanguage),
|
||||
// TODO: check user's settings before fallbacking to false.
|
||||
sql`coalesce(${preferOriginal ?? null}::boolean, false)`,
|
||||
),
|
||||
)
|
||||
.where(
|
||||
@ -139,10 +139,12 @@ export async function getShow(
|
||||
extras: {
|
||||
airDate: sql<string>`${shows.startAir}`.as("airDate"),
|
||||
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),
|
||||
with: {
|
||||
selectedTranslation: selectTranslationQuery(showTranslations, languages),
|
||||
...(preferOriginal && {
|
||||
originalTranslation: {
|
||||
columns: {
|
||||
poster: true,
|
||||
@ -150,14 +152,8 @@ export async function getShow(
|
||||
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") && {
|
||||
translations: {
|
||||
columns: {
|
||||
@ -192,7 +188,7 @@ export async function getShow(
|
||||
...ret,
|
||||
...translation,
|
||||
kind: ret.kind as any,
|
||||
...(ot?.preferOriginal && {
|
||||
...(ot && {
|
||||
...(ot.poster && { poster: ot.poster }),
|
||||
...(ot.thumbnail && { thumbnail: ot.thumbnail }),
|
||||
...(ot.banner && { banner: ot.banner }),
|
||||
|
@ -72,6 +72,8 @@ export const shows = schema.table(
|
||||
collectionPk: integer().references((): AnyPgColumn => shows.pk, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
entriesCount: integer().notNull(),
|
||||
availableCount: integer().notNull().default(0),
|
||||
|
||||
externalId: externalid(),
|
||||
|
||||
|
@ -12,7 +12,7 @@ export const madeInAbyssVideo: Video = {
|
||||
title: "Made in abyss",
|
||||
season: [1],
|
||||
episode: [13],
|
||||
type: "episode",
|
||||
kind: "episode",
|
||||
from: "guessit",
|
||||
},
|
||||
createdAt: "2024-11-23T15:01:24.968Z",
|
||||
|
@ -59,7 +59,9 @@ export const Movie = t.Intersect([
|
||||
MovieTranslation,
|
||||
BaseMovie,
|
||||
DbMetadata,
|
||||
// t.Object({ isAvailable: t.Boolean() }),
|
||||
t.Object({
|
||||
isAvailable: t.Boolean(),
|
||||
}),
|
||||
]);
|
||||
export type Movie = Prettify<typeof Movie.static>;
|
||||
|
||||
|
@ -45,6 +45,12 @@ const BaseSerie = t.Object({
|
||||
),
|
||||
|
||||
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(),
|
||||
});
|
||||
@ -82,7 +88,7 @@ export const FullSerie = t.Intersect([
|
||||
export type FullMovie = Prettify<typeof FullSerie.static>;
|
||||
|
||||
export const SeedSerie = t.Intersect([
|
||||
t.Omit(BaseSerie, ["kind", "nextRefresh"]),
|
||||
t.Omit(BaseSerie, ["kind", "nextRefresh", "entriesCount", "availableCount"]),
|
||||
t.Object({
|
||||
slug: t.String({ format: "slug" }),
|
||||
translations: TranslationRecord(
|
||||
|
@ -2,14 +2,15 @@ import { beforeAll, describe, expect, it } from "bun:test";
|
||||
import { expectStatus } from "tests/utils";
|
||||
import { seedMovie } from "~/controllers/seed/movies";
|
||||
import { db } from "~/db";
|
||||
import { shows } from "~/db/schema";
|
||||
import { bubble } from "~/models/examples";
|
||||
import { shows, videos } from "~/db/schema";
|
||||
import { bubble, bubbleVideo } from "~/models/examples";
|
||||
import { getMovie } from "../helpers";
|
||||
|
||||
let bubbleId = "";
|
||||
|
||||
beforeAll(async () => {
|
||||
await db.delete(shows);
|
||||
await db.insert(videos).values(bubbleVideo);
|
||||
const ret = await seedMovie(bubble);
|
||||
if (!("status" in ret)) bubbleId = ret.id;
|
||||
});
|
||||
@ -116,4 +117,21 @@ describe("Get movie", () => {
|
||||
});
|
||||
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