//! Scanning the `channels/` directory tree into channels, playlists, and videos. //! //! # Directory layout expected //! //! ```text //! channels/ //! / //! Title [VIDEO_ID].mkv //! Title [VIDEO_ID].webp ← thumbnail //! Title [VIDEO_ID].description //! Title [VIDEO_ID].info.json //! Title [VIDEO_ID].en.vtt ← subtitle (lang = "en") //! / //! Title [VIDEO_ID].mkv //! … //! ``` //! //! Files that don't match the `Title [ID].ext` naming convention are silently //! ignored. Hidden directories (name starts with `.`) and directories that //! contain no recognisable video files are skipped. use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use crate::database::Database; use crate::download_options::DownloadOptions; use crate::platform::{self, Platform}; const VIDEO_EXTS: &[&str] = &["mkv", "mp4", "webm", "m4v", "mov", "avi"]; const AUDIO_EXTS: &[&str] = &["mp3", "m4a", "opus", "flac", "ogg", "wav", "aac"]; const THUMB_EXTS: &[&str] = &["webp", "jpg", "jpeg", "png"]; /// A single WebVTT subtitle track discovered alongside a video file. #[derive(Clone, Debug)] pub struct Subtitle { /// ISO 639-1/2 language code extracted from the `.lang.vtt` filename suffix. pub lang: String, pub path: PathBuf, } /// An audio track in the music library. #[derive(Clone, Debug)] pub struct Track { pub id: String, pub title: String, pub artist: String, pub path: PathBuf, pub thumb_path: Option, #[allow(dead_code)] pub info_path: Option, pub duration_secs: Option, pub file_size: Option, } /// A fully enriched video entry, ready to serve to the UI. #[derive(Clone, Debug)] pub struct Video { /// yt-dlp video ID (the part inside `[…]` in the filename). pub id: String, pub title: String, #[allow(dead_code)] pub stem: String, pub video_path: Option, pub thumb_path: Option, pub description_path: Option, /// Path to the `.info.json` sidecar — used to read duration, chapters, etc. pub info_path: Option, pub subtitles: Vec, pub has_live_chat: bool, /// Duration read from `info.json`; `None` if the sidecar is missing. pub duration_secs: Option, /// Whether `info.json` lists a non-empty `chapters` array. Cached at scan /// time so the web layer needn't re-read and parse the sidecar per request. pub has_chapters: bool, /// Size of the video file on disk; `None` if the video file is missing. pub file_size: Option, /// Filesystem mtime of the video file as a UNIX timestamp (seconds). /// Used by the activity feed to surface recent additions. `None` if the /// video file is missing or the system clock returned an error. pub mtime_unix: Option, /// Upload date as `YYYYMMDD` (yt-dlp's native format from info.json). /// `None` if the info.json sidecar is missing or lacks the field. pub upload_date: Option, } /// A sub-directory inside a channel that contains videos (treated as a playlist). #[derive(Clone, Debug)] pub struct Playlist { pub name: String, #[allow(dead_code)] pub path: PathBuf, pub videos: Vec