mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-11-04 03:27:14 -05: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