info almost done
This commit is contained in:
@@ -173,7 +173,7 @@ impl JellyfinClient {
|
|||||||
("Recursive", recursive),
|
("Recursive", recursive),
|
||||||
("StartIndex", &start_index.to_string()),
|
("StartIndex", &start_index.to_string()),
|
||||||
("Limit", &limit.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),
|
("SortBy", sort_by),
|
||||||
("SortOrder", "Ascending"),
|
("SortOrder", "Ascending"),
|
||||||
]);
|
]);
|
||||||
@@ -226,7 +226,7 @@ impl JellyfinClient {
|
|||||||
("UserId", user_id),
|
("UserId", user_id),
|
||||||
("StartIndex", &start_index.to_string()),
|
("StartIndex", &start_index.to_string()),
|
||||||
("Limit", &limit.to_string()),
|
("Limit", &limit.to_string()),
|
||||||
("Fields", "PrimaryImageAspectRatio,SortName,ProductionYear,IndexNumber,ParentIndexNumber"),
|
("Fields", "PrimaryImageAspectRatio,SortName,ProductionYear,IndexNumber,ParentIndexNumber,RunTimeTicks,Genres,Studios,Overview,SeriesName"),
|
||||||
])
|
])
|
||||||
.send()
|
.send()
|
||||||
.map_err(|e| format!("request failed: {e}"))?;
|
.map_err(|e| format!("request failed: {e}"))?;
|
||||||
@@ -316,6 +316,12 @@ pub struct PagedItems {
|
|||||||
pub total: usize,
|
pub total: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct JfStudio {
|
||||||
|
#[serde(rename = "Name")]
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct JfItem {
|
pub struct JfItem {
|
||||||
#[serde(rename = "Id")]
|
#[serde(rename = "Id")]
|
||||||
@@ -328,9 +334,25 @@ pub struct JfItem {
|
|||||||
pub production_year: Option<i32>,
|
pub production_year: Option<i32>,
|
||||||
|
|
||||||
#[serde(rename = "IndexNumber")]
|
#[serde(rename = "IndexNumber")]
|
||||||
pub index_number: Option<i32>, // track # OR episode #
|
pub index_number: Option<i32>,
|
||||||
#[serde(rename = "ParentIndexNumber")]
|
#[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)]
|
#[derive(Deserialize)]
|
||||||
@@ -340,3 +362,9 @@ struct ItemsResponse {
|
|||||||
#[serde(rename = "TotalRecordCount")]
|
#[serde(rename = "TotalRecordCount")]
|
||||||
total_record_count: Option<usize>,
|
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::{
|
use ratatui::{
|
||||||
layout::{Constraint, Layout, Rect, Spacing},
|
layout::{Constraint, Layout, Rect, Spacing},
|
||||||
symbols::merge::MergeStrategy,
|
symbols::merge::MergeStrategy,
|
||||||
widgets::{Block, List, ListItem, ListState, Paragraph},
|
widgets::{Block, List, ListItem, ListState, Paragraph, Wrap},
|
||||||
style::{Modifier, Style},
|
style::{Modifier, Style},
|
||||||
Frame, Terminal,
|
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) {
|
fn ui(frame: &mut Frame, app: &App) {
|
||||||
let area = frame.area();
|
let area = frame.area();
|
||||||
let panes = layout(area);
|
let panes = layout(area);
|
||||||
@@ -1214,16 +1327,14 @@ fn render_library(
|
|||||||
|
|
||||||
let detail = if len > 0 {
|
let detail = if len > 0 {
|
||||||
let it = &items[selected];
|
let it = &items[selected];
|
||||||
format!(
|
format_details(it)
|
||||||
"Name: {}\nId: {}\nType: {:?}\nYear: {:?}",
|
|
||||||
it.name, it.id, it.item_type, it.production_year
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
"No items returned".to_string()
|
"No items returned".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(detail)
|
Paragraph::new(detail)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
.block(Block::bordered().title("Details").merge_borders(MergeStrategy::Exact)),
|
.block(Block::bordered().title("Details").merge_borders(MergeStrategy::Exact)),
|
||||||
right,
|
right,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user