info almost done
This commit is contained in:
@@ -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>,
|
||||
}
|
||||
121
src/main.rs
121
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<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,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user