half way to watch status complete
This commit is contained in:
132
src/main.rs
132
src/main.rs
@@ -181,10 +181,6 @@ struct App {
|
||||
autoplay_next_index: Option<usize>,
|
||||
autoplay_eof_latched: bool,
|
||||
autoplay_prev_time_pos: Option<f64>,
|
||||
|
||||
// Cache: container item id -> has resumable episode under it
|
||||
parent_progress: std::collections::HashMap<String, bool>,
|
||||
|
||||
}
|
||||
|
||||
#[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<MpvSession, String> {
|
||||
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<i64>, kind: &str) -> String {
|
||||
let mut parts: Vec<String> = 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<u64> {
|
||||
it.user_data
|
||||
.as_ref()
|
||||
@@ -2478,24 +2437,23 @@ fn playback_position_ticks(it: &core::jellyfin::JfItem) -> Option<u64> {
|
||||
.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<Value, String> {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
struct Panes {
|
||||
top: Rect,
|
||||
left_main: Rect,
|
||||
|
||||
Reference in New Issue
Block a user