Skip to main content

#
jellytau_lib/commands/player/
settings.rs

    1//! Audio and video playback settings commands.
    2//!
    3//! TRACES: UR-022, UR-027, UR-031, UR-032, UR-033, UR-074 | DR-025, DR-030, DR-034, DR-035, DR-036, DR-162, IR-020
    4
    5use std::sync::Arc;
    6
    7use log::{info, warn};
    8use tauri::{Manager, State};
    9
    10use super::{PlayerStateWrapper, VideoSettingsWrapper};
    11use crate::commands::storage::DatabaseWrapper;
    12use crate::player::AutoplaySettings;
    13use crate::settings::{AudioSettings, EqPreset, StreamingQuality, VideoSettings};
    14use crate::storage::db_service::{DatabaseService, Query, QueryParam};
    15use crate::utils::lock::MutexSafe;
    16
    17/// `app_settings` key holding the persisted streaming bandwidth ceiling.
    18///
    19/// The cap is persisted (unlike the rest of `VideoSettings`, which is
    20/// process-lifetime state) because forgetting it is the one failure that costs
    21/// the user something real: a limit set for a metered connection that silently
    22/// reverts to uncapped on the next launch spends their data allowance without
    23/// ever showing them a changed setting.
    24///
    25/// TRACES: UR-074 | DR-162
    26const STREAMING_QUALITY_KEY: &str = "streaming_quality";
    27
    28#[tauri::command]
    29#[specta::specta]
    30pub async fn player_set_audio_settings(
    31    player: State<'_, PlayerStateWrapper>,
    32    settings: AudioSettings,
    33) -> Result<AudioSettings, String> {
    34    // Validate/normalise domain values before applying: clamp crossfade to its
    35    // range and normalise the equalizer band vector (length + gain clamps).
    36    let validated = settings
    37        .with_crossfade_clamped()
    38        .with_equalizer_normalised();
    39    let mut controller = player.0.lock().await;
    40    controller
    41        .set_audio_settings(&validated)
    42        .map_err(|e| e.to_string())?;
    43    Ok(controller.audio_settings())
    44}
    45
    46/// The built-in equalizer presets and their per-band gain curves (dB), for the
    47/// settings UI. The curve numbers are domain data defined by the band layout,
    48/// so the frontend reads them here rather than encoding them.
    49///
    50/// TRACES: UR-027 | DR-030
    51#[tauri::command]
    52#[specta::specta]
    53pub async fn player_get_eq_presets() -> Result<Vec<(EqPreset, Vec<f32>)>, String> {
    54    Ok(EqPreset::ALL
    55        .iter()
    56        .map(|p| (*p, p.gains().to_vec()))
    57        .collect())
    58}
    59
    60#[tauri::command]
    61#[specta::specta]
    62pub async fn player_get_audio_settings(
    63    player: State<'_, PlayerStateWrapper>,
    64) -> Result<AudioSettings, String> {
    65    let controller = player.0.lock().await;
    66    Ok(controller.audio_settings())
    67}
    68
    69#[tauri::command]
    70#[specta::specta]
    71pub async fn player_set_video_settings(
    72    video_settings: State<'_, VideoSettingsWrapper>,
    73    player: State<'_, PlayerStateWrapper>,
    74    db: State<'_, DatabaseWrapper>,
    75    settings: VideoSettings,
    76) -> Result<VideoSettings, String> {
    77    let validated = settings.with_countdown_clamped();
    78    {
    79        let mut current = video_settings.0.lock().map_err(|e| e.to_string())?;
    80        *current = validated.clone();
    81    } // Drop MutexGuard before await
    82
    83    // The bandwidth ceiling is read by the repository's URL builders and by the
    84    // PlaybackInfo negotiation, neither of which can see this wrapper.
    85    // TRACES: UR-074 | DR-162
    86    crate::repository::online::set_streaming_quality(validated.streaming_quality);
    87    persist_streaming_quality(&db, validated.streaming_quality).await;
    88
    89    // Sync to PlayerController's autoplay settings so on_playback_ended() uses current values
    90    let controller = player.0.lock().await;
    91    controller.set_autoplay_settings(AutoplaySettings {
    92        enabled: validated.auto_play_next_episode,
    93        countdown_seconds: validated.auto_play_countdown_seconds,
    94        max_episodes: validated.auto_play_max_episodes,
    95    });
    96
    97    Ok(validated)
    98}
    99
    100/// The bandwidth ceilings the quality picker may offer, each with the label and
    101/// one-line detail to show for it, highest first.
    102///
    103/// The ladder and its numbers are Jellyfin encoding domain vocabulary, so the
    104/// frontend reads them here rather than encoding them — the same arrangement as
    105/// [`player_get_eq_presets`].
    106///
    107/// TRACES: UR-074 | DR-162
    108#[tauri::command]
    109#[specta::specta]
    110pub async fn player_get_streaming_qualities(
    111) -> Result<Vec<(StreamingQuality, String, String)>, String> {
    112    Ok(StreamingQuality::ALL
    113        .iter()
    114        .map(|q| (*q, q.label().to_string(), q.detail().to_string()))
    115        .collect())
    116}
    117
    118/// Write the ceiling to `app_settings`. Failure is logged, not returned: the
    119/// setting has already been applied in memory, and refusing the whole call
    120/// because the write failed would leave the UI showing a cap that *is* active.
    121///
    122/// TRACES: UR-074 | DR-162
    123async fn persist_streaming_quality(db: &State<'_, DatabaseWrapper>, quality: StreamingQuality) {
    124    let db_service = {
    125        let database = db.0.lock_safe();
    126        Arc::new(database.service())
    127    };
    128
    129    let encoded = match serde_json::to_string(&quality) {
    130        Ok(value) => value,
    131        Err(e) => {
    132            warn!("[VideoSettings] Failed to encode streaming quality: {}", e);
    133            return;
    134        }
    135    };
    136
    137    let query = Query::with_params(
    138        "INSERT OR REPLACE INTO app_settings (key, value, updated_at)
    139         VALUES (?, ?, CURRENT_TIMESTAMP)",
    140        vec![
    141            QueryParam::String(STREAMING_QUALITY_KEY.to_string()),
    142            QueryParam::String(encoded),
    143        ],
    144    );
    145
    146    if let Err(e) = db_service.execute(query).await {
    147        warn!("[VideoSettings] Failed to persist streaming quality: {}", e);
    148    }
    149}
    150
    151/// Restore the persisted bandwidth ceiling at startup, into both the repository
    152/// (which enforces it) and `VideoSettings` (which the settings UI reads).
    153///
    154/// Called from the Tauri `setup` hook. A missing or unreadable row leaves the
    155/// default — uncapped — in place, so a database problem degrades to the old
    156/// behaviour rather than to an arbitrary limit.
    157///
    158/// TRACES: UR-074 | DR-162
    159pub async fn restore_streaming_quality(app: &tauri::AppHandle) {
    160    let db_service = {
    161        let Some(db) = app.try_state::<DatabaseWrapper>() else {
    162            warn!("[VideoSettings] No database available; streaming quality stays uncapped");
    163            return;
    164        };
    165        let database = db.0.lock_safe();
    166        Arc::new(database.service())
    167    };
    168
    169    let query = Query::with_params(
    170        "SELECT value FROM app_settings WHERE key = ?",
    171        vec![QueryParam::String(STREAMING_QUALITY_KEY.to_string())],
    172    );
    173
    174    let stored: Option<String> = match db_service.query_optional(query, |row| row.get(0)).await {
    175        Ok(value) => value,
    176        Err(e) => {
    177            warn!("[VideoSettings] Failed to read streaming quality: {}", e);
    178            return;
    179        }
    180    };
    181
    182    let Some(stored) = stored else { return };
    183    let quality: StreamingQuality = match serde_json::from_str(&stored) {
    184        Ok(quality) => quality,
    185        Err(e) => {
    186            warn!(
    187                "[VideoSettings] Ignoring unrecognised persisted streaming quality {:?}: {}",
    188                stored, e
    189            );
    190            return;
    191        }
    192    };
    193
    194    crate::repository::online::set_streaming_quality(quality);
    195    if let Some(video_settings) = app.try_state::<VideoSettingsWrapper>() {
    196        video_settings.0.lock_safe().streaming_quality = quality;
    197    }
    198    info!(
    199        "[VideoSettings] Restored streaming quality cap: {}",
    200        quality.label()
    201    );
    202}
    203
    204#[tauri::command]
    205#[specta::specta]
    206pub async fn player_get_video_settings(
    207    video_settings: State<'_, VideoSettingsWrapper>,
    208) -> Result<VideoSettings, String> {
    209    let current = video_settings.0.lock().map_err(|e| e.to_string())?;
    210    Ok(current.clone())
    211}