diff --git a/src/main.rs b/src/main.rs index 0f34955..d0a6562 100644 --- a/src/main.rs +++ b/src/main.rs @@ -181,10 +181,6 @@ struct App { autoplay_next_index: Option, autoplay_eof_latched: bool, autoplay_prev_time_pos: Option, - - // Cache: container item id -> has resumable episode under it - parent_progress: std::collections::HashMap, - } #[derive(Debug)] @@ -250,7 +246,6 @@ impl Default for App { autoplay_next_index: None, autoplay_eof_latched: false, autoplay_prev_time_pos: None, - parent_progress: std::collections::HashMap::new(), } } } @@ -412,7 +407,6 @@ fn handle_key(app: &mut App, code: KeyCode) { _ => return, } } - // If search mode is active, it captures input first if let Some(search) = &mut app.search { match code { @@ -453,7 +447,6 @@ fn handle_key(app: &mut App, code: KeyCode) { match client.list_items(user_id, query.clone(), 0, PAGE_SIZE) { Ok(page) => { - update_parent_progress_cache(app, &client, user_id, &page.items); app.view_stack.push(View::Library { title, query, @@ -823,7 +816,6 @@ fn handle_key(app: &mut App, code: KeyCode) { match client.list_items(user_id, query.clone(), 0, PAGE_SIZE) { Ok(page) => { - update_parent_progress_cache(app, &client, user_id, &page.items); app.view_stack.push(View::Library { title: cat, query, @@ -903,7 +895,6 @@ fn handle_key(app: &mut App, code: KeyCode) { match client.playlist_items(user_id, &it.id, 0, PAGE_SIZE) { Ok(page) => { - update_parent_progress_cache(app, &client, user_id, &page.items); app.view_stack.push(View::Library { title: format!("{} > {}", title, it.name), // store a query placeholder; playlist paging could be done later if you want @@ -1063,8 +1054,6 @@ fn load_next_library_page(app: &mut App) { Err(e) => { app.status = format!("Fetch failed: {e}"); finish_loading(app); return; } }; - update_parent_progress_cache(app, &client, user_id, &page.items); - if let Some(View::Library { items, total, .. }) = app.view_stack.last_mut() { items.extend(page.items); *total = page.total; @@ -1079,33 +1068,6 @@ fn finish_loading(app: &mut App) { } } -fn update_parent_progress_cache( - app: &mut App, - client: &core::jellyfin::JellyfinClient, - user_id: &str, - new_items: &[core::jellyfin::JfItem], -) { - for it in new_items { - let ty = it.item_type.as_deref().unwrap_or(""); - if ty != "Series" && ty != "Season" { - continue; - } - if app.parent_progress.contains_key(&it.id) { - continue; - } - match client.has_resumable_under(user_id, &it.id) { - Ok(has) => { - app.parent_progress.insert(it.id.clone(), has); - } - Err(_) => { - // If the server rejects the query, just leave it unknown. - } - } - } -} - - - fn maybe_prefetch(app: &mut App) { const PREFETCH_ROWS: usize = 0; @@ -1420,7 +1382,7 @@ fn ui(frame: &mut Frame, app: &App) { .unwrap_or("-"); let right_text = format!( - "{connected}\n\np/P: play/autoplay\nq: quit\n/help: more help" + "{connected}\n\np/P: play/autoplay\nr/R: resume/autoplay\nq: quit\n/help: more help" ); frame.render_widget( @@ -1611,7 +1573,6 @@ fn open_children(app: &mut App, title: String, query: core::jellyfin::LibraryQue match client.list_items(user_id, query.clone(), 0, PAGE_SIZE) { Ok(page) => { - update_parent_progress_cache(app, &client, user_id, &page.items); app.view_stack.push(View::Library { title, query, @@ -1710,7 +1671,6 @@ fn mpv_cmd(socket: &PathBuf, cmd: &str) -> Result<(), String> { mpv_send(socket, &format!("{cmd}\n")) } - fn ticks_to_seconds(ticks: u64) -> f64 { // Jellyfin ticks are 10,000,000 per second (ticks as f64) / 10_000_000.0 @@ -1738,7 +1698,6 @@ fn mpv_seek_absolute(socket: &PathBuf, seconds: f64) -> Result<(), String> { Err("mpv seek failed (mpv never became ready or rejected seek)".into()) } - fn spawn_mpv(cfg: &core::config::Config, first_item_id: &str) -> Result { let token = cfg .access_token @@ -2165,10 +2124,6 @@ fn mpv_load_url(cfg: &core::config::Config, mpv: &MpvSession, url: &str) -> Resu ) } - - - - fn track_label(title: Option<&str>, lang: Option<&str>, id: Option, kind: &str) -> String { let mut parts: Vec = Vec::new(); @@ -2394,8 +2349,6 @@ fn unicode_progress_bar(width: usize, ratio: f64) -> String { s } -// --- watched / in-progress helpers --- - fn is_music_item(it: &core::jellyfin::JfItem) -> bool { matches!(it.item_type.as_deref(), Some("Audio" | "MusicAlbum" | "MusicArtist")) } @@ -2406,20 +2359,21 @@ fn is_downloaded(_it: &core::jellyfin::JfItem) -> bool { } fn is_played(it: &core::jellyfin::JfItem) -> bool { + // direct played flag if it.user_data.as_ref().and_then(|u| u.played) == Some(true) { return true; } - // If Jellyfin provides played percentage - if let Some(p) = it.user_data.as_ref().and_then(|u| u.played_percentage) { - if p >= 99.9 { + // aggregate containers: Season/Series can have UnplayedItemCount + if matches!(it.item_type.as_deref(), Some("Season" | "Series")) { + if it.user_data.as_ref().and_then(|u| u.unplayed_item_count) == Some(0) { return true; } } - // Series/Season often only has UnplayedItemCount - if matches!(it.item_type.as_deref(), Some("Series" | "Season")) { - if it.user_data.as_ref().and_then(|u| u.unplayed_item_count) == Some(0) { + // percent-based fallback (works for many types) + if let Some(p) = it.user_data.as_ref().and_then(|u| u.played_percentage) { + if p >= 99.9 { return true; } } @@ -2427,28 +2381,27 @@ fn is_played(it: &core::jellyfin::JfItem) -> bool { false } - - -fn is_in_progress_with_cache(app: &App, it: &core::jellyfin::JfItem) -> bool { +fn is_in_progress_fast(it: &core::jellyfin::JfItem) -> bool { if is_played(it) { return false; } - // movies/episodes - if playback_position_ticks(it).is_some() { - return true; - } + match it.item_type.as_deref() { + // Movies/Episodes: only if resume ticks exist + Some("Movie" | "Episode") => playback_position_ticks(it).is_some(), - // series/seasons: ask cache first (based on resumable episodes) - if matches!(it.item_type.as_deref(), Some("Series") | Some("Season")) { - if let Some(v) = app.parent_progress.get(&it.id) { - return *v; + // Seasons: only percent-based + Some("Season") => { + if let Some(p) = it.user_data.as_ref().and_then(|u| u.played_percentage) { + p > 0.1 && p < 99.9 + } else { + false + } } - } - // fallback: percentage-based if server provides it - match it.user_data.as_ref().and_then(|u| u.played_percentage) { - Some(p) if p > 0.1 && p < 99.9 => true, + // Series: NO in-progress marker (by your requirement) + Some("Series") => false, + _ => false, } } @@ -2458,19 +2411,25 @@ fn is_in_progress(it: &core::jellyfin::JfItem) -> bool { return false; } - // movies/episodes - if playback_position_ticks(it).is_some() { - return true; - } + match it.item_type.as_deref() { + // Movies/Episodes: only if resume ticks exist + Some("Movie" | "Episode") => playback_position_ticks(it).is_some(), + + // Seasons: percent-based + Some("Season") => it.user_data + .as_ref() + .and_then(|u| u.played_percentage) + .is_some_and(|p| p > 0.1 && p < 99.9), + + // Series: no in-progress marker (your requirement) + Some("Series") => false, - // series/seasons (percentage-based) - match it.user_data.as_ref().and_then(|u| u.played_percentage) { - Some(p) if p > 0.1 && p < 99.9 => true, _ => false, } } + fn playback_position_ticks(it: &core::jellyfin::JfItem) -> Option { it.user_data .as_ref() @@ -2478,24 +2437,23 @@ fn playback_position_ticks(it: &core::jellyfin::JfItem) -> Option { .and_then(|v| if v > 0 { Some(v as u64) } else { None }) } -fn emoji_prefix_for(app: &App, it: &core::jellyfin::JfItem) -> &'static str { +fn emoji_prefix_for(_app: &App, it: &core::jellyfin::JfItem) -> &'static str { // no emoji for music if is_music_item(it) { return ""; } - let played = is_played(it); - let prog = is_in_progress_with_cache(app, it); - - match (played, prog) { - (true, _) => "✅ ", - (false, true) => "🟠 ", // orange in-progress - (false, false) => "", + if is_played(it) { + return "✅ "; } + + if is_in_progress(it) { + return "🟠 "; + } + + "" } - - // --- transcoding + resume URL builder (Approach B) --- fn clamp_mbps_to_bps(mbps: u32) -> u32 { mbps.saturating_mul(1_000_000) // Mb/s -> bits/s @@ -2548,8 +2506,6 @@ fn mpv_cmd_reply(socket: &PathBuf, cmd: &str) -> Result { - - struct Panes { top: Rect, left_main: Rect,