diff --git a/drizzle/0009_many_carlie_cooper.sql b/drizzle/0009_many_carlie_cooper.sql new file mode 100644 index 0000000..9ffe95f --- /dev/null +++ b/drizzle/0009_many_carlie_cooper.sql @@ -0,0 +1 @@ +ALTER TABLE `queue_items` ADD `error_category` text; \ No newline at end of file diff --git a/drizzle/0010_special_ghost_rider.sql b/drizzle/0010_special_ghost_rider.sql new file mode 100644 index 0000000..adcd59e --- /dev/null +++ b/drizzle/0010_special_ghost_rider.sql @@ -0,0 +1,3 @@ +ALTER TABLE `channels` ADD `banner_url` text;--> statement-breakpoint +ALTER TABLE `channels` ADD `description` text;--> statement-breakpoint +ALTER TABLE `channels` ADD `subscriber_count` integer; \ No newline at end of file diff --git a/drizzle/meta/0009_snapshot.json b/drizzle/meta/0009_snapshot.json new file mode 100644 index 0000000..755adda --- /dev/null +++ b/drizzle/meta/0009_snapshot.json @@ -0,0 +1,976 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "eaac3184-0b4a-45d4-b2a9-da09dbd4bd56", + "prevId": "b2c3d4e5-f6a7-8901-bcde-f12345678901", + "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'" + } + }, + "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": true, + "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 + }, + "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 + }, + "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": {} + }, + "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'))" + } + }, + "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/0010_snapshot.json b/drizzle/meta/0010_snapshot.json new file mode 100644 index 0000000..ffe76c8 --- /dev/null +++ b/drizzle/meta/0010_snapshot.json @@ -0,0 +1,997 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "2032ed4f-0e7d-4a3c-9e00-96716084f3f6", + "prevId": "eaac3184-0b4a-45d4-b2a9-da09dbd4bd56", + "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 + } + }, + "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": true, + "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 + }, + "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 + }, + "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": {} + }, + "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'))" + } + }, + "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 063a6a8..445549e 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -64,6 +64,20 @@ "when": 1774839000000, "tag": "0008_add_default_monitoring_mode", "breakpoints": true + }, + { + "idx": 9, + "version": "6", + "when": 1775192114394, + "tag": "0009_many_carlie_cooper", + "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1775196046744, + "tag": "0010_special_ghost_rider", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/__tests__/sources.test.ts b/src/__tests__/sources.test.ts index 60dc603..4174ab5 100644 --- a/src/__tests__/sources.test.ts +++ b/src/__tests__/sources.test.ts @@ -135,6 +135,9 @@ function makeChannel(overrides: Partial = {}): Channel { metadata: null, formatProfileId: null, monitoringMode: 'all', + bannerUrl: null, + description: null, + subscriberCount: null, createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', lastCheckedAt: null, @@ -237,6 +240,9 @@ describe('YouTubeSource', () => { imageUrl: 'https://i.ytimg.com/vi/thumb_large.jpg', url: 'https://www.youtube.com/channel/UCXuqSBlHAE6Xw-yeJA0Tunw', platform: 'youtube', + bannerUrl: 'https://i.ytimg.com/vi/thumb_large.jpg', + description: null, + subscriberCount: null, }); // Verify yt-dlp was called with correct args @@ -669,6 +675,9 @@ describe('SoundCloudSource', () => { imageUrl: 'https://i1.sndcdn.com/avatars-large.jpg', url: 'https://soundcloud.com/deadmau5', platform: 'soundcloud', + bannerUrl: null, + description: null, + subscriberCount: null, }); }); }); diff --git a/src/__tests__/yt-dlp-classification.test.ts b/src/__tests__/yt-dlp-classification.test.ts new file mode 100644 index 0000000..94713a1 --- /dev/null +++ b/src/__tests__/yt-dlp-classification.test.ts @@ -0,0 +1,161 @@ +import { describe, it, expect } from 'vitest'; +import { + classifyYtDlpError, + YtDlpError, + type YtDlpErrorCategory, +} from '../sources/yt-dlp'; + +describe('classifyYtDlpError', () => { + // ── rate_limit ── + + it('classifies HTTP 429 as rate_limit', () => { + expect(classifyYtDlpError('ERROR: HTTP Error 429: Too Many Requests')).toBe('rate_limit'); + }); + + it('classifies "too many requests" as rate_limit', () => { + expect(classifyYtDlpError('ERROR: too many requests, please retry later')).toBe('rate_limit'); + }); + + // ── format_unavailable ── + + it('classifies "requested format" as format_unavailable', () => { + expect(classifyYtDlpError('ERROR: requested format not available')).toBe('format_unavailable'); + }); + + it('classifies "format is not available" as format_unavailable', () => { + expect(classifyYtDlpError('ERROR: format is not available')).toBe('format_unavailable'); + }); + + // ── geo_blocked ── + + it('classifies geo-restriction as geo_blocked', () => { + expect( + classifyYtDlpError('ERROR: Video not available in your country') + ).toBe('geo_blocked'); + }); + + it('classifies "geo" keyword as geo_blocked', () => { + expect(classifyYtDlpError('ERROR: geo-restricted content')).toBe('geo_blocked'); + }); + + // ── age_restricted ── + + it('classifies age-restricted content', () => { + expect(classifyYtDlpError('ERROR: age restricted video')).toBe('age_restricted'); + }); + + it('classifies age verify as age_restricted', () => { + expect(classifyYtDlpError('Sign in to confirm your age. This video may be inappropriate for some users. Verify your age')).toBe('age_restricted'); + }); + + // ── private ── + + it('classifies private video', () => { + expect(classifyYtDlpError('ERROR: Private video. Sign in if you\'ve been granted access')).toBe('private'); + }); + + it('classifies "video unavailable"', () => { + expect(classifyYtDlpError('ERROR: Video unavailable')).toBe('private'); + }); + + it('classifies "been removed"', () => { + expect(classifyYtDlpError('ERROR: This video has been removed by the uploader')).toBe('private'); + }); + + // ── sign_in_required ── + + it('classifies "sign in" as sign_in_required', () => { + expect(classifyYtDlpError('ERROR: Sign in to confirm you are not a bot')).toBe('sign_in_required'); + }); + + it('classifies "login required" as sign_in_required', () => { + expect(classifyYtDlpError('ERROR: This video requires login required authentication')).toBe('sign_in_required'); + }); + + // ── copyright ── + + it('classifies "copyright" keyword', () => { + expect(classifyYtDlpError('ERROR: This video contains content from UMG, who has blocked it on copyright grounds')).toBe('copyright'); + }); + + it('classifies "blocked...claim" pattern', () => { + expect(classifyYtDlpError('ERROR: Video blocked due to a claim by Sony Music')).toBe('copyright'); + }); + + // ── network ── + + it('classifies connection error as network', () => { + expect(classifyYtDlpError('ERROR: unable to download webpage: connection refused')).toBe('network'); + }); + + it('classifies timeout as network', () => { + expect(classifyYtDlpError('ERROR: timed out')).toBe('network'); + }); + + it('classifies urlopen error as network', () => { + expect(classifyYtDlpError('ERROR: ')).toBe('network'); + }); + + // ── unknown ── + + it('returns unknown for empty string', () => { + expect(classifyYtDlpError('')).toBe('unknown'); + }); + + it('returns unknown for unrecognized error', () => { + expect(classifyYtDlpError('ERROR: Something completely unexpected happened')).toBe('unknown'); + }); + + // ── Priority / first-match-wins ── + + it('first match wins when multiple signals present', () => { + // Contains both '429' (rate_limit) and 'connection' (network) — rate_limit is checked first + const result = classifyYtDlpError('ERROR: 429 connection refused'); + expect(result).toBe('rate_limit'); + }); +}); + +describe('YtDlpError.category', () => { + it('auto-populates category from stderr in constructor', () => { + const err = new YtDlpError( + 'yt-dlp failed', + 'ERROR: HTTP Error 429: Too Many Requests', + 1 + ); + expect(err.category).toBe('rate_limit'); + expect(err.isRateLimit).toBe(true); + }); + + it('sets category to unknown for unrecognized errors', () => { + const err = new YtDlpError('yt-dlp failed', 'some weird error', 1); + expect(err.category).toBe('unknown'); + expect(err.isRateLimit).toBe(false); + }); + + it('sets sign_in_required category', () => { + const err = new YtDlpError( + 'yt-dlp failed', + 'ERROR: Sign in to confirm you are not a bot', + 1 + ); + expect(err.category).toBe('sign_in_required'); + }); + + it('sets copyright category', () => { + const err = new YtDlpError( + 'yt-dlp failed', + 'ERROR: blocked on copyright grounds', + 1 + ); + expect(err.category).toBe('copyright'); + }); + + it('preserves all existing YtDlpError properties', () => { + const err = new YtDlpError('msg', 'stderr text', 42); + expect(err.name).toBe('YtDlpError'); + expect(err.message).toBe('msg'); + expect(err.stderr).toBe('stderr text'); + expect(err.exitCode).toBe(42); + expect(err.category).toBe('unknown'); + }); +}); diff --git a/src/db/repositories/channel-repository.ts b/src/db/repositories/channel-repository.ts index ecc955b..0c9872f 100644 --- a/src/db/repositories/channel-repository.ts +++ b/src/db/repositories/channel-repository.ts @@ -9,12 +9,17 @@ import type { Channel, Platform, MonitoringMode } from '../../types/index'; /** Fields needed to create a new channel (auto-generated fields excluded). */ export type CreateChannelData = Omit< Channel, - 'id' | 'createdAt' | 'updatedAt' | 'lastCheckedAt' | 'lastCheckStatus' | 'monitoringMode' -> & { monitoringMode?: Channel['monitoringMode'] }; + 'id' | 'createdAt' | 'updatedAt' | 'lastCheckedAt' | 'lastCheckStatus' | 'monitoringMode' | 'bannerUrl' | 'description' | 'subscriberCount' +> & { + monitoringMode?: Channel['monitoringMode']; + bannerUrl?: string | null; + description?: string | null; + subscriberCount?: number | null; +}; /** Fields that can be updated on an existing channel. */ export type UpdateChannelData = Partial< - Pick + Pick >; type Db = LibSQLDatabase; @@ -39,6 +44,9 @@ export async function createChannel( metadata: data.metadata, formatProfileId: data.formatProfileId, monitoringMode: data.monitoringMode ?? 'all', + bannerUrl: data.bannerUrl ?? null, + description: data.description ?? null, + subscriberCount: data.subscriberCount ?? null, }) .returning(); @@ -185,6 +193,9 @@ function mapRow(row: typeof channels.$inferSelect): Channel { metadata: row.metadata as Record | null, formatProfileId: row.formatProfileId, monitoringMode: (row.monitoringMode ?? 'all') as Channel['monitoringMode'], + bannerUrl: row.bannerUrl ?? null, + description: row.description ?? null, + subscriberCount: row.subscriberCount ?? null, createdAt: row.createdAt, updatedAt: row.updatedAt, lastCheckedAt: row.lastCheckedAt, diff --git a/src/db/repositories/queue-repository.ts b/src/db/repositories/queue-repository.ts index 68b28be..a589a25 100644 --- a/src/db/repositories/queue-repository.ts +++ b/src/db/repositories/queue-repository.ts @@ -16,6 +16,7 @@ export interface CreateQueueItemData { /** Optional fields when updating queue item status. */ export interface UpdateQueueItemFields { error?: string | null; + errorCategory?: string | null; startedAt?: string | null; completedAt?: string | null; attempts?: number; @@ -72,6 +73,7 @@ export async function getQueueItemsByStatus( attempts: queueItems.attempts, maxAttempts: queueItems.maxAttempts, error: queueItems.error, + errorCategory: queueItems.errorCategory, startedAt: queueItems.startedAt, completedAt: queueItems.completedAt, createdAt: queueItems.createdAt, @@ -101,6 +103,7 @@ export async function getAllQueueItems( attempts: queueItems.attempts, maxAttempts: queueItems.maxAttempts, error: queueItems.error, + errorCategory: queueItems.errorCategory, startedAt: queueItems.startedAt, completedAt: queueItems.completedAt, createdAt: queueItems.createdAt, @@ -154,6 +157,7 @@ export async function updateQueueItemStatus( }; if (updates?.error !== undefined) setData.error = updates.error; + if (updates?.errorCategory !== undefined) setData.errorCategory = updates.errorCategory; if (updates?.startedAt !== undefined) setData.startedAt = updates.startedAt; if (updates?.completedAt !== undefined) setData.completedAt = updates.completedAt; if (updates?.attempts !== undefined) setData.attempts = updates.attempts; @@ -239,6 +243,7 @@ function mapRow(row: typeof queueItems.$inferSelect): QueueItem { attempts: row.attempts, maxAttempts: row.maxAttempts, error: row.error, + errorCategory: row.errorCategory, startedAt: row.startedAt, completedAt: row.completedAt, createdAt: row.createdAt, @@ -255,6 +260,7 @@ interface JoinedQueueRow { attempts: number; maxAttempts: number; error: string | null; + errorCategory: string | null; startedAt: string | null; completedAt: string | null; createdAt: string; @@ -273,6 +279,7 @@ function mapJoinedRow(row: JoinedQueueRow): QueueItem { attempts: row.attempts, maxAttempts: row.maxAttempts, error: row.error, + errorCategory: row.errorCategory, startedAt: row.startedAt, completedAt: row.completedAt, createdAt: row.createdAt, diff --git a/src/db/schema/channels.ts b/src/db/schema/channels.ts index dfc9620..58f187b 100644 --- a/src/db/schema/channels.ts +++ b/src/db/schema/channels.ts @@ -28,4 +28,7 @@ export const channels = sqliteTable('channels', { lastCheckedAt: text('last_checked_at'), // null until first monitoring check lastCheckStatus: text('last_check_status'), // 'success' | 'error' | 'rate_limited' monitoringMode: text('monitoring_mode').notNull().default('all'), // 'all' | 'future' | 'existing' | 'none' + bannerUrl: text('banner_url'), + description: text('description'), + subscriberCount: integer('subscriber_count'), }); diff --git a/src/db/schema/queue.ts b/src/db/schema/queue.ts index 7820384..5babed0 100644 --- a/src/db/schema/queue.ts +++ b/src/db/schema/queue.ts @@ -13,6 +13,7 @@ export const queueItems = sqliteTable('queue_items', { attempts: integer('attempts').notNull().default(0), maxAttempts: integer('max_attempts').notNull().default(3), error: text('error'), + errorCategory: text('error_category'), // rate_limit|format_unavailable|geo_blocked|age_restricted|private|network|sign_in_required|copyright|unknown startedAt: text('started_at'), completedAt: text('completed_at'), createdAt: text('created_at') diff --git a/src/server/routes/channel.ts b/src/server/routes/channel.ts index 9e19c25..4a55165 100644 --- a/src/server/routes/channel.ts +++ b/src/server/routes/channel.ts @@ -176,6 +176,9 @@ export async function channelRoutes(fastify: FastifyInstance): Promise { imageUrl: metadata.imageUrl, metadata: null, formatProfileId: formatProfileId ?? null, + bannerUrl: metadata.bannerUrl ?? null, + description: metadata.description ?? null, + subscriberCount: metadata.subscriberCount ?? null, }); // Notify scheduler of new channel diff --git a/src/sources/soundcloud.ts b/src/sources/soundcloud.ts index 7338781..280ca7b 100644 --- a/src/sources/soundcloud.ts +++ b/src/sources/soundcloud.ts @@ -47,12 +47,23 @@ export class SoundCloudSource implements PlatformSource { ? (thumbnails[thumbnails.length - 1]?.url ?? null) : null; + // Extract enrichment metadata (limited availability on SoundCloud) + const description = typeof data.description === 'string' ? data.description : null; + const subscriberCount = typeof data.channel_follower_count === 'number' + ? data.channel_follower_count + : typeof data.uploader_follower_count === 'number' + ? data.uploader_follower_count + : null; + return { name: channelName, platformId: uploaderId, imageUrl, url: uploaderUrl, platform: Platform.SoundCloud, + bannerUrl: null, // SoundCloud doesn't provide banner URLs via yt-dlp + description, + subscriberCount, }; } diff --git a/src/sources/youtube.ts b/src/sources/youtube.ts index e3ed739..11c31e3 100644 --- a/src/sources/youtube.ts +++ b/src/sources/youtube.ts @@ -44,17 +44,35 @@ export class YouTubeSource implements PlatformSource { url; // Pick the best thumbnail — yt-dlp returns an array sorted by quality - const thumbnails = data.thumbnails as Array<{ url?: string }> | undefined; + const thumbnails = data.thumbnails as Array<{ url?: string; width?: number }> | undefined; const imageUrl = thumbnails?.length ? (thumbnails[thumbnails.length - 1]?.url ?? null) : null; + // Extract enrichment metadata + const description = typeof data.description === 'string' ? data.description : null; + const subscriberCount = typeof data.channel_follower_count === 'number' + ? data.channel_follower_count + : null; + + // Banner: try channel_banner_url first, then look for wide thumbnails (>=1024px) + let bannerUrl: string | null = null; + if (typeof data.channel_banner_url === 'string') { + bannerUrl = data.channel_banner_url; + } else if (thumbnails?.length) { + const wideThumbnail = thumbnails.find((t) => (t.width ?? 0) >= 1024); + if (wideThumbnail?.url) bannerUrl = wideThumbnail.url; + } + return { name: channelName, platformId: channelId, imageUrl, url: channelUrl, platform: Platform.YouTube, + bannerUrl, + description, + subscriberCount, }; } diff --git a/src/sources/yt-dlp.ts b/src/sources/yt-dlp.ts index 2977828..a8f42ec 100644 --- a/src/sources/yt-dlp.ts +++ b/src/sources/yt-dlp.ts @@ -26,6 +26,7 @@ export class YtDlpError extends Error { readonly stderr: string; readonly exitCode: number; readonly isRateLimit: boolean; + readonly category: YtDlpErrorCategory; constructor(message: string, stderr: string, exitCode: number) { super(message); @@ -33,6 +34,7 @@ export class YtDlpError extends Error { this.stderr = stderr; this.exitCode = exitCode; this.isRateLimit = detectRateLimit(stderr); + this.category = classifyYtDlpError(stderr); } } @@ -283,6 +285,8 @@ export type YtDlpErrorCategory = | 'age_restricted' // age-gated content | 'private' // private or removed video | 'network' // DNS, connection, timeout + | 'sign_in_required' // sign-in or login required + | 'copyright' // copyright claim or block | 'unknown'; export function classifyYtDlpError(stderr: string): YtDlpErrorCategory { @@ -293,6 +297,8 @@ export function classifyYtDlpError(stderr: string): YtDlpErrorCategory { if (lower.includes('not available in your country') || lower.includes('geo')) return 'geo_blocked'; if (lower.includes('age') && (lower.includes('restricted') || lower.includes('verify'))) return 'age_restricted'; if (lower.includes('private video') || lower.includes('video unavailable') || lower.includes('been removed')) return 'private'; + if (lower.includes('sign in') || lower.includes('login required')) return 'sign_in_required'; + if (lower.includes('copyright') || /blocked.*claim/.test(lower)) return 'copyright'; if (lower.includes('unable to download') || lower.includes('connection') || lower.includes('timed out') || lower.includes('urlopen error')) return 'network'; return 'unknown'; diff --git a/src/types/index.ts b/src/types/index.ts index ecc7532..0faa198 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -43,6 +43,9 @@ export interface PlatformSourceMetadata { imageUrl: string | null; url: string; platform: Platform; + bannerUrl?: string | null; + description?: string | null; + subscriberCount?: number | null; } /** Metadata for a single piece of content from a platform. */ @@ -70,6 +73,9 @@ export interface Channel { metadata: Record | null; formatProfileId: number | null; monitoringMode: MonitoringMode; + bannerUrl: string | null; + description: string | null; + subscriberCount: number | null; createdAt: string; updatedAt: string; lastCheckedAt: string | null; @@ -113,6 +119,7 @@ export interface QueueItem { attempts: number; maxAttempts: number; error: string | null; + errorCategory: string | null; startedAt: string | null; completedAt: string | null; createdAt: string;