Add default audio/subtitle language config and in-TUI settings editor

* New audio_lang / subtitle_lang fields in config (file, CLI flags, env
  vars). Forwarded to mpv's alang/slang as a comma-separated ISO 639
  priority list, e.g. "jpn,eng".
* New Settings screen (press `s` in the main view) for editing server,
  user, password, and language preferences without leaving the app.
  Credential changes trigger re-authentication against the new endpoint;
  on success the libraries/resume list is refreshed. Everything is
  persisted back to the config file the app loaded from.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
janisslsmandClaude Opus 4.7 committed 2026-05-18 02:12:08 +03:00
1 parent a9495389e8
commit f0ebb4b9a4
8 files changed
+342 -13

No files matched your search

+20 -3
View File
@@ -1,6 +1,8 @@
use anyhow::Result;
use ratatui::widgets::ListState;
use std::path::PathBuf;
use crate::config::Config;
use crate::emby::{Client, Item};
use crate::player::Player;
@@ -40,10 +42,20 @@ pub struct App {
/// brings you to the same season/series row, not row 0).
pub selection_stack: Vec<usize>,
pub status: String,
/// Live, editable configuration. Mirrors what's on disk; the settings
/// screen mutates this and persists it through `config_path`.
pub config: Config,
/// Where to persist updated settings. `None` when no writable location was
/// determined (rare; the settings screen will then refuse to save).
pub config_path: Option<PathBuf>,
}
impl App {
pub async fn new(client: Client) -> Result<Self> {
pub async fn new(
client: Client,
config: Config,
config_path: Option<PathBuf>,
) -> Result<Self> {
let libraries = client.views().await?;
// Best-effort: don't fail startup if the resume endpoint hiccups.
let resume = client.resume_items(20).await.unwrap_or_default();
@@ -72,6 +84,8 @@ impl App {
breadcrumbs: vec![],
selection_stack: vec![],
status,
config,
config_path,
})
}
@@ -228,7 +242,10 @@ impl App {
self.status = format!("Playing {title}...");
let start_seconds = (*start_ticks as f64) / 10_000_000.0;
let reporter = self.client.reporter(id.clone());
let mut player = Player::new()?;
let mut player = Player::new(
self.config.audio_lang.as_deref(),
self.config.subtitle_lang.as_deref(),
)?;
let r = player.play_blocking(&url, title, start_seconds, Some(&reporter));
self.status = match &r {
Ok(_) => format!("Finished: {title}"),
@@ -261,7 +278,7 @@ fn restore_selection(saved: Option<usize>, len: usize) -> Option<usize> {
Some(saved.unwrap_or(0).min(len - 1))
}
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);
if !resume.is_empty() {
entries.push(Item {