diff --git a/src/core/jellyfin.rs b/src/core/jellyfin.rs index 733a5e9..4d79b61 100644 --- a/src/core/jellyfin.rs +++ b/src/core/jellyfin.rs @@ -143,6 +143,43 @@ impl JellyfinClient { false } + pub fn mark_played(&self, user_id: &str, item_id: &str) -> Result<(), String> { + let base = self.base_url.trim_end_matches('/'); + let url = format!("{base}/Users/{user_id}/PlayedItems/{item_id}"); + + let resp = self + .client + .post(url) + .header("X-Emby-Token", &self.token) + .send() + .map_err(|e| format!("mark_played request failed: {e}"))?; + + if !resp.status().is_success() { + return Err(format!("mark_played failed: HTTP {}", resp.status())); + } + + Ok(()) + } + + pub fn mark_unplayed(&self, user_id: &str, item_id: &str) -> Result<(), String> { + let base = self.base_url.trim_end_matches('/'); + let url = format!("{base}/Users/{user_id}/PlayedItems/{item_id}"); + + let resp = self + .client + .delete(url) + .header("X-Emby-Token", &self.token) + .send() + .map_err(|e| format!("mark_unplayed request failed: {e}"))?; + + if !resp.status().is_success() { + return Err(format!("mark_unplayed failed: HTTP {}", resp.status())); + } + + Ok(()) + } + + pub fn list_views(&self, user_id: &str) -> Result, String> { let url = format!("{}/Users/{}/Views", self.base_url, user_id); @@ -416,19 +453,19 @@ pub struct JfStudio { pub name: String, } -#[derive(Debug, Clone, Deserialize)] +#[derive(Deserialize, Clone, Debug, Default)] pub struct JfUserData { #[serde(rename = "Played")] pub played: Option, - #[serde(rename = "PlaybackPositionTicks")] - pub playback_position_ticks: Option, - #[serde(rename = "PlayedPercentage")] pub played_percentage: Option, + #[serde(rename = "PlaybackPositionTicks")] + pub playback_position_ticks: Option, + #[serde(rename = "UnplayedItemCount")] - pub unplayed_item_count: Option, + pub unplayed_item_count: Option, } diff --git a/src/main.rs b/src/main.rs index 9ee9ca9..26e4fc5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -668,6 +668,74 @@ fn handle_key(app: &mut App, code: KeyCode) { } } + // Mark watched (w) / Unmark watched (W) + if matches!(code, KeyCode::Char('w') | KeyCode::Char('W')) { + let unmark = matches!(code, KeyCode::Char('W')); + + let cfg = match &app.config { + Some(c) => c.clone(), + None => { app.status = "No config loaded".into(); return; } + }; + + let user_id = match cfg.user_id.as_deref() { + Some(u) => u.to_string(), + None => { app.status = "Missing user_id (login again)".into(); return; } + }; + + let (item_id, item_type) = match app.current_view() { + View::Library { items, selected, .. } if !items.is_empty() => { + let it = &items[*selected]; + (it.id.clone(), it.item_type.clone().unwrap_or_default()) + } + _ => { app.status = "Not in a library view".into(); return; } + }; + + // Only do this for playables (you can loosen this later if you want) + if item_type != "Episode" && item_type != "Movie" { + app.status = "Watched toggle only for Episodes/Movies (for now)".into(); + return; + } + + let client = match core::jellyfin::JellyfinClient::from_config(&cfg) { + Ok(c) => c, + Err(e) => { app.status = e; return; } + }; + + let r = if unmark { + client.mark_unplayed(&user_id, &item_id) + } else { + client.mark_played(&user_id, &item_id) + }; + + match r { + Ok(_) => { + // Update local UI state so ✅ disappears/appears immediately + if let Some(View::Library { items, selected, .. }) = app.view_stack.last_mut() { + if let Some(it) = items.get_mut(*selected) { + let ud = it.user_data.get_or_insert_with(core::jellyfin::JfUserData::default); + + if unmark { + ud.played = Some(false); + ud.played_percentage = Some(0.0); + ud.playback_position_ticks = Some(0); + } else { + ud.played = Some(true); + ud.played_percentage = Some(100.0); + ud.playback_position_ticks = Some(0); + } + } + } + + app.status = if unmark { "Marked unplayed".into() } else { "Marked played".into() }; + } + Err(e) => { + app.status = format!("Watched update failed: {e}"); + } + } + + return; + } + // mpv controls (only when not typing into search/login) if app.search.is_none() && !matches!(app.current_view(), View::Login) { @@ -910,8 +978,6 @@ fn handle_key(app: &mut App, code: KeyCode) { } } } - - KeyCode::Char('r') => { app.autoplay = false; app.autoplay_next_index = None;