From 3facd0072126df6be1daf3f4c08306e763c651eb Mon Sep 17 00:00:00 2001 From: cwj3rcb Date: Sat, 14 Feb 2026 03:14:12 -0600 Subject: [PATCH] implimented first itteration of drill down --- src/core/jellyfin.rs | 70 +++++++++++++++++++++++++++++++------ src/main.rs | 83 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 134 insertions(+), 19 deletions(-) diff --git a/src/core/jellyfin.rs b/src/core/jellyfin.rs index 11390cf..dbbddc7 100644 --- a/src/core/jellyfin.rs +++ b/src/core/jellyfin.rs @@ -123,7 +123,7 @@ impl JellyfinClient { .get(url) .header("X-Emby-Authorization", self.auth_header_value()) .query(&[ - ("Recursive", "true"), + ("Recursive", if query.recursive { "true" } else { "false" }), ("StartIndex", &start_index.to_string()), ("Limit", &limit.to_string()), ("Fields", "PrimaryImageAspectRatio,SortName,ProductionYear"), @@ -131,6 +131,17 @@ impl JellyfinClient { ("SortOrder", "Ascending"), ]); + if let Some(types) = query.include_item_types.as_deref() { + req = req.query(&[("IncludeItemTypes", types)]); + } + if let Some(parent) = query.parent_id.as_deref() { + req = req.query(&[("ParentId", parent)]); + } + if let Some(term) = query.search_term.as_deref() { + req = req.query(&[("SearchTerm", term)]); + } + + // apply query (movie/series/etc) if let Some(types) = query.include_item_types.as_deref() { req = req.query(&[("IncludeItemTypes", types)]); @@ -159,32 +170,69 @@ impl JellyfinClient { } } +/// What kind of library list we want. #[derive(Clone)] pub struct LibraryQuery { - pub include_item_types: Option, - pub search_term: Option, + pub include_item_types: Option, // e.g. Some("Movie".into()) + pub parent_id: Option, // e.g. Some(series_id) + pub search_term: Option, // e.g. Some("matrix".into()) + pub recursive: bool, // true for big lists, false for drilldown } impl LibraryQuery { pub fn movies() -> Self { - Self { include_item_types: Some("Movie".into()), search_term: None } + Self { + include_item_types: Some("Movie".into()), + parent_id: None, + search_term: None, + recursive: true, + } } pub fn shows() -> Self { - Self { include_item_types: Some("Series".into()), search_term: None } + Self { + include_item_types: Some("Series".into()), + parent_id: None, + search_term: None, + recursive: true, + } } pub fn music() -> Self { - Self { include_item_types: Some("Audio".into()), search_term: None } - } - pub fn all() -> Self { - Self { include_item_types: None, search_term: None } + Self { + include_item_types: Some("Audio".into()), + parent_id: None, + search_term: None, + recursive: true, + } } - pub fn with_search(mut self, term: String) -> Self { - self.search_term = Some(term); + // true "All" (no filter) + pub fn all() -> Self { + Self { + include_item_types: None, + parent_id: None, + search_term: None, + recursive: true, + } + } + + pub fn with_parent(mut self, parent_id: impl Into) -> Self { + self.parent_id = Some(parent_id.into()); + self.recursive = false; // drilldown is usually direct children + self + } + + pub fn with_search(mut self, term: impl Into) -> Self { + self.search_term = Some(term.into()); + self + } + + pub fn with_item_types(mut self, types: impl Into) -> Self { + self.include_item_types = Some(types.into()); self } } + pub struct PagedItems { pub items: Vec, pub total: usize, diff --git a/src/main.rs b/src/main.rs index 78e40ef..be2af0c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -432,10 +432,50 @@ fn handle_key(app: &mut App, code: KeyCode) { } } } + View::Library { title, items, selected, .. } => { + if items.is_empty() { return; } + let it = &items[*selected]; + + let ty = it.item_type.as_deref().unwrap_or(""); + + // Shows -> Seasons -> Episodes + if ty == "Series" { + let q = core::jellyfin::LibraryQuery::all() + .with_parent(it.id.clone()) + .with_item_types("Season"); + open_children(app, format!("{} > {}", title, it.name), q); + return; + } + if ty == "Season" { + let q = core::jellyfin::LibraryQuery::all() + .with_parent(it.id.clone()) + .with_item_types("Episode"); + open_children(app, format!("{} > {}", title, it.name), q); + return; + } + + // MusicArtist -> MusicAlbum -> Audio + if ty == "MusicArtist" { + let q = core::jellyfin::LibraryQuery::all() + .with_parent(it.id.clone()) + .with_item_types("MusicAlbum"); + open_children(app, format!("{} > {}", title, it.name), q); + return; + } + if ty == "MusicAlbum" { + let q = core::jellyfin::LibraryQuery::all() + .with_parent(it.id.clone()) + .with_item_types("Audio"); + open_children(app, format!("{} > {}", title, it.name), q); + return; + } + + // Otherwise: later we can make Enter “play”, “details”, etc. + app.status = format!("No drilldown for type: {}", ty); + } _ => {} } } - KeyCode::Esc | KeyCode::Backspace => { if app.view_stack.len() > 1 { app.view_stack.pop(); @@ -443,7 +483,6 @@ fn handle_key(app: &mut App, code: KeyCode) { app.selected = 0; } } - KeyCode::Up | KeyCode::Char('k') => { match app.view_stack.last_mut().unwrap() { View::Categories => { @@ -475,7 +514,6 @@ fn handle_key(app: &mut App, code: KeyCode) { _ => {} } } - KeyCode::Down | KeyCode::Char('j') => { match app.view_stack.last_mut().unwrap() { View::Categories => { @@ -510,7 +548,6 @@ fn handle_key(app: &mut App, code: KeyCode) { _ => {} } } - KeyCode::Char('/') => { let scope = match app.current_view() { View::Categories => SearchScope::All, @@ -527,12 +564,10 @@ fn handle_key(app: &mut App, code: KeyCode) { input: String::new(), }); } - _ => {} } } - fn load_next_library_page(app: &mut App) { // pull info + mark loading let (query, next_start, total, page_size) = match app.view_stack.last_mut() { @@ -580,7 +615,6 @@ fn finish_loading(app: &mut App) { } } - fn maybe_prefetch(app: &mut App) { const PREFETCH_ROWS: usize = 0; @@ -615,7 +649,6 @@ fn ensure_loaded_to_end(app: &mut App) { } } - fn ui(frame: &mut Frame, app: &App) { let area = frame.area(); let panes = layout(area); @@ -805,6 +838,40 @@ fn render_library( ); } +fn open_children(app: &mut App, title: String, query: core::jellyfin::LibraryQuery) { + 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, + None => { app.status = "Missing user_id in config (login again)".into(); return; } + }; + let client = match core::jellyfin::JellyfinClient::from_config(&cfg) { + Ok(c) => c, + Err(e) => { app.status = e; return; } + }; + + app.status = format!("Fetching {}...", title); + + match client.list_items(user_id, query.clone(), 0, PAGE_SIZE) { + Ok(page) => { + app.view_stack.push(View::Library { + title, + query, + 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!("Fetch failed: {e}"), + } +} + struct Panes { top: Rect, left_main: Rect,