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 repository;
19mod session_poller;
20pub mod settings;
21mod storage;
22mod thumbnail;
23pub mod utils;
24
25#[cfg(target_os = "android")]
26use log::warn;
27use log::{error, info};
28use std::sync::{Arc, Mutex};
29use tauri::{Emitter, Manager};
30use tauri_specta::Builder;
31use tokio::sync::Mutex as TokioMutex;
32
33use auth::AuthManager;
34use commands::{
35 auth_connect_to_server,
36 auth_get_session,
37 auth_initialize,
39 auth_login,
40 auth_logout,
41 auth_reauthenticate,
42 auth_set_session,
43 auth_start_verification,
44 auth_stop_verification,
45 auth_verify_session,
46 calc_progress,
47 cancel_download,
48 catalog_sync_status,
49 clear_stale_downloads,
50 connectivity_check_server,
52 connectivity_get_status,
53 connectivity_mark_reachable,
54 connectivity_mark_unreachable,
55 connectivity_set_server_url,
56 connectivity_start_monitoring,
57 connectivity_stop_monitoring,
58 convert_percent_to_volume,
59 convert_ticks_to_seconds,
60 delete_album_downloads,
61 delete_all_downloads,
62 delete_download,
63 delete_downloads_under,
64 device_get_id,
66 device_set_id,
67 diagnostics_export,
69 diagnostics_get_info,
70 diagnostics_set_level,
71 download_album,
72 download_item,
73 download_item_and_start,
74 download_season,
75 download_series,
76 download_video,
77 enqueue_download,
78 enqueue_video_downloads,
79 format_time_seconds,
81 format_time_seconds_long,
82 get_album_affinity_status,
83 get_album_recommendations,
84 get_download_manager_stats,
85 get_download_storage_stats,
86 get_downloads,
87 get_downloads_allowed,
88 get_smart_cache_config,
89 get_smart_cache_stats,
90 image_get_url,
91 is_item_pinned,
92 library_get_exclusion_candidates,
94 library_get_settings,
95 library_set_settings,
96 lms_create_sync_group,
97 lms_dissolve_sync_group,
98 lms_get_sync_groups,
100 lms_unsync_player,
101 mark_download_completed,
102 mark_download_failed,
103 media_local_selection,
104 media_local_url,
105 offline_get_items,
106 offline_is_available,
107 offline_search,
108 pause_download,
109 pin_item,
110 playback_mark_played,
111 playback_mode_get_current,
113 playback_mode_get_remote_status,
114 playback_mode_is_transferring,
115 playback_mode_set,
116 playback_mode_set_transferring,
117 playback_mode_transfer_to_local,
118 playback_mode_transfer_to_remote,
119 playback_report_progress,
120 playback_report_start,
121 playback_report_stopped,
122 playback_reporter_destroy,
123 playback_reporter_init,
125 player_add_to_queue,
127 player_add_track_by_id,
128 player_add_tracks_by_ids,
129 player_background_action,
130 player_cancel_autoplay_countdown,
131 player_cancel_sleep_timer,
132 player_configure_jellyfin,
134 player_cycle_repeat,
135 player_disable_jellyfin,
136 player_dismiss_session,
137 player_enter_background_audio,
138 player_exit_background_audio,
139 player_get_audio_settings,
140 player_get_autoplay_settings,
141 player_get_cache_config,
142 player_get_capabilities,
143 player_get_eq_presets,
144 player_get_queue,
145 player_get_session,
147 player_get_sleep_timer,
148 player_get_status,
149 player_get_streaming_qualities,
150 player_get_video_settings,
151 player_local_media_path,
153 player_move_in_queue,
154 player_next,
155 player_on_playback_ended,
156 player_pause,
157 player_play,
158 player_play_album_track,
159 player_play_item,
160 player_play_next_episode,
161 player_play_queue,
162 player_play_tracks,
163 player_preload_upcoming,
164 player_previous,
165 player_recover_stream,
166 player_remove_from_queue,
167 player_report_media_loaded,
168 player_report_position,
169 player_report_state,
171 player_seek,
172 player_seek_video,
173 player_set_audio_settings,
174 player_set_audio_track,
175 player_set_autoplay_settings,
176 player_set_cache_config,
177 player_set_sleep_timer,
179 player_set_stream_quality,
180 player_set_subtitle_track,
181 player_set_video_settings,
182 player_set_volume,
183 player_skip_to,
184 player_stop,
185 player_switch_audio_track,
186 player_toggle,
187 player_toggle_mute,
188 player_toggle_shuffle,
189 playlist_add_items,
190 playlist_create,
192 playlist_delete,
193 playlist_get_items,
194 playlist_move_item,
195 playlist_remove_items,
196 playlist_rename,
197 remote_play_on_session,
199 remote_send_command,
200 remote_session_seek,
201 remote_session_set_volume,
202 remote_session_toggle_mute,
203 repository_clear_watch_history,
205 repository_create,
206 repository_destroy,
207 repository_get_audio_only_stream_url_for_video,
208 repository_get_audio_stream_url,
209 repository_get_channels,
210 repository_get_download_disk_usage,
211 repository_get_downloaded_items,
212 repository_get_downloaded_libraries,
213 repository_get_favorites,
214 repository_get_genres,
215 repository_get_image_url,
216 repository_get_item,
217 repository_get_items,
218 repository_get_items_by_person,
219 repository_get_latest_items,
220 repository_get_libraries,
221 repository_get_live_tv_channels,
222 repository_get_next_up_episodes,
223 repository_get_person,
224 repository_get_playback_info,
225 repository_get_recently_played_audio,
226 repository_get_rediscover_albums,
227 repository_get_resume_items,
228 repository_get_resume_movies,
229 repository_get_series_current_episode,
230 repository_get_series_episodes,
231 repository_get_similar_items,
232 repository_get_stream_selection,
233 repository_get_subtitle_url,
234 repository_get_video_download_url,
235 repository_get_video_stream_url,
236 repository_jray_actors_at,
237 repository_mark_favorite,
238 repository_open_live_stream,
239 repository_report_playback_progress,
240 repository_report_playback_start,
241 repository_report_playback_stopped,
242 repository_search,
243 repository_unmark_favorite,
244 resume_download,
245 resume_queued_downloads,
246 sessions_poll_now,
247 sessions_set_polling_hint,
249 set_max_concurrent_downloads,
250 set_network_state,
251 set_show_server_catalog,
252 start_download,
253 storage_delete_server,
255 storage_delete_user,
256 storage_get_access_token,
257 storage_get_active_session,
258 storage_get_active_user,
259 storage_get_item,
260 storage_get_item_people,
261 storage_get_items,
262 storage_get_libraries,
264 storage_get_path,
265 storage_get_pending_sync_count,
266 storage_get_person,
267 storage_get_playback_progress,
268 storage_get_security_status,
269 storage_get_series_audio_preference,
270 storage_get_servers,
271 storage_get_size,
272 storage_get_users,
273 storage_init,
274 storage_mark_played,
275 storage_mark_synced,
276 storage_save_item,
277 storage_save_item_people,
278 storage_save_library,
279 storage_save_person,
281 storage_save_series_audio_preference,
283 storage_save_server,
284 storage_save_user,
285 storage_search_items,
286 storage_set_active_user,
287 storage_set_watched,
288 storage_toggle_favorite,
289 storage_update_playback_context,
290 storage_update_playback_progress,
291 sync_cleanup_completed,
292 sync_clear_user,
293 sync_full_catalog,
294 sync_get_pending,
295 sync_get_pending_count,
296 sync_mark_completed,
297 sync_mark_failed,
298 sync_mark_processing,
299 sync_process_pending,
300 sync_queue_mutation,
302 thumbnail_clear_cache,
303 thumbnail_delete_item,
304 thumbnail_get_cached,
306 thumbnail_get_stats,
307 thumbnail_save,
308 thumbnail_set_limit,
309 unpin_item,
310 update_smart_cache_config,
311 AuthManagerWrapper,
312 ConnectivityMonitorWrapper,
313 CredentialStoreWrapper,
314 DatabaseWrapper,
315 DownloadManagerWrapper,
316 MediaSessionManagerWrapper,
317 PlaybackModeManagerWrapper,
318 PlaybackReporterWrapper,
319 PlayerStateWrapper,
320 RepositoryManagerWrapper,
321 SessionPollerWrapper,
322 SessionVerifierWrapper,
323 SmartCacheWrapper,
324 ThumbnailCacheWrapper,
325 VideoSettingsWrapper,
326};
327use connectivity::ConnectivityMonitor;
328use credentials::CredentialStore;
329use download::cache::{CacheConfig as SmartCacheConfig, SmartCache};
330use download::DownloadManager;
331use jellyfin::{HttpClient, HttpConfig};
332#[cfg(target_os = "android")]
333use playback_mode::PlaybackModeManager;
334#[cfg(target_os = "android")]
337use player::seek::{resolve_skip_action, SkipAction};
338use player::{MediaSessionManager, PlayerBackend, PlayerController, TauriEventEmitter};
339use player::NullBackend;
343
344#[cfg(target_os = "linux")]
345use player::MpvBackend;
346use settings::VideoSettings;
347use storage::Database;
348use thumbnail::{CacheConfig as ThumbnailCacheConfig, ThumbnailCache};
349
350#[cfg(target_os = "android")]
351use credentials::initialize_secure_storage;
352
353#[cfg(target_os = "android")]
354use player::ExoPlayerBackend;
355
356#[cfg(target_os = "android")]
357use player::{
358 set_media_command_handler, set_remote_volume_handler, MediaCommandHandler, RemoteVolumeHandler,
359};
360
361#[cfg(target_os = "android")]
369struct MediaSessionHandler {
370 player: Arc<TokioMutex<PlayerController>>,
371 playback_mode: Arc<PlaybackModeManager>,
372 event_emitter: Arc<TauriEventEmitter>,
373}
374
375#[cfg(target_os = "android")]
376impl MediaSessionHandler {
377 fn handle_remote_command(&self, command: &str, session_id: String) {
382 use crate::player::{PlayerEventEmitter, PlayerStatusEvent};
383
384 if command == "stop" {
388 self.event_emitter
389 .emit(PlayerStatusEvent::RemoteDisconnectRequested);
390 return;
391 }
392
393 let jellyfin_client = {
394 let player = self.player.blocking_lock();
395 player.jellyfin_client()
396 };
397 let command = command.to_string();
398
399 tauri::async_runtime::spawn(async move {
400 let client = {
401 let guard = match jellyfin_client.lock() {
402 Ok(g) => g,
403 Err(e) => {
404 error!("[MediaSession] Failed to lock Jellyfin client: {}", e);
405 return;
406 }
407 };
408 match guard.as_ref() {
409 Some(c) => c.clone(),
410 None => {
411 warn!("[MediaSession] No Jellyfin client for remote command");
412 return;
413 }
414 }
415 };
416
417 let result = match command.as_str() {
419 "play" => client.send_session_command(session_id, "Unpause").await,
420 "pause" => client.send_session_command(session_id, "Pause").await,
421 "next" => client.send_session_command(session_id, "NextTrack").await,
422 "previous" => {
423 client
424 .send_session_command(session_id, "PreviousTrack")
425 .await
426 }
427 cmd if cmd.starts_with("seek:") => match cmd[5..].parse::<f64>() {
428 Ok(seconds) => {
429 let ticks = (seconds * 10_000_000.0) as i64;
430 client.session_seek(session_id, ticks).await
431 }
432 Err(_) => {
433 warn!("[MediaSession] Bad seek command: {}", command);
434 Ok(())
435 }
436 },
437 _ => {
438 warn!("[MediaSession] Unknown remote command: {}", command);
439 Ok(())
440 }
441 };
442
443 if let Err(e) = result {
444 error!("[MediaSession] Remote command '{}' failed: {}", command, e);
445 }
446 });
447 }
448
449 fn handle_local_command(&self, command: &str) {
451 if let Some(raw) = command.strip_prefix("seek:") {
458 match raw.parse::<f64>() {
459 Ok(position) => {
460 let player = self.player.clone();
461 tokio::spawn(async move {
462 let controller = player.lock().await;
463 if let Err(e) = controller.seek_absolute(position).await {
464 error!("[MediaSession] Seek to {:.1}s failed: {}", position, e);
465 }
466 });
467 }
468 Err(_) => warn!("[MediaSession] Bad seek command: {}", command),
469 }
470 return;
471 }
472
473 if command == "next" || command == "previous" {
483 let is_next = command == "next";
484 let player = self.player.clone();
485 tokio::spawn(async move {
486 let controller = player.lock().await;
487 let action = resolve_skip_action(
488 is_next,
489 controller.is_background_audio_active(),
490 controller.position(),
491 controller.duration(),
492 );
493 let label = if is_next { "next" } else { "previous" };
494 let result: Result<(), String> = match action {
495 SkipAction::Advance => if is_next {
496 controller.next()
497 } else {
498 controller.previous()
499 }
500 .map_err(|e| e.to_string()),
501 SkipAction::SeekTo(position) => {
502 info!(
503 "[MediaSession] Background audio: '{}' scrubs to {:.1}s",
504 label, position
505 );
506 controller.seek_absolute(position).await
507 }
508 };
509 if let Err(e) = result {
510 error!("[MediaSession] Skip '{}' failed: {}", label, e);
511 }
512 });
513 return;
514 }
515
516 let controller = self.player.blocking_lock();
518
519 let result = match command {
520 "play" => controller.play(),
521 "pause" => controller.pause(),
522 "stop" => controller.stop(),
523 _ => {
524 warn!("[MediaSession] Unknown command: {}", command);
525 Ok(())
526 }
527 };
528
529 if let Err(e) = result {
530 error!("[MediaSession] Command '{}' failed: {}", command, e);
531 }
532 }
533}
534
535#[cfg(target_os = "android")]
536impl MediaCommandHandler for MediaSessionHandler {
537 fn on_command(&self, command: &str) {
538 match self.playback_mode.get_mode() {
539 playback_mode::PlaybackMode::Remote { session_id } => {
540 self.handle_remote_command(command, session_id);
541 }
542 _ => self.handle_local_command(command),
543 }
544 }
545}
546
547#[cfg(target_os = "android")]
551struct RemoteVolumeSessionHandler {
552 playback_mode: Arc<PlaybackModeManager>,
553}
554
555#[cfg(target_os = "android")]
556impl RemoteVolumeHandler for RemoteVolumeSessionHandler {
557 fn on_remote_volume_change(&self, command: &str, volume: i32) {
558 log::info!("[RemoteVolume] Command: {}, Volume: {}", command, volume);
559
560 let playback_mode = Arc::clone(&self.playback_mode);
562 let command_str = command.to_string();
563
564 log::info!("[RemoteVolume] Spawning async task to send volume command...");
567 tauri::async_runtime::spawn(async move {
568 log::info!("[RemoteVolume] Async task started, calling send_remote_volume_command...");
569 match playback_mode
570 .send_remote_volume_command(&command_str, volume)
571 .await
572 {
573 Ok(_) => log::info!("[RemoteVolume] Volume command completed successfully"),
574 Err(e) => log::error!("[RemoteVolume] Failed to send volume command: {}", e),
575 }
576 log::info!("[RemoteVolume] Async task completed");
577 });
578 log::info!("[RemoteVolume] Async task spawned, returning from JNI callback");
579 }
580}
581
582#[derive(Clone, serde::Serialize)]
585struct BackendInitError {
586 platform: &'static str,
587 backend: &'static str,
588 message: String,
589}
590
591fn emit_backend_init_failed(app_handle: &tauri::AppHandle, backend: &'static str, message: String) {
594 error!(
595 "[INIT] Player backend '{}' failed to initialize: {}. Falling back to NullBackend (playback disabled).",
596 backend, message
597 );
598 let _ = app_handle.emit(
599 "backend-init-failed",
600 BackendInitError {
601 platform: std::env::consts::OS,
602 backend,
603 message,
604 },
605 );
606}
607
608#[cfg_attr(
612 not(any(target_os = "linux", target_os = "android")),
613 allow(unused_variables)
614)]
615fn create_player_backend(
616 app_handle: tauri::AppHandle,
617 playback_reporter: Arc<tokio::sync::Mutex<Option<playback_reporting::PlaybackReporter>>>,
618 position_throttler: Arc<playback_reporting::EventThrottler>,
619) -> Box<dyn PlayerBackend> {
620 let _event_emitter = Arc::new(TauriEventEmitter::new(app_handle.clone()));
621
622 #[cfg(target_os = "android")]
623 {
624 info!("Android platform detected - initializing ExoPlayer backend");
625
626 if let Err(e) = crate::android_context::ensure_initialized() {
631 log::error!("[INIT] Android context unavailable for the player: {e}");
632 }
633
634 let ctx = ndk_context::android_context();
636
637 let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) };
639
640 match vm {
641 Ok(java_vm) => {
642 match java_vm.attach_current_thread() {
643 Ok(mut env) => {
644 let context_obj =
645 unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) };
646
647 match ExoPlayerBackend::new(
648 &mut env,
649 &context_obj,
650 _event_emitter.clone(),
651 playback_reporter.clone(),
652 position_throttler.clone(),
653 ) {
654 Ok(backend) => {
655 info!("Successfully initialized ExoPlayer backend for Android");
656 return Box::new(backend);
657 }
658 Err(e) => {
659 emit_backend_init_failed(&app_handle, "exoplayer", e.to_string());
661 return Box::new(NullBackend::new());
662 }
663 }
664 }
665 Err(e) => {
666 emit_backend_init_failed(
667 &app_handle,
668 "exoplayer",
669 format!("attach JNI thread failed: {}", e),
670 );
671 return Box::new(NullBackend::new());
672 }
673 }
674 }
675 Err(e) => {
676 emit_backend_init_failed(
677 &app_handle,
678 "exoplayer",
679 format!("create JavaVM failed: {}", e),
680 );
681 return Box::new(NullBackend::new());
682 }
683 }
684 }
685
686 #[cfg(target_os = "linux")]
688 {
689 info!("Linux platform detected - initializing MPV backend for audio");
690 match MpvBackend::new(Some(_event_emitter), playback_reporter, position_throttler) {
691 Ok(backend) => {
692 info!("Successfully initialized MPV backend for Linux");
693 Box::new(backend)
694 }
695 Err(e) => {
696 error!("\n========================================");
697 error!("FATAL ERROR: Failed to initialize MPV backend");
698 error!("========================================");
699 error!("Error: {}", e);
700 error!("\nCommon causes:");
701 error!(" 1. MPV is not installed");
702 error!(" Solution: Install MPV using your package manager");
703 error!(" - Arch/CachyOS: sudo pacman -S mpv");
704 error!(" - Ubuntu/Debian: sudo apt install mpv libmpv-dev");
705 error!(" - Fedora: sudo dnf install mpv mpv-libs-devel");
706 error!("\n 2. MPV version mismatch (app was built with different libmpv version)");
707 error!(" Solution: Rebuild the application");
708 error!(" - cd src-tauri && cargo clean && cargo build --release");
709 error!("\n 3. Audio system not working");
710 error!(" Solution: Verify audio works with: pactl info");
711 error!("\nAudio playback will NOT work until this is fixed.");
712 error!("========================================\n");
713
714 emit_backend_init_failed(&app_handle, "mpv", e.to_string());
718 Box::new(NullBackend::new())
719 }
720 }
721 }
722
723 #[cfg(not(any(target_os = "linux", target_os = "android")))]
727 {
728 info!("No native audio backend for this platform - using webview <audio> backend");
729 match player::WebviewAudioBackend::new(_event_emitter) {
730 Ok(backend) => Box::new(backend),
731 Err(e) => {
732 emit_backend_init_failed(&app_handle, "webview-audio", e.to_string());
733 Box::new(NullBackend::new())
734 }
735 }
736 }
737}
738
739fn engine_capabilities() -> crate::player::media_player::Capabilities {
752 #[cfg(target_os = "android")]
753 {
754 crate::player::media_player::Capabilities::exoplayer()
755 }
756 #[cfg(not(target_os = "android"))]
757 {
758 crate::player::media_player::Capabilities::mpv()
759 }
760}
761
762fn specta_builder() -> Builder<tauri::Wry> {
763 Builder::<tauri::Wry>::new()
764 .error_handling(tauri_specta::ErrorHandlingMode::Throw)
767 .events(tauri_specta::collect_events![
768 crate::player::events::PlayerStatusEvent
769 ])
770 .commands(tauri_specta::collect_commands![
771 player_play_item,
773 player_background_action,
774 player_enter_background_audio,
775 player_exit_background_audio,
776 player_play_queue,
777 player_play_album_track,
778 player_play_tracks,
779 player_play,
780 player_pause,
781 player_toggle,
782 player_stop,
783 player_next,
784 player_previous,
785 player_seek,
786 player_seek_video,
787 player_set_volume,
788 player_toggle_mute,
789 player_set_audio_track,
790 player_switch_audio_track,
791 player_set_subtitle_track,
792 player_toggle_shuffle,
793 player_cycle_repeat,
794 player_get_status,
795 player_get_queue,
796 player_get_capabilities,
797 player_add_to_queue,
798 player_add_track_by_id,
799 player_add_tracks_by_ids,
800 player_remove_from_queue,
801 player_move_in_queue,
802 player_skip_to,
803 player_set_audio_settings,
804 player_get_audio_settings,
805 player_get_eq_presets,
806 player_set_video_settings,
807 player_get_video_settings,
808 player_get_streaming_qualities,
809 player_set_stream_quality,
810 player_set_sleep_timer,
812 player_cancel_sleep_timer,
813 player_get_sleep_timer,
814 player_get_autoplay_settings,
815 player_set_autoplay_settings,
816 player_cancel_autoplay_countdown,
817 player_play_next_episode,
818 player_on_playback_ended,
819 player_recover_stream,
820 player_report_state,
821 player_report_position,
822 player_report_media_loaded,
823 player_local_media_path,
825 player_preload_upcoming,
826 player_set_cache_config,
827 player_get_cache_config,
828 player_configure_jellyfin,
830 player_disable_jellyfin,
831 player_get_session,
833 player_dismiss_session,
834 remote_play_on_session,
836 remote_send_command,
837 remote_session_seek,
838 remote_session_set_volume,
839 remote_session_toggle_mute,
840 lms_get_sync_groups,
842 lms_create_sync_group,
843 lms_unsync_player,
844 lms_dissolve_sync_group,
845 sessions_set_polling_hint,
847 sessions_poll_now,
848 playback_mode_get_current,
850 playback_mode_set,
851 playback_mode_is_transferring,
852 playback_mode_transfer_to_remote,
853 playback_mode_get_remote_status,
854 playback_mode_transfer_to_local,
855 playback_mode_set_transferring,
856 playback_reporter_init,
858 playback_reporter_destroy,
859 playback_report_start,
860 playback_report_progress,
861 playback_report_stopped,
862 playback_mark_played,
863 auth_initialize,
865 auth_connect_to_server,
866 auth_login,
867 auth_verify_session,
868 auth_logout,
869 auth_get_session,
870 auth_set_session,
871 auth_start_verification,
872 auth_stop_verification,
873 auth_reauthenticate,
874 device_get_id,
876 device_set_id,
877 connectivity_check_server,
879 connectivity_set_server_url,
880 connectivity_get_status,
881 connectivity_start_monitoring,
882 connectivity_stop_monitoring,
883 connectivity_mark_reachable,
884 connectivity_mark_unreachable,
885 storage_init,
887 storage_get_path,
888 storage_get_size,
889 storage_get_security_status,
890 storage_save_server,
891 storage_get_servers,
892 storage_delete_server,
893 storage_save_user,
894 storage_get_users,
895 storage_set_active_user,
896 storage_get_active_user,
897 storage_get_active_session,
898 storage_get_access_token,
899 storage_delete_user,
900 storage_update_playback_progress,
902 storage_update_playback_context,
903 storage_mark_played,
904 storage_set_watched,
905 storage_get_playback_progress,
906 storage_mark_synced,
907 storage_toggle_favorite,
908 download_item,
910 download_item_and_start,
911 download_album,
912 download_video,
913 download_series,
914 download_season,
915 get_downloads,
916 pause_download,
917 resume_download,
918 cancel_download,
919 delete_download,
920 delete_all_downloads,
921 delete_album_downloads,
922 delete_downloads_under,
923 clear_stale_downloads,
924 get_download_storage_stats,
925 mark_download_completed,
926 mark_download_failed,
927 media_local_url,
928 media_local_selection,
929 start_download,
930 enqueue_download,
931 enqueue_video_downloads,
932 sync_full_catalog,
933 catalog_sync_status,
934 set_show_server_catalog,
935 library_get_settings,
937 library_set_settings,
938 library_get_exclusion_candidates,
939 resume_queued_downloads,
940 get_download_manager_stats,
941 set_max_concurrent_downloads,
942 get_smart_cache_stats,
943 update_smart_cache_config,
944 get_smart_cache_config,
945 set_network_state,
947 get_downloads_allowed,
948 get_album_recommendations,
949 get_album_affinity_status,
950 pin_item,
952 unpin_item,
953 is_item_pinned,
954 offline_is_available,
956 offline_get_items,
957 offline_search,
958 storage_get_libraries,
960 storage_get_items,
961 storage_get_item,
962 storage_search_items,
963 storage_save_library,
964 storage_save_item,
965 storage_get_pending_sync_count,
966 sync_queue_mutation,
968 sync_get_pending,
969 sync_mark_processing,
970 sync_mark_completed,
971 sync_mark_failed,
972 sync_get_pending_count,
973 sync_process_pending,
974 sync_cleanup_completed,
975 sync_clear_user,
976 thumbnail_get_cached,
978 thumbnail_save,
979 thumbnail_get_stats,
980 thumbnail_set_limit,
981 thumbnail_clear_cache,
982 thumbnail_delete_item,
983 image_get_url,
984 storage_save_person,
986 storage_get_person,
987 storage_save_item_people,
988 storage_get_item_people,
989 storage_save_series_audio_preference,
991 storage_get_series_audio_preference,
992 repository_create,
994 repository_destroy,
995 repository_get_libraries,
996 repository_get_items,
997 repository_get_item,
998 repository_get_downloaded_libraries,
999 repository_get_downloaded_items,
1000 repository_get_download_disk_usage,
1001 repository_jray_actors_at,
1002 repository_get_latest_items,
1003 repository_get_resume_items,
1004 repository_get_next_up_episodes,
1005 repository_get_series_episodes,
1006 repository_get_series_current_episode,
1007 repository_clear_watch_history,
1008 repository_get_recently_played_audio,
1009 repository_get_resume_movies,
1010 repository_get_rediscover_albums,
1011 repository_get_genres,
1012 repository_search,
1013 repository_get_playback_info,
1014 repository_get_video_stream_url,
1015 repository_get_stream_selection,
1016 repository_get_audio_stream_url,
1017 repository_get_audio_only_stream_url_for_video,
1018 repository_get_live_tv_channels,
1019 repository_get_channels,
1020 repository_open_live_stream,
1021 repository_report_playback_start,
1022 repository_report_playback_progress,
1023 repository_report_playback_stopped,
1024 repository_get_image_url,
1025 repository_mark_favorite,
1026 repository_unmark_favorite,
1027 repository_get_favorites,
1028 repository_get_person,
1029 repository_get_items_by_person,
1030 repository_get_similar_items,
1031 repository_get_subtitle_url,
1032 repository_get_video_download_url,
1033 playlist_create,
1035 playlist_delete,
1036 playlist_rename,
1037 playlist_get_items,
1038 playlist_add_items,
1039 playlist_remove_items,
1040 playlist_move_item,
1041 diagnostics_get_info,
1044 diagnostics_set_level,
1045 diagnostics_export,
1046 format_time_seconds,
1048 format_time_seconds_long,
1049 convert_ticks_to_seconds,
1050 calc_progress,
1051 convert_percent_to_volume,
1052 ])
1053}
1054
1055#[cfg(target_os = "linux")]
1064fn enable_linux_hardware_video_decoding() {
1065 let rank_overrides = "vah264dec:MAX,vah265dec:MAX,vavp9dec:MAX,vaav1dec:MAX,\
1070 vampeg2dec:MAX,vavp8dec:MAX";
1071
1072 set_env_if_unset("GST_PLUGIN_FEATURE_RANK", rank_overrides);
1073
1074 set_env_if_unset("WEBKIT_GST_ENABLE_HW_VIDEO_DECODER", "1");
1077
1078 info!("[INIT] Linux hardware video decoding (VAAPI) enabled where supported");
1079
1080 log_available_vaapi_decoders();
1081}
1082
1083#[cfg(target_os = "linux")]
1088fn log_available_vaapi_decoders() {
1089 const HW_DECODERS: &[&str] = &[
1090 "vah264dec",
1091 "vah265dec",
1092 "vavp9dec",
1093 "vaav1dec",
1094 "vampeg2dec",
1095 "vavp8dec",
1096 ];
1097
1098 let available: Vec<&str> = HW_DECODERS
1099 .iter()
1100 .copied()
1101 .filter(|name| {
1102 std::process::Command::new("gst-inspect-1.0")
1103 .arg(name)
1104 .stdout(std::process::Stdio::null())
1105 .stderr(std::process::Stdio::null())
1106 .status()
1107 .map(|s| s.success())
1108 .unwrap_or(false)
1109 })
1110 .collect();
1111
1112 if available.is_empty() {
1113 log::warn!(
1114 "[INIT] No VAAPI hardware video decoders found via gst-inspect-1.0; \
1115 video will use software decoding. Install the GStreamer 'va' plugin \
1116 (gst-plugins-bad) and a VAAPI driver to enable hardware decoding."
1117 );
1118 } else {
1119 info!(
1120 "[INIT] VAAPI hardware video decoders available to GStreamer: {}",
1121 available.join(", ")
1122 );
1123 }
1124}
1125
1126#[cfg(target_os = "linux")]
1127fn set_env_if_unset(key: &str, value: &str) {
1128 if std::env::var_os(key).is_none() {
1129 std::env::set_var(key, value);
1132 }
1133}
1134
1135fn build_log_plugin() -> tauri::plugin::TauriPlugin<tauri::Wry> {
1174 use tauri_plugin_log::{Target, TargetKind};
1175
1176 let mut targets = vec![
1177 Target::new(TargetKind::Stdout),
1178 Target::new(TargetKind::LogDir {
1179 file_name: Some("jellytau".to_string()),
1180 }),
1181 ];
1182
1183 if cfg!(debug_assertions) {
1187 targets.push(Target::new(TargetKind::Webview));
1188 }
1189
1190 tauri_plugin_log::Builder::new()
1191 .targets(targets)
1192 .level(log::LevelFilter::Info)
1193 .max_file_size(5 * 1024 * 1024)
1194 .rotation_strategy(tauri_plugin_log::RotationStrategy::KeepOne)
1195 .format(|out, message, record| {
1196 out.finish(format_args!(
1197 "[{}][{}] {}",
1198 record.level(),
1199 record.target(),
1200 crate::utils::diagnostics::redact(&message.to_string())
1201 ))
1202 })
1203 .build()
1204}
1205
1206#[cfg_attr(mobile, tauri::mobile_entry_point)]
1207pub fn run() {
1208 crate::utils::diagnostics::install_panic_hook();
1213
1214 #[cfg(target_os = "linux")]
1219 enable_linux_hardware_video_decoding();
1220
1221 let builder = specta_builder();
1226 let invoke_handler = builder.invoke_handler();
1227
1228 tauri::Builder::default()
1229 .plugin(build_log_plugin())
1230 .plugin(tauri_plugin_opener::init())
1231 .plugin(tauri_plugin_os::init())
1232 .invoke_handler(invoke_handler)
1233 .setup(move |app| {
1234 builder.mount_events(app);
1237
1238
1239 if let Ok(config_dir) = app.path().app_config_dir() {
1256 if let Some(level) = crate::commands::diagnostics::stored_level(&config_dir) {
1257 log::set_max_level(level);
1258 log::info!("[DIAG] restored log level from settings: {level}");
1259 }
1260 }
1261
1262 #[cfg(desktop)]
1264 {
1265 app.handle()
1266 .plugin(tauri_plugin_updater::Builder::new().build())?;
1267 app.handle().plugin(tauri_plugin_process::init())?;
1268 }
1269
1270 let db_path = if let Ok(test_data_dir) = std::env::var("JELLYTAU_DATA_DIR") {
1273 let test_path = std::path::PathBuf::from(test_data_dir);
1274 info!("[INIT] Using test data directory: {:?}", test_path);
1275 test_path.join("jellytau.db")
1276 } else {
1277 app
1278 .path()
1279 .app_data_dir()
1280 .expect("Failed to get app data directory")
1281 .join("jellytau.db")
1282 };
1283
1284 info!("[INIT] Initializing database at: {:?}", db_path);
1285
1286 if let Some(parent) = db_path.parent() {
1288 info!("[INIT] Creating database directory: {:?}", parent);
1289 match std::fs::create_dir_all(parent) {
1290 Ok(_) => info!("[INIT] Database directory ready"),
1291 Err(e) => {
1292 error!("[INIT ERROR] Failed to create database directory: {}", e);
1293 panic!("Failed to create database directory: {}", e);
1294 }
1295 }
1296 }
1297
1298 info!("[INIT] Opening database...");
1299 let database = match Database::open(&db_path) {
1300 Ok(db) => {
1301 info!("[INIT] Database initialized successfully");
1302 db
1303 }
1304 Err(e) => {
1305 error!("[INIT ERROR] Failed to initialize database: {}", e);
1306 panic!("Failed to initialize database: {}", e);
1307 }
1308 };
1309 let db_wrapper = DatabaseWrapper(Mutex::new(database));
1310 app.manage(db_wrapper);
1311
1312 #[cfg(target_os = "android")]
1314 {
1315 if let Err(e) = crate::android_context::ensure_initialized() {
1326 log::error!("[INIT] Android context unavailable: {e}");
1327 log::error!("[INIT] Secure credential storage will fall back to the encrypted file.");
1328 }
1329
1330 info!("[INIT] Initializing Android SecureStorage for credentials...");
1331 let ctx = ndk_context::android_context();
1332 let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) };
1333
1334 match vm {
1335 Ok(java_vm) => {
1336 match java_vm.attach_current_thread() {
1337 Ok(mut env) => {
1338 let context_obj = unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) };
1339
1340 match initialize_secure_storage(&mut env, &context_obj) {
1341 Ok(()) => {
1342 info!("[INIT] Android SecureStorage initialized successfully");
1343 }
1344 Err(e) => {
1345 warn!("[INIT WARNING] Failed to initialize SecureStorage: {}. Credentials will use encrypted file fallback.", e);
1346 }
1347 }
1348 }
1349 Err(e) => {
1350 warn!("[INIT WARNING] Failed to attach JNI thread: {}. Credentials will use encrypted file fallback.", e);
1351 }
1352 }
1353 }
1354 Err(e) => {
1355 warn!("[INIT WARNING] Failed to create JavaVM: {}. Credentials will use encrypted file fallback.", e);
1356 }
1357 }
1358 }
1359
1360 info!("[INIT] Initializing credential store...");
1362 let credential_store = CredentialStore::new();
1363 let creds_wrapper = CredentialStoreWrapper(Mutex::new(credential_store));
1364 app.manage(creds_wrapper);
1365
1366 info!("[INIT] Creating shared playback reporting infrastructure...");
1368 let playback_reporter = Arc::new(tokio::sync::Mutex::new(None));
1369 let position_throttler = Arc::new(playback_reporting::EventThrottler::new());
1370
1371 info!("[INIT] Creating player backend...");
1373 let backend = create_player_backend(
1374 app.handle().clone(),
1375 playback_reporter.clone(),
1376 position_throttler.clone(),
1377 );
1378 #[cfg(target_os = "linux")]
1410 if crate::player::native_video::enabled() {
1411 use tauri::Manager;
1412 log::warn!(
1413 "[INIT] JELLYTAU_NATIVE_VIDEO=1 — attaching the experimental \
1414 video surface (mpv drawn behind the webview, no reparenting)"
1415 );
1416 if let Some(window) = app.get_webview_window("main") {
1417 match window.default_vbox() {
1418 Ok(vbox) => {
1419 let handle = crate::player::mpv_backend::registered_handle();
1420 if crate::player::video_surface::attach(&vbox, handle) {
1421 info!("[INIT] Native video surface attached");
1422 } else {
1423 log::warn!("[INIT] Native video surface unavailable");
1424 }
1425 }
1426 Err(e) => {
1427 log::warn!("[INIT] No GTK vbox for the main window: {e}")
1428 }
1429 }
1430 }
1431 }
1432
1433 let player_controller = PlayerController::new(
1438 Box::new(crate::player::LegacyPlayer::new(
1439 backend,
1440 engine_capabilities(),
1441 )),
1442 playback_reporter.clone(),
1443 position_throttler.clone(),
1444 );
1445
1446 let event_emitter = Arc::new(TauriEventEmitter::new(app.handle().clone()));
1448 player_controller.set_event_emitter(event_emitter.clone());
1449
1450 let player_arc = Arc::new(TokioMutex::new(player_controller));
1451
1452 #[cfg(target_os = "android")]
1456 {
1457 player::android::set_player_controller(player_arc.clone());
1458 }
1459
1460 let player_state = PlayerStateWrapper(player_arc.clone());
1461 app.manage(player_state);
1462
1463 info!("[INIT] Initializing media session manager...");
1465 let session_manager = MediaSessionManager::new();
1466 let session_wrapper = MediaSessionManagerWrapper(Mutex::new(session_manager));
1467 app.manage(session_wrapper);
1468
1469 info!("[INIT] Initializing playback mode manager...");
1471 let jellyfin_client = {
1472 let player = player_arc.blocking_lock();
1473 player.jellyfin_client()
1474 };
1475 let playback_mode_manager = playback_mode::PlaybackModeManager::new(
1476 jellyfin_client.clone(),
1477 player_arc.clone(),
1478 );
1479 let playback_mode_arc = Arc::new(playback_mode_manager);
1480 playback_mode_arc.set_event_emitter(event_emitter.clone());
1483 let playback_mode_wrapper = PlaybackModeManagerWrapper(playback_mode_arc.clone());
1484 app.manage(playback_mode_wrapper);
1485
1486 info!("[INIT] Initializing session poller manager...");
1488 let session_poller = session_poller::SessionPollerManager::new(
1489 jellyfin_client,
1490 playback_mode_arc.clone(),
1491 );
1492 session_poller.set_event_emitter(event_emitter.clone());
1493 let session_poller_arc = Arc::new(session_poller);
1497 let session_poller_wrapper = SessionPollerWrapper(session_poller_arc.clone());
1498 app.manage(session_poller_wrapper);
1499
1500 #[cfg(target_os = "android")]
1504 {
1505 info!("[INIT] Setting up MediaSession handler for lockscreen controls...");
1506 let media_handler = Arc::new(MediaSessionHandler {
1507 player: player_arc.clone(),
1508 playback_mode: playback_mode_arc.clone(),
1509 event_emitter: event_emitter.clone(),
1510 });
1511 set_media_command_handler(media_handler);
1512
1513 info!("[INIT] Setting up remote volume handler for Android...");
1514 let handler = Arc::new(RemoteVolumeSessionHandler {
1515 playback_mode: playback_mode_arc.clone(),
1516 });
1517 set_remote_volume_handler(handler);
1518 }
1519
1520 let video_settings = VideoSettingsWrapper(Mutex::new(VideoSettings::default()));
1522 app.manage(video_settings);
1523
1524 {
1532 let handle = app.handle().clone();
1533 tauri::async_runtime::spawn(async move {
1534 crate::commands::restore_streaming_quality(&handle).await;
1535 });
1536 }
1537
1538 {
1544 let handle = app.handle().clone();
1545 tauri::async_runtime::spawn(async move {
1546 crate::commands::restore_library_settings(&handle).await;
1547 });
1548 }
1549
1550 info!("[INIT] Initializing thumbnail cache...");
1552 let app_data_dir = if let Ok(test_data_dir) = std::env::var("JELLYTAU_DATA_DIR") {
1553 std::path::PathBuf::from(test_data_dir)
1554 } else {
1555 app
1556 .path()
1557 .app_data_dir()
1558 .expect("Failed to get app data directory")
1559 };
1560 let thumbnail_cache = ThumbnailCache::new(app_data_dir.clone(), ThumbnailCacheConfig::default());
1561 let thumbnail_wrapper = ThumbnailCacheWrapper(Arc::new(thumbnail_cache));
1562 app.manage(thumbnail_wrapper);
1563
1564 info!("[INIT] Initializing smart cache...");
1566 let smart_cache = SmartCache::new(SmartCacheConfig::default());
1567 let smart_cache_wrapper = SmartCacheWrapper(Mutex::new(smart_cache));
1568 app.manage(smart_cache_wrapper);
1569
1570 info!("[INIT] Starting local media server...");
1575 let media_server = match media_server::start(app_data_dir.clone()) {
1576 Ok(s) => Some(s),
1577 Err(e) => {
1578 error!("[INIT ERROR] Local media server failed to start: {}", e);
1581 None
1582 }
1583 };
1584 app.manage(media_server::MediaServerWrapper(media_server));
1585
1586 info!("[INIT] Initializing download manager...");
1588 let download_dir = app_data_dir.join("downloads");
1589 let download_manager = DownloadManager::new(download_dir);
1590 let download_manager_wrapper = DownloadManagerWrapper(Mutex::new(download_manager));
1591 app.manage(download_manager_wrapper);
1592
1593 app.manage(commands::download::NetworkStateWrapper(
1597 download::network::NetworkStateHandle::new(),
1598 ));
1599
1600 info!("[INIT] Initializing connectivity monitor...");
1602 let http_config = HttpConfig::default();
1603 let http_client = HttpClient::new(http_config)
1604 .expect("Failed to create HTTP client");
1605 let mut connectivity_monitor = ConnectivityMonitor::new(http_client);
1606 connectivity_monitor.set_app_handle(app.handle().clone());
1607
1608 session_poller_arc.set_connectivity_reporter(connectivity_monitor.reporter());
1612 session_poller_arc.start();
1613
1614 let connectivity_arc = Arc::new(tokio::sync::Mutex::new(connectivity_monitor));
1616 let connectivity_wrapper = ConnectivityMonitorWrapper(connectivity_arc.clone());
1617 app.manage(connectivity_wrapper);
1618
1619 info!("[INIT] Initializing auth manager...");
1621 let auth_http_config = HttpConfig::default();
1622 let auth_http_client = HttpClient::new(auth_http_config)
1623 .expect("Failed to create HTTP client for auth");
1624 let mut auth_manager = AuthManager::new(auth_http_client);
1625
1626 auth_manager.set_connectivity_monitor(connectivity_arc.clone());
1628
1629 let auth_manager_wrapper = AuthManagerWrapper(Arc::new(auth_manager));
1630 app.manage(auth_manager_wrapper);
1631
1632 info!("[INIT] Initializing session verifier wrapper...");
1634 let session_verifier_wrapper = SessionVerifierWrapper(Arc::new(tokio::sync::Mutex::new(None)));
1635 app.manage(session_verifier_wrapper);
1636
1637 info!("[INIT] Initializing repository manager...");
1639 let repository_manager = commands::RepositoryManager::new();
1640 let repository_manager_wrapper = RepositoryManagerWrapper(repository_manager);
1641 app.manage(repository_manager_wrapper);
1642
1643 info!("[INIT] Initializing playback reporter wrapper...");
1649 let playback_reporter_wrapper = PlaybackReporterWrapper(playback_reporter.clone());
1650 app.manage(playback_reporter_wrapper);
1651
1652 info!("[INIT] Starting background catalog indexer...");
1658 commands::catalog::spawn_catalog_indexer(app.handle().clone());
1659
1660 info!("[INIT] Starting favourites drain...");
1665 commands::favorites::spawn_favorites_drain(app.handle().clone());
1666
1667 info!("[INIT] Starting sync-queue drain...");
1672 commands::sync_drain::spawn_sync_queue_drain(app.handle().clone());
1673
1674 info!("[INIT] Application setup completed successfully");
1675 Ok(())
1676 })
1677 .run(tauri::generate_context!())
1678 .expect("error while running tauri application");
1679}
1680
1681#[cfg(test)]
1682mod specta_bindings {
1683 #[test]
1685 fn export_typescript_bindings() {
1686 super::specta_builder()
1687 .export(
1688 specta_typescript::Typescript::default()
1689 .bigint(specta_typescript::BigIntExportBehavior::Number),
1690 "../src/lib/api/bindings.ts",
1691 )
1692 .expect("failed to export typescript bindings");
1693 }
1694}