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(target_os = "linux")]
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(target_os = "linux")]
699 {
700 info!("Linux platform detected - initializing MPV backend for audio");
701 match MpvBackend::new(Some(_event_emitter), playback_reporter, position_throttler) {
702 Ok(backend) => {
703 info!("Successfully initialized MPV backend for Linux");
704 Box::new(backend)
705 }
706 Err(e) => {
707 error!("\n========================================");
708 error!("FATAL ERROR: Failed to initialize MPV backend");
709 error!("========================================");
710 error!("Error: {}", e);
711 error!("\nCommon causes:");
712 error!(" 1. MPV is not installed");
713 error!(" Solution: Install MPV using your package manager");
714 error!(" - Arch/CachyOS: sudo pacman -S mpv");
715 error!(" - Ubuntu/Debian: sudo apt install mpv libmpv-dev");
716 error!(" - Fedora: sudo dnf install mpv mpv-libs-devel");
717 error!("\n 2. MPV version mismatch (app was built with different libmpv version)");
718 error!(" Solution: Rebuild the application");
719 error!(" - cd src-tauri && cargo clean && cargo build --release");
720 error!("\n 3. Audio system not working");
721 error!(" Solution: Verify audio works with: pactl info");
722 error!("\nAudio playback will NOT work until this is fixed.");
723 error!("========================================\n");
724
725 emit_backend_init_failed(&app_handle, "mpv", e.to_string());
729 Box::new(NullBackend::new())
730 }
731 }
732 }
733
734 #[cfg(not(any(target_os = "linux", target_os = "android")))]
738 {
739 info!("No native audio backend for this platform - using webview <audio> backend");
740 match player::WebviewAudioBackend::new(_event_emitter) {
741 Ok(backend) => Box::new(backend),
742 Err(e) => {
743 emit_backend_init_failed(&app_handle, "webview-audio", e.to_string());
744 Box::new(NullBackend::new())
745 }
746 }
747 }
748}
749
750fn engine_capabilities() -> crate::player::media_player::Capabilities {
763 #[cfg(target_os = "android")]
764 {
765 crate::player::media_player::Capabilities::exoplayer()
766 }
767 #[cfg(not(target_os = "android"))]
768 {
769 crate::player::media_player::Capabilities::mpv()
770 }
771}
772
773fn specta_builder() -> Builder<tauri::Wry> {
774 Builder::<tauri::Wry>::new()
775 .error_handling(tauri_specta::ErrorHandlingMode::Throw)
778 .events(tauri_specta::collect_events![
779 crate::player::events::PlayerStatusEvent
780 ])
781 .commands(tauri_specta::collect_commands![
782 player_play_item,
784 player_background_action,
785 player_enter_background_audio,
786 player_exit_background_audio,
787 player_play_queue,
788 player_play_album_track,
789 player_play_tracks,
790 player_play,
791 player_pause,
792 player_toggle,
793 player_stop,
794 player_next,
795 player_previous,
796 player_seek,
797 player_seek_video,
798 player_set_volume,
799 player_toggle_mute,
800 player_set_audio_track,
801 player_switch_audio_track,
802 player_set_subtitle_track,
803 player_toggle_shuffle,
804 player_cycle_repeat,
805 player_get_status,
806 player_get_queue,
807 player_get_capabilities,
808 player_add_to_queue,
809 player_add_track_by_id,
810 player_add_tracks_by_ids,
811 player_remove_from_queue,
812 player_move_in_queue,
813 player_skip_to,
814 player_set_audio_settings,
815 player_get_audio_settings,
816 player_get_eq_presets,
817 player_set_video_settings,
818 player_get_video_settings,
819 player_get_streaming_qualities,
820 player_set_stream_quality,
821 player_set_sleep_timer,
823 player_cancel_sleep_timer,
824 player_get_sleep_timer,
825 player_get_autoplay_settings,
826 player_set_autoplay_settings,
827 player_cancel_autoplay_countdown,
828 player_play_next_episode,
829 player_on_playback_ended,
830 player_recover_stream,
831 player_report_state,
832 player_report_position,
833 player_report_media_loaded,
834 player_local_media_path,
836 player_preload_upcoming,
837 player_set_cache_config,
838 player_get_cache_config,
839 player_configure_jellyfin,
841 player_disable_jellyfin,
842 player_get_session,
844 player_dismiss_session,
845 remote_play_on_session,
847 remote_send_command,
848 remote_session_seek,
849 remote_session_set_volume,
850 remote_session_toggle_mute,
851 lms_get_sync_groups,
853 lms_create_sync_group,
854 lms_unsync_player,
855 lms_dissolve_sync_group,
856 sessions_set_polling_hint,
858 sessions_poll_now,
859 playback_mode_get_current,
861 playback_mode_set,
862 playback_mode_is_transferring,
863 playback_mode_transfer_to_remote,
864 playback_mode_get_remote_status,
865 playback_mode_transfer_to_local,
866 playback_mode_set_transferring,
867 playback_reporter_init,
869 playback_reporter_destroy,
870 playback_report_start,
871 playback_report_progress,
872 playback_report_stopped,
873 playback_mark_played,
874 auth_initialize,
876 auth_connect_to_server,
877 auth_login,
878 auth_verify_session,
879 auth_logout,
880 auth_get_session,
881 auth_set_session,
882 auth_start_verification,
883 auth_stop_verification,
884 auth_reauthenticate,
885 device_get_id,
887 device_set_id,
888 connectivity_check_server,
890 connectivity_set_server_url,
891 connectivity_get_status,
892 connectivity_start_monitoring,
893 connectivity_stop_monitoring,
894 connectivity_mark_reachable,
895 connectivity_mark_unreachable,
896 storage_init,
898 storage_get_path,
899 storage_get_size,
900 storage_get_security_status,
901 storage_save_server,
902 storage_get_servers,
903 storage_delete_server,
904 storage_save_user,
905 storage_get_users,
906 storage_set_active_user,
907 storage_get_active_user,
908 storage_get_active_session,
909 storage_get_access_token,
910 storage_delete_user,
911 storage_update_playback_progress,
913 storage_update_playback_context,
914 storage_mark_played,
915 storage_set_watched,
916 storage_get_playback_progress,
917 storage_mark_synced,
918 storage_toggle_favorite,
919 download_item,
921 download_item_and_start,
922 download_album,
923 download_video,
924 download_series,
925 download_season,
926 get_downloads,
927 pause_download,
928 resume_download,
929 cancel_download,
930 delete_download,
931 delete_all_downloads,
932 delete_album_downloads,
933 delete_downloads_under,
934 clear_stale_downloads,
935 get_download_storage_stats,
936 mark_download_completed,
937 mark_download_failed,
938 media_local_url,
939 media_local_selection,
940 start_download,
941 enqueue_download,
942 enqueue_video_downloads,
943 sync_full_catalog,
944 catalog_sync_status,
945 set_show_server_catalog,
946 library_get_settings,
948 library_set_settings,
949 library_get_exclusion_candidates,
950 resume_queued_downloads,
951 get_download_manager_stats,
952 set_max_concurrent_downloads,
953 get_smart_cache_stats,
954 update_smart_cache_config,
955 get_smart_cache_config,
956 set_network_state,
958 get_downloads_allowed,
959 get_album_recommendations,
960 get_album_affinity_status,
961 pin_item,
963 unpin_item,
964 is_item_pinned,
965 offline_is_available,
967 offline_get_items,
968 offline_search,
969 storage_get_libraries,
971 storage_get_items,
972 storage_get_item,
973 storage_search_items,
974 storage_save_library,
975 storage_save_item,
976 storage_get_pending_sync_count,
977 sync_queue_mutation,
979 sync_get_pending,
980 sync_mark_processing,
981 sync_mark_completed,
982 sync_mark_failed,
983 sync_get_pending_count,
984 sync_process_pending,
985 sync_cleanup_completed,
986 sync_clear_user,
987 thumbnail_get_cached,
989 thumbnail_save,
990 thumbnail_get_stats,
991 thumbnail_set_limit,
992 thumbnail_clear_cache,
993 thumbnail_delete_item,
994 image_get_url,
995 storage_save_person,
997 storage_get_person,
998 storage_save_item_people,
999 storage_get_item_people,
1000 storage_save_series_audio_preference,
1002 storage_get_series_audio_preference,
1003 repository_create,
1005 repository_destroy,
1006 repository_get_libraries,
1007 repository_get_items,
1008 repository_get_item,
1009 repository_get_downloaded_libraries,
1010 repository_get_downloaded_items,
1011 repository_get_download_disk_usage,
1012 repository_jray_actors_at,
1013 repository_get_latest_items,
1014 repository_get_resume_items,
1015 repository_get_next_up_episodes,
1016 repository_get_series_episodes,
1017 repository_get_series_current_episode,
1018 repository_get_series_view,
1019 repository_clear_watch_history,
1020 repository_get_recently_played_audio,
1021 repository_get_resume_movies,
1022 repository_get_rediscover_albums,
1023 repository_get_genres,
1024 repository_search,
1025 repository_get_playback_info,
1026 repository_get_video_stream_url,
1027 repository_get_stream_selection,
1028 repository_get_audio_stream_url,
1029 repository_get_audio_only_stream_url_for_video,
1030 repository_get_live_tv_channels,
1031 repository_get_channels,
1032 repository_open_live_stream,
1033 repository_report_playback_start,
1034 repository_report_playback_progress,
1035 repository_report_playback_stopped,
1036 repository_get_image_url,
1037 repository_mark_favorite,
1038 repository_unmark_favorite,
1039 repository_get_favorites,
1040 repository_get_person,
1041 repository_get_items_by_person,
1042 repository_get_similar_items,
1043 repository_get_subtitle_url,
1044 repository_get_video_download_url,
1045 playlist_create,
1047 playlist_delete,
1048 playlist_rename,
1049 playlist_get_items,
1050 playlist_add_items,
1051 profiles_add,
1052 profiles_get_ask_on_start,
1053 profiles_list,
1054 profiles_remove,
1055 profiles_set_ask_on_start,
1056 profiles_set_pin,
1057 profiles_startup_target,
1058 profiles_unlock,
1059 profiles_unlock_with_password,
1060 playlist_remove_items,
1061 playlist_move_item,
1062 diagnostics_get_info,
1065 diagnostics_set_level,
1066 diagnostics_export,
1067 format_time_seconds,
1069 format_time_seconds_long,
1070 convert_ticks_to_seconds,
1071 calc_progress,
1072 convert_percent_to_volume,
1073 ])
1074}
1075
1076#[cfg(target_os = "linux")]
1085fn enable_linux_hardware_video_decoding() {
1086 let rank_overrides = "vah264dec:MAX,vah265dec:MAX,vavp9dec:MAX,vaav1dec:MAX,\
1091 vampeg2dec:MAX,vavp8dec:MAX";
1092
1093 set_env_if_unset("GST_PLUGIN_FEATURE_RANK", rank_overrides);
1094
1095 set_env_if_unset("WEBKIT_GST_ENABLE_HW_VIDEO_DECODER", "1");
1098
1099 info!("[INIT] Linux hardware video decoding (VAAPI) enabled where supported");
1100
1101 log_available_vaapi_decoders();
1102}
1103
1104#[cfg(target_os = "linux")]
1109fn log_available_vaapi_decoders() {
1110 const HW_DECODERS: &[&str] = &[
1111 "vah264dec",
1112 "vah265dec",
1113 "vavp9dec",
1114 "vaav1dec",
1115 "vampeg2dec",
1116 "vavp8dec",
1117 ];
1118
1119 let available: Vec<&str> = HW_DECODERS
1120 .iter()
1121 .copied()
1122 .filter(|name| {
1123 std::process::Command::new("gst-inspect-1.0")
1124 .arg(name)
1125 .stdout(std::process::Stdio::null())
1126 .stderr(std::process::Stdio::null())
1127 .status()
1128 .map(|s| s.success())
1129 .unwrap_or(false)
1130 })
1131 .collect();
1132
1133 if available.is_empty() {
1134 log::warn!(
1135 "[INIT] No VAAPI hardware video decoders found via gst-inspect-1.0; \
1136 video will use software decoding. Install the GStreamer 'va' plugin \
1137 (gst-plugins-bad) and a VAAPI driver to enable hardware decoding."
1138 );
1139 } else {
1140 info!(
1141 "[INIT] VAAPI hardware video decoders available to GStreamer: {}",
1142 available.join(", ")
1143 );
1144 }
1145}
1146
1147#[cfg(target_os = "linux")]
1148fn set_env_if_unset(key: &str, value: &str) {
1149 if std::env::var_os(key).is_none() {
1150 std::env::set_var(key, value);
1153 }
1154}
1155
1156fn build_log_plugin() -> tauri::plugin::TauriPlugin<tauri::Wry> {
1195 use tauri_plugin_log::{Target, TargetKind};
1196
1197 let mut targets = vec![
1198 Target::new(TargetKind::Stdout),
1199 Target::new(TargetKind::LogDir {
1200 file_name: Some("jellytau".to_string()),
1201 }),
1202 ];
1203
1204 if cfg!(debug_assertions) {
1208 targets.push(Target::new(TargetKind::Webview));
1209 }
1210
1211 tauri_plugin_log::Builder::new()
1212 .targets(targets)
1213 .level(log::LevelFilter::Info)
1214 .max_file_size(5 * 1024 * 1024)
1215 .rotation_strategy(tauri_plugin_log::RotationStrategy::KeepOne)
1216 .format(|out, message, record| {
1217 out.finish(format_args!(
1218 "[{}][{}] {}",
1219 record.level(),
1220 record.target(),
1221 crate::utils::diagnostics::redact(&message.to_string())
1222 ))
1223 })
1224 .build()
1225}
1226
1227#[cfg_attr(mobile, tauri::mobile_entry_point)]
1228pub fn run() {
1229 crate::utils::diagnostics::install_panic_hook();
1234
1235 #[cfg(target_os = "linux")]
1240 enable_linux_hardware_video_decoding();
1241
1242 let builder = specta_builder();
1247 let invoke_handler = builder.invoke_handler();
1248
1249 tauri::Builder::default()
1250 .plugin(build_log_plugin())
1251 .plugin(tauri_plugin_opener::init())
1252 .plugin(tauri_plugin_os::init())
1253 .invoke_handler(invoke_handler)
1254 .setup(move |app| {
1255 builder.mount_events(app);
1258
1259
1260 if let Ok(config_dir) = app.path().app_config_dir() {
1277 if let Some(level) = crate::commands::diagnostics::stored_level(&config_dir) {
1278 log::set_max_level(level);
1279 log::info!("[DIAG] restored log level from settings: {level}");
1280 }
1281 }
1282
1283 #[cfg(desktop)]
1285 {
1286 app.handle()
1287 .plugin(tauri_plugin_updater::Builder::new().build())?;
1288 app.handle().plugin(tauri_plugin_process::init())?;
1289 }
1290
1291 let db_path = if let Ok(test_data_dir) = std::env::var("JELLYTAU_DATA_DIR") {
1294 let test_path = std::path::PathBuf::from(test_data_dir);
1295 info!("[INIT] Using test data directory: {:?}", test_path);
1296 test_path.join("jellytau.db")
1297 } else {
1298 app
1299 .path()
1300 .app_data_dir()
1301 .expect("Failed to get app data directory")
1302 .join("jellytau.db")
1303 };
1304
1305 info!("[INIT] Initializing database at: {:?}", db_path);
1306
1307 if let Some(parent) = db_path.parent() {
1309 info!("[INIT] Creating database directory: {:?}", parent);
1310 match std::fs::create_dir_all(parent) {
1311 Ok(_) => info!("[INIT] Database directory ready"),
1312 Err(e) => {
1313 error!("[INIT ERROR] Failed to create database directory: {}", e);
1314 panic!("Failed to create database directory: {}", e);
1315 }
1316 }
1317 }
1318
1319 info!("[INIT] Opening database...");
1320 let database = match Database::open(&db_path) {
1321 Ok(db) => {
1322 info!("[INIT] Database initialized successfully");
1323 db
1324 }
1325 Err(e) => {
1326 error!("[INIT ERROR] Failed to initialize database: {}", e);
1327 panic!("Failed to initialize database: {}", e);
1328 }
1329 };
1330 let db_wrapper = DatabaseWrapper(Mutex::new(database));
1331 app.manage(db_wrapper);
1332
1333 #[cfg(target_os = "android")]
1335 {
1336 if let Err(e) = crate::android_context::ensure_initialized() {
1347 log::error!("[INIT] Android context unavailable: {e}");
1348 log::error!("[INIT] Secure credential storage will fall back to the encrypted file.");
1349 }
1350
1351 info!("[INIT] Initializing Android SecureStorage for credentials...");
1352 let ctx = ndk_context::android_context();
1353 let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) };
1354
1355 match vm {
1356 Ok(java_vm) => {
1357 match java_vm.attach_current_thread() {
1358 Ok(mut env) => {
1359 let context_obj = unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) };
1360
1361 match initialize_secure_storage(&mut env, &context_obj) {
1362 Ok(()) => {
1363 info!("[INIT] Android SecureStorage initialized successfully");
1364 }
1365 Err(e) => {
1366 warn!("[INIT WARNING] Failed to initialize SecureStorage: {}. Credentials will use encrypted file fallback.", e);
1367 }
1368 }
1369 }
1370 Err(e) => {
1371 warn!("[INIT WARNING] Failed to attach JNI thread: {}. Credentials will use encrypted file fallback.", e);
1372 }
1373 }
1374 }
1375 Err(e) => {
1376 warn!("[INIT WARNING] Failed to create JavaVM: {}. Credentials will use encrypted file fallback.", e);
1377 }
1378 }
1379 }
1380
1381 info!("[INIT] Initializing credential store...");
1383 let credential_store = CredentialStore::new();
1384 let creds_wrapper = CredentialStoreWrapper(Mutex::new(credential_store));
1385 app.manage(creds_wrapper);
1386
1387 info!("[INIT] Creating shared playback reporting infrastructure...");
1389 let playback_reporter = Arc::new(tokio::sync::Mutex::new(None));
1390 let position_throttler = Arc::new(playback_reporting::EventThrottler::new());
1391
1392 info!("[INIT] Creating player backend...");
1394 let backend = create_player_backend(
1395 app.handle().clone(),
1396 playback_reporter.clone(),
1397 position_throttler.clone(),
1398 );
1399 #[cfg(target_os = "linux")]
1431 if crate::player::native_video::enabled() {
1432 use tauri::Manager;
1433 log::warn!(
1434 "[INIT] JELLYTAU_NATIVE_VIDEO=1 — attaching the experimental \
1435 video surface (mpv drawn behind the webview, no reparenting)"
1436 );
1437 if let Some(window) = app.get_webview_window("main") {
1438 match window.default_vbox() {
1439 Ok(vbox) => {
1440 let handle = crate::player::mpv_backend::registered_handle();
1441 if crate::player::video_surface::attach(&vbox, handle) {
1442 info!("[INIT] Native video surface attached");
1443 } else {
1444 log::warn!("[INIT] Native video surface unavailable");
1445 }
1446 }
1447 Err(e) => {
1448 log::warn!("[INIT] No GTK vbox for the main window: {e}")
1449 }
1450 }
1451 }
1452 }
1453
1454 let player_controller = PlayerController::new(
1459 Box::new(crate::player::LegacyPlayer::new(
1460 backend,
1461 engine_capabilities(),
1462 )),
1463 playback_reporter.clone(),
1464 position_throttler.clone(),
1465 );
1466
1467 let event_emitter = Arc::new(TauriEventEmitter::new(app.handle().clone()));
1469 player_controller.set_event_emitter(event_emitter.clone());
1470
1471 let player_arc = Arc::new(TokioMutex::new(player_controller));
1472
1473 #[cfg(target_os = "android")]
1477 {
1478 player::android::set_player_controller(player_arc.clone());
1479 }
1480
1481 let player_state = PlayerStateWrapper(player_arc.clone());
1482 app.manage(player_state);
1483
1484 info!("[INIT] Initializing media session manager...");
1486 let session_manager = MediaSessionManager::new();
1487 let session_wrapper = MediaSessionManagerWrapper(Mutex::new(session_manager));
1488 app.manage(session_wrapper);
1489
1490 info!("[INIT] Initializing playback mode manager...");
1492 let jellyfin_client = {
1493 let player = player_arc.blocking_lock();
1494 player.jellyfin_client()
1495 };
1496 let playback_mode_manager = playback_mode::PlaybackModeManager::new(
1497 jellyfin_client.clone(),
1498 player_arc.clone(),
1499 );
1500 let playback_mode_arc = Arc::new(playback_mode_manager);
1501 playback_mode_arc.set_event_emitter(event_emitter.clone());
1504 let playback_mode_wrapper = PlaybackModeManagerWrapper(playback_mode_arc.clone());
1505 app.manage(playback_mode_wrapper);
1506
1507 info!("[INIT] Initializing session poller manager...");
1509 let session_poller = session_poller::SessionPollerManager::new(
1510 jellyfin_client,
1511 playback_mode_arc.clone(),
1512 );
1513 session_poller.set_event_emitter(event_emitter.clone());
1514 let session_poller_arc = Arc::new(session_poller);
1518 let session_poller_wrapper = SessionPollerWrapper(session_poller_arc.clone());
1519 app.manage(session_poller_wrapper);
1520
1521 #[cfg(target_os = "android")]
1525 {
1526 info!("[INIT] Setting up MediaSession handler for lockscreen controls...");
1527 let media_handler = Arc::new(MediaSessionHandler {
1528 player: player_arc.clone(),
1529 playback_mode: playback_mode_arc.clone(),
1530 event_emitter: event_emitter.clone(),
1531 });
1532 set_media_command_handler(media_handler);
1533
1534 info!("[INIT] Setting up remote volume handler for Android...");
1535 let handler = Arc::new(RemoteVolumeSessionHandler {
1536 playback_mode: playback_mode_arc.clone(),
1537 });
1538 set_remote_volume_handler(handler);
1539 }
1540
1541 let video_settings = VideoSettingsWrapper(Mutex::new(VideoSettings::default()));
1543 app.manage(video_settings);
1544
1545 {
1553 let handle = app.handle().clone();
1554 tauri::async_runtime::spawn(async move {
1555 crate::commands::restore_streaming_quality(&handle).await;
1556 });
1557 }
1558
1559 {
1565 let handle = app.handle().clone();
1566 tauri::async_runtime::spawn(async move {
1567 crate::commands::restore_library_settings(&handle).await;
1568 });
1569 }
1570
1571 info!("[INIT] Initializing thumbnail cache...");
1573 let app_data_dir = if let Ok(test_data_dir) = std::env::var("JELLYTAU_DATA_DIR") {
1574 std::path::PathBuf::from(test_data_dir)
1575 } else {
1576 app
1577 .path()
1578 .app_data_dir()
1579 .expect("Failed to get app data directory")
1580 };
1581 let thumbnail_cache = ThumbnailCache::new(app_data_dir.clone(), ThumbnailCacheConfig::default());
1582 let thumbnail_wrapper = ThumbnailCacheWrapper(Arc::new(thumbnail_cache));
1583 app.manage(thumbnail_wrapper);
1584
1585 info!("[INIT] Initializing smart cache...");
1587 let smart_cache = SmartCache::new(SmartCacheConfig::default());
1588 let smart_cache_wrapper = SmartCacheWrapper(Mutex::new(smart_cache));
1589 app.manage(smart_cache_wrapper);
1590
1591 info!("[INIT] Starting local media server...");
1596 let media_server = match media_server::start(app_data_dir.clone()) {
1597 Ok(s) => Some(s),
1598 Err(e) => {
1599 error!("[INIT ERROR] Local media server failed to start: {}", e);
1602 None
1603 }
1604 };
1605 app.manage(media_server::MediaServerWrapper(media_server));
1606
1607 info!("[INIT] Initializing download manager...");
1609 let download_dir = app_data_dir.join("downloads");
1610 let download_manager = DownloadManager::new(download_dir);
1611 let download_manager_wrapper = DownloadManagerWrapper(Mutex::new(download_manager));
1612 app.manage(download_manager_wrapper);
1613
1614 app.manage(commands::download::NetworkStateWrapper(
1618 download::network::NetworkStateHandle::new(),
1619 ));
1620
1621 info!("[INIT] Initializing connectivity monitor...");
1623 let http_config = HttpConfig::default();
1624 let http_client = HttpClient::new(http_config)
1625 .expect("Failed to create HTTP client");
1626 let mut connectivity_monitor = ConnectivityMonitor::new(http_client);
1627 connectivity_monitor.set_app_handle(app.handle().clone());
1628
1629 session_poller_arc.set_connectivity_reporter(connectivity_monitor.reporter());
1633 session_poller_arc.start();
1634
1635 let connectivity_arc = Arc::new(tokio::sync::Mutex::new(connectivity_monitor));
1637 let connectivity_wrapper = ConnectivityMonitorWrapper(connectivity_arc.clone());
1638 app.manage(connectivity_wrapper);
1639
1640 info!("[INIT] Initializing auth manager...");
1642 let auth_http_config = HttpConfig::default();
1643 let auth_http_client = HttpClient::new(auth_http_config)
1644 .expect("Failed to create HTTP client for auth");
1645 let mut auth_manager = AuthManager::new(auth_http_client);
1646
1647 auth_manager.set_connectivity_monitor(connectivity_arc.clone());
1649
1650 let auth_manager_wrapper = AuthManagerWrapper(Arc::new(auth_manager));
1651 app.manage(auth_manager_wrapper);
1652
1653 info!("[INIT] Initializing session verifier wrapper...");
1655 let session_verifier_wrapper = SessionVerifierWrapper(Arc::new(tokio::sync::Mutex::new(None)));
1656 app.manage(session_verifier_wrapper);
1657
1658 info!("[INIT] Initializing repository manager...");
1660 let repository_manager = commands::RepositoryManager::new();
1661 let repository_manager_wrapper = RepositoryManagerWrapper(repository_manager);
1662 app.manage(repository_manager_wrapper);
1663
1664 info!("[INIT] Initializing playback reporter wrapper...");
1670 let playback_reporter_wrapper = PlaybackReporterWrapper(playback_reporter.clone());
1671 app.manage(playback_reporter_wrapper);
1672
1673 info!("[INIT] Starting background catalog indexer...");
1679 commands::catalog::spawn_catalog_indexer(app.handle().clone());
1680
1681 info!("[INIT] Starting favourites drain...");
1686 commands::favorites::spawn_favorites_drain(app.handle().clone());
1687
1688 info!("[INIT] Starting sync-queue drain...");
1693 commands::sync_drain::spawn_sync_queue_drain(app.handle().clone());
1694
1695 info!("[INIT] Application setup completed successfully");
1696 Ok(())
1697 })
1698 .run(tauri::generate_context!())
1699 .expect("error while running tauri application");
1700}
1701
1702#[cfg(test)]
1703mod specta_bindings {
1704 #[test]
1706 fn export_typescript_bindings() {
1707 super::specta_builder()
1708 .export(
1709 specta_typescript::Typescript::default()
1710 .bigint(specta_typescript::BigIntExportBehavior::Number),
1711 "../src/lib/api/bindings.ts",
1712 )
1713 .expect("failed to export typescript bindings");
1714 }
1715}