search feature added

This commit is contained in:
2026-02-14 01:42:31 -06:00
parent fb86573c2c
commit 235b02246d
2 changed files with 111 additions and 6 deletions

View File

@@ -136,6 +136,10 @@ impl JellyfinClient {
req = req.query(&[("IncludeItemTypes", types)]); 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() {
@@ -155,21 +159,29 @@ impl JellyfinClient {
} }
} }
/// What kind of library list we want.
#[derive(Clone)] #[derive(Clone)]
pub struct LibraryQuery { pub struct LibraryQuery {
pub include_item_types: Option<String>, // e.g. Some("Movie".into()) pub include_item_types: Option<String>,
pub search_term: Option<String>,
} }
impl LibraryQuery { impl LibraryQuery {
pub fn movies() -> Self { pub fn movies() -> Self {
Self { include_item_types: Some("Movie".into()) } Self { include_item_types: Some("Movie".into()), search_term: None }
} }
pub fn shows() -> Self { pub fn shows() -> Self {
Self { include_item_types: Some("Series".into()) } Self { include_item_types: Some("Series".into()), search_term: None }
} }
pub fn music() -> Self { pub fn music() -> Self {
Self { include_item_types: Some("Audio".into()) } Self { include_item_types: Some("Audio".into()), search_term: None }
}
pub fn all() -> Self {
Self { include_item_types: None, search_term: None }
}
pub fn with_search(mut self, term: String) -> Self {
self.search_term = Some(term);
self
} }
} }

View File

@@ -43,6 +43,20 @@ enum LoginFocus {
Pass, Pass,
} }
#[derive(Clone)]
enum SearchScope {
All,
Library {
title: String,
query: core::jellyfin::LibraryQuery, // includes item types
},
}
struct SearchState {
scope: SearchScope,
input: String,
}
struct App { struct App {
should_exit: bool, should_exit: bool,
status: String, status: String,
@@ -58,6 +72,8 @@ struct App {
login_pass: String, login_pass: String,
login_focus: LoginFocus, login_focus: LoginFocus,
login_error: Option<String>, login_error: Option<String>,
search: Option<SearchState>,
} }
impl Default for App { impl Default for App {
@@ -104,6 +120,7 @@ impl Default for App {
login_pass: String::new(), login_pass: String::new(),
login_focus: LoginFocus::User, login_focus: LoginFocus::User,
login_error: None, login_error: None,
search: None,
} }
} }
} }
@@ -228,6 +245,63 @@ fn handle_key(app: &mut App, code: KeyCode) {
} }
} }
// If search mode is active, it captures input first
if let Some(search) = &mut app.search {
match code {
KeyCode::Esc => { app.search = None; return; }
KeyCode::Backspace => { search.input.pop(); return; }
KeyCode::Enter => {
let term = search.input.trim().to_string();
if term.is_empty() {
app.search = None;
return;
}
// build query based on scope
let (title, query) = match &search.scope {
SearchScope::All => (
format!("Search: {}", term),
core::jellyfin::LibraryQuery::all().with_search(term.clone()),
),
SearchScope::Library { title, query } => (
format!("{}: {}", title, term),
query.clone().with_search(term.clone()),
),
};
app.search = None;
// run search (same as your library fetch)
let cfg = match &app.config { Some(c) => c.clone(), None => { app.status = "No config".into(); return; } };
let user_id = match cfg.user_id.as_deref() { Some(u) => u, None => { app.status = "Missing user_id".into(); return; } };
let client = match core::jellyfin::JellyfinClient::from_config(&cfg) { Ok(c) => c, Err(e) => { app.status = e; return; } };
app.status = format!("Searching… {}", term);
match client.list_items(user_id, query.clone(), 0, PAGE_SIZE) {
Ok(page) => {
app.view_stack.push(View::Library {
title,
query,
items: page.items,
selected: 0,
start_index: 0,
total: page.total,
page_size: PAGE_SIZE,
loading: false,
});
app.status = "Search loaded".into();
}
Err(e) => app.status = format!("Search failed: {e}"),
}
return;
}
KeyCode::Char(c) => { search.input.push(c); return; }
_ => return,
}
}
match code { match code {
KeyCode::Char('q') => app.should_exit = true, KeyCode::Char('q') => app.should_exit = true,
@@ -371,6 +445,19 @@ fn handle_key(app: &mut App, code: KeyCode) {
} }
} }
KeyCode::Char('/') => {
let scope = match app.current_view() {
View::Categories => SearchScope::All,
View::Library { title, query, .. } => SearchScope::Library {
title: title.clone(),
query: query.clone(),
},
_ => SearchScope::All,
};
app.search = Some(SearchState { scope, input: String::new() });
}
_ => {} _ => {}
} }
} }
@@ -481,8 +568,14 @@ fn ui(frame: &mut Frame, app: &App) {
} }
// Bottom Left // Bottom Left
let status_text = if let Some(s) = &app.search {
format!("/{}", s.input)
} else {
app.status.clone()
};
frame.render_widget( frame.render_widget(
Paragraph::new(app.status.as_str()) Paragraph::new(status_text)
.block(Block::bordered().title("Status").merge_borders(MergeStrategy::Exact)), .block(Block::bordered().title("Status").merge_borders(MergeStrategy::Exact)),
panes.left_output, panes.left_output,
); );