// ─── Database row types ─────────────────────────────────────────────────────── export interface MediaItem { id: number; jellyfin_id: string; type: 'Movie' | 'Episode'; name: string; series_name: string | null; series_jellyfin_id: string | null; season_number: number | null; episode_number: number | null; year: number | null; file_path: string; file_size: number | null; container: string | null; original_language: string | null; orig_lang_source: 'jellyfin' | 'radarr' | 'sonarr' | 'manual' | null; needs_review: number; imdb_id: string | null; tmdb_id: string | null; tvdb_id: string | null; scan_status: 'pending' | 'scanned' | 'error'; scan_error: string | null; last_scanned_at: string | null; created_at: string; } export interface MediaStream { id: number; item_id: number; stream_index: number; type: 'Video' | 'Audio' | 'Subtitle' | 'Data' | 'EmbeddedImage'; codec: string | null; language: string | null; language_display: string | null; title: string | null; is_default: number; is_forced: number; is_hearing_impaired: number; channels: number | null; channel_layout: string | null; bit_rate: number | null; sample_rate: number | null; } export interface ReviewPlan { id: number; item_id: number; status: 'pending' | 'approved' | 'skipped' | 'done' | 'error'; is_noop: number; subs_extracted: number; notes: string | null; reviewed_at: string | null; created_at: string; } export interface SubtitleFile { id: number; item_id: number; file_path: string; language: string | null; codec: string | null; is_forced: number; is_hearing_impaired: number; file_size: number | null; created_at: string; } export interface StreamDecision { id: number; plan_id: number; stream_id: number; action: 'keep' | 'remove'; target_index: number | null; custom_title: string | null; } export interface Job { id: number; item_id: number; command: string; node_id: number | null; status: 'pending' | 'running' | 'done' | 'error'; output: string | null; exit_code: number | null; created_at: string; started_at: string | null; completed_at: string | null; } export interface Node { id: number; name: string; host: string; port: number; username: string; private_key: string; ffmpeg_path: string; work_dir: string; status: 'unknown' | 'ok' | 'error'; last_checked_at: string | null; created_at: string; } // ─── Analyzer types ─────────────────────────────────────────────────────────── export interface StreamWithDecision extends MediaStream { action: 'keep' | 'remove'; target_index: number | null; } export interface PlanResult { is_noop: boolean; has_subs: boolean; decisions: Array<{ stream_id: number; action: 'keep' | 'remove'; target_index: number | null }>; notes: string | null; } // ─── Jellyfin API types ─────────────────────────────────────────────────────── export interface JellyfinMediaStream { Type: string; Index: number; Codec?: string; Language?: string; DisplayLanguage?: string; Title?: string; IsDefault?: boolean; IsForced?: boolean; IsHearingImpaired?: boolean; IsExternal?: boolean; Channels?: number; ChannelLayout?: string; BitRate?: number; SampleRate?: number; } export interface JellyfinItem { Id: string; Type: string; Name: string; SeriesName?: string; SeriesId?: string; ParentIndexNumber?: number; IndexNumber?: number; ProductionYear?: number; Path?: string; Size?: number; Container?: string; MediaStreams?: JellyfinMediaStream[]; ProviderIds?: Record; } export interface JellyfinUser { Id: string; Name: string; } // ─── Scan state ─────────────────────────────────────────────────────────────── export interface ScanProgress { total: number; scanned: number; current_item: string; errors: number; running: boolean; } // ─── SSE event helpers ──────────────────────────────────────────────────────── export type SseEventType = 'progress' | 'log' | 'complete' | 'error'; export interface SseEvent { type: SseEventType; data: unknown; }