diff --git a/src/app.rs b/src/app.rs index cccdfad..cc11dce 100644 --- a/src/app.rs +++ b/src/app.rs @@ -240,11 +240,13 @@ impl App { let channels_root = config.backup.directory.clone(); let settings_dir = channels_root.display().to_string(); let _ = std::fs::create_dir_all(&channels_root); - let library_root = channels_root - .parent() - .map(|p| p.to_path_buf()) - .unwrap_or_else(|| channels_root.clone()); - let _ = std::fs::create_dir_all(&library_root); + // library_root is the parent of every platform's per-creator + // folder. With the post-2026-05 nested layout every platform + // (including YouTube's `channels/`) lives under channels_root, + // so the two paths are the same. `library_root` is kept as a + // separate field for places that previously expected the + // sibling-platform layout (file_url, /files/ mount, maintenance). + let library_root = channels_root.clone(); // Pre-create every platform's folder so scans see them. for &p in Platform::all() { let dir = platform::platform_root(&channels_root, p); @@ -275,7 +277,8 @@ impl App { let flags = db.get_video_flags().unwrap_or_default(); let resume_positions = db.get_positions().unwrap_or_default(); - let music_root = channels_root.with_file_name("music"); + // Music dir nests under channels_root like the platform dirs. + let music_root = channels_root.join("music"); let music_library = library::scan_music(&music_root); let max_concurrent = config.backup.max_concurrent; diff --git a/src/downloader.rs b/src/downloader.rs index 239fc0a..ea2cc45 100644 --- a/src/downloader.rs +++ b/src/downloader.rs @@ -494,9 +494,10 @@ impl Downloader { self.enqueue(cmd, url, label); } - /// Path to the music download directory (sibling of `channels_root`). + /// Path to the music download directory, nested under `channels_root` + /// alongside the platform folders. pub fn music_root(&self) -> PathBuf { - self.channels_root.with_file_name("music") + self.channels_root.join("music") } /// Download `url` as audio-only, storing tracks in `music//`. diff --git a/src/platform.rs b/src/platform.rs index 1a76828..0cebc4a 100644 --- a/src/platform.rs +++ b/src/platform.rs @@ -351,15 +351,20 @@ fn extract_after<'a>(url: &'a str, marker: &str) -> Option<&'a str> { // ── Filesystem layout ───────────────────────────────────────────────────────── -/// Absolute path to a given platform's video folder, derived from the -/// configured YouTube `channels_root` (its parent is treated as the -/// implicit library root, with each platform as a sibling folder). +/// Absolute path to a given platform's video folder. +/// +/// All platforms (including YouTube) are nested as subdirectories of the +/// configured `channels_root`. So a config with +/// `directory = "/mnt/library/yt-offline"` puts YouTube channels at +/// `/mnt/library/yt-offline/channels//` and Bandcamp artists at +/// `/mnt/library/yt-offline/bandcamp//`. This keeps everything a +/// user might want to archive under one tidy umbrella directory. +/// +/// The function's name predates the layout change — `channels_root` is +/// really "library root" now, but renaming would touch ~40 call sites +/// for no functional benefit. pub fn platform_root(channels_root: &Path, platform: Platform) -> PathBuf { - if platform == Platform::YouTube { - // YouTube keeps the legacy path verbatim. - return channels_root.to_path_buf(); - } - channels_root.with_file_name(platform.dir_name()) + channels_root.join(platform.dir_name()) } /// Where the `.source-url` sidecar lives for a creator folder. @@ -486,16 +491,16 @@ mod tests { } #[test] - fn platform_root_youtube_is_channels_root() { - let cr = Path::new("/foo/channels"); - assert_eq!(platform_root(cr, Platform::YouTube), cr); + fn platform_root_youtube_nests_as_channels_subdir() { + let cr = Path::new("/foo/library"); + assert_eq!(platform_root(cr, Platform::YouTube), Path::new("/foo/library/channels")); } #[test] - fn platform_root_others_are_siblings() { - let cr = Path::new("/foo/channels"); - assert_eq!(platform_root(cr, Platform::TikTok), Path::new("/foo/tiktok")); - assert_eq!(platform_root(cr, Platform::Twitch), Path::new("/foo/twitch")); + fn platform_root_others_nest_too() { + let cr = Path::new("/foo/library"); + assert_eq!(platform_root(cr, Platform::TikTok), Path::new("/foo/library/tiktok")); + assert_eq!(platform_root(cr, Platform::Twitch), Path::new("/foo/library/twitch")); } #[test] diff --git a/src/web.rs b/src/web.rs index 2303163..55d19a9 100644 --- a/src/web.rs +++ b/src/web.rs @@ -2182,13 +2182,11 @@ async fn serve(config: Config, shutdown_rx: std::sync::mpsc::Receiver<()>) { let channels_root = config.backup.directory.clone(); let _ = std::fs::create_dir_all(&channels_root); // library_root holds every platform folder side-by-side (channels/, - // tiktok/, twitch/, …). The implicit anchor is the parent of the - // user-configured channels dir. - let library_root = channels_root - .parent() - .map(|p| p.to_path_buf()) - .unwrap_or_else(|| channels_root.clone()); - let _ = std::fs::create_dir_all(&library_root); + // tiktok/, twitch/, …). Post-2026-05 layout nests them all under + // channels_root, so the two paths are identical. The field is kept + // distinct because file_url() and the /files/ static-file mount + // still resolve URLs against it as a separate concept. + let library_root = channels_root.clone(); // Pre-create every platform's folder so the static-file mount can serve // them without 404ing on first access. for &p in crate::platform::Platform::all() {