half way to watch status complete
This commit is contained in:
128
src/main.rs
128
src/main.rs
@@ -181,10 +181,6 @@ struct App {
|
|||||||
autoplay_next_index: Option<usize>,
|
autoplay_next_index: Option<usize>,
|
||||||
autoplay_eof_latched: bool,
|
autoplay_eof_latched: bool,
|
||||||
autoplay_prev_time_pos: Option<f64>,
|
autoplay_prev_time_pos: Option<f64>,
|
||||||
|
|
||||||
// Cache: container item id -> has resumable episode under it
|
|
||||||
parent_progress: std::collections::HashMap<String, bool>,
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -250,7 +246,6 @@ impl Default for App {
|
|||||||
autoplay_next_index: None,
|
autoplay_next_index: None,
|
||||||
autoplay_eof_latched: false,
|
autoplay_eof_latched: false,
|
||||||
autoplay_prev_time_pos: None,
|
autoplay_prev_time_pos: None,
|
||||||
parent_progress: std::collections::HashMap::new(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -412,7 +407,6 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
|||||||
_ => return,
|
_ => return,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If search mode is active, it captures input first
|
// If search mode is active, it captures input first
|
||||||
if let Some(search) = &mut app.search {
|
if let Some(search) = &mut app.search {
|
||||||
match code {
|
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) {
|
match client.list_items(user_id, query.clone(), 0, PAGE_SIZE) {
|
||||||
Ok(page) => {
|
Ok(page) => {
|
||||||
update_parent_progress_cache(app, &client, user_id, &page.items);
|
|
||||||
app.view_stack.push(View::Library {
|
app.view_stack.push(View::Library {
|
||||||
title,
|
title,
|
||||||
query,
|
query,
|
||||||
@@ -823,7 +816,6 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
|||||||
|
|
||||||
match client.list_items(user_id, query.clone(), 0, PAGE_SIZE) {
|
match client.list_items(user_id, query.clone(), 0, PAGE_SIZE) {
|
||||||
Ok(page) => {
|
Ok(page) => {
|
||||||
update_parent_progress_cache(app, &client, user_id, &page.items);
|
|
||||||
app.view_stack.push(View::Library {
|
app.view_stack.push(View::Library {
|
||||||
title: cat,
|
title: cat,
|
||||||
query,
|
query,
|
||||||
@@ -903,7 +895,6 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
|||||||
|
|
||||||
match client.playlist_items(user_id, &it.id, 0, PAGE_SIZE) {
|
match client.playlist_items(user_id, &it.id, 0, PAGE_SIZE) {
|
||||||
Ok(page) => {
|
Ok(page) => {
|
||||||
update_parent_progress_cache(app, &client, user_id, &page.items);
|
|
||||||
app.view_stack.push(View::Library {
|
app.view_stack.push(View::Library {
|
||||||
title: format!("{} > {}", title, it.name),
|
title: format!("{} > {}", title, it.name),
|
||||||
// store a query placeholder; playlist paging could be done later if you want
|
// 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; }
|
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() {
|
if let Some(View::Library { items, total, .. }) = app.view_stack.last_mut() {
|
||||||
items.extend(page.items);
|
items.extend(page.items);
|
||||||
*total = page.total;
|
*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) {
|
fn maybe_prefetch(app: &mut App) {
|
||||||
const PREFETCH_ROWS: usize = 0;
|
const PREFETCH_ROWS: usize = 0;
|
||||||
|
|
||||||
@@ -1420,7 +1382,7 @@ fn ui(frame: &mut Frame, app: &App) {
|
|||||||
.unwrap_or("-");
|
.unwrap_or("-");
|
||||||
|
|
||||||
let right_text = format!(
|
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(
|
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) {
|
match client.list_items(user_id, query.clone(), 0, PAGE_SIZE) {
|
||||||
Ok(page) => {
|
Ok(page) => {
|
||||||
update_parent_progress_cache(app, &client, user_id, &page.items);
|
|
||||||
app.view_stack.push(View::Library {
|
app.view_stack.push(View::Library {
|
||||||
title,
|
title,
|
||||||
query,
|
query,
|
||||||
@@ -1710,7 +1671,6 @@ fn mpv_cmd(socket: &PathBuf, cmd: &str) -> Result<(), String> {
|
|||||||
mpv_send(socket, &format!("{cmd}\n"))
|
mpv_send(socket, &format!("{cmd}\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn ticks_to_seconds(ticks: u64) -> f64 {
|
fn ticks_to_seconds(ticks: u64) -> f64 {
|
||||||
// Jellyfin ticks are 10,000,000 per second
|
// Jellyfin ticks are 10,000,000 per second
|
||||||
(ticks as f64) / 10_000_000.0
|
(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())
|
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> {
|
fn spawn_mpv(cfg: &core::config::Config, first_item_id: &str) -> Result<MpvSession, String> {
|
||||||
let token = cfg
|
let token = cfg
|
||||||
.access_token
|
.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 {
|
fn track_label(title: Option<&str>, lang: Option<&str>, id: Option<i64>, kind: &str) -> String {
|
||||||
let mut parts: Vec<String> = Vec::new();
|
let mut parts: Vec<String> = Vec::new();
|
||||||
|
|
||||||
@@ -2394,8 +2349,6 @@ fn unicode_progress_bar(width: usize, ratio: f64) -> String {
|
|||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- watched / in-progress helpers ---
|
|
||||||
|
|
||||||
fn is_music_item(it: &core::jellyfin::JfItem) -> bool {
|
fn is_music_item(it: &core::jellyfin::JfItem) -> bool {
|
||||||
matches!(it.item_type.as_deref(), Some("Audio" | "MusicAlbum" | "MusicArtist"))
|
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 {
|
fn is_played(it: &core::jellyfin::JfItem) -> bool {
|
||||||
|
// direct played flag
|
||||||
if it.user_data.as_ref().and_then(|u| u.played) == Some(true) {
|
if it.user_data.as_ref().and_then(|u| u.played) == Some(true) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If Jellyfin provides played percentage
|
// aggregate containers: Season/Series can have UnplayedItemCount
|
||||||
if let Some(p) = it.user_data.as_ref().and_then(|u| u.played_percentage) {
|
if matches!(it.item_type.as_deref(), Some("Season" | "Series")) {
|
||||||
if p >= 99.9 {
|
if it.user_data.as_ref().and_then(|u| u.unplayed_item_count) == Some(0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Series/Season often only has UnplayedItemCount
|
// percent-based fallback (works for many types)
|
||||||
if matches!(it.item_type.as_deref(), Some("Series" | "Season")) {
|
if let Some(p) = it.user_data.as_ref().and_then(|u| u.played_percentage) {
|
||||||
if it.user_data.as_ref().and_then(|u| u.unplayed_item_count) == Some(0) {
|
if p >= 99.9 {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2427,28 +2381,27 @@ fn is_played(it: &core::jellyfin::JfItem) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_in_progress_fast(it: &core::jellyfin::JfItem) -> bool {
|
||||||
|
|
||||||
fn is_in_progress_with_cache(app: &App, it: &core::jellyfin::JfItem) -> bool {
|
|
||||||
if is_played(it) {
|
if is_played(it) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// movies/episodes
|
match it.item_type.as_deref() {
|
||||||
if playback_position_ticks(it).is_some() {
|
// Movies/Episodes: only if resume ticks exist
|
||||||
return true;
|
Some("Movie" | "Episode") => playback_position_ticks(it).is_some(),
|
||||||
}
|
|
||||||
|
|
||||||
// series/seasons: ask cache first (based on resumable episodes)
|
// Seasons: only percent-based
|
||||||
if matches!(it.item_type.as_deref(), Some("Series") | Some("Season")) {
|
Some("Season") => {
|
||||||
if let Some(v) = app.parent_progress.get(&it.id) {
|
if let Some(p) = it.user_data.as_ref().and_then(|u| u.played_percentage) {
|
||||||
return *v;
|
p > 0.1 && p < 99.9
|
||||||
|
} else {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback: percentage-based if server provides it
|
// Series: NO in-progress marker (by your requirement)
|
||||||
match it.user_data.as_ref().and_then(|u| u.played_percentage) {
|
Some("Series") => false,
|
||||||
Some(p) if p > 0.1 && p < 99.9 => true,
|
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2458,19 +2411,25 @@ fn is_in_progress(it: &core::jellyfin::JfItem) -> bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// movies/episodes
|
match it.item_type.as_deref() {
|
||||||
if playback_position_ticks(it).is_some() {
|
// Movies/Episodes: only if resume ticks exist
|
||||||
return true;
|
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,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fn playback_position_ticks(it: &core::jellyfin::JfItem) -> Option<u64> {
|
fn playback_position_ticks(it: &core::jellyfin::JfItem) -> Option<u64> {
|
||||||
it.user_data
|
it.user_data
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -2478,23 +2437,22 @@ fn playback_position_ticks(it: &core::jellyfin::JfItem) -> Option<u64> {
|
|||||||
.and_then(|v| if v > 0 { Some(v as u64) } else { None })
|
.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
|
// no emoji for music
|
||||||
if is_music_item(it) {
|
if is_music_item(it) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
let played = is_played(it);
|
if is_played(it) {
|
||||||
let prog = is_in_progress_with_cache(app, it);
|
return "✅ ";
|
||||||
|
|
||||||
match (played, prog) {
|
|
||||||
(true, _) => "✅ ",
|
|
||||||
(false, true) => "🟠 ", // orange in-progress
|
|
||||||
(false, false) => "",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if is_in_progress(it) {
|
||||||
|
return "🟠 ";
|
||||||
|
}
|
||||||
|
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
// --- transcoding + resume URL builder (Approach B) ---
|
// --- transcoding + resume URL builder (Approach B) ---
|
||||||
fn clamp_mbps_to_bps(mbps: u32) -> u32 {
|
fn clamp_mbps_to_bps(mbps: u32) -> u32 {
|
||||||
@@ -2548,8 +2506,6 @@ fn mpv_cmd_reply(socket: &PathBuf, cmd: &str) -> Result<Value, String> {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
struct Panes {
|
struct Panes {
|
||||||
top: Rect,
|
top: Rect,
|
||||||
left_main: Rect,
|
left_main: Rect,
|
||||||
|
|||||||
Reference in New Issue
Block a user