diff --git a/api/drizzle/0031_entry-content.sql b/api/drizzle/0031_entry-content.sql new file mode 100644 index 00000000..9236cb5f --- /dev/null +++ b/api/drizzle/0031_entry-content.sql @@ -0,0 +1,4 @@ +CREATE TYPE "kyoo"."entry_content" AS ENUM('story', 'recap', 'filler', 'ova');--> statement-breakpoint +ALTER TABLE "kyoo"."entries" ADD COLUMN "content" "kyoo"."entry_content";--> statement-breakpoint +UPDATE "kyoo"."entries" SET content = 'story';-->statement-breakpoint +ALTER TABLE "kyoo"."entries" ALTER COLUMN "content" SET NOT NULl;--> statement-breakpoint diff --git a/api/drizzle/meta/0031_snapshot.json b/api/drizzle/meta/0031_snapshot.json new file mode 100644 index 00000000..265515ea --- /dev/null +++ b/api/drizzle/meta/0031_snapshot.json @@ -0,0 +1,2028 @@ +{ + "id": "13b1ed8d-f166-4d39-ba67-39e8c4cfd786", + "prevId": "34fcf5bf-c0a7-4730-a705-0e7fe759d126", + "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 + }, + "content": { + "name": "content", + "type": "entry_content", + "typeSchema": "kyoo", + "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 + }, + "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()" + }, + "external": { + "name": "external", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "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_content": { + "name": "entry_content", + "schema": "kyoo", + "values": ["story", "recap", "filler", "ova"] + }, + "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 67e517a9..8f855cf1 100644 --- a/api/drizzle/meta/_journal.json +++ b/api/drizzle/meta/_journal.json @@ -218,6 +218,13 @@ "when": 1774974162419, "tag": "0030_external_hist", "breakpoints": true + }, + { + "idx": 31, + "version": "7", + "when": 1775238108619, + "tag": "0031_entry-content", + "breakpoints": true } ] } diff --git a/api/src/controllers/entries.ts b/api/src/controllers/entries.ts index 70913a45..35c2c042 100644 --- a/api/src/controllers/entries.ts +++ b/api/src/controllers/entries.ts @@ -31,6 +31,7 @@ import { MovieEntry, Special, } from "~/models/entry"; +import { EntryContent } from "~/models/entry/base-entry"; import { KError } from "~/models/error"; import { madeInAbyss } from "~/models/examples"; import { Season } from "~/models/season"; @@ -83,6 +84,7 @@ export const entryFilters: FilterDef = { order: { column: entries.order, type: "float" }, runtime: { column: entries.runtime, type: "float" }, airDate: { column: entries.airDate, type: "date" }, + content: { column: entries.content, type: "enum", values: EntryContent.enum }, playedDate: { column: entryProgressQ.playedDate, type: "date" }, isAvailable: { column: isNotNull(entries.availableSince), type: "bool" }, }; diff --git a/api/src/controllers/profiles/history.ts b/api/src/controllers/profiles/history.ts index c8c2155a..e024dcc6 100644 --- a/api/src/controllers/profiles/history.ts +++ b/api/src/controllers/profiles/history.ts @@ -20,6 +20,7 @@ import { coalesce, sqlarr } from "~/db/utils"; import { Entry } from "~/models/entry"; import { KError } from "~/models/error"; import { SeedHistory } from "~/models/history"; +import { Show } from "~/models/show"; import { AcceptLanguage, createPage, @@ -198,6 +199,7 @@ async function updateWatchlist( and( eq(nextEntry.showPk, entries.showPk), ne(nextEntry.kind, "extra"), + eq(nextEntry.content, "story"), gt(nextEntry.order, entries.order), ), ) @@ -366,15 +368,16 @@ export const historyH = new Elysia({ tags: ["profiles"] }) query, sort, filter: and( - isNotNull(entryProgressQ.playedDate), - eq(entryProgressQ.external, false), + isNotNull(historyProgressQ.playedDate), + eq(historyProgressQ.external, false), ne(entries.kind, "extra"), filter, ), languages: langs, userId: sub, progressQ: historyProgressQ, - })) as Entry[]; + relations: ["show"], + })) as (Entry & { show: Show })[]; return createPage(items, { url, sort, limit, headers }); }, @@ -386,7 +389,7 @@ export const historyH = new Elysia({ tags: ["profiles"] }) "accept-language": AcceptLanguage({ autoFallback: true }), }), response: { - 200: Page(Entry), + 200: Page(t.Intersect([Entry, t.Object({ show: Show })])), }, }, ) diff --git a/api/src/controllers/profiles/nextup.ts b/api/src/controllers/profiles/nextup.ts index f7ec7264..8107f4bd 100644 --- a/api/src/controllers/profiles/nextup.ts +++ b/api/src/controllers/profiles/nextup.ts @@ -1,4 +1,4 @@ -import { and, eq, sql } from "drizzle-orm"; +import { and, eq, isNotNull, lt, or, sql } from "drizzle-orm"; import Elysia, { t } from "elysia"; import { auth } from "~/auth"; import { db } from "~/db"; @@ -118,6 +118,10 @@ export const nextup = new Elysia({ tags: ["profiles"] }) .leftJoin(entryProgressQ, eq(entries.pk, entryProgressQ.entryPk)) .where( and( + or( + lt(entries.airDate, sql`now()`), + isNotNull(entries.availableSince), + ), filter, query ? sql`${transQ.name} %> ${query}::text` : undefined, keysetPaginate({ after, sort }), diff --git a/api/src/controllers/seed/insert/entries.ts b/api/src/controllers/seed/insert/entries.ts index 6569aa73..3e58fa69 100644 --- a/api/src/controllers/seed/insert/entries.ts +++ b/api/src/controllers/seed/insert/entries.ts @@ -59,6 +59,7 @@ export const insertEntries = record( const { translations, videos, video, ...entry } = seed; return { ...entry, + content: entry.kind !== "extra" ? entry.content : "story", showPk: show.pk, slug: generateSlug(show.slug, seed), thumbnail: enqueueOptImage(imgQueue, { diff --git a/api/src/controllers/seed/movies.ts b/api/src/controllers/seed/movies.ts index cf95c312..60b759f6 100644 --- a/api/src/controllers/seed/movies.ts +++ b/api/src/controllers/seed/movies.ts @@ -96,6 +96,7 @@ export const seedMovie = async ( { ...movie, kind: "movie", + content: "story", order: 1, thumbnail: (movie.originalLanguage ? translations[movie.originalLanguage] diff --git a/api/src/controllers/videos.ts b/api/src/controllers/videos.ts index d670d55f..e7a877ec 100644 --- a/api/src/controllers/videos.ts +++ b/api/src/controllers/videos.ts @@ -247,7 +247,7 @@ function getNextVideoEntry({ .leftJoin(entryProgressQ, eq(entries.pk, entryProgressQ.entryPk)) .crossJoinLateral(entryVideosQ) .leftJoin(entryVideoJoin, eq(entries.pk, entryVideoJoin.entryPk)) - .innerJoin(vids, eq(vids.pk, entryVideoJoin.videoPk)) + .leftJoin(vids, eq(vids.pk, entryVideoJoin.videoPk)) .where( and( // either way it needs to be of the same show @@ -277,6 +277,7 @@ function getNextVideoEntry({ eq(vids.part, sql`${videos.part} ${sql.raw(prev ? "-" : "+")} 1`), ), ), + eq(entries.content, "story"), ), ) .orderBy( diff --git a/api/src/db/schema/entries.ts b/api/src/db/schema/entries.ts index e35309ae..1a20562e 100644 --- a/api/src/db/schema/entries.ts +++ b/api/src/db/schema/entries.ts @@ -24,6 +24,13 @@ export const entryType = schema.enum("entry_type", [ "extra", ]); +export const entryContent = schema.enum("entry_content", [ + "story", + "recap", + "filler", + "ova", +]); + export const entry_extid = () => jsonb() .$type< @@ -68,6 +75,7 @@ export const entries = schema.table( airDate: date(), runtime: integer(), thumbnail: image(), + content: entryContent().notNull(), externalId: entry_extid(), diff --git a/api/src/models/entry/base-entry.ts b/api/src/models/entry/base-entry.ts index 99d24df2..f98e0784 100644 --- a/api/src/models/entry/base-entry.ts +++ b/api/src/models/entry/base-entry.ts @@ -1,6 +1,8 @@ import { t } from "elysia"; import { Image } from "../utils/image"; +export const EntryContent = t.UnionEnum(["story", "recap", "filler", "ova"]); + export const BaseEntry = () => t.Object({ airDate: t.Nullable(t.String({ format: "date" })), @@ -11,6 +13,7 @@ export const BaseEntry = () => }), ), thumbnail: t.Nullable(Image), + content: EntryContent, }); export const EntryTranslation = () => diff --git a/api/src/models/entry/extra.ts b/api/src/models/entry/extra.ts index ed951ab1..d0446408 100644 --- a/api/src/models/entry/extra.ts +++ b/api/src/models/entry/extra.ts @@ -22,7 +22,7 @@ export const BaseExtra = t.Composite( kind: ExtraType, name: t.String(), }), - t.Omit(BaseEntry(), ["nextRefresh", "airDate"]), + t.Omit(BaseEntry(), ["nextRefresh", "airDate", "content"]), ], { description: comment` diff --git a/api/src/models/examples/made-in-abyss.ts b/api/src/models/examples/made-in-abyss.ts index da3b55fa..72c9f3d0 100644 --- a/api/src/models/examples/made-in-abyss.ts +++ b/api/src/models/examples/made-in-abyss.ts @@ -175,6 +175,7 @@ export const madeInAbyss = { entries: [ { kind: "episode", + content: "story", order: 13, seasonNumber: 1, episodeNumber: 13, @@ -203,6 +204,7 @@ export const madeInAbyss = { }, { kind: "special", + content: "ova", // between s1e13 & movie (which has 13.5 for the `order field`) order: 13.25, number: 3, @@ -230,6 +232,7 @@ export const madeInAbyss = { }, { kind: "movie", + content: "story", slug: "made-in-abyss-dawn-of-the-deep-soul", order: 13.5, translations: { @@ -257,6 +260,7 @@ export const madeInAbyss = { }, { kind: "episode", + content: "story", order: 14, seasonNumber: 2, episodeNumber: 1, @@ -284,6 +288,7 @@ export const madeInAbyss = { }, { kind: "episode", + content: "story", order: 15, seasonNumber: 2, episodeNumber: 2, @@ -311,6 +316,7 @@ export const madeInAbyss = { }, { kind: "episode", + content: "story", order: 16, seasonNumber: 2, episodeNumber: 3, @@ -338,6 +344,7 @@ export const madeInAbyss = { }, { kind: "episode", + content: "story", order: 17, seasonNumber: 2, episodeNumber: 4, diff --git a/api/src/models/full-video.ts b/api/src/models/full-video.ts index bf94237c..a63bf4e6 100644 --- a/api/src/models/full-video.ts +++ b/api/src/models/full-video.ts @@ -16,10 +16,12 @@ export const FullVideo = t.Composite([ previous: t.Optional( t.Nullable( t.Object({ - video: t.String({ - format: "slug", - examples: ["made-in-abyss-s1e12"], - }), + video: t.Nullable( + t.String({ + format: "slug", + examples: ["made-in-abyss-s1e12"], + }), + ), entry: Entry, }), ), @@ -27,10 +29,12 @@ export const FullVideo = t.Composite([ next: t.Optional( t.Nullable( t.Object({ - video: t.String({ - format: "slug", - examples: ["made-in-abyss-dawn-of-the-deep-soul"], - }), + video: t.Nullable( + t.String({ + format: "slug", + examples: ["made-in-abyss-dawn-of-the-deep-soul"], + }), + ), entry: Entry, }), ), diff --git a/api/tests/series/nextup.test.ts b/api/tests/series/nextup.test.ts index ce34f600..3353d745 100644 --- a/api/tests/series/nextup.test.ts +++ b/api/tests/series/nextup.test.ts @@ -28,7 +28,7 @@ beforeAll(async () => { }); const miaEntrySlug = `${madeInAbyss.slug}-s1e13`; -const miaNextEntrySlug = `${madeInAbyss.slug}-sp3`; +const miaNextEntrySlug = "made-in-abyss-dawn-of-the-deep-soul"; describe("nextup", () => { it("Watchlist populates nextup", async () => { diff --git a/front/public/translations/en.json b/front/public/translations/en.json index 6fe0a950..e7992e29 100644 --- a/front/public/translations/en.json +++ b/front/public/translations/en.json @@ -49,7 +49,7 @@ "part": "Part {{number}}", "videos-map": "Edit video mappings", "remap": "Remap", - "staff-as":"as {{character}}", + "staff-as": "as {{character}}", "staff-kind": { "actor": "Actor", "director": "Director", @@ -253,7 +253,9 @@ "transmux": "Original", "auto": "Auto", "notInPristine": "Unavailable in pristine", - "unsupportedError": "Video codec not supported, transcoding in progress..." + "unsupportedError": "Video codec not supported, transcoding in progress...", + "not-available": "{{entry}} is not available on kyoo yet, ask your server admins about it", + "fatal": "Fatal playback error" }, "search": { "empty": "No result found. Try a different query." diff --git a/front/src/components/entries/entry-box.tsx b/front/src/components/entries/entry-box.tsx index 4666a43d..a22de70b 100644 --- a/front/src/components/entries/entry-box.tsx +++ b/front/src/components/entries/entry-box.tsx @@ -36,7 +36,7 @@ export const EntryBox = ({ serieSlug: string | null; name: string | null; description: string | null; - href: string; + href: string | null; thumbnail: KImage | null; watchedPercent: number; videos: Entry["videos"]; @@ -51,7 +51,11 @@ export const EntryBox = ({ href={moreOpened || videos.length > 1 ? undefined : href} onPress={videos.length > 1 ? onSelectVideos : undefined} onLongPress={() => setMoreOpened(true)} - className={cn("group w-[350px] items-center p-1 outline-0", className)} + className={cn( + "group w-[350px] items-center p-1 outline-0", + href === null && "opacity-50", + className, + )} {...props} > - {account && ( - markAsSeenMutation.mutate()} - /> - )} {serieSlug && ( )} + {account && ( + markAsSeenMutation.mutate()} + /> + )} {/* ; diff --git a/front/src/query/fetch-infinite.tsx b/front/src/query/fetch-infinite.tsx index 0c60aca0..de640c90 100644 --- a/front/src/query/fetch-infinite.tsx +++ b/front/src/query/fetch-infinite.tsx @@ -24,6 +24,7 @@ export const InfiniteFetch = ({ query, placeholderCount = 4, incremental = false, + getKey, getItemType, getItemSizeMult, getStickyIndices, @@ -43,6 +44,7 @@ export const InfiniteFetch = ({ placeholderCount?: number; layout: Layout; horizontal?: boolean; + getKey?: (item: Data, index: number) => string; getItemType?: (item: Data, index: number) => Type; getItemSizeMult?: (item: Data, index: number, type: Type) => number; getStickyIndices?: (items: Data[]) => number[]; @@ -94,7 +96,10 @@ export const InfiniteFetch = ({ renderItem={({ item, index }) => item ? : } - keyExtractor={(item: any, index) => (item ? item.id : index + 1)} + keyExtractor={(item: any, index) => { + if (!item) return index + 1; + return getKey ? getKey(item, index) : item.id; + }} horizontal={layout.layout === "horizontal"} numColumns={layout.layout === "horizontal" ? 1 : numColumns} onEndReached={ diff --git a/front/src/ui/details/header.tsx b/front/src/ui/details/header.tsx index c398f11a..90668bb3 100644 --- a/front/src/ui/details/header.tsx +++ b/front/src/ui/details/header.tsx @@ -37,7 +37,6 @@ import { HR, IconButton, IconFab, - Image, ImageBackground, LI, Link, diff --git a/front/src/ui/details/season.tsx b/front/src/ui/details/season.tsx index 79599655..d18726ce 100644 --- a/front/src/ui/details/season.tsx +++ b/front/src/ui/details/season.tsx @@ -246,8 +246,13 @@ EntryList.query = ( path: ["api", "series", slug, "entries"], params: { query, - // TODO: use a better filter, it removes specials and movies - filter: season ? `seasonNumber ge ${season}` : undefined, + filter: [ + // TODO: use a better filter, it removes specials and movies + season && `seasonNumber ge ${season}`, + "(kind eq episode or isAvailable eq true or content eq story)", + ] + .filter((x) => x) + .join(" and "), includeSeasons: true, }, infinite: true, diff --git a/front/src/ui/details/staff.tsx b/front/src/ui/details/staff.tsx index 79b0b4c4..066dcfd4 100644 --- a/front/src/ui/details/staff.tsx +++ b/front/src/ui/details/staff.tsx @@ -3,6 +3,7 @@ import { View } from "react-native"; import { type KImage, Role } from "~/models"; import { Container, H2, Link, P, Poster, Skeleton, SubP } from "~/primitives"; import { InfiniteGrid, type QueryIdentifier } from "~/query"; +import { cn } from "~/utils"; import { EmptyView } from "../empty-view"; export const CharacterCard = ({ @@ -21,11 +22,17 @@ export const CharacterCard = ({ return ( -

+

{name}

diff --git a/front/src/ui/home/news.tsx b/front/src/ui/home/news.tsx index 5d352ea5..5dfba286 100644 --- a/front/src/ui/home/news.tsx +++ b/front/src/ui/home/news.tsx @@ -45,7 +45,7 @@ export const NewsList = () => { name={`${item.show!.name} ${entryDisplayNumber(item)}`} description={item.name} thumbnail={item.thumbnail ?? item.show!.thumbnail} - href={item.href ?? "#"} + href={item.href} watchedPercent={item.progress.percent} videos={item.videos} onSelectVideos={() => diff --git a/front/src/ui/home/nextup.tsx b/front/src/ui/home/nextup.tsx index e961ffa0..5b2211ae 100644 --- a/front/src/ui/home/nextup.tsx +++ b/front/src/ui/home/nextup.tsx @@ -66,7 +66,7 @@ export const NextupList = () => { name={`${item.show!.name} ${entryDisplayNumber(item)}`} description={item.name} thumbnail={item.thumbnail ?? item.show!.thumbnail} - href={item.href ?? "#"} + href={item.href} watchedPercent={item.progress.percent} videos={item.videos} onSelectVideos={() => diff --git a/front/src/ui/player/controls/error-popup.tsx b/front/src/ui/player/controls/error-popup.tsx new file mode 100644 index 00000000..651bfc9a --- /dev/null +++ b/front/src/ui/player/controls/error-popup.tsx @@ -0,0 +1,29 @@ +import Close from "@material-symbols/svg-400/rounded/close-fill.svg"; +import { useTranslation } from "react-i18next"; +import { View } from "react-native"; +import { Heading, IconButton, P } from "~/primitives"; +import { cn } from "~/utils"; + +export const ErrorPopup = ({ + message, + dismiss, +}: { + message: string; + dismiss: () => void; +}) => { + const { t } = useTranslation(); + return ( + + + {t("player.fatal")} +

{message}

+
+ +
+ ); +}; diff --git a/front/src/ui/player/controls/index.tsx b/front/src/ui/player/controls/index.tsx index 1a29b047..cc13e608 100644 --- a/front/src/ui/player/controls/index.tsx +++ b/front/src/ui/player/controls/index.tsx @@ -20,6 +20,7 @@ export const Controls = ({ chapters, playPrev, playNext, + forceShow, }: { player: VideoPlayer; showHref?: string; @@ -31,6 +32,7 @@ export const Controls = ({ chapters: Chapter[]; playPrev: (() => boolean) | null; playNext: (() => boolean) | null; + forceShow?: boolean; }) => { const isTouch = useIsTouch(); @@ -56,7 +58,7 @@ export const Controls = ({ { ); const playModeState = useState(defaultPlayMode); const [playMode, setPlayMode] = playModeState; + const [playbackError, setPlaybackError] = useState(); const player = useVideoPlayer( { uri: `${apiUrl}/api/videos/${slug}/${playMode === "direct" ? "direct" : "master.m3u8"}?clientId=${clientId}`, @@ -101,18 +104,39 @@ export const Player = () => { }, [player, info?.fonts]); const router = useRouter(); + const { t } = useTranslation(); const playPrev = useCallback(() => { if (!data?.previous) return false; + if (!data.previous.video) { + setPlaybackError({ + status: "not-available", + message: t("player.not-available", { + entry: `${entryDisplayNumber(data.previous.entry)} ${data.previous.entry.name}`, + }), + }); + return true; + } + setPlaybackError(undefined); setStart("0"); setSlug(data.previous.video); return true; - }, [data?.previous, setSlug, setStart]); + }, [data?.previous, setSlug, setStart, t]); const playNext = useCallback(() => { if (!data?.next) return false; + if (!data.next.video) { + setPlaybackError({ + status: "not-available", + message: t("player.not-available", { + entry: `${entryDisplayNumber(data.next.entry)} ${data.next.entry.name}`, + }), + }); + return true; + } + setPlaybackError(undefined); setStart("0"); setSlug(data.next.video); return true; - }, [data?.next, setSlug, setStart]); + }, [data?.next, setSlug, setStart, t]); useProgressObserver( player, @@ -148,7 +172,6 @@ export const Player = () => { }; }, []); - const [playbackError, setPlaybackError] = useState(); useEvent(player, "onError", (error) => { if ( error.code === "source/unsupported-content-type" && @@ -157,9 +180,6 @@ export const Player = () => { setPlayMode("hls"); else setPlaybackError({ status: error.code, message: error.message }); }); - if (playbackError) { - throw playbackError; - } return ( @@ -201,10 +221,17 @@ export const Player = () => { : data?.path } chapters={info?.chapters ?? []} - playPrev={data?.previous?.video ? playPrev : null} - playNext={data?.next?.video ? playNext : null} + playPrev={data?.previous ? playPrev : null} + playNext={data?.next ? playNext : null} + forceShow={!!playbackError} /> + {playbackError && ( + setPlaybackError(undefined)} + /> + )} ); }; diff --git a/front/src/ui/profile/index.tsx b/front/src/ui/profile/index.tsx index efd2e988..877e01a4 100644 --- a/front/src/ui/profile/index.tsx +++ b/front/src/ui/profile/index.tsx @@ -3,12 +3,14 @@ import Cancel from "@material-symbols/svg-400/rounded/cancel-fill.svg"; import CheckCircle from "@material-symbols/svg-400/rounded/check_circle-fill.svg"; import Replay from "@material-symbols/svg-400/rounded/replay.svg"; import Clock from "@material-symbols/svg-400/rounded/schedule-fill.svg"; +import { useCallback } from "react"; import { useTranslation } from "react-i18next"; import { View } from "react-native"; import { EntryBox, entryDisplayNumber } from "~/components/entries"; +import { EntrySelect } from "~/components/entries/select"; import { ItemGrid, itemMap } from "~/components/items"; import { Entry, Show, type User, User as UserModel } from "~/models"; -import { Avatar, H1, H3, P, Tabs } from "~/primitives"; +import { Avatar, H1, H3, P, Tabs, usePopup } from "~/primitives"; import { Fetch, InfiniteFetch, type QueryIdentifier } from "~/query"; import { EmptyView } from "~/ui/empty-view"; import { useQueryState } from "~/utils"; @@ -58,6 +60,25 @@ const ProfileHeader = ({ setStatus: (value: WatchlistFilter) => void; }) => { const { t } = useTranslation(); + const [setPopup, closePopup] = usePopup(); + + const openEntrySelect = useCallback( + (entry: { + displayNumber: string; + name: string | null; + videos: Entry["videos"]; + }) => { + setPopup( + , + ); + }, + [setPopup, closePopup], + ); return ( @@ -90,6 +111,7 @@ const ProfileHeader = ({ `${x.id}-${x.progress.playedDate?.toISOString()}`} Empty={} Render={({ item }) => ( {}} + onSelectVideos={() => + openEntrySelect({ + displayNumber: entryDisplayNumber(item), + name: item.name, + videos: item.videos, + }) + } /> )} Loader={EntryBox.Loader} @@ -170,9 +198,6 @@ ProfilePage.historyQuery = (slug: string): QueryIdentifier => ({ parser: Entry, infinite: true, path: ["api", "profiles", slug, "history"], - params: { - with: ["show"], - }, }); ProfilePage.userQuery = (slug: string): QueryIdentifier => ({ diff --git a/scanner/scanner/models/entry.py b/scanner/scanner/models/entry.py index b8f4071f..96a8787e 100644 --- a/scanner/scanner/models/entry.py +++ b/scanner/scanner/models/entry.py @@ -1,6 +1,7 @@ from __future__ import annotations from datetime import date +from enum import Enum from typing import Any, Literal from pydantic import Field @@ -9,12 +10,20 @@ from ..utils import Language, Model from .metadataid import EpisodeId, MetadataId +class EntryContent(str, Enum): + STORY = "story" + RECAP = "recap" + FILLER = "filler" + OVA = "ova" + + class Entry(Model): kind: Literal["episode", "movie", "special"] order: float runtime: int | None air_date: date | None thumbnail: str | None + content: EntryContent # Movie-specific fields slug: str | None diff --git a/scanner/scanner/providers/themoviedatabase.py b/scanner/scanner/providers/themoviedatabase.py index 0d4fe60c..8c470020 100644 --- a/scanner/scanner/providers/themoviedatabase.py +++ b/scanner/scanner/providers/themoviedatabase.py @@ -11,7 +11,7 @@ from aiohttp import ClientResponseError, ClientSession from langcodes import Language from ..models.collection import Collection, CollectionTranslation -from ..models.entry import Entry, EntryTranslation +from ..models.entry import Entry, EntryContent, EntryTranslation from ..models.genre import Genre from ..models.metadataid import EpisodeId, MetadataId, SeasonId from ..models.movie import Movie, MovieStatus, MovieTranslation, SearchMovie @@ -556,6 +556,7 @@ class TheMovieDatabase(Provider): if episode["air_date"] else None, thumbnail=self._map_image(episode["still_path"]), + content=EntryContent.STORY, slug=None, season_number=episode["season_number"], episode_number=episode["episode_number"], diff --git a/scanner/scanner/providers/thetvdb.py b/scanner/scanner/providers/thetvdb.py index 5d3eb21c..54351e0e 100644 --- a/scanner/scanner/providers/thetvdb.py +++ b/scanner/scanner/providers/thetvdb.py @@ -11,7 +11,7 @@ from langcodes.data_dicts import LANGUAGE_REPLACEMENTS from ..cache import cache from ..models.collection import Collection, CollectionTranslation -from ..models.entry import Entry, EntryTranslation +from ..models.entry import Entry, EntryContent, EntryTranslation from ..models.genre import Genre from ..models.metadataid import EpisodeId, MetadataId, SeasonId from ..models.movie import Movie, MovieStatus, MovieTranslation, SearchMovie @@ -541,6 +541,12 @@ class TVDB(Provider): thumbnail=f"https://artworks.thetvdb.com{entry['image']}" if entry["image"] else None, + # Mark specials as ova, waiting for https://github.com/thetvdb/v4-api/issues/350 + content=( + EntryContent.STORY + if entry["seasonNumber"] != 0 or entry["isMovie"] + else EntryContent.OVA + ), slug=None, season_number=entry["seasonNumber"], episode_number=entry["number"], @@ -620,7 +626,7 @@ class TVDB(Provider): ) # handle specials and such that are between seasons - for entry in ret: + for entry in reversed(ret): if entry.order != 0: continue @@ -635,7 +641,11 @@ class TVDB(Provider): ) after = min((x.order for x in ret if x.order > before), default=before) entry.order = (before + after) / 2 - elif entry.extra["airs_before_season"] is not None: + + for entry in ret: + if entry.order != 0: + continue + if entry.extra["airs_before_season"] is not None: before = ( next( (