feat: Added banner_url, description, subscriber_count columns with Driz…

- "src/db/schema/channels.ts"
- "drizzle/0010_special_ghost_rider.sql"
- "src/types/index.ts"
- "src/sources/youtube.ts"
- "src/sources/soundcloud.ts"
- "src/db/repositories/channel-repository.ts"
- "src/server/routes/channel.ts"
- "src/__tests__/sources.test.ts"

GSD-Task: S01/T01
This commit is contained in:
jlightner 2026-04-03 06:04:49 +00:00
parent 0bf63fed9c
commit 6a5402ce8d
16 changed files with 2232 additions and 4 deletions

View file

@ -0,0 +1 @@
ALTER TABLE `queue_items` ADD `error_category` text;

View file

@ -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;

View file

@ -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": {}
}
}

View file

@ -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": {}
}
}

View file

@ -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
}
]
}

View file

@ -135,6 +135,9 @@ function makeChannel(overrides: Partial<Channel> = {}): 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,
});
});
});

View file

@ -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: <urlopen error [Errno -2] Name or service not known>')).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');
});
});

View file

@ -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<Channel, 'name' | 'checkInterval' | 'monitoringEnabled' | 'imageUrl' | 'formatProfileId' | 'lastCheckedAt' | 'lastCheckStatus' | 'monitoringMode'>
Pick<Channel, 'name' | 'checkInterval' | 'monitoringEnabled' | 'imageUrl' | 'formatProfileId' | 'lastCheckedAt' | 'lastCheckStatus' | 'monitoringMode' | 'bannerUrl' | 'description' | 'subscriberCount'>
>;
type Db = LibSQLDatabase<typeof schema>;
@ -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<string, unknown> | 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,

View file

@ -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,

View file

@ -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'),
});

View file

@ -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')

View file

@ -176,6 +176,9 @@ export async function channelRoutes(fastify: FastifyInstance): Promise<void> {
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

View file

@ -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,
};
}

View file

@ -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,
};
}

View file

@ -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';

View file

@ -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<string, unknown> | 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;