1 parent
3945d6f3c9
commit
7698e27d41
3 files changed
+116
-2
No files matched your search
+83
@@ -280,6 +280,33 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn mark_selected_watched(&mut self) -> Result<()> {
|
||||
let Some(i) = self.list_state.selected() else {
|
||||
self.status = "Select a movie, episode, series, or season to mark watched".into();
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(item) = self
|
||||
.items
|
||||
.get(i)
|
||||
.filter(|item| can_mark_watched(item))
|
||||
.cloned()
|
||||
else {
|
||||
self.status = "Select a movie, episode, series, or season to mark watched".into();
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let title = item.display_title();
|
||||
self.status = format!("Marking watched: {title}...");
|
||||
self.client.mark_watched(&item.id).await?;
|
||||
self.reload_current_items(Some(i)).await?;
|
||||
if !self.is_resume_view() {
|
||||
self.refresh_resume().await;
|
||||
}
|
||||
self.status = format!("Marked watched: {title}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn back(&mut self) -> Result<()> {
|
||||
if self.view != View::Items {
|
||||
return Ok(());
|
||||
@@ -312,6 +339,33 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn reload_current_items(&mut self, restore: Option<usize>) -> Result<()> {
|
||||
if self.view != View::Items {
|
||||
return Ok(());
|
||||
}
|
||||
let Some((parent_id, parent_name)) = self.breadcrumbs.last().cloned() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if parent_id == RESUME_VIRTUAL_ID {
|
||||
self.resume = self.client.resume_items(20).await?;
|
||||
self.home_entries = build_home_entries(&self.libraries, &self.resume);
|
||||
self.items = self.resume.clone();
|
||||
} else {
|
||||
self.items = self.client.items(&parent_id).await?;
|
||||
}
|
||||
self.current_title = Some(parent_name);
|
||||
self.list_state
|
||||
.select(restore_selection(restore, self.items.len()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_resume_view(&self) -> bool {
|
||||
self.breadcrumbs
|
||||
.last()
|
||||
.is_some_and(|(id, _)| id == RESUME_VIRTUAL_ID)
|
||||
}
|
||||
|
||||
pub fn picker_move_up(&mut self) {
|
||||
if let Some(p) = self.picker.as_mut() {
|
||||
p.cursor = match p.cursor {
|
||||
@@ -520,6 +574,13 @@ fn can_rescan(item: &Item) -> bool {
|
||||
item.is_folder && matches!(item.item_type.as_str(), "Series" | "Season")
|
||||
}
|
||||
|
||||
fn can_mark_watched(item: &Item) -> bool {
|
||||
matches!(
|
||||
item.item_type.as_str(),
|
||||
"Episode" | "Movie" | "Series" | "Season"
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn build_home_entries(libraries: &[Item], resume: &[Item]) -> Vec<Item> {
|
||||
let mut entries: Vec<Item> = Vec::with_capacity(libraries.len() + 1);
|
||||
if !resume.is_empty() {
|
||||
@@ -571,6 +632,28 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mark_watched_targets_playable_and_show_folders() {
|
||||
let series = Item {
|
||||
id: "show".into(),
|
||||
name: "Show".into(),
|
||||
item_type: "Series".into(),
|
||||
is_folder: true,
|
||||
..Item::default()
|
||||
};
|
||||
assert!(can_mark_watched(&movie("m1", "Movie One")));
|
||||
assert!(can_mark_watched(&ep("e1", "Show", 1, 1)));
|
||||
assert!(can_mark_watched(&series));
|
||||
assert!(can_mark_watched(&folder("s1", "Season 1")));
|
||||
assert!(!can_mark_watched(&Item {
|
||||
id: "collection".into(),
|
||||
name: "Collection".into(),
|
||||
item_type: "CollectionFolder".into(),
|
||||
is_folder: true,
|
||||
..Item::default()
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queue_for_episode_includes_rest_of_same_series() {
|
||||
let items = vec![
|
||||
|
||||
+24
@@ -105,6 +105,8 @@ pub struct UserData {
|
||||
pub position_ticks: i64,
|
||||
#[serde(rename = "PlayedPercentage", default)]
|
||||
pub played_percentage: Option<f64>,
|
||||
#[serde(rename = "Played", default)]
|
||||
pub played: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -267,6 +269,28 @@ impl Client {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark an item watched for the current user.
|
||||
pub async fn mark_watched(&self, item_id: &str) -> Result<()> {
|
||||
let url = format!(
|
||||
"{}/Users/{}/PlayedItems/{item_id}",
|
||||
self.server, self.user_id
|
||||
);
|
||||
let resp = self
|
||||
.http
|
||||
.post(&url)
|
||||
.headers(self.headers())
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("POST {url}"))?;
|
||||
|
||||
let status = resp.status();
|
||||
if !status.is_success() {
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
return Err(anyhow!("mark watched failed: HTTP {status}: {body}"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stream_url(&self, item_id: &str) -> String {
|
||||
format!(
|
||||
"{}/Videos/{}/stream?Static=true&api_key={}&DeviceId={}",
|
||||
|
||||
@@ -101,6 +101,11 @@ async fn run_loop(term: &mut Term, app: &mut App) -> Result<()> {
|
||||
app.status = format!("rescan error: {e}");
|
||||
}
|
||||
}
|
||||
KeyCode::Char('w') => {
|
||||
if let Err(e) = app.mark_selected_watched().await {
|
||||
app.status = format!("mark watched error: {e}");
|
||||
}
|
||||
}
|
||||
KeyCode::Char('s') => settings::run(term, app).await?,
|
||||
_ => {}
|
||||
}
|
||||
@@ -206,7 +211,7 @@ fn draw(f: &mut ratatui::Frame, app: &mut App) {
|
||||
let help = if app.picker.is_some() {
|
||||
"↑/↓ select · Enter confirm · [E]pisode [S]eason [W]hole show · Esc cancel"
|
||||
} else {
|
||||
"↑/↓ move · Enter open/play · r rescan · Backspace back · s settings · q quit (Q force)"
|
||||
"↑/↓ move · Enter open/play · w watched · r rescan · Backspace back · s settings · q quit (Q force)"
|
||||
};
|
||||
let status = Paragraph::new(vec![
|
||||
Line::from(Span::styled(
|
||||
@@ -322,7 +327,9 @@ fn format_item(it: &Item) -> ListItem<'static> {
|
||||
|
||||
// Progress badge for in-progress items.
|
||||
if let Some(ud) = &it.user_data {
|
||||
if let Some(pct) = ud.played_percentage {
|
||||
if ud.played {
|
||||
line.push_str(" (watched)");
|
||||
} else if let Some(pct) = ud.played_percentage {
|
||||
if pct > 0.0 && pct < 100.0 {
|
||||
line.push_str(&format!(" ({:.0}%)", pct));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user