jellytau_lib/commands/
playback_mode.rs1use std::sync::Arc;
6use tauri::State;
7
8use crate::playback_mode::{PlaybackMode, PlaybackModeManager};
9
10pub struct PlaybackModeManagerWrapper(pub Arc<PlaybackModeManager>);
12
13#[tauri::command]
15#[specta::specta]
16pub fn playback_mode_get_current(
17 manager: State<'_, PlaybackModeManagerWrapper>,
18) -> Result<PlaybackMode, String> {
19 Ok(manager.0.get_mode())
20}
21
22#[tauri::command]
24#[specta::specta]
25pub fn playback_mode_set(
26 manager: State<'_, PlaybackModeManagerWrapper>,
27 mode: PlaybackMode,
28) -> Result<(), String> {
29 manager.0.set_mode(mode);
30 Ok(())
31}
32
33#[tauri::command]
35#[specta::specta]
36pub fn playback_mode_is_transferring(
37 manager: State<'_, PlaybackModeManagerWrapper>,
38) -> Result<bool, String> {
39 Ok(manager.0.is_transferring())
40}
41
42#[tauri::command]
44#[specta::specta]
45pub async fn playback_mode_transfer_to_remote(
46 manager: State<'_, PlaybackModeManagerWrapper>,
47 session_id: String,
48 position: Option<f64>,
49) -> Result<(), String> {
50 log::info!(
51 "[PlaybackModeCommands] Transferring to remote session: {} (position override: {:?})",
52 session_id,
53 position
54 );
55 manager.0.transfer_to_remote(session_id, position).await
56}
57
58#[tauri::command]
65#[specta::specta]
66pub async fn playback_mode_set_transferring(
67 manager: State<'_, PlaybackModeManagerWrapper>,
68 transferring: bool,
69) -> Result<(), String> {
70 manager.0.set_transferring(transferring);
71 Ok(())
72}
73
74#[tauri::command]
80#[specta::specta]
81pub async fn playback_mode_transfer_to_local(
82 manager: State<'_, PlaybackModeManagerWrapper>,
83 current_item_id: String,
84 position_ticks: i64,
85) -> Result<(), String> {
86 log::info!(
87 "[PlaybackModeCommands] Transferring to local: item_id={}, position={}",
88 current_item_id,
89 position_ticks
90 );
91 manager
92 .0
93 .transfer_to_local(current_item_id, position_ticks)
94 .await
95}
96
97#[tauri::command]
99#[specta::specta]
100pub async fn playback_mode_get_remote_status(
101 manager: State<'_, PlaybackModeManagerWrapper>,
102 player: State<'_, crate::commands::PlayerStateWrapper>,
103) -> Result<RemoteSessionStatus, String> {
104 let mode = manager.0.get_mode();
105
106 if let crate::playback_mode::PlaybackMode::Remote { session_id } = mode {
107 let client = {
109 let controller = player.0.lock().await;
110 let client_arc = controller.jellyfin_client();
111 let client_opt = client_arc.lock().map_err(|e| e.to_string())?;
112 client_opt
113 .as_ref()
114 .ok_or("Jellyfin client not configured")?
115 .clone()
116 };
117
118 match client.get_session(&session_id).await {
120 Ok(Some(session)) => {
121 let position_ticks = session
122 .play_state
123 .as_ref()
124 .and_then(|ps| ps.position_ticks)
125 .unwrap_or(0);
126
127 let duration_ticks = session
128 .now_playing_item
129 .as_ref()
130 .and_then(|item| item.run_time_ticks)
131 .unwrap_or(0);
132
133 let is_paused = session
134 .play_state
135 .as_ref()
136 .and_then(|ps| ps.is_paused)
137 .unwrap_or(true);
138
139 Ok(RemoteSessionStatus {
140 position: position_ticks as f64 / 10_000_000.0,
141 duration: if duration_ticks > 0 {
142 Some(duration_ticks as f64 / 10_000_000.0)
143 } else {
144 None
145 },
146 is_playing: !is_paused,
147 now_playing_item: session.now_playing_item.clone(),
148 })
149 }
150 Ok(None) => Err("Remote session not found".to_string()),
151 Err(e) => Err(format!("Failed to get session status: {}", e)),
152 }
153 } else {
154 Err("Not in remote playback mode".to_string())
155 }
156}
157
158#[derive(specta::Type, serde::Serialize)]
160#[serde(rename_all = "camelCase")]
161pub struct RemoteSessionStatus {
162 pub position: f64,
163 pub duration: Option<f64>,
164 pub is_playing: bool,
165 pub now_playing_item: Option<crate::jellyfin::NowPlayingItem>,
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn test_playback_mode_serialization() {
174 let local_mode = PlaybackMode::Local;
176 let json = serde_json::to_string(&local_mode);
177 assert!(json.is_ok());
178
179 let remote_mode = PlaybackMode::Remote {
181 session_id: "session-123".to_string(),
182 };
183 let json = serde_json::to_string(&remote_mode);
184 assert!(json.is_ok());
185 }
186
187 #[test]
188 fn test_remote_session_status_serialization() {
189 let status = RemoteSessionStatus {
190 position: 123.45,
191 duration: Some(600.0),
192 is_playing: true,
193 now_playing_item: None,
194 };
195
196 let json = serde_json::to_string(&status);
198 assert!(json.is_ok());
199
200 let serialized = json.unwrap();
201 assert!(serialized.contains("123.45"));
202 assert!(serialized.contains("600"));
203 assert!(serialized.contains("true"));
204 }
205
206 #[test]
207 fn test_remote_session_status_with_no_duration() {
208 let status = RemoteSessionStatus {
209 position: 0.0,
210 duration: None,
211 is_playing: false,
212 now_playing_item: None,
213 };
214
215 let json = serde_json::to_string(&status).unwrap();
216 assert!(json.contains("null") || json.contains("\"duration\":null"));
217 }
218
219 #[test]
220 fn test_remote_session_status_various_positions() {
221 let positions = vec![0.0, 30.5, 100.0, 3600.0];
222
223 for pos in positions {
224 let status = RemoteSessionStatus {
225 position: pos,
226 duration: Some(7200.0),
227 is_playing: true,
228 now_playing_item: None,
229 };
230
231 let json = serde_json::to_string(&status).unwrap();
232 assert!(json.contains(&pos.to_string()));
233 }
234 }
235
236 #[test]
237 fn test_playback_mode_deserialization_from_frontend() {
238 let idle_json = r#"{"type":"idle"}"#;
240 let mode: PlaybackMode =
241 serde_json::from_str(idle_json).expect("Failed to deserialize idle");
242 assert_eq!(mode, PlaybackMode::Idle);
243
244 let local_json = r#"{"type":"local"}"#;
246 let mode: PlaybackMode =
247 serde_json::from_str(local_json).expect("Failed to deserialize local");
248 assert_eq!(mode, PlaybackMode::Local);
249
250 let remote_json = r#"{"type":"remote","session_id":"session-123"}"#;
252 let mode: PlaybackMode =
253 serde_json::from_str(remote_json).expect("Failed to deserialize remote");
254 match mode {
255 PlaybackMode::Remote { session_id } => assert_eq!(session_id, "session-123"),
256 _ => panic!("Expected Remote mode"),
257 }
258 }
259
260 #[test]
261 fn test_play_tracks_context_deserialization() {
262 use crate::commands::PlayTracksContext;
263
264 let search_json = r#"{"type":"search","searchQuery":"test query"}"#;
266 let context: PlayTracksContext =
267 serde_json::from_str(search_json).expect("Failed to deserialize search context");
268 match context {
269 PlayTracksContext::Search { search_query } => {
270 assert_eq!(search_query, "test query");
271 }
272 _ => panic!("Expected Search context"),
273 }
274
275 let playlist_json =
277 r#"{"type":"playlist","playlistId":"pl-123","playlistName":"My Playlist"}"#;
278 let context: PlayTracksContext =
279 serde_json::from_str(playlist_json).expect("Failed to deserialize playlist context");
280 match context {
281 PlayTracksContext::Playlist {
282 playlist_id,
283 playlist_name,
284 } => {
285 assert_eq!(playlist_id, "pl-123");
286 assert_eq!(playlist_name, "My Playlist");
287 }
288 _ => panic!("Expected Playlist context"),
289 }
290
291 let custom_json = r#"{"type":"custom","label":"Custom Queue"}"#;
293 let context: PlayTracksContext =
294 serde_json::from_str(custom_json).expect("Failed to deserialize custom context");
295 match context {
296 PlayTracksContext::Custom { label } => {
297 assert_eq!(label, Some("Custom Queue".to_string()));
298 }
299 _ => panic!("Expected Custom context"),
300 }
301 }
302}