diff --git a/src/core/jellyfin.rs b/src/core/jellyfin.rs index de442cc..9d1bcad 100644 --- a/src/core/jellyfin.rs +++ b/src/core/jellyfin.rs @@ -42,6 +42,14 @@ pub struct ViewsResponse { pub items: Vec, } +#[derive(Deserialize)] +struct PlaylistItemsResponse { + #[serde(rename = "Items", default)] + items: Vec, + #[serde(rename = "TotalRecordCount")] + total_record_count: Option, +} + impl JellyfinClient { pub fn from_config(cfg: &Config) -> Result { let token = cfg @@ -200,6 +208,44 @@ impl JellyfinClient { total: parsed.total_record_count.unwrap_or(0), }) } + + pub fn playlist_items( + &self, + user_id: &str, + playlist_id: &str, + start_index: usize, + limit: usize, + ) -> Result { + let url = format!("{}/Playlists/{}/Items", self.base_url, playlist_id); + + let resp = self + .client + .get(url) + .header("X-Emby-Authorization", self.auth_header_value()) + .query(&[ + ("UserId", user_id), + ("StartIndex", &start_index.to_string()), + ("Limit", &limit.to_string()), + ("Fields", "PrimaryImageAspectRatio,SortName,ProductionYear,IndexNumber,ParentIndexNumber"), + ]) + .send() + .map_err(|e| format!("request failed: {e}"))?; + + if !resp.status().is_success() { + let status = resp.status(); + let text = resp.text().unwrap_or_default(); + return Err(format!("playlist_items failed: {status} {text}")); + } + + let parsed: PlaylistItemsResponse = resp + .json() + .map_err(|e| format!("bad response json: {e}"))?; + + Ok(PagedItems { + items: parsed.items, + total: parsed.total_record_count.unwrap_or(0), + }) + } } /// What kind of library list we want. diff --git a/src/main.rs b/src/main.rs index c72fa67..cd09497 100644 --- a/src/main.rs +++ b/src/main.rs @@ -571,6 +571,7 @@ fn handle_key(app: &mut App, code: KeyCode) { "Movies" => core::jellyfin::LibraryQuery::movies(), "Shows" => core::jellyfin::LibraryQuery::shows(), "Music" => core::jellyfin::LibraryQuery::music(), + "Playlists" => core::jellyfin::LibraryQuery::all().with_item_types("Playlist"), _ => { app.status = format!("Not wired yet: {}", cat); return; } }; @@ -606,7 +607,6 @@ fn handle_key(app: &mut App, code: KeyCode) { open_children(app, format!("{} > {}", title, it.name), q); return; } - // Shows -> Seasons -> Episodes if ty == "Series" { let q = core::jellyfin::LibraryQuery::all() @@ -622,7 +622,6 @@ fn handle_key(app: &mut App, code: KeyCode) { open_children(app, format!("{} > {}", title, it.name), q); return; } - // MusicArtist -> MusicAlbum -> Audio if ty == "MusicArtist" { let q = core::jellyfin::LibraryQuery::all() @@ -643,6 +642,39 @@ fn handle_key(app: &mut App, code: KeyCode) { open_children(app, format!("{} > {}", title, it.name), q); return; } + if ty == "UserView" { + // Most UserViews are libraries like Shows/Movies/Music/Playlists. + // For Playlists view, drill into playlists. + let q = core::jellyfin::LibraryQuery::all() + .with_parent(it.id.clone()) + .with_item_types("Playlist"); + open_children(app, format!("{} > {}", title, it.name), q); + return; + } + if ty == "Playlist" { + let cfg = match &app.config { Some(c) => c.clone(), None => { app.status="No config".into(); return; } }; + let user_id = match cfg.user_id.as_deref() { Some(u) => u, None => { app.status="Missing user_id".into(); return; } }; + let client = match core::jellyfin::JellyfinClient::from_config(&cfg) { Ok(c)=>c, Err(e)=>{ app.status=e; return; } }; + + match client.playlist_items(user_id, &it.id, 0, PAGE_SIZE) { + Ok(page) => { + app.view_stack.push(View::Library { + title: format!("{} > {}", title, it.name), + // store a query placeholder; playlist paging could be done later if you want + query: core::jellyfin::LibraryQuery::all(), + items: page.items, + selected: 0, + start_index: 0, + total: page.total, + page_size: PAGE_SIZE, + loading: false, + }); + app.status = "Loaded".into(); + } + Err(e) => app.status = format!("Playlist load failed: {e}"), + } + return; + } // Playables -> mpv // Playables: do nothing on Enter (use 'p' to play) if ty == "Episode" || ty == "Audio" || ty == "Movie" {