Fix collection insertion

This commit is contained in:
Zoe Roux 2026-02-17 09:20:01 +01:00
parent 752f091dbf
commit 7c781e533e
No known key found for this signature in database
13 changed files with 105 additions and 68 deletions

View File

@ -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({

View File

@ -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;

View File

@ -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;

View File

@ -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: [] })),
}),

View File

@ -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: [] })),
}),

View File

@ -60,7 +60,7 @@ export const A = ({
<P
{...linkProps}
className={cn(
"select-text text-accent hover:underline focus:underline",
"select-text text-accent hover:underline focus:underline dark:text-accent",
className,
)}
{...props}

View File

@ -10,13 +10,11 @@ export const useMobileHover = () => {
// 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(() => {

View File

@ -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

View File

@ -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] = []

View File

@ -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)

View File

@ -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

View File

@ -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=[],
)

View File

@ -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 = {}