1#![allow(dead_code)]
12
13use std::sync::Arc;
14use tauri::State;
15use tokio::sync::Mutex as TokioMutex;
16
17use crate::commands::connectivity::ConnectivityMonitorWrapper;
18use crate::commands::storage::DatabaseWrapper;
19use crate::jellyfin::client::JellyfinClient;
20use crate::jellyfin::JellyfinConfig;
21use crate::playback_reporting::{PlaybackContext, PlaybackOperation, PlaybackReporter};
22use crate::utils::conversions::seconds_to_ticks;
23
24pub struct PlaybackReporterWrapper(pub Arc<TokioMutex<Option<PlaybackReporter>>>);
26
27#[tauri::command]
29#[specta::specta]
30pub async fn playback_reporter_init(
31 reporter_wrapper: State<'_, PlaybackReporterWrapper>,
32 db: State<'_, DatabaseWrapper>,
33 server_url: String,
34 user_id: String,
35 access_token: String,
36 device_id: String,
37) -> Result<(), String> {
38 log::info!("[PlaybackReporter] Initializing for user: {}", user_id);
39
40 let db_service = {
42 let database = db.0.lock().map_err(|e| e.to_string())?;
43 Arc::new(database.service())
44 };
45
46 let jellyfin_config = JellyfinConfig {
48 server_url,
49 access_token,
50 device_id,
51 };
52
53 let jellyfin_client = JellyfinClient::new(jellyfin_config)
54 .map_err(|e| format!("Failed to create JellyfinClient: {}", e))?;
55
56 let reporter = PlaybackReporter::new(
58 db_service,
59 Arc::new(TokioMutex::new(Some(jellyfin_client))),
60 user_id.clone(),
61 );
62
63 *reporter_wrapper.0.lock().await = Some(reporter);
65
66 log::info!(
67 "[PlaybackReporter] Initialized successfully for user: {}",
68 user_id
69 );
70 Ok(())
71}
72
73#[tauri::command]
75#[specta::specta]
76pub async fn playback_reporter_destroy(
77 reporter_wrapper: State<'_, PlaybackReporterWrapper>,
78) -> Result<(), String> {
79 log::info!("[PlaybackReporter] Destroying reporter");
80 *reporter_wrapper.0.lock().await = None;
81 Ok(())
82}
83
84#[tauri::command]
86#[specta::specta]
87pub async fn playback_report_start(
88 reporter: State<'_, PlaybackReporterWrapper>,
89 connectivity: State<'_, ConnectivityMonitorWrapper>,
90 item_id: String,
91 position_seconds: f64,
92 context_type: Option<String>,
93 context_id: Option<String>,
94) -> Result<(), String> {
95 let reporter_guard = reporter.0.lock().await;
96 let reporter_instance = reporter_guard
97 .as_ref()
98 .ok_or("PlaybackReporter not initialized")?;
99
100 let position_ticks = seconds_to_ticks(position_seconds);
101 let context = context_type.map(|ct| PlaybackContext {
102 context_type: ct,
103 context_id,
104 });
105
106 let operation = PlaybackOperation::Start {
107 item_id,
108 position_ticks,
109 context,
110 };
111
112 let monitor = connectivity.0.lock().await;
113 let is_online = monitor.get_status().await.is_server_reachable;
114 drop(monitor);
115
116 reporter_instance.report(operation, is_online).await
117}
118
119#[tauri::command]
121#[specta::specta]
122pub async fn playback_report_progress(
123 reporter: State<'_, PlaybackReporterWrapper>,
124 connectivity: State<'_, ConnectivityMonitorWrapper>,
125 item_id: String,
126 position_seconds: f64,
127 is_paused: bool,
128) -> Result<(), String> {
129 let reporter_guard = reporter.0.lock().await;
130 let reporter_instance = reporter_guard
131 .as_ref()
132 .ok_or("PlaybackReporter not initialized")?;
133
134 let position_ticks = seconds_to_ticks(position_seconds);
135 let operation = PlaybackOperation::Progress {
136 item_id,
137 position_ticks,
138 is_paused,
139 };
140
141 let monitor = connectivity.0.lock().await;
142 let is_online = monitor.get_status().await.is_server_reachable;
143 drop(monitor);
144
145 reporter_instance.report(operation, is_online).await
146}
147
148#[tauri::command]
150#[specta::specta]
151pub async fn playback_report_stopped(
152 reporter: State<'_, PlaybackReporterWrapper>,
153 connectivity: State<'_, ConnectivityMonitorWrapper>,
154 item_id: String,
155 position_seconds: f64,
156) -> Result<(), String> {
157 let reporter_guard = reporter.0.lock().await;
158 let reporter_instance = reporter_guard
159 .as_ref()
160 .ok_or("PlaybackReporter not initialized")?;
161
162 let position_ticks = seconds_to_ticks(position_seconds);
163 let operation = PlaybackOperation::Stopped {
164 item_id,
165 position_ticks,
166 };
167
168 let monitor = connectivity.0.lock().await;
169 let is_online = monitor.get_status().await.is_server_reachable;
170 drop(monitor);
171
172 reporter_instance.report(operation, is_online).await
173}
174
175#[tauri::command]
177#[specta::specta]
178pub async fn playback_mark_played(
179 reporter: State<'_, PlaybackReporterWrapper>,
180 connectivity: State<'_, ConnectivityMonitorWrapper>,
181 item_id: String,
182) -> Result<(), String> {
183 let reporter_guard = reporter.0.lock().await;
184 let reporter_instance = reporter_guard
185 .as_ref()
186 .ok_or("PlaybackReporter not initialized")?;
187
188 let operation = PlaybackOperation::MarkPlayed { item_id };
189
190 let monitor = connectivity.0.lock().await;
191 let is_online = monitor.get_status().await.is_server_reachable;
192 drop(monitor);
193
194 reporter_instance.report(operation, is_online).await
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_playback_operation_start_creation() {
203 let operation = PlaybackOperation::Start {
204 item_id: "item-123".to_string(),
205 position_ticks: 15_000_000,
206 context: Some(PlaybackContext {
207 context_type: "series".to_string(),
208 context_id: Some("series-456".to_string()),
209 }),
210 };
211
212 if let PlaybackOperation::Start {
214 item_id,
215 position_ticks,
216 context,
217 } = operation
218 {
219 assert_eq!(item_id, "item-123");
220 assert_eq!(position_ticks, 15_000_000);
221 assert!(context.is_some());
222 let ctx = context.unwrap();
223 assert_eq!(ctx.context_type, "series");
224 assert_eq!(ctx.context_id, Some("series-456".to_string()));
225 } else {
226 panic!("Expected Start variant");
227 }
228 }
229
230 #[test]
231 fn test_playback_operation_start_without_context() {
232 let operation = PlaybackOperation::Start {
233 item_id: "item-789".to_string(),
234 position_ticks: 5_000_000,
235 context: None,
236 };
237
238 if let PlaybackOperation::Start {
239 item_id, context, ..
240 } = operation
241 {
242 assert_eq!(item_id, "item-789");
243 assert!(context.is_none());
244 } else {
245 panic!("Expected Start variant");
246 }
247 }
248
249 #[test]
250 fn test_playback_operation_progress_creation() {
251 let operation = PlaybackOperation::Progress {
252 item_id: "item-999".to_string(),
253 position_ticks: 30_000_000,
254 is_paused: true,
255 };
256
257 if let PlaybackOperation::Progress {
258 item_id,
259 position_ticks,
260 is_paused,
261 } = operation
262 {
263 assert_eq!(item_id, "item-999");
264 assert_eq!(position_ticks, 30_000_000);
265 assert!(is_paused);
266 } else {
267 panic!("Expected Progress variant");
268 }
269 }
270
271 #[test]
272 fn test_playback_operation_progress_playing() {
273 let operation = PlaybackOperation::Progress {
274 item_id: "item-555".to_string(),
275 position_ticks: 45_000_000,
276 is_paused: false,
277 };
278
279 if let PlaybackOperation::Progress { is_paused, .. } = operation {
280 assert!(!is_paused);
281 } else {
282 panic!("Expected Progress variant");
283 }
284 }
285
286 #[test]
287 fn test_playback_operation_stopped_creation() {
288 let operation = PlaybackOperation::Stopped {
289 item_id: "item-111".to_string(),
290 position_ticks: 120_000_000,
291 };
292
293 if let PlaybackOperation::Stopped {
294 item_id,
295 position_ticks,
296 } = operation
297 {
298 assert_eq!(item_id, "item-111");
299 assert_eq!(position_ticks, 120_000_000);
300 } else {
301 panic!("Expected Stopped variant");
302 }
303 }
304
305 #[test]
306 fn test_playback_operation_mark_played_creation() {
307 let operation = PlaybackOperation::MarkPlayed {
308 item_id: "item-222".to_string(),
309 };
310
311 if let PlaybackOperation::MarkPlayed { item_id } = operation {
312 assert_eq!(item_id, "item-222");
313 } else {
314 panic!("Expected MarkPlayed variant");
315 }
316 }
317
318 #[test]
319 fn test_playback_context_with_series() {
320 let context = PlaybackContext {
321 context_type: "series".to_string(),
322 context_id: Some("series-789".to_string()),
323 };
324
325 assert_eq!(context.context_type, "series");
326 assert_eq!(context.context_id, Some("series-789".to_string()));
327 }
328
329 #[test]
330 fn test_playback_context_without_id() {
331 let context = PlaybackContext {
332 context_type: "folder".to_string(),
333 context_id: None,
334 };
335
336 assert_eq!(context.context_type, "folder");
337 assert!(context.context_id.is_none());
338 }
339
340 #[test]
341 fn test_playback_context_clone() {
342 let context = PlaybackContext {
343 context_type: "container".to_string(),
344 context_id: Some("container-123".to_string()),
345 };
346
347 let cloned = context.clone();
348 assert_eq!(cloned.context_type, "container");
349 assert_eq!(cloned.context_id, Some("container-123".to_string()));
350 }
351
352 #[test]
353 fn test_seconds_to_ticks_conversion() {
354 assert_eq!(seconds_to_ticks(0.0), 0);
355 assert_eq!(seconds_to_ticks(1.0), 10_000_000);
356 assert_eq!(seconds_to_ticks(1.5), 15_000_000);
357 assert_eq!(seconds_to_ticks(120.0), 1_200_000_000);
358 }
359
360 #[test]
361 fn test_playback_reporter_wrapper_structure() {
362 assert!(std::mem::size_of::<PlaybackReporterWrapper>() > 0);
364 }
365
366 #[test]
367 fn test_playback_operation_debug_trait() {
368 let operation = PlaybackOperation::Start {
370 item_id: "item-1".to_string(),
371 position_ticks: 0,
372 context: None,
373 };
374
375 let debug_str = format!("{:?}", operation);
376 assert!(debug_str.contains("Start"));
377 assert!(debug_str.contains("item-1"));
378 }
379
380 #[test]
381 fn test_playback_operation_clone() {
382 let operation = PlaybackOperation::Progress {
383 item_id: "item-clone".to_string(),
384 position_ticks: 50_000_000,
385 is_paused: true,
386 };
387
388 let cloned = operation.clone();
389 if let PlaybackOperation::Progress {
390 item_id, is_paused, ..
391 } = cloned
392 {
393 assert_eq!(item_id, "item-clone");
394 assert!(is_paused);
395 } else {
396 panic!("Clone failed to preserve variant");
397 }
398 }
399
400 #[test]
401 fn test_playback_operation_all_variants() {
402 let start_op = PlaybackOperation::Start {
404 item_id: "i1".to_string(),
405 position_ticks: 0,
406 context: None,
407 };
408 assert!(matches!(start_op, PlaybackOperation::Start { .. }));
409
410 let progress_op = PlaybackOperation::Progress {
411 item_id: "i2".to_string(),
412 position_ticks: 100,
413 is_paused: false,
414 };
415 assert!(matches!(progress_op, PlaybackOperation::Progress { .. }));
416
417 let stopped_op = PlaybackOperation::Stopped {
418 item_id: "i3".to_string(),
419 position_ticks: 200,
420 };
421 assert!(matches!(stopped_op, PlaybackOperation::Stopped { .. }));
422
423 let played_op = PlaybackOperation::MarkPlayed {
424 item_id: "i4".to_string(),
425 };
426 assert!(matches!(played_op, PlaybackOperation::MarkPlayed { .. }));
427 }
428}