From 15e601da7f8317c850e93939ff69367bd7dfaae4 Mon Sep 17 00:00:00 2001 From: cwj3rcb Date: Mon, 16 Feb 2026 20:36:51 -0600 Subject: [PATCH] Playlist downloading and offline hardlinks --- src/main.rs | 66 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/src/main.rs b/src/main.rs index e33b550..153139a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1076,19 +1076,17 @@ fn handle_key(app: &mut App, code: KeyCode) { None => { app.status = "No user_id (login first)".into(); return; } }; - let (it, playlist_name, playlist_index) = match app.current_view() { + let (selected_item, playlist_ctx, selected_index) = match app.current_view() { View::Library { items, selected, playlist_ctx, .. } if !items.is_empty() => { - let it = items[*selected].clone(); - let pname = playlist_ctx.as_ref().map(|p| p.name.clone()); - (it, pname, *selected) + (items[*selected].clone(), playlist_ctx.clone(), *selected) } _ => { app.status = "Nothing to download here".into(); return; } }; + // ensure DM let dm = match &app.downloads { Some(dm) => dm, None => { - // try start downloads now if we have a token if cfg.access_token.is_some() { app.downloads = Some(downloads::DownloadManager::start(cfg.clone())); app.downloads.as_ref().unwrap() @@ -1099,14 +1097,17 @@ fn handle_key(app: &mut App, code: KeyCode) { } }; - let jf = match core::jellyfin::JellyfinClient::from_config(&cfg) { + let client = match core::jellyfin::JellyfinClient::from_config(&cfg) { Ok(c) => c, Err(e) => { app.status = format!("downloads: {e}"); return; } }; - if let Some(pname) = playlist_name { - // only allow direct playables from playlists - let ty = it.item_type.as_deref().unwrap_or(""); + let ty = selected_item.item_type.as_deref().unwrap_or(""); + + // ------------------------- + // CASE 1: We are INSIDE a playlist view (already works) + // ------------------------- + if let Some(pctx) = playlist_ctx { let playable = matches!(ty, "Movie" | "Episode" | "Audio"); if !playable { app.status = "Playlist downloads only support Movie/Episode/Audio".into(); @@ -1114,15 +1115,53 @@ fn handle_key(app: &mut App, code: KeyCode) { } dm.send(downloads::DownloadCommand::EnqueuePlaylistItem { - item: it, - playlist_name: pname, - playlist_index, + item: selected_item, + playlist_name: pctx.name, + playlist_index: selected_index, }); app.status = "queued 1 (playlist)".into(); return; } - match downloads::expand_for_enqueue(&jf, user_id, &it) { + // ------------------------- + // CASE 2: We are on the PLAYLIST LIST and selected item IS a playlist + // -> enqueue via playlist-aware path so we get hardlinks/order + // ------------------------- + if ty == "Playlist" { + app.status = format!("Fetching playlist… {}", selected_item.name); + + match client.playlist_items(user_id, &selected_item.id, 0, 200) { + Ok(page) => { + let pname = selected_item.name.clone(); + let mut n = 0usize; + + for (i, it) in page.items.into_iter().enumerate() { + let it_ty = it.item_type.as_deref().unwrap_or(""); + let playable = matches!(it_ty, "Movie" | "Episode" | "Audio"); + if !playable { continue; } + + dm.send(downloads::DownloadCommand::EnqueuePlaylistItem { + item: it, + playlist_name: pname.clone(), + playlist_index: i, + }); + n += 1; + } + + app.status = format!("queued {n} (playlist)"); + } + Err(e) => { + app.status = format!("Playlist load failed: {e}"); + } + } + + return; + } + + // ------------------------- + // CASE 3: Normal (non-playlist-container) download behavior + // ------------------------- + match downloads::expand_for_enqueue(&client, user_id, &selected_item) { Ok(items) => { let n = items.len(); dm.send(downloads::DownloadCommand::Enqueue(items)); @@ -1133,6 +1172,7 @@ fn handle_key(app: &mut App, code: KeyCode) { return; } + KeyCode::Char('q') => app.should_exit = true, //Enter code Block