1#[cfg(target_os = "android")]
2mod android_context;
3mod auth;
4mod commands;
5#[cfg(feature = "conformance")]
8pub mod conformance_runner;
9mod connectivity;
10mod credentials;
11mod domain;
12mod download;
13mod jellyfin;
14mod media_server;
15mod playback_mode;
16mod playback_reporting;
17mod player;
18mod profiles;
19mod repository;
20mod session_poller;
21pub mod settings;
22mod storage;
23mod thumbnail;
24pub mod utils;
25
26#[cfg(target_os = "android")]
27use log::warn;
28use log::{error, info};
29use std::sync::{Arc, Mutex};
30use tauri::{Emitter, Manager};
31use tauri_specta::Builder;
32use tokio::sync::Mutex as TokioMutex;
33
34use auth::AuthManager;
35use commands::{
36 auth_connect_to_server,
37 auth_get_session,
38 auth_initialize,
40 auth_login,
41 auth_logout,
42 auth_reauthenticate,
43 auth_set_session,
44 auth_start_verification,
45 auth_stop_verification,
46 auth_verify_session,
47 calc_progress,
48 cancel_download,
49 catalog_sync_status,
50 clear_stale_downloads,
51 connectivity_check_server,
53 connectivity_get_status,
54 connectivity_mark_reachable,
55 connectivity_mark_unreachable,
56 connectivity_set_server_url,
57 connectivity_start_monitoring,
58 connectivity_stop_monitoring,
59 convert_percent_to_volume,
60 convert_ticks_to_seconds,
61 delete_album_downloads,
62 delete_all_downloads,
63 delete_download,
64 delete_downloads_under,
65 device_get_id,
67 device_set_id,
68 diagnostics_export,
70 diagnostics_get_info,
71 diagnostics_set_level,
72 download_album,
73 download_item,
74 download_item_and_start,
75 download_season,
76 download_series,
77 download_video,
78 enqueue_download,
79 enqueue_video_downloads,
80 format_time_seconds,
82 format_time_seconds_long,
83 get_album_affinity_status,
84 get_album_recommendations,
85 get_download_manager_stats,
86 get_download_storage_stats,
87 get_downloads,
88 get_downloads_allowed,
89 get_smart_cache_config,
90 get_smart_cache_stats,
91 image_get_url,
92 is_item_pinned,
93 library_get_exclusion_candidates,
95 library_get_settings,
96 library_set_settings,
97 lms_create_sync_group,
98 lms_dissolve_sync_group,
99 lms_get_sync_groups,
101 lms_unsync_player,
102 mark_download_completed,
103 mark_download_failed,
104 media_local_selection,
105 media_local_url,
106 offline_get_items,
107 offline_is_available,
108 offline_search,
109 pause_download,
110 pin_item,
111 playback_mark_played,
112 playback_mode_get_current,
114 playback_mode_get_remote_status,
115 playback_mode_is_transferring,
116 playback_mode_set,
117 playback_mode_set_transferring,
118 playback_mode_transfer_to_local,
119 playback_mode_transfer_to_remote,
120 playback_report_progress,
121 playback_report_start,
122 playback_report_stopped,
123 playback_reporter_destroy,
124 playback_reporter_init,
126 player_add_to_queue,
128 player_add_track_by_id,
129 player_add_tracks_by_ids,
130 player_background_action,
131 player_cancel_autoplay_countdown,
132 player_cancel_sleep_timer,
133 player_configure_jellyfin,
135 player_cycle_repeat,
136 player_disable_jellyfin,
137 player_dismiss_session,
138 player_enter_background_audio,
139 player_exit_background_audio,
140 player_get_audio_settings,
141 player_get_autoplay_settings,
142 player_get_cache_config,
143 player_get_capabilities,
144 player_get_eq_presets,
145 player_get_queue,
146 player_get_session,
148 player_get_sleep_timer,
149 player_get_status,
150 player_get_streaming_qualities,
151 player_get_video_settings,
152 player_local_media_path,
154 player_move_in_queue,
155 player_next,
156 player_on_playback_ended,
157 player_pause,
158 player_play,
159 player_play_album_track,
160 player_play_item,
161 player_play_next_episode,
162 player_play_queue,
163 player_play_tracks,
164 player_preload_upcoming,
165 player_previous,
166 player_recover_stream,
167 player_remove_from_queue,
168 player_report_media_loaded,
169 player_report_position,
170 player_report_state,
172 player_seek,
173 player_seek_video,
174 player_set_audio_settings,
175 player_set_audio_track,
176 player_set_autoplay_settings,
177 player_set_cache_config,
178 player_set_sleep_timer,
180 player_set_stream_quality,
181 player_set_subtitle_track,
182 player_set_video_settings,
183 player_set_volume,
184 player_skip_to,
185 player_stop,
186 player_switch_audio_track,
187 player_toggle,
188 player_toggle_mute,
189 player_toggle_shuffle,
190 playlist_add_items,
191 playlist_create,
193 playlist_delete,
194 playlist_get_items,
195 playlist_move_item,
196 playlist_remove_items,
197 playlist_rename,
198 profiles_add,
199 profiles_get_ask_on_start,
200 profiles_list,
201 profiles_remove,
202 profiles_set_ask_on_start,
203 profiles_set_pin,
204 profiles_startup_target,
205 profiles_unlock,
206 profiles_unlock_with_password,
207 remote_play_on_session,
209 remote_send_command,
210 remote_session_seek,
211 remote_session_set_volume,
212 remote_session_toggle_mute,
213 repository_clear_watch_history,
215 repository_create,
216 repository_destroy,
217 repository_get_audio_only_stream_url_for_video,
218 repository_get_audio_stream_url,
219 repository_get_channels,
220 repository_get_download_disk_usage,
221 repository_get_downloaded_items,
222 repository_get_downloaded_libraries,
223 repository_get_favorites,
224 repository_get_genres,
225 repository_get_image_url,
226 repository_get_item,
227 repository_get_items,
228 repository_get_items_by_person,
229 repository_get_latest_items,
230 repository_get_libraries,
231 repository_get_live_tv_channels,
232 repository_get_next_up_episodes,
233 repository_get_person,
234 repository_get_playback_info,
235 repository_get_recently_played_audio,
236 repository_get_rediscover_albums,
237 repository_get_resume_items,
238 repository_get_resume_movies,
239 repository_get_series_current_episode,
240 repository_get_series_episodes,
241 repository_get_series_view,
242 repository_get_similar_items,
243 repository_get_stream_selection,
244 repository_get_subtitle_url,
245 repository_get_video_download_url,
246 repository_get_video_stream_url,
247 repository_jray_actors_at,
248 repository_mark_favorite,
249 repository_open_live_stream,
250 repository_report_playback_progress,
251 repository_report_playback_start,
252 repository_report_playback_stopped,
253 repository_search,
254 repository_unmark_favorite,
255 resume_download,
256 resume_queued_downloads,
257 sessions_poll_now,
258 sessions_set_polling_hint,
260 set_max_concurrent_downloads,
261 set_network_state,
262 set_show_server_catalog,
263 start_download,
264 storage_delete_server,
266 storage_delete_user,
267 storage_get_access_token,
268 storage_get_active_session,
269 storage_get_active_user,
270 storage_get_item,
271 storage_get_item_people,
272 storage_get_items,
273 storage_get_libraries,
275 storage_get_path,
276 storage_get_pending_sync_count,
277 storage_get_person,
278 storage_get_playback_progress,
279 storage_get_security_status,
280 storage_get_series_audio_preference,
281 storage_get_servers,
282 storage_get_size,
283 storage_get_users,
284 storage_init,
285 storage_mark_played,
286 storage_mark_synced,
287 storage_save_item,
288 storage_save_item_people,
289 storage_save_library,
290 storage_save_person,
292 storage_save_series_audio_preference,
294 storage_save_server,
295 storage_save_user,
296 storage_search_items,
297 storage_set_active_user,
298 storage_set_watched,
299 storage_toggle_favorite,
300 storage_update_playback_context,
301 storage_update_playback_progress,
302 sync_cleanup_completed,
303 sync_clear_user,
304 sync_full_catalog,
305 sync_get_pending,
306 sync_get_pending_count,
307 sync_mark_completed,
308 sync_mark_failed,
309 sync_mark_processing,
310 sync_process_pending,
311 sync_queue_mutation,
313 thumbnail_clear_cache,
314 thumbnail_delete_item,
315 thumbnail_get_cached,
317 thumbnail_get_stats,
318 thumbnail_save,
319 thumbnail_set_limit,
320 unpin_item,
321 update_smart_cache_config,
322 AuthManagerWrapper,
323 ConnectivityMonitorWrapper,
324 CredentialStoreWrapper,
325 DatabaseWrapper,
326 DownloadManagerWrapper,
327 MediaSessionManagerWrapper,
328 PlaybackModeManagerWrapper,
329 PlaybackReporterWrapper,
330 PlayerStateWrapper,
331 RepositoryManagerWrapper,
332 SessionPollerWrapper,
333 SessionVerifierWrapper,
334 SmartCacheWrapper,
335 ThumbnailCacheWrapper,
336 VideoSettingsWrapper,
337};
338use connectivity::ConnectivityMonitor;
339use credentials::CredentialStore;
340use download::cache::{CacheConfig as SmartCacheConfig, SmartCache};
341use download::DownloadManager;
342use jellyfin::{HttpClient, HttpConfig};
343#[cfg(target_os = "android")]
344use playback_mode::PlaybackModeManager;
345#[cfg(target_os = "android")]
348use player::seek::{resolve_skip_action, SkipAction};
349use player::{MediaSessionManager, PlayerBackend, PlayerController, TauriEventEmitter};
350use player::NullBackend;
354
355#[cfg(any(target_os = "linux", target_os = "windows"))]
356use player::MpvBackend;
357use settings::VideoSettings;
358use storage::Database;
359use thumbnail::{CacheConfig as ThumbnailCacheConfig, ThumbnailCache};
360
361#[cfg(target_os = "android")]
362use credentials::initialize_secure_storage;
363
364#[cfg(target_os = "android")]
365use player::ExoPlayerBackend;
366
367#[cfg(target_os = "android")]
368use player::{
369 set_media_command_handler, set_remote_volume_handler, MediaCommandHandler, RemoteVolumeHandler,
370};
371
372#[cfg(target_os = "android")]
380struct MediaSessionHandler {
381 player: Arc<TokioMutex<PlayerController>>,
382 playback_mode: Arc<PlaybackModeManager>,
383 event_emitter: Arc<TauriEventEmitter>,
384}
385
386#[cfg(target_os = "android")]
387impl MediaSessionHandler {
388 fn handle_remote_command(&self, command: &str, session_id: String) {
393 use crate::player::{PlayerEventEmitter, PlayerStatusEvent};
394
395 if command == "stop" {
399 self.event_emitter
400 .emit(PlayerStatusEvent::RemoteDisconnectRequested);
401 return;
402 }
403
404 let jellyfin_client = {
405 let player = self.player.blocking_lock();
406 player.jellyfin_client()
407 };
408 let command = command.to_string();
409
410 tauri::async_runtime::spawn(async move {
411 let client = {
412 let guard = match jellyfin_client.lock() {
413 Ok(g) => g,
414 Err(e) => {
415 error!("[MediaSession] Failed to lock Jellyfin client: {}", e);
416 return;
417 }
418 };
419 match guard.as_ref() {
420 Some(c) => c.clone(),
421 None => {
422 warn!("[MediaSession] No Jellyfin client for remote command");
423 return;
424 }
425 }
426 };
427
428 let result = match command.as_str() {
430 "play" => client.send_session_command(session_id, "Unpause").await,
431 "pause" => client.send_session_command(session_id, "Pause").await,
432 "next" => client.send_session_command(session_id, "NextTrack").await,
433 "previous" => {
434 client
435 .send_session_command(session_id, "PreviousTrack")
436 .await
437 }
438 cmd if cmd.starts_with("seek:") => match cmd[5..].parse::<f64>() {
439 Ok(seconds) => {
440 let ticks = (seconds * 10_000_000.0) as i64;
441 client.session_seek(session_id, ticks).await
442 }
443 Err(_) => {
444 warn!("[MediaSession] Bad seek command: {}", command);
445 Ok(())
446 }
447 },
448 _ => {
449 warn!("[MediaSession] Unknown remote command: {}", command);
450 Ok(())
451 }
452 };
453
454 if let Err(e) = result {
455 error!("[MediaSession] Remote command '{}' failed: {}", command, e);
456 }
457 });
458 }
459
460 fn handle_local_command(&self, command: &str) {
462 if let Some(raw) = command.strip_prefix("seek:") {
469 match raw.parse::<f64>() {
470 Ok(position) => {
471 let player = self.player.clone();
472 tokio::spawn(async move {
473 let controller = player.lock().await;
474 if let Err(e) = controller.seek_absolute(position).await {
475 error!("[MediaSession] Seek to {:.1}s failed: {}", position, e);
476 }
477 });
478 }
479 Err(_) => warn!("[MediaSession] Bad seek command: {}", command),
480 }
481 return;
482 }
483
484 if command == "next" || command == "previous" {
494 let is_next = command == "next";
495 let player = self.player.clone();
496 tokio::spawn(async move {
497 let controller = player.lock().await;
498 let action = resolve_skip_action(
499 is_next,
500 controller.is_background_audio_active(),
501 controller.position(),
502 controller.duration(),
503 );
504 let label = if is_next { "next" } else { "previous" };
505 let result: Result<(), String> = match action {
506 SkipAction::Advance => if is_next {
507 controller.next()
508 } else {
509 controller.previous()
510 }
511 .map_err(|e| e.to_string()),
512 SkipAction::SeekTo(position) => {
513 info!(
514 "[MediaSession] Background audio: '{}' scrubs to {:.1}s",
515 label, position
516 );
517 controller.seek_absolute(position).await
518 }
519 };
520 if let Err(e) = result {
521 error!("[MediaSession] Skip '{}' failed: {}", label, e);
522 }
523 });
524 return;
525 }
526
527 let controller = self.player.blocking_lock();
529
530 let result = match command {
531 "play" => controller.play(),
532 "pause" => controller.pause(),
533 "stop" => controller.stop(),
534 _ => {
535 warn!("[MediaSession] Unknown command: {}", command);
536 Ok(())
537 }
538 };
539
540 if let Err(e) = result {
541 error!("[MediaSession] Command '{}' failed: {}", command, e);
542 }
543 }
544}
545
546#[cfg(target_os = "android")]
547impl MediaCommandHandler for MediaSessionHandler {
548 fn on_command(&self, command: &str) {
549 match self.playback_mode.get_mode() {
550 playback_mode::PlaybackMode::Remote { session_id } => {
551 self.handle_remote_command(command, session_id);
552 }
553 _ => self.handle_local_command(command),
554 }
555 }
556}
557
558#[cfg(target_os = "android")]
562struct RemoteVolumeSessionHandler {
563 playback_mode: Arc<PlaybackModeManager>,
564}
565
566#[cfg(target_os = "android")]
567impl RemoteVolumeHandler for RemoteVolumeSessionHandler {
568 fn on_remote_volume_change(&self, command: &str, volume: i32) {
569 log::info!("[RemoteVolume] Command: {}, Volume: {}", command, volume);
570
571 let playback_mode = Arc::clone(&self.playback_mode);
573 let command_str = command.to_string();
574
575 log::info!("[RemoteVolume] Spawning async task to send volume command...");
578 tauri::async_runtime::spawn(async move {
579 log::info!("[RemoteVolume] Async task started, calling send_remote_volume_command...");
580 match playback_mode
581 .send_remote_volume_command(&command_str, volume)
582 .await
583 {
584 Ok(_) => log::info!("[RemoteVolume] Volume command completed successfully"),
585 Err(e) => log::error!("[RemoteVolume] Failed to send volume command: {}", e),
586 }
587 log::info!("[RemoteVolume] Async task completed");
588 });
589 log::info!("[RemoteVolume] Async task spawned, returning from JNI callback");
590 }
591}
592
593#[derive(Clone, serde::Serialize)]
596struct BackendInitError {
597 platform: &'static str,
598 backend: &'static str,
599 message: String,
600}
601
602fn emit_backend_init_failed(app_handle: &tauri::AppHandle, backend: &'static str, message: String) {
605 error!(
606 "[INIT] Player backend '{}' failed to initialize: {}. Falling back to NullBackend (playback disabled).",
607 backend, message
608 );
609 let _ = app_handle.emit(
610 "backend-init-failed",
611 BackendInitError {
612 platform: std::env::consts::OS,
613 backend,
614 message,
615 },
616 );
617}
618
619#[cfg_attr(
623 not(any(target_os = "linux", target_os = "android")),
624 allow(unused_variables)
625)]
626fn create_player_backend(
627 app_handle: tauri::AppHandle,
628 playback_reporter: Arc<tokio::sync::Mutex<Option<playback_reporting::PlaybackReporter>>>,
629 position_throttler: Arc<playback_reporting::EventThrottler>,
630) -> Box<dyn PlayerBackend> {
631 let _event_emitter = Arc::new(TauriEventEmitter::new(app_handle.clone()));
632
633 #[cfg(target_os = "android")]
634 {
635 info!("Android platform detected - initializing ExoPlayer backend");
636
637 if let Err(e) = crate::android_context::ensure_initialized() {
642 log::error!("[INIT] Android context unavailable for the player: {e}");
643 }
644
645 let ctx = ndk_context::android_context();
647
648 let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) };
650
651 match vm {
652 Ok(java_vm) => {
653 match java_vm.attach_current_thread() {
654 Ok(mut env) => {
655 let context_obj =
656 unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) };
657
658 match ExoPlayerBackend::new(
659 &mut env,
660 &context_obj,
661 _event_emitter.clone(),
662 playback_reporter.clone(),
663 position_throttler.clone(),
664 ) {
665 Ok(backend) => {
666 info!("Successfully initialized ExoPlayer backend for Android");
667 return Box::new(backend);
668 }
669 Err(e) => {
670 emit_backend_init_failed(&app_handle, "exoplayer", e.to_string());
672 return Box::new(NullBackend::new());
673 }
674 }
675 }
676 Err(e) => {
677 emit_backend_init_failed(
678 &app_handle,
679 "exoplayer",
680 format!("attach JNI thread failed: {}", e),
681 );
682 return Box::new(NullBackend::new());
683 }
684 }
685 }
686 Err(e) => {
687 emit_backend_init_failed(
688 &app_handle,
689 "exoplayer",
690 format!("create JavaVM failed: {}", e),
691 );
692 return Box::new(NullBackend::new());
693 }
694 }
695 }
696
697 #[cfg(any(target_os = "linux", target_os = "windows"))]
700 {
701 info!("Initializing MPV backend");
702 #[cfg(target_os = "windows")]
706 let video_window = {
707 use tauri::Manager;
708 app_handle
709 .get_webview_window("main")
710 .and_then(|w| w.hwnd().ok())
711 .map(|hwnd| hwnd.0 as i64)
712 };
713 #[cfg(not(target_os = "windows"))]
714 let video_window = None;
715 match MpvBackend::new(
716 Some(_event_emitter),
717 playback_reporter,
718 position_throttler,
719 video_window,
720 ) {
721 Ok(backend) => {
722 info!("Successfully initialized MPV backend");
723 Box::new(backend)
724 }
725 #[cfg(target_os = "windows")]
726 Err(e) => {
727 error!("FATAL ERROR: Failed to initialize MPV backend: {}", e);
730 error!("libmpv-2.dll should sit beside jellytau.exe; reinstall JellyTau.");
731 emit_backend_init_failed(&app_handle, "mpv", e.to_string());
732 Box::new(NullBackend::new())
733 }
734 #[cfg(target_os = "linux")]
735 Err(e) => {
736 error!("\n========================================");
737 error!("FATAL ERROR: Failed to initialize MPV backend");
738 error!("========================================");
739 error!("Error: {}", e);
740 error!("\nCommon causes:");
741 error!(" 1. MPV is not installed");
742 error!(" Solution: Install MPV using your package manager");
743 error!(" - Arch/CachyOS: sudo pacman -S mpv");
744 error!(" - Ubuntu/Debian: sudo apt install mpv libmpv-dev");
745 error!(" - Fedora: sudo dnf install mpv mpv-libs-devel");
746 error!("\n 2. MPV version mismatch (app was built with different libmpv version)");
747 error!(" Solution: Rebuild the application");
748 error!(" - cd src-tauri && cargo clean && cargo build --release");
749 error!("\n 3. Audio system not working");
750 error!(" Solution: Verify audio works with: pactl info");
751 error!("\nAudio playback will NOT work until this is fixed.");
752 error!("========================================\n");
753
754 emit_backend_init_failed(&app_handle, "mpv", e.to_string());
758 Box::new(NullBackend::new())
759 }
760 }
761 }
762
763 #[cfg(not(any(target_os = "linux", target_os = "android", target_os = "windows")))]
767 {
768 info!("No native audio backend for this platform - using webview <audio> backend");
769 match player::WebviewAudioBackend::new(_event_emitter) {
770 Ok(backend) => Box::new(backend),
771 Err(e) => {
772 emit_backend_init_failed(&app_handle, "webview-audio", e.to_string());
773 Box::new(NullBackend::new())
774 }
775 }
776 }
777}
778
779fn engine_capabilities() -> crate::player::media_player::Capabilities {
792 #[cfg(target_os = "android")]
793 {
794 crate::player::media_player::Capabilities::exoplayer()
795 }
796 #[cfg(not(target_os = "android"))]
797 {
798 crate::player::media_player::Capabilities::mpv()
799 }
800}
801
802fn specta_builder() -> Builder<tauri::Wry> {
803 Builder::<tauri::Wry>::new()
804 .error_handling(tauri_specta::ErrorHandlingMode::Throw)
807 .events(tauri_specta::collect_events![
808 crate::player::events::PlayerStatusEvent
809 ])
810 .commands(tauri_specta::collect_commands![
811 player_play_item,
813 player_background_action,
814 player_enter_background_audio,
815 player_exit_background_audio,
816 player_play_queue,
817 player_play_album_track,
818 player_play_tracks,
819 player_play,
820 player_pause,
821 player_toggle,
822 player_stop,
823 player_next,
824 player_previous,
825 player_seek,
826 player_seek_video,
827 player_set_volume,
828 player_toggle_mute,
829 player_set_audio_track,
830 player_switch_audio_track,
831 player_set_subtitle_track,
832 player_toggle_shuffle,
833 player_cycle_repeat,
834 player_get_status,
835 player_get_queue,
836 player_get_capabilities,
837 player_add_to_queue,
838 player_add_track_by_id,
839 player_add_tracks_by_ids,
840 player_remove_from_queue,
841 player_move_in_queue,
842 player_skip_to,
843 player_set_audio_settings,
844 player_get_audio_settings,
845 player_get_eq_presets,
846 player_set_video_settings,
847 player_get_video_settings,
848 player_get_streaming_qualities,
849 player_set_stream_quality,
850 player_set_sleep_timer,
852 player_cancel_sleep_timer,
853 player_get_sleep_timer,
854 player_get_autoplay_settings,
855 player_set_autoplay_settings,
856 player_cancel_autoplay_countdown,
857 player_play_next_episode,
858 player_on_playback_ended,
859 player_recover_stream,
860 player_report_state,
861 player_report_position,
862 player_report_media_loaded,
863 player_local_media_path,
865 player_preload_upcoming,
866 player_set_cache_config,
867 player_get_cache_config,
868 player_configure_jellyfin,
870 player_disable_jellyfin,
871 player_get_session,
873 player_dismiss_session,
874 remote_play_on_session,
876 remote_send_command,
877 remote_session_seek,
878 remote_session_set_volume,
879 remote_session_toggle_mute,
880 lms_get_sync_groups,
882 lms_create_sync_group,
883 lms_unsync_player,
884 lms_dissolve_sync_group,
885 sessions_set_polling_hint,
887 sessions_poll_now,
888 playback_mode_get_current,
890 playback_mode_set,
891 playback_mode_is_transferring,
892 playback_mode_transfer_to_remote,
893 playback_mode_get_remote_status,
894 playback_mode_transfer_to_local,
895 playback_mode_set_transferring,
896 playback_reporter_init,
898 playback_reporter_destroy,
899 playback_report_start,
900 playback_report_progress,
901 playback_report_stopped,
902 playback_mark_played,
903 auth_initialize,
905 auth_connect_to_server,
906 auth_login,
907 auth_verify_session,
908 auth_logout,
909 auth_get_session,
910 auth_set_session,
911 auth_start_verification,
912 auth_stop_verification,
913 auth_reauthenticate,
914 device_get_id,
916 device_set_id,
917 connectivity_check_server,
919 connectivity_set_server_url,
920 connectivity_get_status,
921 connectivity_start_monitoring,
922 connectivity_stop_monitoring,
923 connectivity_mark_reachable,
924 connectivity_mark_unreachable,
925 storage_init,
927 storage_get_path,
928 storage_get_size,
929 storage_get_security_status,
930 storage_save_server,
931 storage_get_servers,
932 storage_delete_server,
933 storage_save_user,
934 storage_get_users,
935 storage_set_active_user,
936 storage_get_active_user,
937 storage_get_active_session,
938 storage_get_access_token,
939 storage_delete_user,
940 storage_update_playback_progress,
942 storage_update_playback_context,
943 storage_mark_played,
944 storage_set_watched,
945 storage_get_playback_progress,
946 storage_mark_synced,
947 storage_toggle_favorite,
948 download_item,
950 download_item_and_start,
951 download_album,
952 download_video,
953 download_series,
954 download_season,
955 get_downloads,
956 pause_download,
957 resume_download,
958 cancel_download,
959 delete_download,
960 delete_all_downloads,
961 delete_album_downloads,
962 delete_downloads_under,
963 clear_stale_downloads,
964 get_download_storage_stats,
965 mark_download_completed,
966 mark_download_failed,
967 media_local_url,
968 media_local_selection,
969 start_download,
970 enqueue_download,
971 enqueue_video_downloads,
972 sync_full_catalog,
973 catalog_sync_status,
974 set_show_server_catalog,
975 library_get_settings,
977 library_set_settings,
978 library_get_exclusion_candidates,
979 resume_queued_downloads,
980 get_download_manager_stats,
981 set_max_concurrent_downloads,
982 get_smart_cache_stats,
983 update_smart_cache_config,
984 get_smart_cache_config,
985 set_network_state,
987 get_downloads_allowed,
988 get_album_recommendations,
989 get_album_affinity_status,
990 pin_item,
992 unpin_item,
993 is_item_pinned,
994 offline_is_available,
996 offline_get_items,
997 offline_search,
998 storage_get_libraries,
1000 storage_get_items,
1001 storage_get_item,
1002 storage_search_items,
1003 storage_save_library,
1004 storage_save_item,
1005 storage_get_pending_sync_count,
1006 sync_queue_mutation,
1008 sync_get_pending,
1009 sync_mark_processing,
1010 sync_mark_completed,
1011 sync_mark_failed,
1012 sync_get_pending_count,
1013 sync_process_pending,
1014 sync_cleanup_completed,
1015 sync_clear_user,
1016 thumbnail_get_cached,
1018 thumbnail_save,
1019 thumbnail_get_stats,
1020 thumbnail_set_limit,
1021 thumbnail_clear_cache,
1022 thumbnail_delete_item,
1023 image_get_url,
1024 storage_save_person,
1026 storage_get_person,
1027 storage_save_item_people,
1028 storage_get_item_people,
1029 storage_save_series_audio_preference,
1031 storage_get_series_audio_preference,
1032 repository_create,
1034 repository_destroy,
1035 repository_get_libraries,
1036 repository_get_items,
1037 repository_get_item,
1038 repository_get_downloaded_libraries,
1039 repository_get_downloaded_items,
1040 repository_get_download_disk_usage,
1041 repository_jray_actors_at,
1042 repository_get_latest_items,
1043 repository_get_resume_items,
1044 repository_get_next_up_episodes,
1045 repository_get_series_episodes,
1046 repository_get_series_current_episode,
1047 repository_get_series_view,
1048 repository_clear_watch_history,
1049 repository_get_recently_played_audio,
1050 repository_get_resume_movies,
1051 repository_get_rediscover_albums,
1052 repository_get_genres,
1053 repository_search,
1054 repository_get_playback_info,
1055 repository_get_video_stream_url,
1056 repository_get_stream_selection,
1057 repository_get_audio_stream_url,
1058 repository_get_audio_only_stream_url_for_video,
1059 repository_get_live_tv_channels,
1060 repository_get_channels,
1061 repository_open_live_stream,
1062 repository_report_playback_start,
1063 repository_report_playback_progress,
1064 repository_report_playback_stopped,
1065 repository_get_image_url,
1066 repository_mark_favorite,
1067 repository_unmark_favorite,
1068 repository_get_favorites,
1069 repository_get_person,
1070 repository_get_items_by_person,
1071 repository_get_similar_items,
1072 repository_get_subtitle_url,
1073 repository_get_video_download_url,
1074 playlist_create,
1076 playlist_delete,
1077 playlist_rename,
1078 playlist_get_items,
1079 playlist_add_items,
1080 profiles_add,
1081 profiles_get_ask_on_start,
1082 profiles_list,
1083 profiles_remove,
1084 profiles_set_ask_on_start,
1085 profiles_set_pin,
1086 profiles_startup_target,
1087 profiles_unlock,
1088 profiles_unlock_with_password,
1089 playlist_remove_items,
1090 playlist_move_item,
1091 diagnostics_get_info,
1094 diagnostics_set_level,
1095 diagnostics_export,
1096 format_time_seconds,
1098 format_time_seconds_long,
1099 convert_ticks_to_seconds,
1100 calc_progress,
1101 convert_percent_to_volume,
1102 ])
1103}
1104
1105fn build_log_plugin() -> tauri::plugin::TauriPlugin<tauri::Wry> {
1144 use tauri_plugin_log::{Target, TargetKind};
1145
1146 let mut targets = vec![
1147 Target::new(TargetKind::Stdout),
1148 Target::new(TargetKind::LogDir {
1149 file_name: Some("jellytau".to_string()),
1150 }),
1151 ];
1152
1153 if cfg!(debug_assertions) {
1157 targets.push(Target::new(TargetKind::Webview));
1158 }
1159
1160 tauri_plugin_log::Builder::new()
1161 .targets(targets)
1162 .level(log::LevelFilter::Info)
1163 .max_file_size(5 * 1024 * 1024)
1164 .rotation_strategy(tauri_plugin_log::RotationStrategy::KeepOne)
1165 .format(|out, message, record| {
1166 out.finish(format_args!(
1167 "[{}][{}] {}",
1168 record.level(),
1169 record.target(),
1170 crate::utils::diagnostics::redact(&message.to_string())
1171 ))
1172 })
1173 .build()
1174}
1175
1176#[cfg_attr(mobile, tauri::mobile_entry_point)]
1177pub fn run() {
1178 crate::utils::diagnostics::install_panic_hook();
1183
1184 let builder = specta_builder();
1189 let invoke_handler = builder.invoke_handler();
1190
1191 tauri::Builder::default()
1192 .plugin(build_log_plugin())
1193 .plugin(tauri_plugin_opener::init())
1194 .plugin(tauri_plugin_os::init())
1195 .invoke_handler(invoke_handler)
1196 .setup(move |app| {
1197 builder.mount_events(app);
1200
1201
1202 if let Ok(config_dir) = app.path().app_config_dir() {
1219 if let Some(level) = crate::commands::diagnostics::stored_level(&config_dir) {
1220 log::set_max_level(level);
1221 log::info!("[DIAG] restored log level from settings: {level}");
1222 }
1223 }
1224
1225 #[cfg(desktop)]
1227 {
1228 app.handle()
1229 .plugin(tauri_plugin_updater::Builder::new().build())?;
1230 app.handle().plugin(tauri_plugin_process::init())?;
1231 }
1232
1233 let db_path = if let Ok(test_data_dir) = std::env::var("JELLYTAU_DATA_DIR") {
1236 let test_path = std::path::PathBuf::from(test_data_dir);
1237 info!("[INIT] Using test data directory: {:?}", test_path);
1238 test_path.join("jellytau.db")
1239 } else {
1240 app
1241 .path()
1242 .app_data_dir()
1243 .expect("Failed to get app data directory")
1244 .join("jellytau.db")
1245 };
1246
1247 info!("[INIT] Initializing database at: {:?}", db_path);
1248
1249 if let Some(parent) = db_path.parent() {
1251 info!("[INIT] Creating database directory: {:?}", parent);
1252 match std::fs::create_dir_all(parent) {
1253 Ok(_) => info!("[INIT] Database directory ready"),
1254 Err(e) => {
1255 error!("[INIT ERROR] Failed to create database directory: {}", e);
1256 panic!("Failed to create database directory: {}", e);
1257 }
1258 }
1259 }
1260
1261 info!("[INIT] Opening database...");
1262 let database = match Database::open(&db_path) {
1263 Ok(db) => {
1264 info!("[INIT] Database initialized successfully");
1265 db
1266 }
1267 Err(e) => {
1268 error!("[INIT ERROR] Failed to initialize database: {}", e);
1269 panic!("Failed to initialize database: {}", e);
1270 }
1271 };
1272 let db_wrapper = DatabaseWrapper(Mutex::new(database));
1273 app.manage(db_wrapper);
1274
1275 #[cfg(target_os = "android")]
1277 {
1278 if let Err(e) = crate::android_context::ensure_initialized() {
1289 log::error!("[INIT] Android context unavailable: {e}");
1290 log::error!("[INIT] Secure credential storage will fall back to the encrypted file.");
1291 }
1292
1293 info!("[INIT] Initializing Android SecureStorage for credentials...");
1294 let ctx = ndk_context::android_context();
1295 let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) };
1296
1297 match vm {
1298 Ok(java_vm) => {
1299 match java_vm.attach_current_thread() {
1300 Ok(mut env) => {
1301 let context_obj = unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) };
1302
1303 match initialize_secure_storage(&mut env, &context_obj) {
1304 Ok(()) => {
1305 info!("[INIT] Android SecureStorage initialized successfully");
1306 }
1307 Err(e) => {
1308 warn!("[INIT WARNING] Failed to initialize SecureStorage: {}. Credentials will use encrypted file fallback.", e);
1309 }
1310 }
1311 }
1312 Err(e) => {
1313 warn!("[INIT WARNING] Failed to attach JNI thread: {}. Credentials will use encrypted file fallback.", e);
1314 }
1315 }
1316 }
1317 Err(e) => {
1318 warn!("[INIT WARNING] Failed to create JavaVM: {}. Credentials will use encrypted file fallback.", e);
1319 }
1320 }
1321 }
1322
1323 info!("[INIT] Initializing credential store...");
1325 let credential_store = CredentialStore::new();
1326 let creds_wrapper = CredentialStoreWrapper(Mutex::new(credential_store));
1327 app.manage(creds_wrapper);
1328
1329 info!("[INIT] Creating shared playback reporting infrastructure...");
1331 let playback_reporter = Arc::new(tokio::sync::Mutex::new(None));
1332 let position_throttler = Arc::new(playback_reporting::EventThrottler::new());
1333
1334 info!("[INIT] Creating player backend...");
1336 let backend = create_player_backend(
1337 app.handle().clone(),
1338 playback_reporter.clone(),
1339 position_throttler.clone(),
1340 );
1341 #[cfg(target_os = "linux")]
1357 if crate::player::native_video::enabled() {
1358 use tauri::Manager;
1359 if let Some(window) = app.get_webview_window("main") {
1360 match window.default_vbox() {
1361 Ok(vbox) => {
1362 let handle = crate::player::mpv_backend::registered_handle();
1363 if crate::player::video_surface::attach(&vbox, handle) {
1364 info!("[INIT] Native video surface attached");
1365 } else {
1366 log::warn!("[INIT] Native video surface unavailable");
1367 }
1368 }
1369 Err(e) => {
1370 log::warn!("[INIT] No GTK vbox for the main window: {e}")
1371 }
1372 }
1373 }
1374 }
1375
1376 let player_controller = PlayerController::new(
1381 Box::new(crate::player::LegacyPlayer::new(
1382 backend,
1383 engine_capabilities(),
1384 )),
1385 playback_reporter.clone(),
1386 position_throttler.clone(),
1387 );
1388
1389 let event_emitter = Arc::new(TauriEventEmitter::new(app.handle().clone()));
1391 player_controller.set_event_emitter(event_emitter.clone());
1392
1393 let player_arc = Arc::new(TokioMutex::new(player_controller));
1394
1395 #[cfg(target_os = "android")]
1399 {
1400 player::android::set_player_controller(player_arc.clone());
1401 }
1402
1403 let player_state = PlayerStateWrapper(player_arc.clone());
1404 app.manage(player_state);
1405
1406 info!("[INIT] Initializing media session manager...");
1408 let session_manager = MediaSessionManager::new();
1409 let session_wrapper = MediaSessionManagerWrapper(Mutex::new(session_manager));
1410 app.manage(session_wrapper);
1411
1412 info!("[INIT] Initializing playback mode manager...");
1414 let jellyfin_client = {
1415 let player = player_arc.blocking_lock();
1416 player.jellyfin_client()
1417 };
1418 let playback_mode_manager = playback_mode::PlaybackModeManager::new(
1419 jellyfin_client.clone(),
1420 player_arc.clone(),
1421 );
1422 let playback_mode_arc = Arc::new(playback_mode_manager);
1423 playback_mode_arc.set_event_emitter(event_emitter.clone());
1426 let playback_mode_wrapper = PlaybackModeManagerWrapper(playback_mode_arc.clone());
1427 app.manage(playback_mode_wrapper);
1428
1429 info!("[INIT] Initializing session poller manager...");
1431 let session_poller = session_poller::SessionPollerManager::new(
1432 jellyfin_client,
1433 playback_mode_arc.clone(),
1434 );
1435 session_poller.set_event_emitter(event_emitter.clone());
1436 let session_poller_arc = Arc::new(session_poller);
1440 let session_poller_wrapper = SessionPollerWrapper(session_poller_arc.clone());
1441 app.manage(session_poller_wrapper);
1442
1443 #[cfg(target_os = "android")]
1447 {
1448 info!("[INIT] Setting up MediaSession handler for lockscreen controls...");
1449 let media_handler = Arc::new(MediaSessionHandler {
1450 player: player_arc.clone(),
1451 playback_mode: playback_mode_arc.clone(),
1452 event_emitter: event_emitter.clone(),
1453 });
1454 set_media_command_handler(media_handler);
1455
1456 info!("[INIT] Setting up remote volume handler for Android...");
1457 let handler = Arc::new(RemoteVolumeSessionHandler {
1458 playback_mode: playback_mode_arc.clone(),
1459 });
1460 set_remote_volume_handler(handler);
1461 }
1462
1463 let video_settings = VideoSettingsWrapper(Mutex::new(VideoSettings::default()));
1465 app.manage(video_settings);
1466
1467 {
1475 let handle = app.handle().clone();
1476 tauri::async_runtime::spawn(async move {
1477 crate::commands::restore_streaming_quality(&handle).await;
1478 });
1479 }
1480
1481 {
1487 let handle = app.handle().clone();
1488 tauri::async_runtime::spawn(async move {
1489 crate::commands::restore_library_settings(&handle).await;
1490 });
1491 }
1492
1493 info!("[INIT] Initializing thumbnail cache...");
1495 let app_data_dir = if let Ok(test_data_dir) = std::env::var("JELLYTAU_DATA_DIR") {
1496 std::path::PathBuf::from(test_data_dir)
1497 } else {
1498 app
1499 .path()
1500 .app_data_dir()
1501 .expect("Failed to get app data directory")
1502 };
1503 let thumbnail_cache = ThumbnailCache::new(app_data_dir.clone(), ThumbnailCacheConfig::default());
1504 let thumbnail_wrapper = ThumbnailCacheWrapper(Arc::new(thumbnail_cache));
1505 app.manage(thumbnail_wrapper);
1506
1507 info!("[INIT] Initializing smart cache...");
1509 let smart_cache = SmartCache::new(SmartCacheConfig::default());
1510 let smart_cache_wrapper = SmartCacheWrapper(Mutex::new(smart_cache));
1511 app.manage(smart_cache_wrapper);
1512
1513 info!("[INIT] Starting local media server...");
1518 let media_server = match media_server::start(app_data_dir.clone()) {
1519 Ok(s) => Some(s),
1520 Err(e) => {
1521 error!("[INIT ERROR] Local media server failed to start: {}", e);
1524 None
1525 }
1526 };
1527 app.manage(media_server::MediaServerWrapper(media_server));
1528
1529 info!("[INIT] Initializing download manager...");
1531 let download_dir = app_data_dir.join("downloads");
1532 let download_manager = DownloadManager::new(download_dir);
1533 let download_manager_wrapper = DownloadManagerWrapper(Mutex::new(download_manager));
1534 app.manage(download_manager_wrapper);
1535
1536 app.manage(commands::download::NetworkStateWrapper(
1540 download::network::NetworkStateHandle::new(),
1541 ));
1542
1543 info!("[INIT] Initializing connectivity monitor...");
1545 let http_config = HttpConfig::default();
1546 let http_client = HttpClient::new(http_config)
1547 .expect("Failed to create HTTP client");
1548 let mut connectivity_monitor = ConnectivityMonitor::new(http_client);
1549 connectivity_monitor.set_app_handle(app.handle().clone());
1550
1551 session_poller_arc.set_connectivity_reporter(connectivity_monitor.reporter());
1555 session_poller_arc.start();
1556
1557 let connectivity_arc = Arc::new(tokio::sync::Mutex::new(connectivity_monitor));
1559 let connectivity_wrapper = ConnectivityMonitorWrapper(connectivity_arc.clone());
1560 app.manage(connectivity_wrapper);
1561
1562 info!("[INIT] Initializing auth manager...");
1564 let auth_http_config = HttpConfig::default();
1565 let auth_http_client = HttpClient::new(auth_http_config)
1566 .expect("Failed to create HTTP client for auth");
1567 let mut auth_manager = AuthManager::new(auth_http_client);
1568
1569 auth_manager.set_connectivity_monitor(connectivity_arc.clone());
1571
1572 let auth_manager_wrapper = AuthManagerWrapper(Arc::new(auth_manager));
1573 app.manage(auth_manager_wrapper);
1574
1575 info!("[INIT] Initializing session verifier wrapper...");
1577 let session_verifier_wrapper = SessionVerifierWrapper(Arc::new(tokio::sync::Mutex::new(None)));
1578 app.manage(session_verifier_wrapper);
1579
1580 info!("[INIT] Initializing repository manager...");
1582 let repository_manager = commands::RepositoryManager::new();
1583 let repository_manager_wrapper = RepositoryManagerWrapper(repository_manager);
1584 app.manage(repository_manager_wrapper);
1585
1586 info!("[INIT] Initializing playback reporter wrapper...");
1592 let playback_reporter_wrapper = PlaybackReporterWrapper(playback_reporter.clone());
1593 app.manage(playback_reporter_wrapper);
1594
1595 info!("[INIT] Starting background catalog indexer...");
1601 commands::catalog::spawn_catalog_indexer(app.handle().clone());
1602
1603 info!("[INIT] Starting favourites drain...");
1608 commands::favorites::spawn_favorites_drain(app.handle().clone());
1609
1610 info!("[INIT] Starting sync-queue drain...");
1615 commands::sync_drain::spawn_sync_queue_drain(app.handle().clone());
1616
1617 info!("[INIT] Application setup completed successfully");
1618 Ok(())
1619 })
1620 .run(tauri::generate_context!())
1621 .expect("error while running tauri application");
1622}
1623
1624#[cfg(test)]
1625mod specta_bindings {
1626 #[test]
1628 fn export_typescript_bindings() {
1629 super::specta_builder()
1630 .export(
1631 specta_typescript::Typescript::default()
1632 .bigint(specta_typescript::BigIntExportBehavior::Number),
1633 "../src/lib/api/bindings.ts",
1634 )
1635 .expect("failed to export typescript bindings");
1636 }
1637}