From 59187a024bb9d4cdd56e894534a196fa758e8581 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 27 Mar 2026 10:40:22 +0100 Subject: [PATCH] Implement metadata refresh --- api/drizzle/0029_next_refresh.sql | 4 + api/drizzle/meta/0029_snapshot.json | 2010 +++++++++++++++++ api/drizzle/meta/_journal.json | 7 + api/src/controllers/entries.ts | 1 - api/src/controllers/seasons.ts | 1 - api/src/controllers/seed/insert/collection.ts | 2 +- api/src/controllers/seed/insert/entries.ts | 5 - api/src/controllers/seed/insert/seasons.ts | 2 - api/src/controllers/seed/movies.ts | 2 +- api/src/controllers/seed/refresh.ts | 25 +- api/src/controllers/seed/series.ts | 2 +- api/src/controllers/shows/logic.ts | 1 + api/src/db/schema/entries.ts | 1 - api/src/db/schema/seasons.ts | 1 - api/src/db/schema/shows.ts | 2 +- api/src/models/collections.ts | 1 - api/src/models/entry/base-entry.ts | 2 - api/src/models/movie.ts | 2 +- api/src/models/season.ts | 2 - api/src/models/serie.ts | 2 +- api/src/models/utils/filters/parser.ts | 4 +- scanner/scanner/__init__.py | 16 +- scanner/scanner/client.py | 11 + scanner/scanner/models/show.py | 15 + scanner/scanner/providers/thetvdb.py | 6 +- scanner/scanner/refresh.py | 56 + transcoder/src/thumbnails.go | 5 +- 27 files changed, 2153 insertions(+), 35 deletions(-) create mode 100644 api/drizzle/0029_next_refresh.sql create mode 100644 api/drizzle/meta/0029_snapshot.json create mode 100644 scanner/scanner/models/show.py create mode 100644 scanner/scanner/refresh.py diff --git a/api/drizzle/0029_next_refresh.sql b/api/drizzle/0029_next_refresh.sql new file mode 100644 index 00000000..5ee8793e --- /dev/null +++ b/api/drizzle/0029_next_refresh.sql @@ -0,0 +1,4 @@ +CREATE INDEX "evj_video_pk" ON "kyoo"."entry_video_join" USING btree ("video_pk");--> statement-breakpoint +ALTER TABLE "kyoo"."entries" DROP COLUMN "next_refresh";--> statement-breakpoint +ALTER TABLE "kyoo"."seasons" DROP COLUMN "next_refresh"; +ALTER TABLE "kyoo"."shows" ALTER COLUMN "next_refresh" SET DATA TYPE date; diff --git a/api/drizzle/meta/0029_snapshot.json b/api/drizzle/meta/0029_snapshot.json new file mode 100644 index 00000000..9f6d1cfe --- /dev/null +++ b/api/drizzle/meta/0029_snapshot.json @@ -0,0 +1,2010 @@ +{ + "id": "882399a3-e081-4094-a0aa-c306011f3eec", + "prevId": "c347b7f3-a7b9-4165-bf5b-e856378f82ae", + "version": "7", + "dialect": "postgresql", + "tables": { + "kyoo.entries": { + "name": "entries", + "schema": "kyoo", + "columns": { + "pk": { + "name": "pk", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "entries_pk_seq", + "schema": "kyoo", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "show_pk": { + "name": "show_pk", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "season_number": { + "name": "season_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "episode_number": { + "name": "episode_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "entry_type", + "typeSchema": "kyoo", + "primaryKey": false, + "notNull": true + }, + "extra_kind": { + "name": "extra_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "air_date": { + "name": "air_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "runtime": { + "name": "runtime", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "thumbnail": { + "name": "thumbnail", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "available_since": { + "name": "available_since", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "entry_kind": { + "name": "entry_kind", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hash", + "with": {} + }, + "entry_order": { + "name": "entry_order", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entries_show_pk_shows_pk_fk": { + "name": "entries_show_pk_shows_pk_fk", + "tableFrom": "entries", + "tableTo": "shows", + "schemaTo": "kyoo", + "columnsFrom": ["show_pk"], + "columnsTo": ["pk"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "entries_id_unique": { + "name": "entries_id_unique", + "nullsNotDistinct": false, + "columns": ["id"] + }, + "entries_slug_unique": { + "name": "entries_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + }, + "entries_showPk_seasonNumber_episodeNumber_unique": { + "name": "entries_showPk_seasonNumber_episodeNumber_unique", + "nullsNotDistinct": false, + "columns": ["show_pk", "season_number", "episode_number"] + } + }, + "policies": {}, + "checkConstraints": { + "order_positive": { + "name": "order_positive", + "value": "\"kyoo\".\"entries\".\"order\" >= 0" + } + }, + "isRLSEnabled": false + }, + "kyoo.entry_translations": { + "name": "entry_translations", + "schema": "kyoo", + "columns": { + "pk": { + "name": "pk", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tagline": { + "name": "tagline", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "poster": { + "name": "poster", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "entry_name_trgm": { + "name": "entry_name_trgm", + "columns": [ + { + "expression": "\"name\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "entry_translations_pk_entries_pk_fk": { + "name": "entry_translations_pk_entries_pk_fk", + "tableFrom": "entry_translations", + "tableTo": "entries", + "schemaTo": "kyoo", + "columnsFrom": ["pk"], + "columnsTo": ["pk"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "entry_translations_pk_language_pk": { + "name": "entry_translations_pk_language_pk", + "columns": ["pk", "language"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "kyoo.history": { + "name": "history", + "schema": "kyoo", + "columns": { + "pk": { + "name": "pk", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "history_pk_seq", + "schema": "kyoo", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "profile_pk": { + "name": "profile_pk", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "entry_pk": { + "name": "entry_pk", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "video_pk": { + "name": "video_pk", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "percent": { + "name": "percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "time": { + "name": "time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "played_date": { + "name": "played_date", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "history_play_date": { + "name": "history_play_date", + "columns": [ + { + "expression": "played_date", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "history_profile_pk_profiles_pk_fk": { + "name": "history_profile_pk_profiles_pk_fk", + "tableFrom": "history", + "tableTo": "profiles", + "schemaTo": "kyoo", + "columnsFrom": ["profile_pk"], + "columnsTo": ["pk"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "history_entry_pk_entries_pk_fk": { + "name": "history_entry_pk_entries_pk_fk", + "tableFrom": "history", + "tableTo": "entries", + "schemaTo": "kyoo", + "columnsFrom": ["entry_pk"], + "columnsTo": ["pk"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "history_video_pk_videos_pk_fk": { + "name": "history_video_pk_videos_pk_fk", + "tableFrom": "history", + "tableTo": "videos", + "schemaTo": "kyoo", + "columnsFrom": ["video_pk"], + "columnsTo": ["pk"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "percent_valid": { + "name": "percent_valid", + "value": "\"kyoo\".\"history\".\"percent\" between 0 and 100" + } + }, + "isRLSEnabled": false + }, + "kyoo.images": { + "name": "images", + "schema": "kyoo", + "columns": { + "pk": { + "name": "pk", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "images_pk_seq", + "schema": "kyoo", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "id": { + "name": "id", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "blurhash": { + "name": "blurhash", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "targets": { + "name": "targets", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "img_status", + "typeSchema": "kyoo", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "downloaded_at": { + "name": "downloaded_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "imgqueue_sort": { + "name": "imgqueue_sort", + "columns": [ + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempt", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "images_id_unique": { + "name": "images_id_unique", + "nullsNotDistinct": false, + "columns": ["id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "kyoo.mqueue": { + "name": "mqueue", + "schema": "kyoo", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kind": { + "name": "kind", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mqueue_created": { + "name": "mqueue_created", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "kyoo.profiles": { + "name": "profiles", + "schema": "kyoo", + "columns": { + "pk": { + "name": "pk", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "profiles_pk_seq", + "schema": "kyoo", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "profiles_id_unique": { + "name": "profiles_id_unique", + "nullsNotDistinct": false, + "columns": ["id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "kyoo.season_translations": { + "name": "season_translations", + "schema": "kyoo", + "columns": { + "pk": { + "name": "pk", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "poster": { + "name": "poster", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "thumbnail": { + "name": "thumbnail", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "banner": { + "name": "banner", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "season_name_trgm": { + "name": "season_name_trgm", + "columns": [ + { + "expression": "\"name\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "season_translations_pk_seasons_pk_fk": { + "name": "season_translations_pk_seasons_pk_fk", + "tableFrom": "season_translations", + "tableTo": "seasons", + "schemaTo": "kyoo", + "columnsFrom": ["pk"], + "columnsTo": ["pk"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "season_translations_pk_language_pk": { + "name": "season_translations_pk_language_pk", + "columns": ["pk", "language"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "kyoo.seasons": { + "name": "seasons", + "schema": "kyoo", + "columns": { + "pk": { + "name": "pk", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "seasons_pk_seq", + "schema": "kyoo", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "show_pk": { + "name": "show_pk", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "season_number": { + "name": "season_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "start_air": { + "name": "start_air", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "end_air": { + "name": "end_air", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "entries_count": { + "name": "entries_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "available_count": { + "name": "available_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "external_id": { + "name": "external_id", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "show_fk": { + "name": "show_fk", + "columns": [ + { + "expression": "show_pk", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hash", + "with": {} + }, + "season_nbr": { + "name": "season_nbr", + "columns": [ + { + "expression": "season_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "seasons_show_pk_shows_pk_fk": { + "name": "seasons_show_pk_shows_pk_fk", + "tableFrom": "seasons", + "tableTo": "shows", + "schemaTo": "kyoo", + "columnsFrom": ["show_pk"], + "columnsTo": ["pk"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "seasons_id_unique": { + "name": "seasons_id_unique", + "nullsNotDistinct": false, + "columns": ["id"] + }, + "seasons_slug_unique": { + "name": "seasons_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + }, + "seasons_showPk_seasonNumber_unique": { + "name": "seasons_showPk_seasonNumber_unique", + "nullsNotDistinct": false, + "columns": ["show_pk", "season_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "kyoo.show_translations": { + "name": "show_translations", + "schema": "kyoo", + "columns": { + "pk": { + "name": "pk", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tagline": { + "name": "tagline", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aliases": { + "name": "aliases", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "poster": { + "name": "poster", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "thumbnail": { + "name": "thumbnail", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "banner": { + "name": "banner", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "trailer_url": { + "name": "trailer_url", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "name_trgm": { + "name": "name_trgm", + "columns": [ + { + "expression": "\"name\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "tags": { + "name": "tags", + "columns": [ + { + "expression": "tags", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "show_translations_pk_shows_pk_fk": { + "name": "show_translations_pk_shows_pk_fk", + "tableFrom": "show_translations", + "tableTo": "shows", + "schemaTo": "kyoo", + "columnsFrom": ["pk"], + "columnsTo": ["pk"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "show_translations_pk_language_pk": { + "name": "show_translations_pk_language_pk", + "columns": ["pk", "language"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "kyoo.shows": { + "name": "shows", + "schema": "kyoo", + "columns": { + "pk": { + "name": "pk", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "shows_pk_seq", + "schema": "kyoo", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "show_kind", + "typeSchema": "kyoo", + "primaryKey": false, + "notNull": true + }, + "genres": { + "name": "genres", + "type": "genres[]", + "typeSchema": "kyoo", + "primaryKey": false, + "notNull": true + }, + "rating": { + "name": "rating", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "runtime": { + "name": "runtime", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "show_status", + "typeSchema": "kyoo", + "primaryKey": false, + "notNull": true + }, + "start_air": { + "name": "start_air", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "end_air": { + "name": "end_air", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "original": { + "name": "original", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "collection_pk": { + "name": "collection_pk", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "entries_count": { + "name": "entries_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "available_count": { + "name": "available_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "external_id": { + "name": "external_id", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + }, + "next_refresh": { + "name": "next_refresh", + "type": "date", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "kind": { + "name": "kind", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hash", + "with": {} + }, + "slug": { + "name": "slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "startAir": { + "name": "startAir", + "columns": [ + { + "expression": "start_air", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shows_collection_pk_shows_pk_fk": { + "name": "shows_collection_pk_shows_pk_fk", + "tableFrom": "shows", + "tableTo": "shows", + "schemaTo": "kyoo", + "columnsFrom": ["collection_pk"], + "columnsTo": ["pk"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "shows_id_unique": { + "name": "shows_id_unique", + "nullsNotDistinct": false, + "columns": ["id"] + }, + "kind_slug": { + "name": "kind_slug", + "nullsNotDistinct": false, + "columns": ["kind", "slug"] + } + }, + "policies": {}, + "checkConstraints": { + "runtime_valid": { + "name": "runtime_valid", + "value": "\"kyoo\".\"shows\".\"runtime\" >= 0" + } + }, + "isRLSEnabled": false + }, + "kyoo.roles": { + "name": "roles", + "schema": "kyoo", + "columns": { + "pk": { + "name": "pk", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "roles_pk_seq", + "schema": "kyoo", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "show_pk": { + "name": "show_pk", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "staff_pk": { + "name": "staff_pk", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "role_kind", + "typeSchema": "kyoo", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "character": { + "name": "character", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "role_kind": { + "name": "role_kind", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hash", + "with": {} + }, + "role_order": { + "name": "role_order", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "roles_show_pk_shows_pk_fk": { + "name": "roles_show_pk_shows_pk_fk", + "tableFrom": "roles", + "tableTo": "shows", + "schemaTo": "kyoo", + "columnsFrom": ["show_pk"], + "columnsTo": ["pk"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "roles_staff_pk_staff_pk_fk": { + "name": "roles_staff_pk_staff_pk_fk", + "tableFrom": "roles", + "tableTo": "staff", + "schemaTo": "kyoo", + "columnsFrom": ["staff_pk"], + "columnsTo": ["pk"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "kyoo.staff": { + "name": "staff", + "schema": "kyoo", + "columns": { + "pk": { + "name": "pk", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "staff_pk_seq", + "schema": "kyoo", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "latin_name": { + "name": "latin_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "staff_id_unique": { + "name": "staff_id_unique", + "nullsNotDistinct": false, + "columns": ["id"] + }, + "staff_slug_unique": { + "name": "staff_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "kyoo.show_studio_join": { + "name": "show_studio_join", + "schema": "kyoo", + "columns": { + "show_pk": { + "name": "show_pk", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "studio_pk": { + "name": "studio_pk", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "show_studio_join_show_pk_shows_pk_fk": { + "name": "show_studio_join_show_pk_shows_pk_fk", + "tableFrom": "show_studio_join", + "tableTo": "shows", + "schemaTo": "kyoo", + "columnsFrom": ["show_pk"], + "columnsTo": ["pk"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "show_studio_join_studio_pk_studios_pk_fk": { + "name": "show_studio_join_studio_pk_studios_pk_fk", + "tableFrom": "show_studio_join", + "tableTo": "studios", + "schemaTo": "kyoo", + "columnsFrom": ["studio_pk"], + "columnsTo": ["pk"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "show_studio_join_show_pk_studio_pk_pk": { + "name": "show_studio_join_show_pk_studio_pk_pk", + "columns": ["show_pk", "studio_pk"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "kyoo.studio_translations": { + "name": "studio_translations", + "schema": "kyoo", + "columns": { + "pk": { + "name": "pk", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "studio_name_trgm": { + "name": "studio_name_trgm", + "columns": [ + { + "expression": "\"name\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "studio_translations_pk_studios_pk_fk": { + "name": "studio_translations_pk_studios_pk_fk", + "tableFrom": "studio_translations", + "tableTo": "studios", + "schemaTo": "kyoo", + "columnsFrom": ["pk"], + "columnsTo": ["pk"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "studio_translations_pk_language_pk": { + "name": "studio_translations_pk_language_pk", + "columns": ["pk", "language"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "kyoo.studios": { + "name": "studios", + "schema": "kyoo", + "columns": { + "pk": { + "name": "pk", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "studios_pk_seq", + "schema": "kyoo", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "studios_id_unique": { + "name": "studios_id_unique", + "nullsNotDistinct": false, + "columns": ["id"] + }, + "studios_slug_unique": { + "name": "studios_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "kyoo.entry_video_join": { + "name": "entry_video_join", + "schema": "kyoo", + "columns": { + "entry_pk": { + "name": "entry_pk", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "video_pk": { + "name": "video_pk", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "evj_video_pk": { + "name": "evj_video_pk", + "columns": [ + { + "expression": "video_pk", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entry_video_join_entry_pk_entries_pk_fk": { + "name": "entry_video_join_entry_pk_entries_pk_fk", + "tableFrom": "entry_video_join", + "tableTo": "entries", + "schemaTo": "kyoo", + "columnsFrom": ["entry_pk"], + "columnsTo": ["pk"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entry_video_join_video_pk_videos_pk_fk": { + "name": "entry_video_join_video_pk_videos_pk_fk", + "tableFrom": "entry_video_join", + "tableTo": "videos", + "schemaTo": "kyoo", + "columnsFrom": ["video_pk"], + "columnsTo": ["pk"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "entry_video_join_entry_pk_video_pk_pk": { + "name": "entry_video_join_entry_pk_video_pk_pk", + "columns": ["entry_pk", "video_pk"] + } + }, + "uniqueConstraints": { + "entry_video_join_slug_unique": { + "name": "entry_video_join_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "kyoo.videos": { + "name": "videos", + "schema": "kyoo", + "columns": { + "pk": { + "name": "pk", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "videos_pk_seq", + "schema": "kyoo", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rendering": { + "name": "rendering", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "part": { + "name": "part", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "guess": { + "name": "guess", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "videos_id_unique": { + "name": "videos_id_unique", + "nullsNotDistinct": false, + "columns": ["id"] + }, + "videos_path_unique": { + "name": "videos_path_unique", + "nullsNotDistinct": false, + "columns": ["path"] + }, + "rendering_unique": { + "name": "rendering_unique", + "nullsNotDistinct": true, + "columns": ["rendering", "part", "version"] + } + }, + "policies": {}, + "checkConstraints": { + "part_pos": { + "name": "part_pos", + "value": "\"kyoo\".\"videos\".\"part\" >= 0" + }, + "version_pos": { + "name": "version_pos", + "value": "\"kyoo\".\"videos\".\"version\" >= 0" + } + }, + "isRLSEnabled": false + }, + "kyoo.watchlist": { + "name": "watchlist", + "schema": "kyoo", + "columns": { + "profile_pk": { + "name": "profile_pk", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "show_pk": { + "name": "show_pk", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "watchlist_status", + "typeSchema": "kyoo", + "primaryKey": false, + "notNull": true + }, + "seen_count": { + "name": "seen_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_entry": { + "name": "next_entry", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "last_played_at": { + "name": "last_played_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "watchlist_profile_pk_profiles_pk_fk": { + "name": "watchlist_profile_pk_profiles_pk_fk", + "tableFrom": "watchlist", + "tableTo": "profiles", + "schemaTo": "kyoo", + "columnsFrom": ["profile_pk"], + "columnsTo": ["pk"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "watchlist_show_pk_shows_pk_fk": { + "name": "watchlist_show_pk_shows_pk_fk", + "tableFrom": "watchlist", + "tableTo": "shows", + "schemaTo": "kyoo", + "columnsFrom": ["show_pk"], + "columnsTo": ["pk"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "watchlist_next_entry_entries_pk_fk": { + "name": "watchlist_next_entry_entries_pk_fk", + "tableFrom": "watchlist", + "tableTo": "entries", + "schemaTo": "kyoo", + "columnsFrom": ["next_entry"], + "columnsTo": ["pk"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "watchlist_profile_pk_show_pk_pk": { + "name": "watchlist_profile_pk_show_pk_pk", + "columns": ["profile_pk", "show_pk"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "score_percent": { + "name": "score_percent", + "value": "\"kyoo\".\"watchlist\".\"score\" between 0 and 100" + } + }, + "isRLSEnabled": false + } + }, + "enums": { + "kyoo.entry_type": { + "name": "entry_type", + "schema": "kyoo", + "values": ["episode", "movie", "special", "extra"] + }, + "kyoo.img_status": { + "name": "img_status", + "schema": "kyoo", + "values": ["pending", "link", "ready"] + }, + "kyoo.genres": { + "name": "genres", + "schema": "kyoo", + "values": [ + "action", + "adventure", + "animation", + "comedy", + "crime", + "documentary", + "drama", + "family", + "fantasy", + "history", + "horror", + "music", + "mystery", + "romance", + "science-fiction", + "thriller", + "war", + "western", + "kids", + "reality", + "politics", + "soap", + "talk" + ] + }, + "kyoo.show_kind": { + "name": "show_kind", + "schema": "kyoo", + "values": ["serie", "movie", "collection"] + }, + "kyoo.show_status": { + "name": "show_status", + "schema": "kyoo", + "values": ["unknown", "finished", "airing", "planned"] + }, + "kyoo.role_kind": { + "name": "role_kind", + "schema": "kyoo", + "values": [ + "actor", + "director", + "writter", + "producer", + "music", + "crew", + "other" + ] + }, + "kyoo.watchlist_status": { + "name": "watchlist_status", + "schema": "kyoo", + "values": ["watching", "rewatching", "completed", "dropped", "planned"] + } + }, + "schemas": { + "kyoo": "kyoo" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/api/drizzle/meta/_journal.json b/api/drizzle/meta/_journal.json index ef374890..316e183c 100644 --- a/api/drizzle/meta/_journal.json +++ b/api/drizzle/meta/_journal.json @@ -204,6 +204,13 @@ "when": 1771600000000, "tag": "0028_rating_jsonb", "breakpoints": true + }, + { + "idx": 29, + "version": "7", + "when": 1774623568394, + "tag": "0029_next_refresh", + "breakpoints": true } ] } diff --git a/api/src/controllers/entries.ts b/api/src/controllers/entries.ts index 4b4b2090..d3520a2b 100644 --- a/api/src/controllers/entries.ts +++ b/api/src/controllers/entries.ts @@ -99,7 +99,6 @@ export const entrySort = Sort( episodeNumber: entries.episodeNumber, number: entries.episodeNumber, airDate: entries.airDate, - nextRefresh: entries.nextRefresh, playedDate: entryProgressQ.playedDate, }, { diff --git a/api/src/controllers/seasons.ts b/api/src/controllers/seasons.ts index fe2fb69a..5e1aec62 100644 --- a/api/src/controllers/seasons.ts +++ b/api/src/controllers/seasons.ts @@ -35,7 +35,6 @@ const seasonSort = Sort( endAir: seasons.endAir, entriesCount: seasons.entriesCount, availableCount: seasons.availableCount, - nextRefresh: seasons.nextRefresh, }, { default: ["seasonNumber"], diff --git a/api/src/controllers/seed/insert/collection.ts b/api/src/controllers/seed/insert/collection.ts index 0668b32d..b321219b 100644 --- a/api/src/controllers/seed/insert/collection.ts +++ b/api/src/controllers/seed/insert/collection.ts @@ -20,7 +20,7 @@ export const insertCollection = record( | ({ kind: "movie" } & SeedMovie) | ({ kind: "serie" } & SeedSerie) ) & { - nextRefresh: Date; + nextRefresh: string; }, original: Original, ) => { diff --git a/api/src/controllers/seed/insert/entries.ts b/api/src/controllers/seed/insert/entries.ts index d1bec65e..6569aa73 100644 --- a/api/src/controllers/seed/insert/entries.ts +++ b/api/src/controllers/seed/insert/entries.ts @@ -12,7 +12,6 @@ import { KErrorT } from "~/models/error"; import { record } from "~/otel"; import { duplicates } from "~/utils"; import { enqueueOptImage, flushImageQueue, type ImageTask } from "../images"; -import { guessNextRefresh } from "../refresh"; import { updateAvailableCount, updateAvailableSince } from "./shows"; type SeedEntry = SEntry & { @@ -66,10 +65,6 @@ export const insertEntries = record( url: seed.thumbnail, column: entries.thumbnail, }), - nextRefresh: - entry.kind !== "extra" - ? guessNextRefresh(entry.airDate ?? new Date()) - : guessNextRefresh(new Date()), episodeNumber: entry.kind === "episode" ? entry.episodeNumber diff --git a/api/src/controllers/seed/insert/seasons.ts b/api/src/controllers/seed/insert/seasons.ts index 84b11898..66d6fa37 100644 --- a/api/src/controllers/seed/insert/seasons.ts +++ b/api/src/controllers/seed/insert/seasons.ts @@ -4,7 +4,6 @@ import { conflictUpdateAllExcept, unnestValues } from "~/db/utils"; import type { SeedSeason } from "~/models/season"; import { record } from "~/otel"; import { enqueueOptImage, flushImageQueue, type ImageTask } from "../images"; -import { guessNextRefresh } from "../refresh"; type SeasonI = typeof seasons.$inferInsert; type SeasonTransI = typeof seasonTranslations.$inferInsert; @@ -25,7 +24,6 @@ export const insertSeasons = record( season.seasonNumber === 0 ? `${show.slug}-specials` : `${show.slug}-s${season.seasonNumber}`, - nextRefresh: guessNextRefresh(season.startAir ?? new Date()), }; }); const ret = await tx diff --git a/api/src/controllers/seed/movies.ts b/api/src/controllers/seed/movies.ts index d9c25900..cf95c312 100644 --- a/api/src/controllers/seed/movies.ts +++ b/api/src/controllers/seed/movies.ts @@ -53,7 +53,7 @@ export const seedMovie = async ( } const { translations, videos, collection, studios, staff, ...movie } = seed; - const nextRefresh = guessNextRefresh(movie.airDate ?? new Date()); + const nextRefresh = guessNextRefresh({ ...seed, kind: "movie" }); const ori = translations[movie.originalLanguage]; const original = ori ? { diff --git a/api/src/controllers/seed/refresh.ts b/api/src/controllers/seed/refresh.ts index 09d5ba6d..19259e9b 100644 --- a/api/src/controllers/seed/refresh.ts +++ b/api/src/controllers/seed/refresh.ts @@ -1,6 +1,27 @@ +import type { SeedMovie } from "~/models/movie"; +import type { SeedSerie } from "~/models/serie"; + +export const guessNextRefresh = ( + show: (SeedSerie & { kind: "serie" }) | (SeedMovie & { kind: "movie" }), +) => { + if (show.kind === "movie") { + return fromAirDate(show.airDate ?? new Date()); + } + const lastAirDate = show.entries + .filter((x) => x.airDate) + .map((x) => new Date(x.airDate!)) + .reduce((max, cur) => (cur > max ? cur : max)); + return fromAirDate(lastAirDate); +}; + // oh i hate js dates so much. -export const guessNextRefresh = (airDate: Date | string) => { +const fromAirDate = (airDate: string | Date) => { if (typeof airDate === "string") airDate = new Date(airDate); + + if (airDate.getTime() > Date.now()) { + return airDate.toISOString().split("T")[0]; + } + const diff = Date.now() - airDate.getTime(); const days = diff / (24 * 60 * 60 * 1000); @@ -8,5 +29,5 @@ export const guessNextRefresh = (airDate: Date | string) => { if (days <= 4) ret.setDate(ret.getDate() + 4); else if (days <= 21) ret.setDate(ret.getDate() + 14); else ret.setMonth(ret.getMonth() + 2); - return ret; + return ret.toISOString().split("T")[0]; }; diff --git a/api/src/controllers/seed/series.ts b/api/src/controllers/seed/series.ts index 3c5eb6f3..9860cdf9 100644 --- a/api/src/controllers/seed/series.ts +++ b/api/src/controllers/seed/series.ts @@ -89,7 +89,7 @@ export const seedSerie = async ( staff, ...serie } = seed; - const nextRefresh = guessNextRefresh(serie.startAir ?? new Date()); + const nextRefresh = guessNextRefresh({ ...seed, kind: "serie" }); const ori = translations[serie.originalLanguage]; const original = ori ? { diff --git a/api/src/controllers/shows/logic.ts b/api/src/controllers/shows/logic.ts index 8394669b..449b429f 100644 --- a/api/src/controllers/shows/logic.ts +++ b/api/src/controllers/shows/logic.ts @@ -91,6 +91,7 @@ export const showFilters: FilterDef = { column: (source: string) => sql`(${shows.rating}->>${source})::int`, type: "int", }, + nextRefresh: { column: shows.nextRefresh, type: "date" }, }; export const showSort = Sort( { diff --git a/api/src/db/schema/entries.ts b/api/src/db/schema/entries.ts index 6c4138c9..e35309ae 100644 --- a/api/src/db/schema/entries.ts +++ b/api/src/db/schema/entries.ts @@ -78,7 +78,6 @@ export const entries = schema.table( .notNull() .$onUpdate(() => new Date()), availableSince: timestamp({ withTimezone: true, precision: 3 }), - nextRefresh: timestamp({ withTimezone: true, precision: 3 }).notNull(), }, (t) => [ unique().on(t.showPk, t.seasonNumber, t.episodeNumber), diff --git a/api/src/db/schema/seasons.ts b/api/src/db/schema/seasons.ts index 8237926b..a8276d47 100644 --- a/api/src/db/schema/seasons.ts +++ b/api/src/db/schema/seasons.ts @@ -51,7 +51,6 @@ export const seasons = schema.table( updatedAt: timestamp({ withTimezone: true, precision: 3 }) .notNull() .$onUpdate(() => new Date()), - nextRefresh: timestamp({ withTimezone: true, precision: 3 }).notNull(), }, (t) => [ unique().on(t.showPk, t.seasonNumber), diff --git a/api/src/db/schema/shows.ts b/api/src/db/schema/shows.ts index 120ef2b5..51713e86 100644 --- a/api/src/db/schema/shows.ts +++ b/api/src/db/schema/shows.ts @@ -93,7 +93,7 @@ export const shows = schema.table( updatedAt: timestamp({ withTimezone: true, precision: 3 }) .notNull() .$onUpdate(() => new Date()), - nextRefresh: timestamp({ withTimezone: true, precision: 3 }).notNull(), + nextRefresh: date().notNull(), }, (t) => [ unique("kind_slug").on(t.kind, t.slug), diff --git a/api/src/models/collections.ts b/api/src/models/collections.ts index 1fa980d4..31514b6d 100644 --- a/api/src/models/collections.ts +++ b/api/src/models/collections.ts @@ -32,7 +32,6 @@ const BaseCollection = t.Object({ descrpition: "Date of the last item of the collection", }), ), - nextRefresh: t.Date(), externalId: ExternalId(), }); diff --git a/api/src/models/entry/base-entry.ts b/api/src/models/entry/base-entry.ts index 4bca8c0b..99d24df2 100644 --- a/api/src/models/entry/base-entry.ts +++ b/api/src/models/entry/base-entry.ts @@ -11,8 +11,6 @@ export const BaseEntry = () => }), ), thumbnail: t.Nullable(Image), - - nextRefresh: t.Date(), }); export const EntryTranslation = () => diff --git a/api/src/models/movie.ts b/api/src/models/movie.ts index fbfcadd7..d0c76499 100644 --- a/api/src/models/movie.ts +++ b/api/src/models/movie.ts @@ -33,7 +33,7 @@ const BaseMovie = t.Object({ t.Number({ minimum: 0, description: "Runtime of the movie in minutes." }), ), airDate: t.Nullable(t.String({ format: "date" })), - nextRefresh: t.Date(), + nextRefresh: t.Nullable(t.String({ format: "date" })), externalId: ExternalId(), }); diff --git a/api/src/models/season.ts b/api/src/models/season.ts index 4ba947a1..91bab5a2 100644 --- a/api/src/models/season.ts +++ b/api/src/models/season.ts @@ -12,8 +12,6 @@ export const BaseSeason = t.Object({ startAir: t.Nullable(t.String({ format: "date" })), endAir: t.Nullable(t.String({ format: "date" })), - nextRefresh: t.Date(), - externalId: SeasonId, }); diff --git a/api/src/models/serie.ts b/api/src/models/serie.ts index 23131d44..930f7b5a 100644 --- a/api/src/models/serie.ts +++ b/api/src/models/serie.ts @@ -43,7 +43,7 @@ const BaseSerie = t.Object({ ), startAir: t.Nullable(t.String({ format: "date" })), endAir: t.Nullable(t.String({ format: "date" })), - nextRefresh: t.Date(), + nextRefresh: t.Nullable(t.String({ format: "date" })), externalId: ExternalId(), }); diff --git a/api/src/models/utils/filters/parser.ts b/api/src/models/utils/filters/parser.ts index 3f2e68b5..bd9bc3a5 100644 --- a/api/src/models/utils/filters/parser.ts +++ b/api/src/models/utils/filters/parser.ts @@ -34,7 +34,7 @@ export type Property = { export type Value = | { type: "int"; value: number } | { type: "float"; value: number } - | { type: "date"; value: string } + | { type: "date"; value: Date } | { type: "string"; value: string } | { type: "enum"; value: string } | { type: "bool"; value: boolean }; @@ -92,7 +92,7 @@ const dateVal = t( ), map(([year, month, day]) => ({ type: "date" as const, - value: `${year}-${month}-${day}`, + value: new Date(`${year}-${month}-${day}`), })), ), ).expects("a date"); diff --git a/scanner/scanner/__init__.py b/scanner/scanner/__init__.py index 62042961..1e21df8d 100644 --- a/scanner/scanner/__init__.py +++ b/scanner/scanner/__init__.py @@ -1,5 +1,7 @@ from asyncio import CancelledError, TaskGroup, create_task, sleep from contextlib import asynccontextmanager +from types import CoroutineType +from typing import Any from asyncpg import Connection from fastapi import FastAPI @@ -9,6 +11,7 @@ from scanner.fsscan import FsScanner from scanner.log import configure_logging from scanner.otel import instrument, setup_otelproviders from scanner.providers.composite import CompositeProvider +from scanner.refresh import ShowRefresh from scanner.providers.themoviedatabase import TheMovieDatabase from scanner.providers.thetvdb import TVDB from scanner.requests import RequestCreator, RequestProcessor @@ -50,10 +53,13 @@ async def lifespan(app: FastAPI): client, app.state.provider, ) - scanner = FsScanner(client, RequestCreator(db)) + requests = RequestCreator(db) + scanner = FsScanner(client, requests) + refresh = ShowRefresh(client, requests) tasks = create_task( background_startup( scanner, + refresh, processor, leader_db, is_master, @@ -65,14 +71,15 @@ async def lifespan(app: FastAPI): async def background_startup( scanner: FsScanner, + refresh: ShowRefresh, processor: RequestProcessor, leader_db: Connection, is_master: bool | None, ): - async def scan(): + async def delay(task: CoroutineType[Any, Any, None]): # wait for everything to startup & resume before scanning await sleep(30) - await scanner.scan(remove_deleted=True) + await task async def leader_worker(tg: TaskGroup): nonlocal is_master @@ -83,7 +90,8 @@ async def background_startup( ) _ = tg.create_task(scanner.monitor()) - _ = tg.create_task(scan()) + _ = tg.create_task(delay(scanner.scan(remove_deleted=True))) + _ = tg.create_task(delay(refresh.monitor())) async with TaskGroup() as tg: _ = tg.create_task(processor.listen(tg)) diff --git a/scanner/scanner/client.py b/scanner/scanner/client.py index 67e89108..7abed1ae 100644 --- a/scanner/scanner/client.py +++ b/scanner/scanner/client.py @@ -1,4 +1,5 @@ import os +from datetime import datetime, timezone from logging import getLogger from types import TracebackType from typing import Literal @@ -7,8 +8,10 @@ from aiohttp import ClientResponse, ClientResponseError, ClientSession from pydantic import TypeAdapter from .models.movie import Movie +from .models.page import Page from .models.request import Request from .models.serie import Serie +from .models.show import Show from .models.videos import For, Resource, Video, VideoCreated, VideoInfo, VideoLink from .utils import Singleton @@ -86,6 +89,14 @@ class KyooClient(metaclass=Singleton): await self.raise_for_status(r) return Resource.model_validate(await r.json()) + async def get_shows_to_refresh(self, next: str | None) -> Page[Show]: + now = datetime.now(timezone.utc).date() + async with self._client.get( + next or f"shows?sort=nextRefresh&filter=nextRefresh le {now}" + ) as r: + await self.raise_for_status(r) + return Page[Show].model_validate(await r.json()) + async def link_videos( self, kind: Literal["movie", "serie"], diff --git a/scanner/scanner/models/show.py b/scanner/scanner/models/show.py new file mode 100644 index 00000000..a5d6fe55 --- /dev/null +++ b/scanner/scanner/models/show.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from datetime import date +from typing import Literal + +from .metadataid import MetadataId +from ..utils import Model + + +class Show(Model): + kind: Literal["movie", "serie", "collection"] + name: str + air_date: date | None = None + start_air: date | None = None + external_id: dict[str, list[MetadataId]] diff --git a/scanner/scanner/providers/thetvdb.py b/scanner/scanner/providers/thetvdb.py index d9bf5c78..80326c4b 100644 --- a/scanner/scanner/providers/thetvdb.py +++ b/scanner/scanner/providers/thetvdb.py @@ -241,7 +241,7 @@ class TVDB(Provider): original_language=Language.get(ret["originalLanguage"]), genres=[ cast(Genre, self._genre_map[x["slug"]]) - for x in ret.get("genres", []) + for x in (ret.get("genres", []) or []) if self._genre_map[x["slug"]] is not None ], rating={}, # TODO: maybe use the `score` value. @@ -438,7 +438,7 @@ class TVDB(Provider): original_language=Language.get(show["originalLanguage"]), genres=[ cast(Genre, self._genre_map[x["slug"]]) - for x in show.get("genres", []) + for x in (show.get("genres", []) or []) if self._genre_map[x["slug"]] is not None ], rating={}, @@ -749,7 +749,7 @@ class TVDB(Provider): original_language=Language.get(ret["originalLanguage"]), genres=[ cast(Genre, self._genre_map[x["slug"]]) - for x in ret.get("genres", []) + for x in (ret.get("genres", []) or []) if self._genre_map[x["slug"]] is not None ], rating={}, # TODO: maybe use the `score` value. diff --git a/scanner/scanner/refresh.py b/scanner/scanner/refresh.py new file mode 100644 index 00000000..fbec95ce --- /dev/null +++ b/scanner/scanner/refresh.py @@ -0,0 +1,56 @@ +from asyncio import sleep +from datetime import timedelta +from logging import getLogger + +from .client import KyooClient +from .models.metadataid import MetadataId +from .models.request import Request +from .requests import RequestCreator + +logger = getLogger(__name__) + + +class ShowRefresh: + def __init__(self, client: KyooClient, requests: RequestCreator): + self._client = client + self._requests = requests + + async def monitor(self): + while True: + try: + queued = await self.refresh_due_shows() + logger.info("Queued %d shows for refresh.", queued) + except Exception as e: + logger.error("Unexpected error while refreshing shows.", exc_info=e) + await sleep(timedelta(days=1).total_seconds()) + + async def refresh_due_shows(self) -> int: + queued = 0 + next_url: str | None = None + + while True: + page = await self._client.get_shows_to_refresh(next_url) + requests = [ + Request( + kind="movie" if show.kind == "movie" else "episode", + title=show.name, + year=show.air_date.year + if show.air_date is not None + else show.start_air.year + if show.start_air is not None + else None, + external_id=MetadataId.map_dict(show.external_id), + videos=[], + ) + for show in page.items + if show.kind != "collection" + ] + if requests: + _ = await self._requests.enqueue(requests) + queued += len(requests) + + if not page.next: + break + next_url = page.next + + return queued diff --git a/transcoder/src/thumbnails.go b/transcoder/src/thumbnails.go index ff301d26..e15bfbf0 100644 --- a/transcoder/src/thumbnails.go +++ b/transcoder/src/thumbnails.go @@ -87,8 +87,9 @@ func (s *MetadataService) extractThumbnail(ctx context.Context, path string, sha vttPath := getThumbVttPath(sha) spritePath := getThumbPath(sha) - alreadyOk, _ := s.storage.DoesItemExist(ctx, spritePath) - if alreadyOk { + spriteOk, _ := s.storage.DoesItemExist(ctx, spritePath) + vttOk, _ := s.storage.DoesItemExist(ctx, vttPath) + if spriteOk && vttOk { return nil }