From 437053395b005d659a8e6f49717879ae7e6f1b56 Mon Sep 17 00:00:00 2001 From: cwj3rcb Date: Sat, 14 Feb 2026 22:37:00 -0600 Subject: [PATCH] info almost done --- src/core/jellyfin.rs | 36 +++++++++++-- src/main.rs | 121 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 148 insertions(+), 9 deletions(-) diff --git a/src/core/jellyfin.rs b/src/core/jellyfin.rs index 9d1bcad..200c9c1 100644 --- a/src/core/jellyfin.rs +++ b/src/core/jellyfin.rs @@ -173,7 +173,7 @@ impl JellyfinClient { ("Recursive", recursive), ("StartIndex", &start_index.to_string()), ("Limit", &limit.to_string()), - ("Fields", "PrimaryImageAspectRatio,SortName,ProductionYear,IndexNumber,ParentIndexNumber"), + ("Fields", "PrimaryImageAspectRatio,SortName,ProductionYear,IndexNumber,ParentIndexNumber,RunTimeTicks,Genres,Studios,Overview,SeriesName"), ("SortBy", sort_by), ("SortOrder", "Ascending"), ]); @@ -226,7 +226,7 @@ impl JellyfinClient { ("UserId", user_id), ("StartIndex", &start_index.to_string()), ("Limit", &limit.to_string()), - ("Fields", "PrimaryImageAspectRatio,SortName,ProductionYear,IndexNumber,ParentIndexNumber"), + ("Fields", "PrimaryImageAspectRatio,SortName,ProductionYear,IndexNumber,ParentIndexNumber,RunTimeTicks,Genres,Studios,Overview,SeriesName"), ]) .send() .map_err(|e| format!("request failed: {e}"))?; @@ -316,6 +316,12 @@ pub struct PagedItems { pub total: usize, } +#[derive(Debug, Clone, Deserialize)] +pub struct JfStudio { + #[serde(rename = "Name")] + pub name: String, +} + #[derive(Debug, Clone, Deserialize)] pub struct JfItem { #[serde(rename = "Id")] @@ -328,9 +334,25 @@ pub struct JfItem { pub production_year: Option, #[serde(rename = "IndexNumber")] - pub index_number: Option, // track # OR episode # + pub index_number: Option, #[serde(rename = "ParentIndexNumber")] - pub parent_index_number: Option, // season # (for episodes) + pub parent_index_number: Option, + + // ✅ add these + #[serde(rename = "RunTimeTicks")] + pub run_time_ticks: Option, + + #[serde(rename = "Genres", default)] + pub genres: Vec, + + #[serde(rename = "Studios", default)] + pub studios: Vec, + + #[serde(rename = "Overview")] + pub overview: Option, + + #[serde(rename = "SeriesName")] + pub series_name: Option, } #[derive(Deserialize)] @@ -339,4 +361,10 @@ struct ItemsResponse { items: Vec, #[serde(rename = "TotalRecordCount")] total_record_count: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct StudioDto { + #[serde(rename = "Name")] + pub name: Option, } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index e85dab9..5e5d23b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,7 +24,7 @@ use crossterm::{ use ratatui::{ layout::{Constraint, Layout, Rect, Spacing}, symbols::merge::MergeStrategy, - widgets::{Block, List, ListItem, ListState, Paragraph}, + widgets::{Block, List, ListItem, ListState, Paragraph, Wrap}, style::{Modifier, Style}, Frame, Terminal, }; @@ -976,6 +976,119 @@ fn ensure_loaded_to_end(app: &mut App) { } } + + +fn ticks_to_runtime_str(ticks: Option) -> Option { + // Jellyfin ticks are 10,000,000 per second + let t = ticks?; + if t <= 0 { return None; } + + let total_seconds = (t as u64 / 10_000_000) as u64; + let minutes = total_seconds / 60; + let seconds = total_seconds % 60; + + if minutes >= 60 { + let h = minutes / 60; + let m = minutes % 60; + Some(format!("{h}h {m}m")) + } else { + Some(format!("{minutes}m {seconds:02}s")) + } +} + + +fn join_genres(genres: &[String]) -> String { + if genres.is_empty() { "-".into() } else { genres.join(", ") } +} + +fn join_studios(studios: &[core::jellyfin::JfStudio]) -> String { + let names: Vec = studios + .iter() + .map(|s| s.name.clone()) + .filter(|s| !s.trim().is_empty()) + .collect(); + + if names.is_empty() { "-".into() } else { names.join(", ") } +} + +fn year_str(y: Option) -> String { + y.map(|v| v.to_string()).unwrap_or_else(|| "-".into()) +} + +fn overview_str(o: &Option) -> String { + o.as_deref() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .unwrap_or("-") + .to_string() +} + +fn format_details(it: &core::jellyfin::JfItem) -> String { + let ty = it.item_type.as_deref().unwrap_or(""); + + match ty { + "Series" => format!( + "Name: {}\nYear: {}\nGenres: {}\nStudio: {}\n\nOverview:\n{}", + it.name, + year_str(it.production_year), + join_genres(&it.genres), + join_studios(&it.studios), + overview_str(&it.overview), + ), + + "Movie" => format!( + "Name: {}\nYear: {}\nRuntime: {}\nGenres: {}\nStudio: {}\n\nOverview:\n{}", + it.name, + year_str(it.production_year), + ticks_to_runtime_str(it.run_time_ticks).unwrap_or_else(|| "-".into()), + join_genres(&it.genres), + join_studios(&it.studios), + overview_str(&it.overview), + ), + + // Episodes: show SeriesName if present + "Episode" => { + let series = it.series_name.clone().unwrap_or_else(|| "-".into()); + let se = match (it.parent_index_number, it.index_number) { + (Some(s), Some(e)) => format!("{:02}x{:02}", s, e), + _ => "-".into(), + }; + + format!( + "Series: {}\nEpisode: {} {}\nYear: {}\n\nOverview:\n{}", + series, + se, + it.name, + year_str(it.production_year), + overview_str(&it.overview), + ) + } + + // Music: you asked only Name + Runtime + "Audio" => format!( + "Name: {}\nRuntime: {}", + it.name, + ticks_to_runtime_str(it.run_time_ticks).unwrap_or_else(|| "-".into()), + ), + + // fallback + _ => format!( + "Name: {}\nId: {}\nType: {:?}\nYear: {:?}", + it.name, it.id, it.item_type, it.production_year + ), + } +} + + + + + + + + + + + fn ui(frame: &mut Frame, app: &App) { let area = frame.area(); let panes = layout(area); @@ -1214,16 +1327,14 @@ fn render_library( let detail = if len > 0 { let it = &items[selected]; - format!( - "Name: {}\nId: {}\nType: {:?}\nYear: {:?}", - it.name, it.id, it.item_type, it.production_year - ) + format_details(it) } else { "No items returned".to_string() }; frame.render_widget( Paragraph::new(detail) + .wrap(Wrap { trim: false }) .block(Block::bordered().title("Details").merge_borders(MergeStrategy::Exact)), right, );