// ─── 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; confidence: "high" | "low"; apple_compat: "direct_play" | "remux" | "audio_transcode" | null; job_type: "copy" | "transcode"; 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; transcode_codec: string | null; } export interface Job { id: number; item_id: number; command: string; job_type: "copy" | "transcode"; status: "pending" | "running" | "done" | "error"; output: string | null; exit_code: number | null; created_at: string; started_at: string | null; completed_at: string | null; } // ─── Analyzer types ─────────────────────────────────────────────────────────── export interface StreamWithDecision extends MediaStream { action: "keep" | "remove"; target_index: number | null; } export interface PlanResult { is_noop: boolean; has_subs: boolean; confidence: "high" | "low"; apple_compat: "direct_play" | "remux" | "audio_transcode" | null; job_type: "copy" | "transcode"; decisions: Array<{ stream_id: number; action: "keep" | "remove"; target_index: number | null; transcode_codec: string | null; }>; notes: string[]; } // ─── 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; }