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_similar_items,
242 repository_get_stream_selection,
243 repository_get_subtitle_url,
244 repository_get_video_download_url,
245 repository_get_video_stream_url,
246 repository_jray_actors_at,
247 repository_mark_favorite,
248 repository_open_live_stream,
249 repository_report_playback_progress,
250 repository_report_playback_start,
251 repository_report_playback_stopped,
252 repository_search,
253 repository_unmark_favorite,
254 resume_download,
255 resume_queued_downloads,
256 sessions_poll_now,
257 sessions_set_polling_hint,
259 set_max_concurrent_downloads,
260 set_network_state,
261 set_show_server_catalog,
262 start_download,
263 storage_delete_server,
265 storage_delete_user,
266 storage_get_access_token,
267 storage_get_active_session,
268 storage_get_active_user,
269 storage_get_item,
270 storage_get_item_people,
271 storage_get_items,
272 storage_get_libraries,
274 storage_get_path,
275 storage_get_pending_sync_count,
276 storage_get_person,
277 storage_get_playback_progress,
278 storage_get_security_status,
279 storage_get_series_audio_preference,
280 storage_get_servers,
281 storage_get_size,
282 storage_get_users,
283 storage_init,
284 storage_mark_played,
285 storage_mark_synced,
286 storage_save_item,
287 storage_save_item_people,
288 storage_save_library,
289 storage_save_person,
291 storage_save_series_audio_preference,
293 storage_save_server,
294 storage_save_user,
295 storage_search_items,
296 storage_set_active_user,
297 storage_set_watched,
298 storage_toggle_favorite,
299 storage_update_playback_context,
300 storage_update_playback_progress,
301 sync_cleanup_completed,
302 sync_clear_user,
303 sync_full_catalog,
304 sync_get_pending,
305 sync_get_pending_count,
306 sync_mark_completed,
307 sync_mark_failed,
308 sync_mark_processing,
309 sync_process_pending,
310 sync_queue_mutation,
312 thumbnail_clear_cache,
313 thumbnail_delete_item,
314 thumbnail_get_cached,
316 thumbnail_get_stats,
317 thumbnail_save,
318 thumbnail_set_limit,
319 unpin_item,
320 update_smart_cache_config,
321 AuthManagerWrapper,
322 ConnectivityMonitorWrapper,
323 CredentialStoreWrapper,
324 DatabaseWrapper,
325 DownloadManagerWrapper,
326 MediaSessionManagerWrapper,
327 PlaybackModeManagerWrapper,
328 PlaybackReporterWrapper,
329 PlayerStateWrapper,
330 RepositoryManagerWrapper,
331 SessionPollerWrapper,
332 SessionVerifierWrapper,
333 SmartCacheWrapper,
334 ThumbnailCacheWrapper,
335 VideoSettingsWrapper,
336};
337use connectivity::ConnectivityMonitor;
338use credentials::CredentialStore;
339use download::cache::{CacheConfig as SmartCacheConfig, SmartCache};
340use download::DownloadManager;
341use jellyfin::{HttpClient, HttpConfig};
342#[cfg(target_os = "android")]
343use playback_mode::PlaybackModeManager;
344#[cfg(target_os = "android")]
347use player::seek::{resolve_skip_action, SkipAction};
348use player::{MediaSessionManager, PlayerBackend, PlayerController, TauriEventEmitter};
349use player::NullBackend;
353
354#[cfg(target_os = "linux")]
355use player::MpvBackend;
356use settings::VideoSettings;
357use storage::Database;
358use thumbnail::{CacheConfig as ThumbnailCacheConfig, ThumbnailCache};
359
360#[cfg(target_os = "android")]
361use credentials::initialize_secure_storage;
362
363#[cfg(target_os = "android")]
364use player::ExoPlayerBackend;
365
366#[cfg(target_os = "android")]
367use player::{
368 set_media_command_handler, set_remote_volume_handler, MediaCommandHandler, RemoteVolumeHandler,
369};
370
371#[cfg(target_os = "android")]
379struct MediaSessionHandler {
380 player: Arc<TokioMutex<PlayerController>>,
381 playback_mode: Arc<PlaybackModeManager>,
382 event_emitter: Arc<TauriEventEmitter>,
383}
384
385#[cfg(target_os = "android")]
386impl MediaSessionHandler {
387 fn handle_remote_command(&self, command: &str, session_id: String) {
392 use crate::player::{PlayerEventEmitter, PlayerStatusEvent};
393
394 if command == "stop" {
398 self.event_emitter
399 .emit(PlayerStatusEvent::RemoteDisconnectRequested);
400 return;
401 }
402
403 let jellyfin_client = {
404 let player = self.player.blocking_lock();
405 player.jellyfin_client()
406 };
407 let command = command.to_string();
408
409 tauri::async_runtime::spawn(async move {
410 let client = {
411 let guard = match jellyfin_client.lock() {
412 Ok(g) => g,
413 Err(e) => {
414 error!("[MediaSession] Failed to lock Jellyfin client: {}", e);
415 return;
416 }
417 };
418 match guard.as_ref() {
419 Some(c) => c.clone(),
420 None => {
421 warn!("[MediaSession] No Jellyfin client for remote command");
422 return;
423 }
424 }
425 };
426
427 let result = match command.as_str() {
429 "play" => client.send_session_command(session_id, "Unpause").await,
430 "pause" => client.send_session_command(session_id, "Pause").await,
431 "next" => client.send_session_command(session_id, "NextTrack").await,
432 "previous" => {
433 client
434 .send_session_command(session_id, "PreviousTrack")
435 .await
436 }
437 cmd if cmd.starts_with("seek:") => match cmd[5..].parse::<f64>() {
438 Ok(seconds) => {
439 let ticks = (seconds * 10_000_000.0) as i64;
440 client.session_seek(session_id, ticks).await
441 }
442 Err(_) => {
443 warn!("[MediaSession] Bad seek command: {}", command);
444 Ok(())
445 }
446 },
447 _ => {
448 warn!("[MediaSession] Unknown remote command: {}", command);
449 Ok(())
450 }
451 };
452
453 if let Err(e) = result {
454 error!("[MediaSession] Remote command '{}' failed: {}", command, e);
455 }
456 });
457 }
458
459 fn handle_local_command(&self, command: &str) {
461 if let Some(raw) = command.strip_prefix("seek:") {
468 match raw.parse::<f64>() {
469 Ok(position) => {
470 let player = self.player.clone();
471 tokio::spawn(async move {
472 let controller = player.lock().await;
473 if let Err(e) = controller.seek_absolute(position).await {
474 error!("[MediaSession] Seek to {:.1}s failed: {}", position, e);
475 }
476 });
477 }
478 Err(_) => warn!("[MediaSession] Bad seek command: {}", command),
479 }
480 return;
481 }
482
483 if command == "next" || command == "previous" {
493 let is_next = command == "next";
494 let player = self.player.clone();
495 tokio::spawn(async move {
496 let controller = player.lock().await;
497 let action = resolve_skip_action(
498 is_next,
499 controller.is_background_audio_active(),
500 controller.position(),
501 controller.duration(),
502 );
503 let label = if is_next { "next" } else { "previous" };
504 let result: Result<(), String> = match action {
505 SkipAction::Advance => if is_next {
506 controller.next()
507 } else {
508 controller.previous()
509 }
510 .map_err(|e| e.to_string()),
511 SkipAction::SeekTo(position) => {
512 info!(
513 "[MediaSession] Background audio: '{}' scrubs to {:.1}s",
514 label, position
515 );
516 controller.seek_absolute(position).await
517 }
518 };
519 if let Err(e) = result {
520 error!("[MediaSession] Skip '{}' failed: {}", label, e);
521 }
522 });
523 return;
524 }
525
526 let controller = self.player.blocking_lock();
528
529 let result = match command {
530 "play" => controller.play(),
531 "pause" => controller.pause(),
532 "stop" => controller.stop(),
533 _ => {
534 warn!("[MediaSession] Unknown command: {}", command);
535 Ok(())
536 }
537 };
538
539 if let Err(e) = result {
540 error!("[MediaSession] Command '{}' failed: {}", command, e);
541 }
542 }
543}
544
545#[cfg(target_os = "android")]
546impl MediaCommandHandler for MediaSessionHandler {
547 fn on_command(&self, command: &str) {
548 match self.playback_mode.get_mode() {
549 playback_mode::PlaybackMode::Remote { session_id } => {
550 self.handle_remote_command(command, session_id);
551 }
552 _ => self.handle_local_command(command),
553 }
554 }
555}
556
557#[cfg(target_os = "android")]
561struct RemoteVolumeSessionHandler {
562 playback_mode: Arc<PlaybackModeManager>,
563}
564
565#[cfg(target_os = "android")]
566impl RemoteVolumeHandler for RemoteVolumeSessionHandler {
567 fn on_remote_volume_change(&self, command: &str, volume: i32) {
568 log::info!("[RemoteVolume] Command: {}, Volume: {}", command, volume);
569
570 let playback_mode = Arc::clone(&self.playback_mode);
572 let command_str = command.to_string();
573
574 log::info!("[RemoteVolume] Spawning async task to send volume command...");
577 tauri::async_runtime::spawn(async move {
578 log::info!("[RemoteVolume] Async task started, calling send_remote_volume_command...");
579 match playback_mode
580 .send_remote_volume_command(&command_str, volume)
581 .await
582 {
583 Ok(_) => log::info!("[RemoteVolume] Volume command completed successfully"),
584 Err(e) => log::error!("[RemoteVolume] Failed to send volume command: {}", e),
585 }
586 log::info!("[RemoteVolume] Async task completed");
587 });
588 log::info!("[RemoteVolume] Async task spawned, returning from JNI callback");
589 }
590}
591
592#[derive(Clone, serde::Serialize)]
595struct BackendInitError {
596 platform: &'static str,
597 backend: &'static str,
598 message: String,
599}
600
601fn emit_backend_init_failed(app_handle: &tauri::AppHandle, backend: &'static str, message: String) {
604 error!(
605 "[INIT] Player backend '{}' failed to initialize: {}. Falling back to NullBackend (playback disabled).",
606 backend, message
607 );
608 let _ = app_handle.emit(
609 "backend-init-failed",
610 BackendInitError {
611 platform: std::env::consts::OS,
612 backend,
613 message,
614 },
615 );
616}
617
618#[cfg_attr(
622 not(any(target_os = "linux", target_os = "android")),
623 allow(unused_variables)
624)]
625fn create_player_backend(
626 app_handle: tauri::AppHandle,
627 playback_reporter: Arc<tokio::sync::Mutex<Option<playback_reporting::PlaybackReporter>>>,
628 position_throttler: Arc<playback_reporting::EventThrottler>,
629) -> Box<dyn PlayerBackend> {
630 let _event_emitter = Arc::new(TauriEventEmitter::new(app_handle.clone()));
631
632 #[cfg(target_os = "android")]
633 {
634 info!("Android platform detected - initializing ExoPlayer backend");
635
636 if let Err(e) = crate::android_context::ensure_initialized() {
641 log::error!("[INIT] Android context unavailable for the player: {e}");
642 }
643
644 let ctx = ndk_context::android_context();
646
647 let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) };
649
650 match vm {
651 Ok(java_vm) => {
652 match java_vm.attach_current_thread() {
653 Ok(mut env) => {
654 let context_obj =
655 unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) };
656
657 match ExoPlayerBackend::new(
658 &mut env,
659 &context_obj,
660 _event_emitter.clone(),
661 playback_reporter.clone(),
662 position_throttler.clone(),
663 ) {
664 Ok(backend) => {
665 info!("Successfully initialized ExoPlayer backend for Android");
666 return Box::new(backend);
667 }
668 Err(e) => {
669 emit_backend_init_failed(&app_handle, "exoplayer", e.to_string());
671 return Box::new(NullBackend::new());
672 }
673 }
674 }
675 Err(e) => {
676 emit_backend_init_failed(
677 &app_handle,
678 "exoplayer",
679 format!("attach JNI thread failed: {}", e),
680 );
681 return Box::new(NullBackend::new());
682 }
683 }
684 }
685 Err(e) => {
686 emit_backend_init_failed(
687 &app_handle,
688 "exoplayer",
689 format!("create JavaVM failed: {}", e),
690 );
691 return Box::new(NullBackend::new());
692 }
693 }
694 }
695
696 #[cfg(target_os = "linux")]
698 {
699 info!("Linux platform detected - initializing MPV backend for audio");
700 match MpvBackend::new(Some(_event_emitter), playback_reporter, position_throttler) {
701 Ok(backend) => {
702 info!("Successfully initialized MPV backend for Linux");
703 Box::new(backend)
704 }
705 Err(e) => {
706 error!("\n========================================");
707 error!("FATAL ERROR: Failed to initialize MPV backend");
708 error!("========================================");
709 error!("Error: {}", e);
710 error!("\nCommon causes:");
711 error!(" 1. MPV is not installed");
712 error!(" Solution: Install MPV using your package manager");
713 error!(" - Arch/CachyOS: sudo pacman -S mpv");
714 error!(" - Ubuntu/Debian: sudo apt install mpv libmpv-dev");
715 error!(" - Fedora: sudo dnf install mpv mpv-libs-devel");
716 error!("\n 2. MPV version mismatch (app was built with different libmpv version)");
717 error!(" Solution: Rebuild the application");
718 error!(" - cd src-tauri && cargo clean && cargo build --release");
719 error!("\n 3. Audio system not working");
720 error!(" Solution: Verify audio works with: pactl info");
721 error!("\nAudio playback will NOT work until this is fixed.");
722 error!("========================================\n");
723
724 emit_backend_init_failed(&app_handle, "mpv", e.to_string());
728 Box::new(NullBackend::new())
729 }
730 }
731 }
732
733 #[cfg(not(any(target_os = "linux", target_os = "android")))]
737 {
738 info!("No native audio backend for this platform - using webview <audio> backend");
739 match player::WebviewAudioBackend::new(_event_emitter) {
740 Ok(backend) => Box::new(backend),
741 Err(e) => {
742 emit_backend_init_failed(&app_handle, "webview-audio", e.to_string());
743 Box::new(NullBackend::new())
744 }
745 }
746 }
747}
748
749fn engine_capabilities() -> crate::player::media_player::Capabilities {
762 #[cfg(target_os = "android")]
763 {
764 crate::player::media_player::Capabilities::exoplayer()
765 }
766 #[cfg(not(target_os = "android"))]
767 {
768 crate::player::media_player::Capabilities::mpv()
769 }
770}
771
772fn specta_builder() -> Builder<tauri::Wry> {
773 Builder::<tauri::Wry>::new()
774 .error_handling(tauri_specta::ErrorHandlingMode::Throw)
777 .events(tauri_specta::collect_events![
778 crate::player::events::PlayerStatusEvent
779 ])
780 .commands(tauri_specta::collect_commands![
781 player_play_item,
783 player_background_action,
784 player_enter_background_audio,
785 player_exit_background_audio,
786 player_play_queue,
787 player_play_album_track,
788 player_play_tracks,
789 player_play,
790 player_pause,
791 player_toggle,
792 player_stop,
793 player_next,
794 player_previous,
795 player_seek,
796 player_seek_video,
797 player_set_volume,
798 player_toggle_mute,
799 player_set_audio_track,
800 player_switch_audio_track,
801 player_set_subtitle_track,
802 player_toggle_shuffle,
803 player_cycle_repeat,
804 player_get_status,
805 player_get_queue,
806 player_get_capabilities,
807 player_add_to_queue,
808 player_add_track_by_id,
809 player_add_tracks_by_ids,
810 player_remove_from_queue,
811 player_move_in_queue,
812 player_skip_to,
813 player_set_audio_settings,
814 player_get_audio_settings,
815 player_get_eq_presets,
816 player_set_video_settings,
817 player_get_video_settings,
818 player_get_streaming_qualities,
819 player_set_stream_quality,
820 player_set_sleep_timer,
822 player_cancel_sleep_timer,
823 player_get_sleep_timer,
824 player_get_autoplay_settings,
825 player_set_autoplay_settings,
826 player_cancel_autoplay_countdown,
827 player_play_next_episode,
828 player_on_playback_ended,
829 player_recover_stream,
830 player_report_state,
831 player_report_position,
832 player_report_media_loaded,
833 player_local_media_path,
835 player_preload_upcoming,
836 player_set_cache_config,
837 player_get_cache_config,
838 player_configure_jellyfin,
840 player_disable_jellyfin,
841 player_get_session,
843 player_dismiss_session,
844 remote_play_on_session,
846 remote_send_command,
847 remote_session_seek,
848 remote_session_set_volume,
849 remote_session_toggle_mute,
850 lms_get_sync_groups,
852 lms_create_sync_group,
853 lms_unsync_player,
854 lms_dissolve_sync_group,
855 sessions_set_polling_hint,
857 sessions_poll_now,
858 playback_mode_get_current,
860 playback_mode_set,
861 playback_mode_is_transferring,
862 playback_mode_transfer_to_remote,
863 playback_mode_get_remote_status,
864 playback_mode_transfer_to_local,
865 playback_mode_set_transferring,
866 playback_reporter_init,
868 playback_reporter_destroy,
869 playback_report_start,
870 playback_report_progress,
871 playback_report_stopped,
872 playback_mark_played,
873 auth_initialize,
875 auth_connect_to_server,
876 auth_login,
877 auth_verify_session,
878 auth_logout,
879 auth_get_session,
880 auth_set_session,
881 auth_start_verification,
882 auth_stop_verification,
883 auth_reauthenticate,
884 device_get_id,
886 device_set_id,
887 connectivity_check_server,
889 connectivity_set_server_url,
890 connectivity_get_status,
891 connectivity_start_monitoring,
892 connectivity_stop_monitoring,
893 connectivity_mark_reachable,
894 connectivity_mark_unreachable,
895 storage_init,
897 storage_get_path,
898 storage_get_size,
899 storage_get_security_status,
900 storage_save_server,
901 storage_get_servers,
902 storage_delete_server,
903 storage_save_user,
904 storage_get_users,
905 storage_set_active_user,
906 storage_get_active_user,
907 storage_get_active_session,
908 storage_get_access_token,
909 storage_delete_user,
910 storage_update_playback_progress,
912 storage_update_playback_context,
913 storage_mark_played,
914 storage_set_watched,
915 storage_get_playback_progress,
916 storage_mark_synced,
917 storage_toggle_favorite,
918 download_item,
920 download_item_and_start,
921 download_album,
922 download_video,
923 download_series,
924 download_season,
925 get_downloads,
926 pause_download,
927 resume_download,
928 cancel_download,
929 delete_download,
930 delete_all_downloads,
931 delete_album_downloads,
932 delete_downloads_under,
933 clear_stale_downloads,
934 get_download_storage_stats,
935 mark_download_completed,
936 mark_download_failed,
937 media_local_url,
938 media_local_selection,
939 start_download,
940 enqueue_download,
941 enqueue_video_downloads,
942 sync_full_catalog,
943 catalog_sync_status,
944 set_show_server_catalog,
945 library_get_settings,
947 library_set_settings,
948 library_get_exclusion_candidates,
949 resume_queued_downloads,
950 get_download_manager_stats,
951 set_max_concurrent_downloads,
952 get_smart_cache_stats,
953 update_smart_cache_config,
954 get_smart_cache_config,
955 set_network_state,
957 get_downloads_allowed,
958 get_album_recommendations,
959 get_album_affinity_status,
960 pin_item,
962 unpin_item,
963 is_item_pinned,
964 offline_is_available,
966 offline_get_items,
967 offline_search,
968 storage_get_libraries,
970 storage_get_items,
971 storage_get_item,
972 storage_search_items,
973 storage_save_library,
974 storage_save_item,
975 storage_get_pending_sync_count,
976 sync_queue_mutation,
978 sync_get_pending,
979 sync_mark_processing,
980 sync_mark_completed,
981 sync_mark_failed,
982 sync_get_pending_count,
983 sync_process_pending,
984 sync_cleanup_completed,
985 sync_clear_user,
986 thumbnail_get_cached,
988 thumbnail_save,
989 thumbnail_get_stats,
990 thumbnail_set_limit,
991 thumbnail_clear_cache,
992 thumbnail_delete_item,
993 image_get_url,
994 storage_save_person,
996 storage_get_person,
997 storage_save_item_people,
998 storage_get_item_people,
999 storage_save_series_audio_preference,
1001 storage_get_series_audio_preference,
1002 repository_create,
1004 repository_destroy,
1005 repository_get_libraries,
1006 repository_get_items,
1007 repository_get_item,
1008 repository_get_downloaded_libraries,
1009 repository_get_downloaded_items,
1010 repository_get_download_disk_usage,
1011 repository_jray_actors_at,
1012 repository_get_latest_items,
1013 repository_get_resume_items,
1014 repository_get_next_up_episodes,
1015 repository_get_series_episodes,
1016 repository_get_series_current_episode,
1017 repository_clear_watch_history,
1018 repository_get_recently_played_audio,
1019 repository_get_resume_movies,
1020 repository_get_rediscover_albums,
1021 repository_get_genres,
1022 repository_search,
1023 repository_get_playback_info,
1024 repository_get_video_stream_url,
1025 repository_get_stream_selection,
1026 repository_get_audio_stream_url,
1027 repository_get_audio_only_stream_url_for_video,
1028 repository_get_live_tv_channels,
1029 repository_get_channels,
1030 repository_open_live_stream,
1031 repository_report_playback_start,
1032 repository_report_playback_progress,
1033 repository_report_playback_stopped,
1034 repository_get_image_url,
1035 repository_mark_favorite,
1036 repository_unmark_favorite,
1037 repository_get_favorites,
1038 repository_get_person,
1039 repository_get_items_by_person,
1040 repository_get_similar_items,
1041 repository_get_subtitle_url,
1042 repository_get_video_download_url,
1043 playlist_create,
1045 playlist_delete,
1046 playlist_rename,
1047 playlist_get_items,
1048 playlist_add_items,
1049 profiles_add,
1050 profiles_get_ask_on_start,
1051 profiles_list,
1052 profiles_remove,
1053 profiles_set_ask_on_start,
1054 profiles_set_pin,
1055 profiles_startup_target,
1056 profiles_unlock,
1057 profiles_unlock_with_password,
1058 playlist_remove_items,
1059 playlist_move_item,
1060 diagnostics_get_info,
1063 diagnostics_set_level,
1064 diagnostics_export,
1065 format_time_seconds,
1067 format_time_seconds_long,
1068 convert_ticks_to_seconds,
1069 calc_progress,
1070 convert_percent_to_volume,
1071 ])
1072}
1073
1074#[cfg(target_os = "linux")]
1083fn enable_linux_hardware_video_decoding() {
1084 let rank_overrides = "vah264dec:MAX,vah265dec:MAX,vavp9dec:MAX,vaav1dec:MAX,\
1089 vampeg2dec:MAX,vavp8dec:MAX";
1090
1091 set_env_if_unset("GST_PLUGIN_FEATURE_RANK", rank_overrides);
1092
1093 set_env_if_unset("WEBKIT_GST_ENABLE_HW_VIDEO_DECODER", "1");
1096
1097 info!("[INIT] Linux hardware video decoding (VAAPI) enabled where supported");
1098
1099 log_available_vaapi_decoders();
1100}
1101
1102#[cfg(target_os = "linux")]
1107fn log_available_vaapi_decoders() {
1108 const HW_DECODERS: &[&str] = &[
1109 "vah264dec",
1110 "vah265dec",
1111 "vavp9dec",
1112 "vaav1dec",
1113 "vampeg2dec",
1114 "vavp8dec",
1115 ];
1116
1117 let available: Vec<&str> = HW_DECODERS
1118 .iter()
1119 .copied()
1120 .filter(|name| {
1121 std::process::Command::new("gst-inspect-1.0")
1122 .arg(name)
1123 .stdout(std::process::Stdio::null())
1124 .stderr(std::process::Stdio::null())
1125 .status()
1126 .map(|s| s.success())
1127 .unwrap_or(false)
1128 })
1129 .collect();
1130
1131 if available.is_empty() {
1132 log::warn!(
1133 "[INIT] No VAAPI hardware video decoders found via gst-inspect-1.0; \
1134 video will use software decoding. Install the GStreamer 'va' plugin \
1135 (gst-plugins-bad) and a VAAPI driver to enable hardware decoding."
1136 );
1137 } else {
1138 info!(
1139 "[INIT] VAAPI hardware video decoders available to GStreamer: {}",
1140 available.join(", ")
1141 );
1142 }
1143}
1144
1145#[cfg(target_os = "linux")]
1146fn set_env_if_unset(key: &str, value: &str) {
1147 if std::env::var_os(key).is_none() {
1148 std::env::set_var(key, value);
1151 }
1152}
1153
1154fn build_log_plugin() -> tauri::plugin::TauriPlugin<tauri::Wry> {
1193 use tauri_plugin_log::{Target, TargetKind};
1194
1195 let mut targets = vec![
1196 Target::new(TargetKind::Stdout),
1197 Target::new(TargetKind::LogDir {
1198 file_name: Some("jellytau".to_string()),
1199 }),
1200 ];
1201
1202 if cfg!(debug_assertions) {
1206 targets.push(Target::new(TargetKind::Webview));
1207 }
1208
1209 tauri_plugin_log::Builder::new()
1210 .targets(targets)
1211 .level(log::LevelFilter::Info)
1212 .max_file_size(5 * 1024 * 1024)
1213 .rotation_strategy(tauri_plugin_log::RotationStrategy::KeepOne)
1214 .format(|out, message, record| {
1215 out.finish(format_args!(
1216 "[{}][{}] {}",
1217 record.level(),
1218 record.target(),
1219 crate::utils::diagnostics::redact(&message.to_string())
1220 ))
1221 })
1222 .build()
1223}
1224
1225#[cfg_attr(mobile, tauri::mobile_entry_point)]
1226pub fn run() {
1227 crate::utils::diagnostics::install_panic_hook();
1232
1233 #[cfg(target_os = "linux")]
1238 enable_linux_hardware_video_decoding();
1239
1240 let builder = specta_builder();
1245 let invoke_handler = builder.invoke_handler();
1246
1247 tauri::Builder::default()
1248 .plugin(build_log_plugin())
1249 .plugin(tauri_plugin_opener::init())
1250 .plugin(tauri_plugin_os::init())
1251 .invoke_handler(invoke_handler)
1252 .setup(move |app| {
1253 builder.mount_events(app);
1256
1257
1258 if let Ok(config_dir) = app.path().app_config_dir() {
1275 if let Some(level) = crate::commands::diagnostics::stored_level(&config_dir) {
1276 log::set_max_level(level);
1277 log::info!("[DIAG] restored log level from settings: {level}");
1278 }
1279 }
1280
1281 #[cfg(desktop)]
1283 {
1284 app.handle()
1285 .plugin(tauri_plugin_updater::Builder::new().build())?;
1286 app.handle().plugin(tauri_plugin_process::init())?;
1287 }
1288
1289 let db_path = if let Ok(test_data_dir) = std::env::var("JELLYTAU_DATA_DIR") {
1292 let test_path = std::path::PathBuf::from(test_data_dir);
1293 info!("[INIT] Using test data directory: {:?}", test_path);
1294 test_path.join("jellytau.db")
1295 } else {
1296 app
1297 .path()
1298 .app_data_dir()
1299 .expect("Failed to get app data directory")
1300 .join("jellytau.db")
1301 };
1302
1303 info!("[INIT] Initializing database at: {:?}", db_path);
1304
1305 if let Some(parent) = db_path.parent() {
1307 info!("[INIT] Creating database directory: {:?}", parent);
1308 match std::fs::create_dir_all(parent) {
1309 Ok(_) => info!("[INIT] Database directory ready"),
1310 Err(e) => {
1311 error!("[INIT ERROR] Failed to create database directory: {}", e);
1312 panic!("Failed to create database directory: {}", e);
1313 }
1314 }
1315 }
1316
1317 info!("[INIT] Opening database...");
1318 let database = match Database::open(&db_path) {
1319 Ok(db) => {
1320 info!("[INIT] Database initialized successfully");
1321 db
1322 }
1323 Err(e) => {
1324 error!("[INIT ERROR] Failed to initialize database: {}", e);
1325 panic!("Failed to initialize database: {}", e);
1326 }
1327 };
1328 let db_wrapper = DatabaseWrapper(Mutex::new(database));
1329 app.manage(db_wrapper);
1330
1331 #[cfg(target_os = "android")]
1333 {
1334 if let Err(e) = crate::android_context::ensure_initialized() {
1345 log::error!("[INIT] Android context unavailable: {e}");
1346 log::error!("[INIT] Secure credential storage will fall back to the encrypted file.");
1347 }
1348
1349 info!("[INIT] Initializing Android SecureStorage for credentials...");
1350 let ctx = ndk_context::android_context();
1351 let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) };
1352
1353 match vm {
1354 Ok(java_vm) => {
1355 match java_vm.attach_current_thread() {
1356 Ok(mut env) => {
1357 let context_obj = unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) };
1358
1359 match initialize_secure_storage(&mut env, &context_obj) {
1360 Ok(()) => {
1361 info!("[INIT] Android SecureStorage initialized successfully");
1362 }
1363 Err(e) => {
1364 warn!("[INIT WARNING] Failed to initialize SecureStorage: {}. Credentials will use encrypted file fallback.", e);
1365 }
1366 }
1367 }
1368 Err(e) => {
1369 warn!("[INIT WARNING] Failed to attach JNI thread: {}. Credentials will use encrypted file fallback.", e);
1370 }
1371 }
1372 }
1373 Err(e) => {
1374 warn!("[INIT WARNING] Failed to create JavaVM: {}. Credentials will use encrypted file fallback.", e);
1375 }
1376 }
1377 }
1378
1379 info!("[INIT] Initializing credential store...");
1381 let credential_store = CredentialStore::new();
1382 let creds_wrapper = CredentialStoreWrapper(Mutex::new(credential_store));
1383 app.manage(creds_wrapper);
1384
1385 info!("[INIT] Creating shared playback reporting infrastructure...");
1387 let playback_reporter = Arc::new(tokio::sync::Mutex::new(None));
1388 let position_throttler = Arc::new(playback_reporting::EventThrottler::new());
1389
1390 info!("[INIT] Creating player backend...");
1392 let backend = create_player_backend(
1393 app.handle().clone(),
1394 playback_reporter.clone(),
1395 position_throttler.clone(),
1396 );
1397 #[cfg(target_os = "linux")]
1429 if crate::player::native_video::enabled() {
1430 use tauri::Manager;
1431 log::warn!(
1432 "[INIT] JELLYTAU_NATIVE_VIDEO=1 — attaching the experimental \
1433 video surface (mpv drawn behind the webview, no reparenting)"
1434 );
1435 if let Some(window) = app.get_webview_window("main") {
1436 match window.default_vbox() {
1437 Ok(vbox) => {
1438 let handle = crate::player::mpv_backend::registered_handle();
1439 if crate::player::video_surface::attach(&vbox, handle) {
1440 info!("[INIT] Native video surface attached");
1441 } else {
1442 log::warn!("[INIT] Native video surface unavailable");
1443 }
1444 }
1445 Err(e) => {
1446 log::warn!("[INIT] No GTK vbox for the main window: {e}")
1447 }
1448 }
1449 }
1450 }
1451
1452 let player_controller = PlayerController::new(
1457 Box::new(crate::player::LegacyPlayer::new(
1458 backend,
1459 engine_capabilities(),
1460 )),
1461 playback_reporter.clone(),
1462 position_throttler.clone(),
1463 );
1464
1465 let event_emitter = Arc::new(TauriEventEmitter::new(app.handle().clone()));
1467 player_controller.set_event_emitter(event_emitter.clone());
1468
1469 let player_arc = Arc::new(TokioMutex::new(player_controller));
1470
1471 #[cfg(target_os = "android")]
1475 {
1476 player::android::set_player_controller(player_arc.clone());
1477 }
1478
1479 let player_state = PlayerStateWrapper(player_arc.clone());
1480 app.manage(player_state);
1481
1482 info!("[INIT] Initializing media session manager...");
1484 let session_manager = MediaSessionManager::new();
1485 let session_wrapper = MediaSessionManagerWrapper(Mutex::new(session_manager));
1486 app.manage(session_wrapper);
1487
1488 info!("[INIT] Initializing playback mode manager...");
1490 let jellyfin_client = {
1491 let player = player_arc.blocking_lock();
1492 player.jellyfin_client()
1493 };
1494 let playback_mode_manager = playback_mode::PlaybackModeManager::new(
1495 jellyfin_client.clone(),
1496 player_arc.clone(),
1497 );
1498 let playback_mode_arc = Arc::new(playback_mode_manager);
1499 playback_mode_arc.set_event_emitter(event_emitter.clone());
1502 let playback_mode_wrapper = PlaybackModeManagerWrapper(playback_mode_arc.clone());
1503 app.manage(playback_mode_wrapper);
1504
1505 info!("[INIT] Initializing session poller manager...");
1507 let session_poller = session_poller::SessionPollerManager::new(
1508 jellyfin_client,
1509 playback_mode_arc.clone(),
1510 );
1511 session_poller.set_event_emitter(event_emitter.clone());
1512 let session_poller_arc = Arc::new(session_poller);
1516 let session_poller_wrapper = SessionPollerWrapper(session_poller_arc.clone());
1517 app.manage(session_poller_wrapper);
1518
1519 #[cfg(target_os = "android")]
1523 {
1524 info!("[INIT] Setting up MediaSession handler for lockscreen controls...");
1525 let media_handler = Arc::new(MediaSessionHandler {
1526 player: player_arc.clone(),
1527 playback_mode: playback_mode_arc.clone(),
1528 event_emitter: event_emitter.clone(),
1529 });
1530 set_media_command_handler(media_handler);
1531
1532 info!("[INIT] Setting up remote volume handler for Android...");
1533 let handler = Arc::new(RemoteVolumeSessionHandler {
1534 playback_mode: playback_mode_arc.clone(),
1535 });
1536 set_remote_volume_handler(handler);
1537 }
1538
1539 let video_settings = VideoSettingsWrapper(Mutex::new(VideoSettings::default()));
1541 app.manage(video_settings);
1542
1543 {
1551 let handle = app.handle().clone();
1552 tauri::async_runtime::spawn(async move {
1553 crate::commands::restore_streaming_quality(&handle).await;
1554 });
1555 }
1556
1557 {
1563 let handle = app.handle().clone();
1564 tauri::async_runtime::spawn(async move {
1565 crate::commands::restore_library_settings(&handle).await;
1566 });
1567 }
1568
1569 info!("[INIT] Initializing thumbnail cache...");
1571 let app_data_dir = if let Ok(test_data_dir) = std::env::var("JELLYTAU_DATA_DIR") {
1572 std::path::PathBuf::from(test_data_dir)
1573 } else {
1574 app
1575 .path()
1576 .app_data_dir()
1577 .expect("Failed to get app data directory")
1578 };
1579 let thumbnail_cache = ThumbnailCache::new(app_data_dir.clone(), ThumbnailCacheConfig::default());
1580 let thumbnail_wrapper = ThumbnailCacheWrapper(Arc::new(thumbnail_cache));
1581 app.manage(thumbnail_wrapper);
1582
1583 info!("[INIT] Initializing smart cache...");
1585 let smart_cache = SmartCache::new(SmartCacheConfig::default());
1586 let smart_cache_wrapper = SmartCacheWrapper(Mutex::new(smart_cache));
1587 app.manage(smart_cache_wrapper);
1588
1589 info!("[INIT] Starting local media server...");
1594 let media_server = match media_server::start(app_data_dir.clone()) {
1595 Ok(s) => Some(s),
1596 Err(e) => {
1597 error!("[INIT ERROR] Local media server failed to start: {}", e);
1600 None
1601 }
1602 };
1603 app.manage(media_server::MediaServerWrapper(media_server));
1604
1605 info!("[INIT] Initializing download manager...");
1607 let download_dir = app_data_dir.join("downloads");
1608 let download_manager = DownloadManager::new(download_dir);
1609 let download_manager_wrapper = DownloadManagerWrapper(Mutex::new(download_manager));
1610 app.manage(download_manager_wrapper);
1611
1612 app.manage(commands::download::NetworkStateWrapper(
1616 download::network::NetworkStateHandle::new(),
1617 ));
1618
1619 info!("[INIT] Initializing connectivity monitor...");
1621 let http_config = HttpConfig::default();
1622 let http_client = HttpClient::new(http_config)
1623 .expect("Failed to create HTTP client");
1624 let mut connectivity_monitor = ConnectivityMonitor::new(http_client);
1625 connectivity_monitor.set_app_handle(app.handle().clone());
1626
1627 session_poller_arc.set_connectivity_reporter(connectivity_monitor.reporter());
1631 session_poller_arc.start();
1632
1633 let connectivity_arc = Arc::new(tokio::sync::Mutex::new(connectivity_monitor));
1635 let connectivity_wrapper = ConnectivityMonitorWrapper(connectivity_arc.clone());
1636 app.manage(connectivity_wrapper);
1637
1638 info!("[INIT] Initializing auth manager...");
1640 let auth_http_config = HttpConfig::default();
1641 let auth_http_client = HttpClient::new(auth_http_config)
1642 .expect("Failed to create HTTP client for auth");
1643 let mut auth_manager = AuthManager::new(auth_http_client);
1644
1645 auth_manager.set_connectivity_monitor(connectivity_arc.clone());
1647
1648 let auth_manager_wrapper = AuthManagerWrapper(Arc::new(auth_manager));
1649 app.manage(auth_manager_wrapper);
1650
1651 info!("[INIT] Initializing session verifier wrapper...");
1653 let session_verifier_wrapper = SessionVerifierWrapper(Arc::new(tokio::sync::Mutex::new(None)));
1654 app.manage(session_verifier_wrapper);
1655
1656 info!("[INIT] Initializing repository manager...");
1658 let repository_manager = commands::RepositoryManager::new();
1659 let repository_manager_wrapper = RepositoryManagerWrapper(repository_manager);
1660 app.manage(repository_manager_wrapper);
1661
1662 info!("[INIT] Initializing playback reporter wrapper...");
1668 let playback_reporter_wrapper = PlaybackReporterWrapper(playback_reporter.clone());
1669 app.manage(playback_reporter_wrapper);
1670
1671 info!("[INIT] Starting background catalog indexer...");
1677 commands::catalog::spawn_catalog_indexer(app.handle().clone());
1678
1679 info!("[INIT] Starting favourites drain...");
1684 commands::favorites::spawn_favorites_drain(app.handle().clone());
1685
1686 info!("[INIT] Starting sync-queue drain...");
1691 commands::sync_drain::spawn_sync_queue_drain(app.handle().clone());
1692
1693 info!("[INIT] Application setup completed successfully");
1694 Ok(())
1695 })
1696 .run(tauri::generate_context!())
1697 .expect("error while running tauri application");
1698}
1699
1700#[cfg(test)]
1701mod specta_bindings {
1702 #[test]
1704 fn export_typescript_bindings() {
1705 super::specta_builder()
1706 .export(
1707 specta_typescript::Typescript::default()
1708 .bigint(specta_typescript::BigIntExportBehavior::Number),
1709 "../src/lib/api/bindings.ts",
1710 )
1711 .expect("failed to export typescript bindings");
1712 }
1713}