diff --git a/drizzle/0018_platform_settings_nfo_view.sql b/drizzle/0018_platform_settings_nfo_view.sql new file mode 100644 index 0000000..c4f52f9 --- /dev/null +++ b/drizzle/0018_platform_settings_nfo_view.sql @@ -0,0 +1,2 @@ +ALTER TABLE `platform_settings` ADD `nfo_enabled` integer NOT NULL DEFAULT false;--> statement-breakpoint +ALTER TABLE `platform_settings` ADD `default_view` text NOT NULL DEFAULT 'list'; \ No newline at end of file diff --git a/drizzle/meta/0018_snapshot.json b/drizzle/meta/0018_snapshot.json new file mode 100644 index 0000000..eede18c --- /dev/null +++ b/drizzle/meta/0018_snapshot.json @@ -0,0 +1,1147 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "0018_platform_settings_nfo_view", + "prevId": "0017_wild_havok", + "tables": { + "channels": { + "name": "channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform_id": { + "name": "platform_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monitoring_enabled": { + "name": "monitoring_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "check_interval": { + "name": "check_interval", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 360 + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "format_profile_id": { + "name": "format_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_check_status": { + "name": "last_check_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monitoring_mode": { + "name": "monitoring_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'all'" + }, + "banner_url": { + "name": "banner_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscriber_count": { + "name": "subscriber_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "include_keywords": { + "name": "include_keywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exclude_keywords": { + "name": "exclude_keywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_rating": { + "name": "content_rating", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "channels_format_profile_id_format_profiles_id_fk": { + "name": "channels_format_profile_id_format_profiles_id_fk", + "tableFrom": "channels", + "tableTo": "format_profiles", + "columnsFrom": [ + "format_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "content_items": { + "name": "content_items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform_content_id": { + "name": "platform_content_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quality_metadata": { + "name": "quality_metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'monitored'" + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "published_at": { + "name": "published_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "downloaded_at": { + "name": "downloaded_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monitored": { + "name": "monitored", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "content_rating": { + "name": "content_rating", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "content_items_channel_id_channels_id_fk": { + "name": "content_items_channel_id_channels_id_fk", + "tableFrom": "content_items", + "tableTo": "channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "format_profiles": { + "name": "format_profiles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "video_resolution": { + "name": "video_resolution", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audio_codec": { + "name": "audio_codec", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "audio_bitrate": { + "name": "audio_bitrate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_format": { + "name": "container_format", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "subtitle_languages": { + "name": "subtitle_languages", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "embed_subtitles": { + "name": "embed_subtitles", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "embed_chapters": { + "name": "embed_chapters", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "embed_thumbnail": { + "name": "embed_thumbnail", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sponsor_block_remove": { + "name": "sponsor_block_remove", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_template": { + "name": "output_template", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "download_history": { + "name": "download_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "content_item_id": { + "name": "content_item_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "download_history_content_item_id_content_items_id_fk": { + "name": "download_history_content_item_id_content_items_id_fk", + "tableFrom": "download_history", + "tableTo": "content_items", + "columnsFrom": [ + "content_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "download_history_channel_id_channels_id_fk": { + "name": "download_history_channel_id_channels_id_fk", + "tableFrom": "download_history", + "tableTo": "channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "content_playlist": { + "name": "content_playlist", + "columns": { + "content_item_id": { + "name": "content_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "playlist_id": { + "name": "playlist_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "content_playlist_content_item_id_content_items_id_fk": { + "name": "content_playlist_content_item_id_content_items_id_fk", + "tableFrom": "content_playlist", + "tableTo": "content_items", + "columnsFrom": [ + "content_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "content_playlist_playlist_id_playlists_id_fk": { + "name": "content_playlist_playlist_id_playlists_id_fk", + "tableFrom": "content_playlist", + "tableTo": "playlists", + "columnsFrom": [ + "playlist_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "content_playlist_content_item_id_playlist_id_pk": { + "columns": [ + "content_item_id", + "playlist_id" + ], + "name": "content_playlist_content_item_id_playlist_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "media_servers": { + "name": "media_servers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "library_section": { + "name": "library_section", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_settings": { + "name": "notification_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "on_grab": { + "name": "on_grab", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "on_download": { + "name": "on_download", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "on_failure": { + "name": "on_failure", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "platform_settings": { + "name": "platform_settings", + "columns": { + "platform": { + "name": "platform", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "default_format_profile_id": { + "name": "default_format_profile_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "check_interval": { + "name": "check_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 360 + }, + "concurrency_limit": { + "name": "concurrency_limit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 2 + }, + "subtitle_languages": { + "name": "subtitle_languages", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "grab_all_enabled": { + "name": "grab_all_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "grab_all_order": { + "name": "grab_all_order", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'newest'" + }, + "scan_limit": { + "name": "scan_limit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 100 + }, + "rate_limit_delay": { + "name": "rate_limit_delay", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 1000 + }, + "default_monitoring_mode": { + "name": "default_monitoring_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'all'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + }, + "nfo_enabled": { + "name": "nfo_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "default_view": { + "name": "default_view", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'list'" + } + }, + "indexes": {}, + "foreignKeys": { + "platform_settings_default_format_profile_id_format_profiles_id_fk": { + "name": "platform_settings_default_format_profile_id_format_profiles_id_fk", + "tableFrom": "platform_settings", + "tableTo": "format_profiles", + "columnsFrom": [ + "default_format_profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "playlists": { + "name": "playlists", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform_playlist_id": { + "name": "platform_playlist_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "playlists_channel_id_channels_id_fk": { + "name": "playlists_channel_id_channels_id_fk", + "tableFrom": "playlists", + "tableTo": "channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "queue_items": { + "name": "queue_items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "content_item_id": { + "name": "content_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_category": { + "name": "error_category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "queue_items_content_item_id_content_items_id_fk": { + "name": "queue_items_content_item_id_content_items_id_fk", + "tableFrom": "queue_items", + "tableTo": "content_items", + "columnsFrom": [ + "content_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "system_config": { + "name": "system_config", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 549e8fb..de8e6f5 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -127,6 +127,13 @@ "when": 1775282773898, "tag": "0017_wild_havok", "breakpoints": true + }, + { + "idx": 18, + "version": "6", + "when": 1775520100000, + "tag": "0018_platform_settings_nfo_view", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/__tests__/platform-settings-api.test.ts b/src/__tests__/platform-settings-api.test.ts index 908d6c9..e373541 100644 --- a/src/__tests__/platform-settings-api.test.ts +++ b/src/__tests__/platform-settings-api.test.ts @@ -344,6 +344,137 @@ describe('Platform Settings API', () => { }); }); + // ── Generic platform ── + + describe('Generic platform', () => { + it('accepts generic as a valid platform', async () => { + const res = await server.inject({ + method: 'PUT', + url: '/api/v1/platform-settings/generic', + headers: { 'x-api-key': apiKey }, + payload: { + checkInterval: 480, + concurrencyLimit: 1, + }, + }); + + expect(res.statusCode).toBe(200); + const body = res.json(); + expect(body.platform).toBe('generic'); + expect(body.checkInterval).toBe(480); + expect(body.nfoEnabled).toBe(false); + expect(body.defaultView).toBe('list'); + }); + + it('shows generic in list of all platform settings', async () => { + const res = await server.inject({ + method: 'GET', + url: '/api/v1/platform-settings', + headers: { 'x-api-key': apiKey }, + }); + + expect(res.statusCode).toBe(200); + const platforms = res.json().map((s: { platform: string }) => s.platform); + expect(platforms).toContain('generic'); + }); + }); + + // ── nfoEnabled ── + + describe('nfoEnabled', () => { + it('persists nfoEnabled through PUT → GET round-trip', async () => { + const putRes = await server.inject({ + method: 'PUT', + url: '/api/v1/platform-settings/youtube', + headers: { 'x-api-key': apiKey }, + payload: { + nfoEnabled: true, + }, + }); + expect(putRes.statusCode).toBe(200); + expect(putRes.json().nfoEnabled).toBe(true); + + const getRes = await server.inject({ + method: 'GET', + url: '/api/v1/platform-settings/youtube', + headers: { 'x-api-key': apiKey }, + }); + expect(getRes.statusCode).toBe(200); + expect(getRes.json().nfoEnabled).toBe(true); + }); + + it('defaults nfoEnabled to false when not specified', async () => { + await server.inject({ + method: 'DELETE', + url: '/api/v1/platform-settings/generic', + headers: { 'x-api-key': apiKey }, + }); + + const putRes = await server.inject({ + method: 'PUT', + url: '/api/v1/platform-settings/generic', + headers: { 'x-api-key': apiKey }, + payload: { checkInterval: 360 }, + }); + expect(putRes.statusCode).toBe(200); + expect(putRes.json().nfoEnabled).toBe(false); + }); + }); + + // ── defaultView ── + + describe('defaultView', () => { + it('persists defaultView through PUT → GET round-trip', async () => { + const putRes = await server.inject({ + method: 'PUT', + url: '/api/v1/platform-settings/youtube', + headers: { 'x-api-key': apiKey }, + payload: { + defaultView: 'poster', + }, + }); + expect(putRes.statusCode).toBe(200); + expect(putRes.json().defaultView).toBe('poster'); + + const getRes = await server.inject({ + method: 'GET', + url: '/api/v1/platform-settings/youtube', + headers: { 'x-api-key': apiKey }, + }); + expect(getRes.statusCode).toBe(200); + expect(getRes.json().defaultView).toBe('poster'); + }); + + it('rejects invalid defaultView value', async () => { + const res = await server.inject({ + method: 'PUT', + url: '/api/v1/platform-settings/youtube', + headers: { 'x-api-key': apiKey }, + payload: { + defaultView: 'grid', + }, + }); + expect(res.statusCode).toBe(400); + }); + + it('defaults defaultView to list when not specified', async () => { + await server.inject({ + method: 'DELETE', + url: '/api/v1/platform-settings/generic', + headers: { 'x-api-key': apiKey }, + }); + + const putRes = await server.inject({ + method: 'PUT', + url: '/api/v1/platform-settings/generic', + headers: { 'x-api-key': apiKey }, + payload: { checkInterval: 360 }, + }); + expect(putRes.statusCode).toBe(200); + expect(putRes.json().defaultView).toBe('list'); + }); + }); + // ── Auth ── describe('Authentication', () => { diff --git a/src/db/repositories/platform-settings-repository.ts b/src/db/repositories/platform-settings-repository.ts index 77d1998..1b14891 100644 --- a/src/db/repositories/platform-settings-repository.ts +++ b/src/db/repositories/platform-settings-repository.ts @@ -18,6 +18,8 @@ export interface UpsertPlatformSettingsData { scanLimit?: number; rateLimitDelay?: number; defaultMonitoringMode?: MonitoringMode; + nfoEnabled?: boolean; + defaultView?: 'list' | 'poster' | 'table'; } type Db = LibSQLDatabase; @@ -67,6 +69,8 @@ export async function upsertPlatformSettings( scanLimit: data.scanLimit ?? 500, rateLimitDelay: data.rateLimitDelay ?? 1000, defaultMonitoringMode: data.defaultMonitoringMode ?? 'all', + nfoEnabled: data.nfoEnabled ?? false, + defaultView: data.defaultView ?? 'list', createdAt: now, updatedAt: now, }) @@ -82,6 +86,8 @@ export async function upsertPlatformSettings( scanLimit: data.scanLimit ?? 500, rateLimitDelay: data.rateLimitDelay ?? 1000, defaultMonitoringMode: data.defaultMonitoringMode ?? 'all', + nfoEnabled: data.nfoEnabled ?? false, + defaultView: data.defaultView ?? 'list', updatedAt: now, }, }) @@ -117,6 +123,8 @@ function mapRow(row: typeof platformSettings.$inferSelect): PlatformSettings { scanLimit: row.scanLimit ?? 500, rateLimitDelay: row.rateLimitDelay ?? 1000, defaultMonitoringMode: (row.defaultMonitoringMode ?? 'all') as MonitoringMode, + nfoEnabled: row.nfoEnabled, + defaultView: (row.defaultView ?? 'list') as 'list' | 'poster' | 'table', createdAt: row.createdAt, updatedAt: row.updatedAt, }; diff --git a/src/db/schema/platform-settings.ts b/src/db/schema/platform-settings.ts index f467fb4..ed08eda 100644 --- a/src/db/schema/platform-settings.ts +++ b/src/db/schema/platform-settings.ts @@ -17,6 +17,8 @@ export const platformSettings = sqliteTable('platform_settings', { scanLimit: integer('scan_limit').default(100), rateLimitDelay: integer('rate_limit_delay').default(1000), defaultMonitoringMode: text('default_monitoring_mode').notNull().default('all'), + nfoEnabled: integer('nfo_enabled', { mode: 'boolean' }).notNull().default(false), + defaultView: text('default_view').notNull().default('list'), createdAt: text('created_at') .notNull() .default(sql`(datetime('now'))`), diff --git a/src/frontend/src/api/hooks/usePlatformSettings.ts b/src/frontend/src/api/hooks/usePlatformSettings.ts index af94307..860e01b 100644 --- a/src/frontend/src/api/hooks/usePlatformSettings.ts +++ b/src/frontend/src/api/hooks/usePlatformSettings.ts @@ -41,6 +41,8 @@ export interface UpdatePlatformSettingsInput { scanLimit?: number; rateLimitDelay?: number; defaultMonitoringMode?: 'all' | 'future' | 'existing' | 'none'; + nfoEnabled?: boolean; + defaultView?: 'list' | 'poster' | 'table'; } // ── Mutations ── diff --git a/src/frontend/src/components/PlatformSettingsForm.tsx b/src/frontend/src/components/PlatformSettingsForm.tsx index 1f263f3..28636a7 100644 --- a/src/frontend/src/components/PlatformSettingsForm.tsx +++ b/src/frontend/src/components/PlatformSettingsForm.tsx @@ -14,6 +14,8 @@ export interface PlatformSettingsFormValues { scanLimit: number; rateLimitDelay: number; defaultMonitoringMode: string; + nfoEnabled: boolean; + defaultView: 'list' | 'poster' | 'table'; } interface PlatformSettingsFormProps { @@ -89,6 +91,8 @@ export function PlatformSettingsForm({ const [scanLimit, setScanLimit] = useState(settings?.scanLimit ?? 100); const [rateLimitDelay, setRateLimitDelay] = useState(settings?.rateLimitDelay ?? 1000); const [defaultMonitoringMode, setDefaultMonitoringMode] = useState(settings?.defaultMonitoringMode ?? 'all'); + const [nfoEnabled, setNfoEnabled] = useState(settings?.nfoEnabled ?? false); + const [defaultView, setDefaultView] = useState<'list' | 'poster' | 'table'>(settings?.defaultView ?? 'list'); const handleSubmit = useCallback( (e: FormEvent) => { @@ -103,12 +107,14 @@ export function PlatformSettingsForm({ scanLimit, rateLimitDelay, defaultMonitoringMode, + nfoEnabled, + defaultView, }); }, - [defaultFormatProfileId, checkInterval, concurrencyLimit, subtitleLanguages, grabAllEnabled, grabAllOrder, scanLimit, rateLimitDelay, defaultMonitoringMode, onSubmit], + [defaultFormatProfileId, checkInterval, concurrencyLimit, subtitleLanguages, grabAllEnabled, grabAllOrder, scanLimit, rateLimitDelay, defaultMonitoringMode, nfoEnabled, defaultView, onSubmit], ); - const platformLabel = platform === 'youtube' ? 'YouTube' : platform === 'soundcloud' ? 'SoundCloud' : platform; + const platformLabel = platform === 'youtube' ? 'YouTube' : platform === 'soundcloud' ? 'SoundCloud' : platform === 'generic' ? 'Generic' : platform; return (
@@ -280,6 +286,41 @@ export function PlatformSettingsForm({ Default monitoring mode for new channels from this platform. + {/* Default View */} +
+ + + Default content view when browsing channels on this platform. +
+ + {/* NFO Enabled */} +
+ setNfoEnabled(e.target.checked)} + style={{ + width: 16, + height: 16, + accentColor: 'var(--accent)', + cursor: 'pointer', + }} + /> + +
+ {/* Action buttons */}
- - {/* NFO Sidecar toggle */} -
-
- - - Write Kodi-compatible .nfo sidecar files alongside downloaded media - -
- -
@@ -1664,7 +1621,7 @@ export function SettingsPage() { {/* ── Platform Settings: Edit modal ── */} setEditingPlatform(null)} width={520} diff --git a/src/server/routes/platform-settings.ts b/src/server/routes/platform-settings.ts index 1914d4e..4ed7bf0 100644 --- a/src/server/routes/platform-settings.ts +++ b/src/server/routes/platform-settings.ts @@ -9,7 +9,7 @@ import { Platform } from '../../types/index'; // ── JSON Schemas for Fastify Validation ── -const VALID_PLATFORMS = [Platform.YouTube, Platform.SoundCloud] as const; +const VALID_PLATFORMS = [Platform.YouTube, Platform.SoundCloud, Platform.Generic] as const; const upsertPlatformSettingsBodySchema = { type: 'object' as const, @@ -23,6 +23,8 @@ const upsertPlatformSettingsBodySchema = { scanLimit: { type: 'integer' as const, minimum: 10, maximum: 1000 }, rateLimitDelay: { type: 'integer' as const, minimum: 0, maximum: 10000 }, defaultMonitoringMode: { type: 'string' as const, enum: ['all', 'future', 'existing', 'none'] }, + nfoEnabled: { type: 'boolean' as const }, + defaultView: { type: 'string' as const, enum: ['list', 'poster', 'table'] }, }, additionalProperties: false, }; @@ -79,6 +81,8 @@ export async function platformSettingsRoutes(fastify: FastifyInstance): Promise< scanLimit?: number; rateLimitDelay?: number; defaultMonitoringMode?: 'all' | 'future' | 'existing' | 'none'; + nfoEnabled?: boolean; + defaultView?: 'list' | 'poster' | 'table'; }; }>( '/api/v1/platform-settings/:platform', diff --git a/src/types/index.ts b/src/types/index.ts index e5c41ba..cb4aa74 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -189,6 +189,8 @@ export interface PlatformSettings { scanLimit: number; rateLimitDelay: number; defaultMonitoringMode: MonitoringMode; + nfoEnabled: boolean; + defaultView: 'list' | 'poster' | 'table'; createdAt: string; updatedAt: string; }