From a0ff1c3dfb0d45911c818c3aca74251d5ee1bcad Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 18 Mar 2026 20:58:43 +0100 Subject: [PATCH] Allow multiple ratings per serie/movie (#1377) --- api/drizzle/0028_rating_jsonb.sql | 5 +++ api/drizzle/meta/_journal.json | 7 ++++ api/src/controllers/shows/logic.ts | 11 +++++- api/src/db/schema/shows.ts | 5 +-- api/src/models/collections.ts | 6 ++- api/src/models/examples/bubble.ts | 2 +- api/src/models/examples/dune-1984.ts | 2 +- api/src/models/examples/dune-2021.ts | 2 +- api/src/models/examples/dune-collection.ts | 2 +- api/src/models/examples/made-in-abyss.ts | 2 +- api/src/models/movie.ts | 6 ++- api/src/models/serie.ts | 6 ++- api/src/models/utils/filters/index.ts | 10 +++-- api/src/models/utils/filters/parser.ts | 23 ++++++++++- api/src/models/utils/filters/to-sql.ts | 38 ++++++++++++++----- api/src/models/utils/sort.ts | 33 +++++++++++++--- api/tests/misc/filter.test.ts | 30 +++++++-------- .../movies/get-all-movies-with-null.test.ts | 2 +- front/src/components/rating.tsx | 10 ++++- front/src/models/collection.ts | 2 +- front/src/models/movie.ts | 2 +- front/src/models/serie.ts | 2 +- front/src/ui/browse/index.tsx | 4 +- front/src/ui/details/header.tsx | 4 +- scanner/scanner/models/collection.py | 2 +- scanner/scanner/models/movie.py | 2 +- scanner/scanner/models/serie.py | 2 +- scanner/scanner/providers/themoviedatabase.py | 12 +++--- scanner/scanner/providers/thetvdb.py | 6 +-- 29 files changed, 171 insertions(+), 69 deletions(-) create mode 100644 api/drizzle/0028_rating_jsonb.sql diff --git a/api/drizzle/0028_rating_jsonb.sql b/api/drizzle/0028_rating_jsonb.sql new file mode 100644 index 00000000..c606e871 --- /dev/null +++ b/api/drizzle/0028_rating_jsonb.sql @@ -0,0 +1,5 @@ +DROP INDEX "kyoo"."rating";--> statement-breakpoint +ALTER TABLE "kyoo"."shows" DROP CONSTRAINT "rating_valid";--> statement-breakpoint +ALTER TABLE "kyoo"."shows" ALTER COLUMN "rating" SET DATA TYPE jsonb USING COALESCE(jsonb_build_object('legacy', "rating"), '{}');--> statement-breakpoint +ALTER TABLE "kyoo"."shows" ALTER COLUMN "rating" SET DEFAULT '{}'::jsonb;--> statement-breakpoint +ALTER TABLE "kyoo"."shows" ALTER COLUMN "rating" SET NOT NULL; diff --git a/api/drizzle/meta/_journal.json b/api/drizzle/meta/_journal.json index 5a17ec15..ef374890 100644 --- a/api/drizzle/meta/_journal.json +++ b/api/drizzle/meta/_journal.json @@ -197,6 +197,13 @@ "when": 1771505332722, "tag": "0027_show_slug", "breakpoints": true + }, + { + "idx": 28, + "version": "7", + "when": 1771600000000, + "tag": "0028_rating_jsonb", + "breakpoints": true } ] } diff --git a/api/src/controllers/shows/logic.ts b/api/src/controllers/shows/logic.ts index 6fdf2da7..8394669b 100644 --- a/api/src/controllers/shows/logic.ts +++ b/api/src/controllers/shows/logic.ts @@ -66,7 +66,6 @@ export const showFilters: FilterDef = { values: Genre.enum, isArray: true, }, - rating: { column: shows.rating, type: "int" }, status: { column: shows.status, type: "enum", values: SerieStatus.enum }, runtime: { column: shows.runtime, type: "float" }, airDate: { column: shows.startAir, type: "date" }, @@ -88,6 +87,10 @@ export const showFilters: FilterDef = { }, score: { column: watchStatusQ.score, type: "int" }, isAvailable: { column: sql`(${shows.availableCount} > 0)`, type: "bool" }, + rating: { + column: (source: string) => sql`(${shows.rating}->>${source})::int`, + type: "int", + }, }; export const showSort = Sort( { @@ -97,7 +100,6 @@ export const showSort = Sort( isNullable: false, accessor: (x) => x.name, }, - rating: shows.rating, airDate: shows.startAir, startAir: shows.startAir, endAir: shows.endAir, @@ -105,6 +107,11 @@ export const showSort = Sort( nextRefresh: shows.nextRefresh, watchStatus: watchStatusQ.status, score: watchStatusQ.score, + rating: (source: string) => ({ + sql: sql`(${shows.rating}->>${source})::int`, + isNullable: true, + accessor: (x: any) => x.rating?.[source] ?? null, + }), }, { default: ["slug"], diff --git a/api/src/db/schema/shows.ts b/api/src/db/schema/shows.ts index f53cbec0..120ef2b5 100644 --- a/api/src/db/schema/shows.ts +++ b/api/src/db/schema/shows.ts @@ -7,7 +7,6 @@ import { integer, jsonb, primaryKey, - smallint, text, timestamp, unique, @@ -73,7 +72,7 @@ export const shows = schema.table( slug: varchar({ length: 255 }).notNull(), kind: showKind().notNull(), genres: genres().array().notNull(), - rating: smallint(), + rating: jsonb().$type>().notNull().default({}), runtime: integer(), status: showStatus().notNull(), startAir: date(), @@ -99,12 +98,10 @@ export const shows = schema.table( (t) => [ unique("kind_slug").on(t.kind, t.slug), - check("rating_valid", sql`${t.rating} between 0 and 100`), check("runtime_valid", sql`${t.runtime} >= 0`), index("kind").using("hash", t.kind), index("slug").on(t.slug), - index("rating").on(t.rating), index("startAir").on(t.startAir), ], ); diff --git a/api/src/models/collections.ts b/api/src/models/collections.ts index 05a37b68..1fa980d4 100644 --- a/api/src/models/collections.ts +++ b/api/src/models/collections.ts @@ -15,7 +15,11 @@ import { const BaseCollection = t.Object({ genres: t.Array(Genre), - rating: t.Nullable(t.Integer({ minimum: 0, maximum: 100 })), + rating: t.Record(t.String(), t.Integer({ minimum: 0, maximum: 100 }), { + default: {}, + description: + "Rating from various sources (0-100 scale). Keys are source names, values are ratings.", + }), startAir: t.Nullable( t.String({ format: "date", diff --git a/api/src/models/examples/bubble.ts b/api/src/models/examples/bubble.ts index a17d674e..87063b18 100644 --- a/api/src/models/examples/bubble.ts +++ b/api/src/models/examples/bubble.ts @@ -52,7 +52,7 @@ export const bubble: SeedMovie = { }, }, genres: ["animation", "adventure", "science-fiction", "fantasy"], - rating: 74, + rating: { themoviedatabase: 74 }, status: "finished", runtime: 101, airDate: "2022-02-14", diff --git a/api/src/models/examples/dune-1984.ts b/api/src/models/examples/dune-1984.ts index 6dc6b364..8837b617 100644 --- a/api/src/models/examples/dune-1984.ts +++ b/api/src/models/examples/dune-1984.ts @@ -38,7 +38,7 @@ export const dune1984: SeedMovie = { }, }, genres: ["adventure", "drama", "science-fiction"], - rating: 60, + rating: { themoviedatabase: 60 }, status: "finished", runtime: 137, airDate: "1984-12-14", diff --git a/api/src/models/examples/dune-2021.ts b/api/src/models/examples/dune-2021.ts index c5d00f5a..7bbeccf8 100644 --- a/api/src/models/examples/dune-2021.ts +++ b/api/src/models/examples/dune-2021.ts @@ -38,7 +38,7 @@ export const dune: SeedMovie = { }, }, genres: ["adventure", "drama", "science-fiction", "action"], - rating: 83, + rating: { themoviedatabase: 83 }, status: "finished", runtime: 155, airDate: "2021-10-22", diff --git a/api/src/models/examples/dune-collection.ts b/api/src/models/examples/dune-collection.ts index eb6c9707..d1346518 100644 --- a/api/src/models/examples/dune-collection.ts +++ b/api/src/models/examples/dune-collection.ts @@ -20,7 +20,7 @@ export const duneCollection: SeedCollection = { }, originalLanguage: "en", genres: ["adventure", "science-fiction"], - rating: 80, + rating: { themoviedatabase: 80 }, externalId: { themoviedatabase: [ { diff --git a/api/src/models/examples/made-in-abyss.ts b/api/src/models/examples/made-in-abyss.ts index e44f8c6b..da3b55fa 100644 --- a/api/src/models/examples/made-in-abyss.ts +++ b/api/src/models/examples/made-in-abyss.ts @@ -103,7 +103,7 @@ export const madeInAbyss = { "fantasy", ], status: "finished", - rating: 84, + rating: { themoviedatabase: 84 }, runtime: 24, originalLanguage: "ja", startAir: "2017-07-07", diff --git a/api/src/models/movie.ts b/api/src/models/movie.ts index ace47e2a..fbfcadd7 100644 --- a/api/src/models/movie.ts +++ b/api/src/models/movie.ts @@ -23,7 +23,11 @@ export type MovieStatus = typeof MovieStatus.static; const BaseMovie = t.Object({ genres: t.Array(Genre), - rating: t.Nullable(t.Integer({ minimum: 0, maximum: 100 })), + rating: t.Record(t.String(), t.Integer({ minimum: 0, maximum: 100 }), { + default: {}, + description: + "Rating from various sources (0-100 scale). Keys are source names, values are ratings.", + }), status: MovieStatus, runtime: t.Nullable( t.Number({ minimum: 0, description: "Runtime of the movie in minutes." }), diff --git a/api/src/models/serie.ts b/api/src/models/serie.ts index 9feb8e41..23131d44 100644 --- a/api/src/models/serie.ts +++ b/api/src/models/serie.ts @@ -29,7 +29,11 @@ export type SerieStatus = typeof SerieStatus.static; const BaseSerie = t.Object({ genres: t.Array(Genre), - rating: t.Nullable(t.Integer({ minimum: 0, maximum: 100 })), + rating: t.Record(t.String(), t.Integer({ minimum: 0, maximum: 100 }), { + default: {}, + description: + "Rating from various sources (0-100 scale). Keys are source names, values are ratings.", + }), status: SerieStatus, runtime: t.Nullable( t.Number({ diff --git a/api/src/models/utils/filters/index.ts b/api/src/models/utils/filters/index.ts index 140c7c37..8ee10e5f 100644 --- a/api/src/models/utils/filters/index.ts +++ b/api/src/models/utils/filters/index.ts @@ -8,7 +8,7 @@ import { toDrizzle } from "./to-sql"; export type FilterDef = { [key: string]: | { - column: Column | SQLWrapper; + column: Column | SQLWrapper | ((param: string) => Column | SQLWrapper); type: "int" | "float" | "date" | "string" | "bool"; isArray?: boolean; } @@ -34,9 +34,13 @@ export const Filter = ({ ${description} This is based on [odata's filter specification](https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionfilter). - Filters available: ${Object.keys(def).join(", ")}. + Filters available: ${Object.keys(def) + .map((x) => + typeof def[x].column === "function" ? `${x}:param` : x, + ) + .join(", ")}. `, - example: "(rating gt 75 and genres has action) or status eq planned", + example: "(runtime gt 75 and genres has action) or rating:tmdb ge 50", }), ) .Decode((filter) => { diff --git a/api/src/models/utils/filters/parser.ts b/api/src/models/utils/filters/parser.ts index ec845d55..3f2e68b5 100644 --- a/api/src/models/utils/filters/parser.ts +++ b/api/src/models/utils/filters/parser.ts @@ -17,6 +17,7 @@ import { many, many1, map, + maybe, or, qthen, recover, @@ -25,7 +26,11 @@ import { thenq, } from "parjs/combinators"; -export type Property = string; +export type Property = { + type: "property"; + name: string; + param: string | null; +}; export type Value = | { type: "int"; value: number } | { type: "float"; value: number } @@ -57,7 +62,20 @@ const enumP = t( .expects("an enum value"), ); -const property = t(letter().pipe(many1(), stringify())).expects("a property"); +const property = t( + letter().pipe( + many1(), + stringify(), + then( + string(":").pipe(qthen(letter().pipe(many1(), stringify())), maybe(null)), + ), + map(([prop, param]) => ({ + type: "property" as const, + name: prop, + param: param, + })), + ), +).expects("a property"); const intVal = t(int().pipe(map((i) => ({ type: "int" as const, value: i })))); const floatVal = t( @@ -87,6 +105,7 @@ const strVal = t(noCharOf('"').pipe(many1(), stringify(), between('"'))).pipe( ); const enumVal = enumP.pipe(map((e) => ({ type: "enum" as const, value: e }))); const value = dateVal + .pipe( // until we get the `-` character, this could be an int or a float. recover(() => ({ kind: "Soft" })), diff --git a/api/src/models/utils/filters/to-sql.ts b/api/src/models/utils/filters/to-sql.ts index 62ca044c..f1ed1f93 100644 --- a/api/src/models/utils/filters/to-sql.ts +++ b/api/src/models/utils/filters/to-sql.ts @@ -30,19 +30,39 @@ const opMap: Record = { export const toDrizzle = (expr: Expression, config: FilterDef): SQL => { switch (expr.type) { case "op": { - const where = `${expr.property} ${expr.operator} ${expr.value.value}`; - const prop = config[expr.property]; + const where = `${expr.property.name}${expr.property.param ? `:${expr.property.param}` : ""} ${expr.operator} ${expr.value.value}`; + const prop = config[expr.property.name]; if (!prop) { throw new KErrorT( comment` - Invalid property: ${expr.property}. + Invalid property: ${expr.property.name}. Expected one of ${Object.keys(config).join(", ")}. `, { in: where }, ); } + if (typeof prop.column === "function" && !expr.property.param) { + throw new KErrorT( + comment` + Property ${expr.property.name} requires a parameter (e.g., ${expr.property.name}:source). + `, + { in: where }, + ); + } else if (typeof prop.column !== "function" && expr.property.param) { + throw new KErrorT( + comment` + Property ${expr.property.name} does not accept a parameter. + `, + { in: where }, + ); + } + const column = + typeof prop.column === "function" + ? prop.column(expr.property.param!) + : prop.column; + if (expr.value.type === "enum" && prop.type === "string") { // promote enum to string since this is legal // but parser doesn't know if an enum should be a string @@ -52,7 +72,7 @@ export const toDrizzle = (expr: Expression, config: FilterDef): SQL => { if (expr.value.value !== "false" && expr.value.value !== "true") { throw new KErrorT( comment` - Invalid value for property ${expr.property}. + Invalid value for property ${expr.property.name}. Get ${expr.value.value} but expected true or false. `, { in: where }, @@ -63,7 +83,7 @@ export const toDrizzle = (expr: Expression, config: FilterDef): SQL => { if (prop.type !== expr.value.type) { throw new KErrorT( comment` - Invalid value for property ${expr.property}. + Invalid value for property ${expr.property.name}. Got ${expr.value.type} but expected ${prop.type}. `, { in: where }, @@ -76,7 +96,7 @@ export const toDrizzle = (expr: Expression, config: FilterDef): SQL => { ) { throw new KErrorT( comment` - Invalid value ${expr.value.value} for property ${expr.property}. + Invalid value ${expr.value.value} for property ${expr.property.name}. Expected one of ${prop.values.join(", ")} but got ${expr.value.value}. `, { in: where }, @@ -87,15 +107,15 @@ export const toDrizzle = (expr: Expression, config: FilterDef): SQL => { if (expr.operator !== "has" && expr.operator !== "eq") { throw new KErrorT( comment` - Property ${expr.property} is an array but you wanted to use the + Property ${expr.property.name} is an array but you wanted to use the operator ${expr.operator}. Only "has" is supported ("eq" is also aliased to "has") `, { in: where }, ); } - return sql`${expr.value.value} = any(${prop.column})`; + return sql`${expr.value.value} = any(${column})`; } - return opMap[expr.operator](prop.column, expr.value.value); + return opMap[expr.operator](column, expr.value.value); } case "and": { const lhs = toDrizzle(expr.lhs, config); diff --git a/api/src/models/utils/sort.ts b/api/src/models/utils/sort.ts index 2decc59a..8f25ed75 100644 --- a/api/src/models/utils/sort.ts +++ b/api/src/models/utils/sort.ts @@ -26,7 +26,7 @@ export type SortVal = }; export const Sort = ( - values: Record, + values: Record SortVal)>, { description = "How to sort the query", default: def, @@ -36,17 +36,26 @@ export const Sort = ( tablePk: SQLWrapper; description?: string; }, -) => - t +) => { + const staticKeys = Object.keys(values).filter( + (k) => typeof values[k] !== "function", + ); + const paramKeys = Object.keys(values).filter( + (k) => typeof values[k] === "function", + ); + + return t .Transform( t.Array( t.Union([ t.UnionEnum([ "random", - ...Object.keys(values), - ...Object.keys(values).map((x) => `-${x}`), + ...staticKeys, + ...staticKeys.map((x) => `-${x}`), ]), t.TemplateLiteral("random:${number}"), + ...paramKeys.map((k) => t.TemplateLiteral(`${k}:\${string}`)), + ...paramKeys.map((k) => t.TemplateLiteral(`-${k}:\${string}`)), ]), { default: def, @@ -67,7 +76,8 @@ export const Sort = ( tablePk, sort: sort.flatMap((x) => { const desc = x[0] === "-"; - const key = desc ? x.substring(1) : x; + const [key, param] = (desc ? x.substring(1) : x).split(":", 2); + const process = (val: SortVal): Sort["sort"][0] => { if ("getSQL" in val) { return { @@ -85,6 +95,16 @@ export const Sort = ( desc, }; }; + + if (typeof values[key] === "function") { + if (!param) { + throw new Error( + `Sort key "${key}" requires a parameter (e.g., ${key}:source)`, + ); + } + return process(values[key](param)); + } + return Array.isArray(values[key]) ? values[key].map(process) : process(values[key]); @@ -94,6 +114,7 @@ export const Sort = ( .Encode(() => { throw new Error("Encode not supported for sort"); }); +}; export const sortToSql = (sort: Sort | undefined) => { if (!sort) return []; diff --git a/api/tests/misc/filter.test.ts b/api/tests/misc/filter.test.ts index 07a263d5..576ecbfd 100644 --- a/api/tests/misc/filter.test.ts +++ b/api/tests/misc/filter.test.ts @@ -28,7 +28,7 @@ describe("Parse filter", () => { value: { type: "op", operator: "eq", - property: "status", + property: { name: "status" }, value: { type: "enum", value: "finished" }, }, }); @@ -40,7 +40,7 @@ describe("Parse filter", () => { value: { type: "op", operator: "lt", - property: "rating", + property: { name: "rating" }, value: { type: "int", value: 10 }, }, }); @@ -52,7 +52,7 @@ describe("Parse filter", () => { value: { type: "op", operator: "ge", - property: "airDate", + property: { name: "airDate" }, value: { type: "date", value: "2022-10-12" }, }, }); @@ -66,7 +66,7 @@ describe("Parse filter", () => { expression: { type: "op", operator: "lt", - property: "rating", + property: { name: "rating" }, value: { type: "int", value: 10 }, }, }, @@ -79,7 +79,7 @@ describe("Parse filter", () => { value: { type: "op", operator: "lt", - property: "rating", + property: { name: "rating" }, value: { type: "int", value: 10 }, }, }); @@ -93,7 +93,7 @@ describe("Parse filter", () => { expression: { type: "op", operator: "lt", - property: "rating", + property: { name: "rating" }, value: { type: "int", value: 10 }, }, }, @@ -110,14 +110,14 @@ describe("Parse filter", () => { expression: { type: "op", operator: "lt", - property: "rating", + property: { name: "rating" }, value: { type: "int", value: 10 }, }, }, rhs: { type: "op", operator: "lt", - property: "rating", + property: { name: "rating" }, value: { type: "int", value: 20 }, }, }, @@ -138,14 +138,14 @@ describe("Parse filter", () => { expression: { type: "op", operator: "lt", - property: "rating", + property: { name: "rating" }, value: { type: "int", value: 10 }, }, }, rhs: { type: "op", operator: "lt", - property: "rating", + property: { name: "rating" }, value: { type: "int", value: 20 }, }, }, @@ -154,7 +154,7 @@ describe("Parse filter", () => { lhs: { type: "op", operator: "eq", - property: "status", + property: { name: "status" }, value: { type: "enum", value: "finished" }, }, rhs: { @@ -162,7 +162,7 @@ describe("Parse filter", () => { expression: { type: "op", operator: "ne", - property: "status", + property: { name: "status" }, value: { type: "enum", value: "airing" }, }, }, @@ -177,7 +177,7 @@ describe("Parse filter", () => { value: { type: "op", operator: "eq", - property: "tags", + property: { name: "tags" }, value: { type: "string", value: "magic armor" }, }, }); @@ -189,7 +189,7 @@ describe("Parse filter", () => { value: { type: "op", operator: "eq", - property: "tags", + property: { name: "tags" }, value: { type: "string", value: "magic armor" }, }, }); @@ -202,7 +202,7 @@ describe("Parse filter", () => { value: { type: "op", operator: "eq", - property: "tags", + property: { name: "tags" }, // this is parsed as enum but is handled afterwards value: { type: "enum", value: "magic" }, }, diff --git a/api/tests/movies/get-all-movies-with-null.test.ts b/api/tests/movies/get-all-movies-with-null.test.ts index 83e6e348..0ad98bfe 100644 --- a/api/tests/movies/get-all-movies-with-null.test.ts +++ b/api/tests/movies/get-all-movies-with-null.test.ts @@ -39,7 +39,7 @@ describe("with a null value", () => { }, genres: [], status: "unknown", - rating: null, + rating: {}, runtime: null, airDate: null, originalLanguage: "en", diff --git a/front/src/components/rating.tsx b/front/src/components/rating.tsx index 2c709d13..b9410d92 100644 --- a/front/src/components/rating.tsx +++ b/front/src/components/rating.tsx @@ -10,16 +10,22 @@ export const Rating = ({ iconClassName, ...props }: { - rating: number | null; + rating: Record; className?: string; textClassName?: string; iconClassName?: string; }) => { + const values = Object.values(rating); + const avg = + values.length > 0 + ? values.reduce((a, b) => a + b, 0) / values.length + : null; + return (

- {rating ? rating / 10 : "??"} / 10 + {avg !== null ? Math.round(avg) / 10 : "??"} / 10

); diff --git a/front/src/models/collection.ts b/front/src/models/collection.ts index 4fbb652f..3181d9c9 100644 --- a/front/src/models/collection.ts +++ b/front/src/models/collection.ts @@ -18,7 +18,7 @@ export const Collection = z aliases: z.array(z.string()), tags: z.array(z.string()), description: z.string().nullable(), - rating: z.number().int().gte(0).lte(100).nullable(), + rating: z.record(z.string(), z.number().int().gte(0).lte(100)), startAir: zdate().nullable(), endAir: zdate().nullable(), genres: z.array(Genre), diff --git a/front/src/models/movie.ts b/front/src/models/movie.ts index 53e361aa..51305028 100644 --- a/front/src/models/movie.ts +++ b/front/src/models/movie.ts @@ -21,7 +21,7 @@ export const Movie = z tags: z.array(z.string()), description: z.string().nullable(), status: z.enum(["unknown", "finished", "planned"]), - rating: z.number().int().gte(0).lte(100), + rating: z.record(z.string(), z.number().int().gte(0).lte(100)), runtime: z.number().int().nullable(), airDate: zdate().nullable(), genres: z.array(Genre), diff --git a/front/src/models/serie.ts b/front/src/models/serie.ts index 68f6abaf..ae46f3be 100644 --- a/front/src/models/serie.ts +++ b/front/src/models/serie.ts @@ -22,7 +22,7 @@ export const Serie = z tags: z.array(z.string()), description: z.string().nullable(), status: z.enum(["unknown", "finished", "airing", "planned"]), - rating: z.number().int().gte(0).lte(100).nullable(), + rating: z.record(z.string(), z.number().int().gte(0).lte(100)), startAir: zdate().nullable(), endAir: zdate().nullable(), genres: z.array(Genre), diff --git a/front/src/ui/browse/index.tsx b/front/src/ui/browse/index.tsx index 6cba0274..fa0ac369 100644 --- a/front/src/ui/browse/index.tsx +++ b/front/src/ui/browse/index.tsx @@ -56,7 +56,9 @@ BrowsePage.query = ({ path: ["api", "shows"], infinite: true, params: { - sort: sortBy ? `${sortOrd === "desc" ? "-" : ""}${sortBy}` : "name", + sort: sortBy + ? `${sortOrd === "desc" ? "-" : ""}${sortBy === "rating" ? "rating:themoviedatabase" : sortBy}` + : "name", filter, query: search, }, diff --git a/front/src/ui/details/header.tsx b/front/src/ui/details/header.tsx index c54eee8e..5c2947f0 100644 --- a/front/src/ui/details/header.tsx +++ b/front/src/ui/details/header.tsx @@ -190,7 +190,7 @@ export const TitleLine = ({ name: string; tagline: string | null; date: string | null; - rating: number | null; + rating: Record; runtime: number | null; poster: KImage | null; trailerUrl: string | null; @@ -231,7 +231,7 @@ export const TitleLine = ({ watchStatus={watchStatus} iconsClassName="lg:fill-slate-200 dark:fill-slate-200" /> - {rating !== null && rating !== 0 && ( + {Object.keys(rating).length > 0 && ( <>