diff --git a/api/src/controllers/seed/insert/collection.ts b/api/src/controllers/seed/insert/collection.ts index d1b80d1c..89ac3faa 100644 --- a/api/src/controllers/seed/insert/collection.ts +++ b/api/src/controllers/seed/insert/collection.ts @@ -5,7 +5,9 @@ import { conflictUpdateAllExcept } from "~/db/utils"; import type { SeedCollection } from "~/models/collections"; import type { SeedMovie } from "~/models/movie"; import type { SeedSerie } from "~/models/serie"; +import type { Original } from "~/models/utils"; import { record } from "~/otel"; +import { uniq } from "~/utils"; import { enqueueOptImage, flushImageQueue, type ImageTask } from "../images"; type ShowTrans = typeof showTranslations.$inferInsert; @@ -13,16 +15,17 @@ type ShowTrans = typeof showTranslations.$inferInsert; export const insertCollection = record( "insertCollection", async ( - collection: SeedCollection | undefined, + collection: SeedCollection | null | undefined, show: ( | ({ kind: "movie" } & SeedMovie) | ({ kind: "serie" } & SeedSerie) ) & { nextRefresh: Date; }, + original: Original, ) => { if (!collection) return null; - const { translations, ...col } = collection; + const { translations, genres, ...col } = collection; return await db.transaction(async (tx) => { const imgQueue: ImageTask[] = []; @@ -35,7 +38,12 @@ export const insertCollection = record( endAir: show.kind === "movie" ? show.airDate : show.endAir, nextRefresh: show.nextRefresh, entriesCount: 0, - original: {} as any, + original: { + language: original.language, + name: original.name, + latinName: original.latinName, + }, + genres: uniq(show.genres), ...col, }) .onConflictDoUpdate({ diff --git a/api/src/controllers/seed/movies.ts b/api/src/controllers/seed/movies.ts index 5fb92f71..d9c25900 100644 --- a/api/src/controllers/seed/movies.ts +++ b/api/src/controllers/seed/movies.ts @@ -54,14 +54,29 @@ export const seedMovie = async ( const { translations, videos, collection, studios, staff, ...movie } = seed; const nextRefresh = guessNextRefresh(movie.airDate ?? new Date()); + const ori = translations[movie.originalLanguage]; + const original = ori + ? { + ...ori, + latinName: ori.latinName ?? null, + language: movie.originalLanguage, + } + : { + name: null, + latinName: null, + language: movie.originalLanguage, + }; - const col = await insertCollection(collection, { - kind: "movie", - nextRefresh, - ...seed, - }); + const col = await insertCollection( + collection, + { + kind: "movie", + nextRefresh, + ...seed, + }, + original, + ); - const original = translations[movie.originalLanguage]; const show = await insertShow( { kind: "movie", @@ -71,17 +86,7 @@ export const seedMovie = async ( entriesCount: 1, ...movie, }, - original - ? { - ...original, - latinName: original.latinName ?? null, - language: movie.originalLanguage, - } - : { - name: null, - latinName: null, - language: movie.originalLanguage, - }, + original, translations, ); if ("status" in show) return show; diff --git a/api/src/controllers/seed/series.ts b/api/src/controllers/seed/series.ts index 36df2a8b..3c5eb6f3 100644 --- a/api/src/controllers/seed/series.ts +++ b/api/src/controllers/seed/series.ts @@ -90,14 +90,29 @@ export const seedSerie = async ( ...serie } = seed; const nextRefresh = guessNextRefresh(serie.startAir ?? new Date()); + const ori = translations[serie.originalLanguage]; + const original = ori + ? { + ...ori, + latinName: ori.latinName ?? null, + language: serie.originalLanguage, + } + : { + name: null, + latinName: null, + language: serie.originalLanguage, + }; - const col = await insertCollection(collection, { - kind: "serie", - nextRefresh, - ...seed, - }); + const col = await insertCollection( + collection, + { + kind: "serie", + nextRefresh, + ...seed, + }, + original, + ); - const original = translations[serie.originalLanguage]; const show = await insertShow( { kind: "serie", @@ -106,17 +121,7 @@ export const seedSerie = async ( entriesCount: entries.length, ...serie, }, - original - ? { - ...original, - latinName: original.latinName ?? null, - language: serie.originalLanguage, - } - : { - name: null, - latinName: null, - language: serie.originalLanguage, - }, + original, translations, ); if ("status" in show) return show; diff --git a/api/src/models/movie.ts b/api/src/models/movie.ts index a2d1706e..e6fb514c 100644 --- a/api/src/models/movie.ts +++ b/api/src/models/movie.ts @@ -102,7 +102,7 @@ export const SeedMovie = t.Composite([ ]), ), videos: t.Optional(t.Array(t.String({ format: "uuid" }), { default: [] })), - collection: t.Optional(SeedCollection), + collection: t.Optional(t.Nullable(SeedCollection)), studios: t.Optional(t.Array(SeedStudio, { default: [] })), staff: t.Optional(t.Array(SeedStaff, { default: [] })), }), diff --git a/api/src/models/serie.ts b/api/src/models/serie.ts index 34fb9461..640d7575 100644 --- a/api/src/models/serie.ts +++ b/api/src/models/serie.ts @@ -120,7 +120,7 @@ export const SeedSerie = t.Composite([ seasons: t.Array(SeedSeason), entries: t.Array(SeedEntry), extras: t.Optional(t.Array(SeedExtra, { default: [] })), - collection: t.Optional(SeedCollection), + collection: t.Optional(t.Nullable(SeedCollection)), studios: t.Optional(t.Array(SeedStudio, { default: [] })), staff: t.Optional(t.Array(SeedStaff, { default: [] })), }), diff --git a/front/src/primitives/links.tsx b/front/src/primitives/links.tsx index 0bf85a95..f6003a08 100644 --- a/front/src/primitives/links.tsx +++ b/front/src/primitives/links.tsx @@ -60,7 +60,7 @@ export const A = ({

{ // biome-ignore lint/correctness/useHookAtTopLevel: const condition useEffect(() => { const enableHover = () => { - console.log("pc"); if (preventHover) return; document.body.classList.remove("noHover"); }; const disableHover = () => { - console.log("mobile"); if (hoverTimeout) clearTimeout(hoverTimeout); preventHover = true; hoverTimeout = setTimeout(() => { diff --git a/front/src/providers/account-provider.tsx b/front/src/providers/account-provider.tsx index 752088a7..6fff812a 100644 --- a/front/src/providers/account-provider.tsx +++ b/front/src/providers/account-provider.tsx @@ -4,7 +4,6 @@ import { type ReactNode, useEffect, useMemo, useRef } from "react"; import { Platform } from "react-native"; import { z } from "zod/v4"; import { Account, User } from "~/models"; -import { RetryableError } from "~/models/retryable-error"; import { useFetch } from "~/query"; import { AccountContext } from "./account-context"; import { removeAccounts, updateAccount } from "./account-store"; @@ -30,9 +29,8 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => { }; }, [accounts]); + const router = useRouter(); if (Platform.OS !== "web") { - // biome-ignore lint/correctness/useHookAtTopLevel: static - const router = useRouter(); // biome-ignore lint/correctness/useHookAtTopLevel: static useEffect(() => { if (!ret.apiUrl) { @@ -57,13 +55,13 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => { options: { apiUrl: ret.apiUrl, authToken: ret.authToken, + returnError: true, }, }); if (userError) { - throw new RetryableError({ - key: "connection", - retry: queryClient.resetQueries, - }); + setTimeout(() => { + router.replace("/login"); + }, 0); } // Use a ref here because we don't want the effect to trigger when the selected // value has changed, only when the fetch result changed diff --git a/scanner/scanner/models/serie.py b/scanner/scanner/models/serie.py index d6796bf0..cdf0402c 100644 --- a/scanner/scanner/models/serie.py +++ b/scanner/scanner/models/serie.py @@ -35,7 +35,7 @@ class Serie(Model): translations: dict[Language, SerieTranslation] = {} seasons: list[Season] = [] entries: list[Entry] = [] - extra: list[Extra] = [] + extras: list[Extra] = [] collection: Collection | None = None studios: list[Studio] = [] staff: list[Staff] = [] diff --git a/scanner/scanner/providers/composite.py b/scanner/scanner/providers/composite.py index 8ba790e8..9b44c031 100644 --- a/scanner/scanner/providers/composite.py +++ b/scanner/scanner/providers/composite.py @@ -3,6 +3,7 @@ from typing import override from langcodes import Language from scanner.models.metadataid import MetadataId +from scanner.utils import uniq_by from ..models.movie import Movie, SearchMovie from ..models.serie import SearchSerie, Serie @@ -52,6 +53,12 @@ class CompositeProvider(Provider): ret = await self._tvdb.get_serie(external_id) if ret is None: return None + + # some series have duplicates special numbers/episode numbers, sensitize them + ret.entries = uniq_by( + ret.entries, lambda x: (x.season_number, x.episode_number, x.number, x.slug) + ) + # themoviedb has better global info than tvdb but tvdb has better entries info info = await self._themoviedb.get_serie( MetadataId.map_dict(ret.external_id), skip_entries=True @@ -60,7 +67,7 @@ class CompositeProvider(Provider): return ret info.seasons = ret.seasons info.entries = ret.entries - info.extra = ret.extra + info.extras = ret.extras if ret.collection is not None: info.collection = ret.collection info.external_id = MetadataId.merge(ret.external_id, info.external_id) diff --git a/scanner/scanner/providers/themoviedatabase.py b/scanner/scanner/providers/themoviedatabase.py index 08a5307b..cadce35c 100644 --- a/scanner/scanner/providers/themoviedatabase.py +++ b/scanner/scanner/providers/themoviedatabase.py @@ -386,7 +386,7 @@ class TheMovieDatabase(Provider): }, seasons=seasons, entries=entries, - extra=[], + extras=[], collection=None, studios=[self._map_studio(x) for x in serie["production_companies"]], # TODO: add crew diff --git a/scanner/scanner/providers/thetvdb.py b/scanner/scanner/providers/thetvdb.py index cffac8e4..67036ad3 100644 --- a/scanner/scanner/providers/thetvdb.py +++ b/scanner/scanner/providers/thetvdb.py @@ -292,8 +292,8 @@ class TVDB(Provider): else [], entries=entries, # TODO: map extra entries in extra instead of entries - extra=[], - collection=await self._get_collection(ret["list"]), + extras=[], + collection=await self._get_collection(ret["lists"]), studios=[], staff=[], ) @@ -358,7 +358,11 @@ class TVDB(Provider): data = (await self._get(f"lists/{col['id']}/extended"))["data"] first_entity = data["entities"][0] - kind = "movie" if "movieId" in first_entity else "series" + kind = ( + "movie" + if "movieId" in first_entity and first_entity["movieId"] is not None + else "series" + ) show = ( ( await self._get( @@ -385,15 +389,11 @@ class TVDB(Provider): (x["name"] for x in trans if x.get("isPrimary")), data["name"] ), latin_name=None, - description=trans.get("overview"), + description=trans[0].get("overview"), tagline=None, - aliases=[ - x["name"] - for x in trans["aliases"] - if x["language"] == lang and x.get("isAlias") - ], + aliases=[x["name"] for x in trans if x.get("isAlias")], tags=[], - poster=trans.get("image") + poster=data.get("image") if lang == "eng" else self._pick_image(show["artworks"], kind, "posters", lang), thumbnail=self._pick_image(show["artworks"], kind, "backgrounds", lang), @@ -618,7 +618,7 @@ class TVDB(Provider): None, ), poster=self._pick_image( - ret["artworks"], "episode", "posters", trans["language"] + ret["artworks"], "movie", "posters", trans["language"] ), ) for trans in ret["translations"]["nameTranslations"] @@ -648,9 +648,12 @@ class TVDB(Provider): search = await self._get( f"search/remoteid/{external_id[ProviderName.IMDB]}" ) - if len(search["data"]) > 0: - id = search["data"][0].get("movie")["id"] - return await self.get_movie({self.name: id}) + if search["data"] is not None and len(search["data"]) > 0: + movie = search["data"][0].get("movie") + mId = movie.get("id") if movie is not None else None + if mId is None: + return None + return await self.get_movie({self.name: mId}) return None ret = ( @@ -675,7 +678,7 @@ class TVDB(Provider): status=MovieStatus.FINISHED if ret["status"]["name"] == "Ended" else MovieStatus.PLANNED, - runtime=ret["averageRuntime"], + runtime=ret["runtime"], air_date=datetime.strptime(ret["first_release"]["date"], "%Y-%m-%d").date() if ret.get("first_release") and ret["first_release"].get("date") else None, @@ -727,7 +730,7 @@ class TVDB(Provider): for trans in ret["translations"]["nameTranslations"] if trans.get("isAlias") is None or False }, - collection=await self._get_collection(ret["list"]), + collection=await self._get_collection(ret["lists"]), studios=[], staff=[], ) diff --git a/scanner/scanner/utils.py b/scanner/scanner/utils.py index a0b2c4a2..af3d4763 100644 --- a/scanner/scanner/utils.py +++ b/scanner/scanner/utils.py @@ -19,6 +19,19 @@ def clean(val: str) -> str | None: return val or None +def uniq_by[T, K](data: list[T], key: Callable[[T], K]) -> list[T]: + seen = set() + ret = [] + + for item in data: + val = key(item) + if val not in seen: + seen.add(val) + ret.append(item) + + return ret + + class Singleton(ABCMeta, type): _instances = {}