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}