mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-03-22 17:37:49 -04:00
Allow multiple ratings per serie/movie (#1377)
This commit is contained in:
parent
02ad4dabcd
commit
a0ff1c3dfb
5
api/drizzle/0028_rating_jsonb.sql
Normal file
5
api/drizzle/0028_rating_jsonb.sql
Normal file
@ -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;
|
||||
@ -197,6 +197,13 @@
|
||||
"when": 1771505332722,
|
||||
"tag": "0027_show_slug",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 28,
|
||||
"version": "7",
|
||||
"when": 1771600000000,
|
||||
"tag": "0028_rating_jsonb",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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"],
|
||||
|
||||
@ -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<Record<string, number>>().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),
|
||||
],
|
||||
);
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -20,7 +20,7 @@ export const duneCollection: SeedCollection = {
|
||||
},
|
||||
originalLanguage: "en",
|
||||
genres: ["adventure", "science-fiction"],
|
||||
rating: 80,
|
||||
rating: { themoviedatabase: 80 },
|
||||
externalId: {
|
||||
themoviedatabase: [
|
||||
{
|
||||
|
||||
@ -103,7 +103,7 @@ export const madeInAbyss = {
|
||||
"fantasy",
|
||||
],
|
||||
status: "finished",
|
||||
rating: 84,
|
||||
rating: { themoviedatabase: 84 },
|
||||
runtime: 24,
|
||||
originalLanguage: "ja",
|
||||
startAir: "2017-07-07",
|
||||
|
||||
@ -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." }),
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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" })),
|
||||
|
||||
@ -30,19 +30,39 @@ const opMap: Record<Operator, BinaryOperator> = {
|
||||
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);
|
||||
|
||||
@ -26,7 +26,7 @@ export type SortVal =
|
||||
};
|
||||
|
||||
export const Sort = (
|
||||
values: Record<string, SortVal | SortVal[]>,
|
||||
values: Record<string, SortVal | SortVal[] | ((param: string) => 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 [];
|
||||
|
||||
@ -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" },
|
||||
},
|
||||
|
||||
@ -39,7 +39,7 @@ describe("with a null value", () => {
|
||||
},
|
||||
genres: [],
|
||||
status: "unknown",
|
||||
rating: null,
|
||||
rating: {},
|
||||
runtime: null,
|
||||
airDate: null,
|
||||
originalLanguage: "en",
|
||||
|
||||
@ -10,16 +10,22 @@ export const Rating = ({
|
||||
iconClassName,
|
||||
...props
|
||||
}: {
|
||||
rating: number | null;
|
||||
rating: Record<string, number>;
|
||||
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 (
|
||||
<View className={cn("flex-row items-center", className)} {...props}>
|
||||
<Icon icon={Star} className={cn("mr-1", iconClassName)} />
|
||||
<P className={cn("align-middle", textClassName)}>
|
||||
{rating ? rating / 10 : "??"} / 10
|
||||
{avg !== null ? Math.round(avg) / 10 : "??"} / 10
|
||||
</P>
|
||||
</View>
|
||||
);
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -190,7 +190,7 @@ export const TitleLine = ({
|
||||
name: string;
|
||||
tagline: string | null;
|
||||
date: string | null;
|
||||
rating: number | null;
|
||||
rating: Record<string, number>;
|
||||
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 && (
|
||||
<>
|
||||
<DottedSeparator className="lg:text-slate-200 dark:text-slate-200" />
|
||||
<Rating
|
||||
|
||||
@ -9,7 +9,7 @@ class Collection(Model):
|
||||
slug: str
|
||||
original_language: Language | None
|
||||
genres: list[Genre]
|
||||
rating: int | None
|
||||
rating: dict[str, int]
|
||||
external_id: dict[str, list[MetadataId]]
|
||||
|
||||
translations: dict[Language, CollectionTranslation] = {}
|
||||
|
||||
@ -21,7 +21,7 @@ class Movie(Model):
|
||||
slug: str
|
||||
original_language: Language | None
|
||||
genres: list[Genre]
|
||||
rating: int | None
|
||||
rating: dict[str, int]
|
||||
status: MovieStatus
|
||||
runtime: int | None
|
||||
air_date: date | None
|
||||
|
||||
@ -25,7 +25,7 @@ class Serie(Model):
|
||||
slug: str
|
||||
original_language: Language | None
|
||||
genres: list[Genre]
|
||||
rating: int | None
|
||||
rating: dict[str, int]
|
||||
status: SerieStatus
|
||||
runtime: int | None
|
||||
start_air: date | None
|
||||
|
||||
@ -155,7 +155,7 @@ class TheMovieDatabase(Provider):
|
||||
slug=to_slug(movie["title"]),
|
||||
original_language=Language.get(movie["original_language"]),
|
||||
genres=self._map_genres(x["id"] for x in movie["genres"]),
|
||||
rating=round(float(movie["vote_average"]) * 10),
|
||||
rating={self.name: round(float(movie["vote_average"]) * 10)},
|
||||
status=MovieStatus.FINISHED
|
||||
if movie["status"] == "Released"
|
||||
else MovieStatus.PLANNED,
|
||||
@ -309,7 +309,7 @@ class TheMovieDatabase(Provider):
|
||||
slug=to_slug(serie["name"]),
|
||||
original_language=Language.get(serie["original_language"]),
|
||||
genres=self._map_genres(x["id"] for x in serie["genres"]),
|
||||
rating=round(float(serie["vote_average"]) * 10),
|
||||
rating={self.name: round(float(serie["vote_average"]) * 10)},
|
||||
status=SerieStatus.FINISHED
|
||||
if serie["status"] == "Released"
|
||||
else SerieStatus.AIRING
|
||||
@ -597,9 +597,11 @@ class TheMovieDatabase(Provider):
|
||||
genres=[
|
||||
y for x in collection["parts"] for y in self._map_genres(x["genre_ids"])
|
||||
],
|
||||
rating=round(
|
||||
mean(float(x["vote_average"]) * 10 for x in collection["parts"])
|
||||
),
|
||||
rating={
|
||||
self.name: round(
|
||||
mean(float(x["vote_average"]) * 10 for x in collection["parts"])
|
||||
)
|
||||
},
|
||||
external_id={
|
||||
self.name: [
|
||||
MetadataId(
|
||||
|
||||
@ -226,7 +226,7 @@ class TVDB(Provider):
|
||||
for x in ret["genres"]
|
||||
if self._genre_map[x["slug"]] is not None
|
||||
],
|
||||
rating=None, # TODO: maybe use the `score` value.
|
||||
rating={}, # TODO: maybe use the `score` value.
|
||||
status=SerieStatus.FINISHED
|
||||
if ret["status"]["name"] == "Ended"
|
||||
else SerieStatus.AIRING
|
||||
@ -423,7 +423,7 @@ class TVDB(Provider):
|
||||
for x in show["genres"]
|
||||
if self._genre_map[x["slug"]] is not None
|
||||
],
|
||||
rating=None,
|
||||
rating={},
|
||||
external_id={
|
||||
self.name: [
|
||||
MetadataId(
|
||||
@ -734,7 +734,7 @@ class TVDB(Provider):
|
||||
for x in ret["genres"]
|
||||
if self._genre_map[x["slug"]] is not None
|
||||
],
|
||||
rating=None, # TODO: maybe use the `score` value.
|
||||
rating={}, # TODO: maybe use the `score` value.
|
||||
status=MovieStatus.FINISHED
|
||||
if ret["status"]["name"] == "Ended"
|
||||
else MovieStatus.PLANNED,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user