Skip to main content

#
jellytau_lib/commands/player/
remote.rs

    1//! Remote Jellyfin session control commands (casting to another device).
    2//!
    3//! TRACES: UR-010, UR-046 | IR-012, IR-028, JA-022, JA-023, JA-025, JA-026 | DR-037, DR-058
    4//!
    5//! These thin command adapters forward control actions to the active Jellyfin
    6//! session via the player's configured `JellyfinClient`.
    7
    8use tauri::State;
    9
    10use super::PlayerStateWrapper;
    11use crate::jellyfin::client::LmsSyncGroup;
    12
    13/// Play items on a remote Jellyfin session (casting)
    14#[tauri::command]
    15#[specta::specta]
    16pub async fn remote_play_on_session(
    17    player: State<'_, PlayerStateWrapper>,
    18    session_id: String,
    19    item_ids: Vec<String>,
    20    start_index: usize,
    21) -> Result<(), String> {
    22    log::info!(
    23        "[RemoteSession] Playing {} items on session {} (start index: {})",
    24        item_ids.len(),
    25        session_id,
    26        start_index
    27    );
    28    log::info!("[RemoteSession] Item IDs: {:?}", item_ids);
    29
    30    let client_opt = {
    31        let controller = player.0.lock().await;
    32        controller
    33            .jellyfin_client()
    34            .lock()
    35            .map_err(|e| e.to_string())?
    36            .clone()
    37    };
    38
    39    if let Some(client) = client_opt {
    40        log::info!("[RemoteSession] Jellyfin client IS configured, calling play_on_session");
    41        client
    42            .play_on_session(session_id, item_ids, start_index, None)
    43            .await?;
    44        log::info!("[RemoteSession] Successfully started playback on remote session");
    45        Ok(())
    46    } else {
    47        log::error!("[RemoteSession] Jellyfin client is NOT configured! User needs to log out/in or restart app");
    48        Err(
    49            "Jellyfin client not configured - please restart the app or log out and log back in"
    50                .to_string(),
    51        )
    52    }
    53}
    54
    55/// Send a playback command to a remote session
    56#[tauri::command]
    57#[specta::specta]
    58pub async fn remote_send_command(
    59    player: State<'_, PlayerStateWrapper>,
    60    session_id: String,
    61    command: String,
    62) -> Result<(), String> {
    63    log::info!(
    64        "[RemoteSession] Sending command '{}' to session {}",
    65        command,
    66        session_id
    67    );
    68
    69    let client_opt = {
    70        let controller = player.0.lock().await;
    71        controller
    72            .jellyfin_client()
    73            .lock()
    74            .map_err(|e| e.to_string())?
    75            .clone()
    76    };
    77
    78    if let Some(client) = client_opt {
    79        client.send_session_command(session_id, &command).await?;
    80        log::info!("[RemoteSession] Command sent successfully");
    81        Ok(())
    82    } else {
    83        Err("Jellyfin client not configured".to_string())
    84    }
    85}
    86
    87/// Seek on a remote session
    88#[tauri::command]
    89#[specta::specta]
    90pub async fn remote_session_seek(
    91    player: State<'_, PlayerStateWrapper>,
    92    session_id: String,
    93    position_ticks: i64,
    94) -> Result<(), String> {
    95    log::info!(
    96        "[RemoteSession] Seeking to {} ticks on session {}",
    97        position_ticks,
    98        session_id
    99    );
    100
    101    let client_opt = {
    102        let controller = player.0.lock().await;
    103        controller
    104            .jellyfin_client()
    105            .lock()
    106            .map_err(|e| e.to_string())?
    107            .clone()
    108    };
    109
    110    if let Some(client) = client_opt {
    111        client.session_seek(session_id, position_ticks).await?;
    112        log::info!("[RemoteSession] Seek successful");
    113        Ok(())
    114    } else {
    115        Err("Jellyfin client not configured".to_string())
    116    }
    117}
    118
    119/// Set volume on a remote session
    120#[tauri::command]
    121#[specta::specta]
    122pub async fn remote_session_set_volume(
    123    player: State<'_, PlayerStateWrapper>,
    124    session_id: String,
    125    volume: i32,
    126) -> Result<(), String> {
    127    log::info!(
    128        "[RemoteSession] Setting volume to {} on session {}",
    129        volume,
    130        session_id
    131    );
    132
    133    let client_opt = {
    134        let controller = player.0.lock().await;
    135        controller
    136            .jellyfin_client()
    137            .lock()
    138            .map_err(|e| e.to_string())?
    139            .clone()
    140    };
    141
    142    if let Some(client) = client_opt {
    143        client.session_set_volume(session_id, volume).await?;
    144        log::info!("[RemoteSession] Volume set successfully");
    145        Ok(())
    146    } else {
    147        Err("Jellyfin client not configured".to_string())
    148    }
    149}
    150
    151/// Toggle mute on a remote session
    152#[tauri::command]
    153#[specta::specta]
    154pub async fn remote_session_toggle_mute(
    155    player: State<'_, PlayerStateWrapper>,
    156    session_id: String,
    157) -> Result<(), String> {
    158    log::info!("[RemoteSession] Toggling mute on session {}", session_id);
    159
    160    let client_opt = {
    161        let controller = player.0.lock().await;
    162        controller
    163            .jellyfin_client()
    164            .lock()
    165            .map_err(|e| e.to_string())?
    166            .clone()
    167    };
    168
    169    if let Some(client) = client_opt {
    170        client.session_toggle_mute(session_id).await?;
    171        log::info!("[RemoteSession] Mute toggled successfully");
    172        Ok(())
    173    } else {
    174        Err("Jellyfin client not configured".to_string())
    175    }
    176}
    177
    178// --- JellyLMS multi-room sync groups (fuse / unfuse LMS zones) --------------
    179//
    180// The frontend addresses LMS players by MAC address, which it derives from a
    181// session's device id (`lms-{mac}`). These commands forward to the JellyLMS
    182// plugin REST API via the configured JellyfinClient.
    183
    184/// List current LMS sync groups.
    185#[tauri::command]
    186#[specta::specta]
    187pub async fn lms_get_sync_groups(
    188    player: State<'_, PlayerStateWrapper>,
    189) -> Result<Vec<LmsSyncGroup>, String> {
    190    let client_opt = {
    191        let controller = player.0.lock().await;
    192        controller
    193            .jellyfin_client()
    194            .lock()
    195            .map_err(|e| e.to_string())?
    196            .clone()
    197    };
    198
    199    if let Some(client) = client_opt {
    200        client.lms_get_sync_groups().await
    201    } else {
    202        Err("Jellyfin client not configured".to_string())
    203    }
    204}
    205
    206/// Fuse LMS zones into a sync group. `master_mac` keeps playing and the
    207/// `slave_macs` zones join it in sync.
    208#[tauri::command]
    209#[specta::specta]
    210pub async fn lms_create_sync_group(
    211    player: State<'_, PlayerStateWrapper>,
    212    master_mac: String,
    213    slave_macs: Vec<String>,
    214) -> Result<(), String> {
    215    log::info!(
    216        "[LmsSync] Fusing zones: master={}, slaves={:?}",
    217        master_mac,
    218        slave_macs
    219    );
    220
    221    let client_opt = {
    222        let controller = player.0.lock().await;
    223        controller
    224            .jellyfin_client()
    225            .lock()
    226            .map_err(|e| e.to_string())?
    227            .clone()
    228    };
    229
    230    if let Some(client) = client_opt {
    231        client.lms_create_sync_group(&master_mac, slave_macs).await
    232    } else {
    233        Err("Jellyfin client not configured".to_string())
    234    }
    235}
    236
    237/// Remove a single LMS zone from its sync group (decouple one player).
    238#[tauri::command]
    239#[specta::specta]
    240pub async fn lms_unsync_player(
    241    player: State<'_, PlayerStateWrapper>,
    242    mac: String,
    243) -> Result<(), String> {
    244    log::info!("[LmsSync] Decoupling zone {}", mac);
    245
    246    let client_opt = {
    247        let controller = player.0.lock().await;
    248        controller
    249            .jellyfin_client()
    250            .lock()
    251            .map_err(|e| e.to_string())?
    252            .clone()
    253    };
    254
    255    if let Some(client) = client_opt {
    256        client.lms_unsync_player(&mac).await
    257    } else {
    258        Err("Jellyfin client not configured".to_string())
    259    }
    260}
    261
    262/// Dissolve an entire LMS sync group, identified by its master's MAC.
    263#[tauri::command]
    264#[specta::specta]
    265pub async fn lms_dissolve_sync_group(
    266    player: State<'_, PlayerStateWrapper>,
    267    master_mac: String,
    268) -> Result<(), String> {
    269    log::info!("[LmsSync] Dissolving group with master {}", master_mac);
    270
    271    let client_opt = {
    272        let controller = player.0.lock().await;
    273        controller
    274            .jellyfin_client()
    275            .lock()
    276            .map_err(|e| e.to_string())?
    277            .clone()
    278    };
    279
    280    if let Some(client) = client_opt {
    281        client.lms_dissolve_sync_group(&master_mac).await
    282    } else {
    283        Err("Jellyfin client not configured".to_string())
    284    }
    285}