Allow rescanning media folders

Co-Authored-By: codex <[email protected]>
This commit is contained in:
janisslsmandcodex committed 2026-06-12 17:44:28 +03:00
1 parent 0a026209aa
commit 8c0986161a
3 files changed
+62 -1

No files matched your search

+30
View File
@@ -261,6 +261,32 @@ impl App {
} }
} }
pub async fn rescan_selected(&mut self) -> Result<()> {
let Some(i) = self.list_state.selected() else {
self.status = "Select a library, series, or season to rescan".into();
return Ok(());
};
let target = match self.view {
View::Libraries => self
.home_entries
.get(i)
.filter(|item| item.id != RESUME_VIRTUAL_ID)
.cloned(),
View::Items => self.items.get(i).filter(|item| can_rescan(item)).cloned(),
};
let Some(item) = target else {
self.status = "Select a library, series, or season to rescan".into();
return Ok(());
};
self.status = format!("Requesting rescan for {}...", item.name);
self.client.refresh_item(&item.id).await?;
self.status = format!("Rescan started for {}", item.name);
Ok(())
}
pub async fn back(&mut self) -> Result<()> { pub async fn back(&mut self) -> Result<()> {
if self.view != View::Items { if self.view != View::Items {
return Ok(()); return Ok(());
@@ -496,6 +522,10 @@ fn queue_item_from(item: &Item) -> QueueItem {
} }
} }
fn can_rescan(item: &Item) -> bool {
item.is_folder && matches!(item.item_type.as_str(), "Series" | "Season")
}
pub(crate) fn build_home_entries(libraries: &[Item], resume: &[Item]) -> Vec<Item> { pub(crate) fn build_home_entries(libraries: &[Item], resume: &[Item]) -> Vec<Item> {
let mut entries: Vec<Item> = Vec::with_capacity(libraries.len() + 1); let mut entries: Vec<Item> = Vec::with_capacity(libraries.len() + 1);
if !resume.is_empty() { if !resume.is_empty() {
+26
View File
@@ -250,6 +250,32 @@ impl Client {
Ok(resp.items) Ok(resp.items)
} }
/// Ask Emby to rescan/refresh a library folder or media folder.
pub async fn refresh_item(&self, item_id: &str) -> Result<()> {
let url = format!("{}/Items/{item_id}/Refresh", self.server);
let resp = self
.http
.post(&url)
.headers(self.headers())
.query(&[
("Recursive", "true"),
("ImageRefreshMode", "Default"),
("MetadataRefreshMode", "Default"),
("ReplaceAllImages", "false"),
("ReplaceAllMetadata", "false"),
])
.send()
.await
.with_context(|| format!("POST {url}"))?;
let status = resp.status();
if !status.is_success() {
let body = resp.text().await.unwrap_or_default();
return Err(anyhow!("refresh failed: HTTP {status}: {body}"));
}
Ok(())
}
pub fn stream_url(&self, item_id: &str) -> String { pub fn stream_url(&self, item_id: &str) -> String {
format!( format!(
"{}/Videos/{}/stream?Static=true&api_key={}&DeviceId={}", "{}/Videos/{}/stream?Static=true&api_key={}&DeviceId={}",
+6 -1
View File
@@ -96,6 +96,11 @@ async fn run_loop(term: &mut Term, app: &mut App) -> Result<()> {
} }
} }
KeyCode::Backspace | KeyCode::Char('h') | KeyCode::Left => app.back().await?, KeyCode::Backspace | KeyCode::Char('h') | KeyCode::Left => app.back().await?,
KeyCode::Char('r') => {
if let Err(e) = app.rescan_selected().await {
app.status = format!("rescan error: {e}");
}
}
KeyCode::Char('s') => settings::run(term, app).await?, KeyCode::Char('s') => settings::run(term, app).await?,
_ => {} _ => {}
} }
@@ -196,7 +201,7 @@ fn draw(f: &mut ratatui::Frame, app: &mut App) {
let help = if app.picker.is_some() { let help = if app.picker.is_some() {
"↑/↓ select · Enter confirm · [E]pisode [S]eason [W]hole show · Esc cancel" "↑/↓ select · Enter confirm · [E]pisode [S]eason [W]hole show · Esc cancel"
} else { } else {
"↑/↓ move · Enter open/play · Backspace back · s settings · q quit (Q force)" "↑/↓ move · Enter open/play · r rescan · Backspace back · s settings · q quit (Q force)"
}; };
let status = Paragraph::new(vec![ let status = Paragraph::new(vec![
Line::from(Span::styled( Line::from(Span::styled(