library integration
This commit is contained in:
@@ -36,6 +36,11 @@ struct UserDto {
|
|||||||
id: Option<String>,
|
id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct ViewsResponse {
|
||||||
|
#[serde(rename = "Items", default)]
|
||||||
|
pub items: Vec<JfItem>,
|
||||||
|
}
|
||||||
|
|
||||||
impl JellyfinClient {
|
impl JellyfinClient {
|
||||||
pub fn from_config(cfg: &Config) -> Result<Self, String> {
|
pub fn from_config(cfg: &Config) -> Result<Self, String> {
|
||||||
@@ -97,8 +102,30 @@ impl JellyfinClient {
|
|||||||
access_token: token,
|
access_token: token,
|
||||||
user_id,
|
user_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn list_views(&self, user_id: &str) -> Result<Vec<JfItem>, String> {
|
||||||
|
let url = format!("{}/Users/{}/Views", self.base_url, user_id);
|
||||||
|
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.get(url)
|
||||||
|
.header("X-Emby-Authorization", self.auth_header_value())
|
||||||
|
.send()
|
||||||
|
.map_err(|e| format!("Views request failed: {e}"))?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
let status = resp.status();
|
||||||
|
let text = resp.text().unwrap_or_default();
|
||||||
|
return Err(format!("Views failed: {status} {text}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: ViewsResponse = resp
|
||||||
|
.json()
|
||||||
|
.map_err(|e| format!("Views parse failed: {e}"))?;
|
||||||
|
|
||||||
|
Ok(parsed.items)
|
||||||
|
}
|
||||||
|
|
||||||
fn auth_header_value(&self) -> String {
|
fn auth_header_value(&self) -> String {
|
||||||
// Identify as a client and include the token.
|
// Identify as a client and include the token.
|
||||||
@@ -128,12 +155,14 @@ impl JellyfinClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let recursive = if query.recursive { "true" } else { "false" };
|
||||||
|
|
||||||
let mut req = self
|
let mut req = self
|
||||||
.client
|
.client
|
||||||
.get(url)
|
.get(url)
|
||||||
.header("X-Emby-Authorization", self.auth_header_value())
|
.header("X-Emby-Authorization", self.auth_header_value())
|
||||||
.query(&[
|
.query(&[
|
||||||
("Recursive", "true"),
|
("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"),
|
||||||
@@ -154,16 +183,6 @@ impl JellyfinClient {
|
|||||||
req = req.query(&[("SearchTerm", term)]);
|
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)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(term) = query.search_term.as_deref() {
|
|
||||||
req = req.query(&[("SearchTerm", term)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let resp = req.send().map_err(|e| format!("request failed: {e}"))?;
|
let resp = req.send().map_err(|e| format!("request failed: {e}"))?;
|
||||||
|
|
||||||
if !resp.status().is_success() {
|
if !resp.status().is_success() {
|
||||||
|
|||||||
86
src/main.rs
86
src/main.rs
@@ -193,6 +193,7 @@ impl Default for App {
|
|||||||
"Music".into(),
|
"Music".into(),
|
||||||
"Playlists".into(),
|
"Playlists".into(),
|
||||||
"Collections".into(),
|
"Collections".into(),
|
||||||
|
"Libraries".into(),
|
||||||
"Continue Watching".into(),
|
"Continue Watching".into(),
|
||||||
"Recently Added".into(),
|
"Recently Added".into(),
|
||||||
"Downloads".into(),
|
"Downloads".into(),
|
||||||
@@ -519,6 +520,7 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
|||||||
match code {
|
match code {
|
||||||
KeyCode::Char('q') => app.should_exit = true,
|
KeyCode::Char('q') => app.should_exit = true,
|
||||||
|
|
||||||
|
//Enter code Block
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
let view = app.current_view().clone();
|
let view = app.current_view().clone();
|
||||||
|
|
||||||
@@ -526,38 +528,50 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
|||||||
View::Categories => {
|
View::Categories => {
|
||||||
let cat = app.items[app.selected].clone();
|
let cat = app.items[app.selected].clone();
|
||||||
|
|
||||||
let query = match cat.as_str() {
|
|
||||||
"Movies" => core::jellyfin::LibraryQuery::movies(),
|
|
||||||
"Shows" => core::jellyfin::LibraryQuery::shows(),
|
|
||||||
"Music" => core::jellyfin::LibraryQuery::music(),
|
|
||||||
_ => {
|
|
||||||
app.status = format!("Not wired yet: {}", cat);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let cfg = match &app.config {
|
let cfg = match &app.config {
|
||||||
Some(c) => c.clone(),
|
Some(c) => c.clone(),
|
||||||
None => {
|
None => { app.status = "No config loaded".into(); return; }
|
||||||
app.status = "No config loaded".to_string();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = match cfg.user_id.as_deref() {
|
let user_id = match cfg.user_id.as_deref() {
|
||||||
Some(u) => u,
|
Some(u) => u,
|
||||||
None => {
|
None => { app.status = "Missing user_id in config (login again)".into(); return; }
|
||||||
app.status = "Missing user_id in config (login again)".to_string();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let client = match core::jellyfin::JellyfinClient::from_config(&cfg) {
|
let client = match core::jellyfin::JellyfinClient::from_config(&cfg) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => { app.status = e; return; }
|
||||||
app.status = e;
|
};
|
||||||
|
|
||||||
|
// Special case: Libraries uses /Views, not /Items
|
||||||
|
if cat == "Libraries" {
|
||||||
|
app.status = "Fetching Libraries...".into();
|
||||||
|
match client.list_views(user_id) {
|
||||||
|
Ok(items) => {
|
||||||
|
let total = items.len();
|
||||||
|
app.view_stack.push(View::Library {
|
||||||
|
title: "Libraries".into(),
|
||||||
|
query: core::jellyfin::LibraryQuery::all(), // placeholder
|
||||||
|
items,
|
||||||
|
selected: 0,
|
||||||
|
start_index: 0,
|
||||||
|
total, // cheap total for UI
|
||||||
|
page_size: PAGE_SIZE,
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
app.status = "Loaded".into();
|
||||||
|
}
|
||||||
|
Err(e) => app.status = format!("Views failed: {e}"),
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normal categories use /Items
|
||||||
|
let query = match cat.as_str() {
|
||||||
|
"Movies" => core::jellyfin::LibraryQuery::movies(),
|
||||||
|
"Shows" => core::jellyfin::LibraryQuery::shows(),
|
||||||
|
"Music" => core::jellyfin::LibraryQuery::music(),
|
||||||
|
_ => { app.status = format!("Not wired yet: {}", cat); return; }
|
||||||
};
|
};
|
||||||
|
|
||||||
app.status = format!("Fetching {}...", cat);
|
app.status = format!("Fetching {}...", cat);
|
||||||
@@ -574,11 +588,9 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
|||||||
page_size: PAGE_SIZE,
|
page_size: PAGE_SIZE,
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
app.status = "Loaded".to_string();
|
app.status = "Loaded".into();
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
app.status = format!("Fetch failed: {e}");
|
|
||||||
}
|
}
|
||||||
|
Err(e) => app.status = format!("Fetch failed: {e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,6 +600,13 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
|||||||
|
|
||||||
let ty = it.item_type.as_deref().unwrap_or("");
|
let ty = it.item_type.as_deref().unwrap_or("");
|
||||||
|
|
||||||
|
// Libraries (/Views) usually return CollectionFolder. Entering it should list its contents.
|
||||||
|
if ty == "CollectionFolder" {
|
||||||
|
let q = core::jellyfin::LibraryQuery::all().with_parent(it.id.clone());
|
||||||
|
open_children(app, format!("{} > {}", title, it.name), q);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Shows -> Seasons -> Episodes
|
// Shows -> Seasons -> Episodes
|
||||||
if ty == "Series" {
|
if ty == "Series" {
|
||||||
let q = core::jellyfin::LibraryQuery::all()
|
let q = core::jellyfin::LibraryQuery::all()
|
||||||
@@ -619,7 +638,11 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
|||||||
open_children(app, format!("{} > {}", title, it.name), q);
|
open_children(app, format!("{} > {}", title, it.name), q);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if ty == "BoxSet" {
|
||||||
|
let q = core::jellyfin::LibraryQuery::all().with_parent(it.id.clone());
|
||||||
|
open_children(app, format!("{} > {}", title, it.name), q);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Playables -> mpv
|
// Playables -> mpv
|
||||||
// Playables: do nothing on Enter (use 'p' to play)
|
// Playables: do nothing on Enter (use 'p' to play)
|
||||||
if ty == "Episode" || ty == "Audio" || ty == "Movie" {
|
if ty == "Episode" || ty == "Audio" || ty == "Movie" {
|
||||||
@@ -1005,15 +1028,24 @@ fn render_library(
|
|||||||
|
|
||||||
let list_items: Vec<ListItem> = items[start..end]
|
let list_items: Vec<ListItem> = items[start..end]
|
||||||
.iter()
|
.iter()
|
||||||
|
.filter(|it| {
|
||||||
|
// Hide the redundant "collections" folder inside the Collections library
|
||||||
|
if title.ends_with("Collections") {
|
||||||
|
if it.item_type.as_deref() == Some("Folder")
|
||||||
|
&& it.name.eq_ignore_ascii_case("collections")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
})
|
||||||
.map(|it| {
|
.map(|it| {
|
||||||
let prefix = left_number_prefix(it, show_audio_track_numbers);
|
let prefix = left_number_prefix(it, show_audio_track_numbers);
|
||||||
|
|
||||||
let year = if show_year_for(it.item_type.as_deref()) {
|
let year = if show_year_for(it.item_type.as_deref()) {
|
||||||
it.production_year.map(|y| format!(" ({y})")).unwrap_or_default()
|
it.production_year.map(|y| format!(" ({y})")).unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
ListItem::new(format!("{}{}{}", prefix, it.name, year))
|
ListItem::new(format!("{}{}{}", prefix, it.name, year))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
Reference in New Issue
Block a user