info almost done

This commit is contained in:
2026-02-14 22:37:00 -06:00
parent 2893387372
commit 437053395b
2 changed files with 148 additions and 9 deletions

View File

@@ -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<i32>,
#[serde(rename = "IndexNumber")]
pub index_number: Option<i32>, // track # OR episode #
pub index_number: Option<i32>,
#[serde(rename = "ParentIndexNumber")]
pub parent_index_number: Option<i32>, // season # (for episodes)
pub parent_index_number: Option<i32>,
// ✅ add these
#[serde(rename = "RunTimeTicks")]
pub run_time_ticks: Option<i64>,
#[serde(rename = "Genres", default)]
pub genres: Vec<String>,
#[serde(rename = "Studios", default)]
pub studios: Vec<JfStudio>,
#[serde(rename = "Overview")]
pub overview: Option<String>,
#[serde(rename = "SeriesName")]
pub series_name: Option<String>,
}
#[derive(Deserialize)]
@@ -340,3 +362,9 @@ struct ItemsResponse {
#[serde(rename = "TotalRecordCount")]
total_record_count: Option<usize>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct StudioDto {
#[serde(rename = "Name")]
pub name: Option<String>,
}

View File

@@ -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<i64>) -> Option<String> {
// 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<String> = 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<i32>) -> String {
y.map(|v| v.to_string()).unwrap_or_else(|| "-".into())
}
fn overview_str(o: &Option<String>) -> 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,
);