// ─── Database row types ─────────────────────────────────────────────────────── export interface MediaItem { id: number; file_path: string; type: "Movie" | "Episode"; name: string; series_name: string | null; series_key: string | null; season_number: number | null; episode_number: number | null; year: number | null; file_size: number | null; container: string | null; duration_seconds: number | null; original_language: string | null; orig_lang_source: "probe" | "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; last_executed_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; profile: string | null; language: 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; bit_depth: number | null; } export interface ReviewPlan { id: number; item_id: number; status: "pending" | "approved" | "skipped" | "done" | "error"; is_noop: number; auto_class: "auto" | "auto_heuristic" | "manual" | null; sorted: number; 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 StreamDecision { id: number; plan_id: number; stream_id: number; action: "keep" | "remove"; target_index: number | null; custom_title: string | null; /** Per-stream language override. When set, the analyzer and ffmpeg * command builder both read this in preference to the raw * media_streams.language. Lets the user correct an "und" or * mislabeled audio track without going through Jellyfin. */ custom_language: 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; auto_class: "auto" | "auto_heuristic" | "manual"; 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[]; } // ─── 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; }