1#[cfg(target_os = "android")]
2mod android_context;
3mod auth;
4mod commands;
5mod connectivity;
6mod credentials;
7mod domain;
8mod download;
9mod jellyfin;
10mod media_server;
11mod playback_mode;
12mod playback_reporting;
13mod player;
14mod repository;
15mod session_poller;
16pub mod settings;
17mod storage;
18mod thumbnail;
19pub mod utils;
20
21#[cfg(target_os = "android")]
22use log::warn;
23use log::{error, info};
24use std::sync::{Arc, Mutex};
25use tauri::{Emitter, Manager};
26use tauri_specta::Builder;
27use tokio::sync::Mutex as TokioMutex;
28
29use auth::AuthManager;
30use commands::{
31 auth_connect_to_server,
32 auth_get_session,
33 auth_initialize,
35 auth_login,
36 auth_logout,
37 auth_reauthenticate,
38 auth_set_session,
39 auth_start_verification,
40 auth_stop_verification,
41 auth_verify_session,
42 calc_progress,
43 cancel_download,
44 catalog_sync_status,
45 clear_stale_downloads,
46 connectivity_check_server,
48 connectivity_get_status,
49 connectivity_mark_reachable,
50 connectivity_mark_unreachable,
51 connectivity_set_server_url,
52 connectivity_start_monitoring,
53 connectivity_stop_monitoring,
54 convert_percent_to_volume,
55 convert_ticks_to_seconds,
56 delete_album_downloads,
57 delete_all_downloads,
58 delete_download,
59 delete_downloads_under,
60 device_get_id,
62 device_set_id,
63 diagnostics_export,
65 diagnostics_get_info,
66 diagnostics_set_level,
67 download_album,
68 download_item,
69 download_item_and_start,
70 download_season,
71 download_series,
72 download_video,
73 enqueue_download,
74 enqueue_video_downloads,
75 format_time_seconds,
77 format_time_seconds_long,
78 get_album_affinity_status,
79 get_album_recommendations,
80 get_download_manager_stats,
81 get_download_storage_stats,
82 get_downloads,
83 get_downloads_allowed,
84 get_smart_cache_config,
85 get_smart_cache_stats,
86 image_get_url,
87 is_item_pinned,
88 library_get_exclusion_candidates,
90 library_get_settings,
91 library_set_settings,
92 lms_create_sync_group,
93 lms_dissolve_sync_group,
94 lms_get_sync_groups,
96 lms_unsync_player,
97 mark_download_completed,
98 mark_download_failed,
99 media_local_url,
100 offline_get_items,
101 offline_is_available,
102 offline_search,
103 pause_download,
104 pin_item,
105 playback_mark_played,
106 playback_mode_get_current,
108 playback_mode_get_remote_status,
109 playback_mode_is_transferring,
110 playback_mode_set,
111 playback_mode_set_transferring,
112 playback_mode_transfer_to_local,
113 playback_mode_transfer_to_remote,
114 playback_report_progress,
115 playback_report_start,
116 playback_report_stopped,
117 playback_reporter_destroy,
118 playback_reporter_init,
120 player_add_to_queue,
122 player_add_track_by_id,
123 player_add_tracks_by_ids,
124 player_background_action,
125 player_cancel_autoplay_countdown,
126 player_cancel_sleep_timer,
127 player_configure_jellyfin,
129 player_cycle_repeat,
130 player_disable_jellyfin,
131 player_dismiss_session,
132 player_enter_background_audio,
133 player_exit_background_audio,
134 player_get_audio_settings,
135 player_get_autoplay_settings,
136 player_get_cache_config,
137 player_get_capabilities,
138 player_get_eq_presets,
139 player_get_queue,
140 player_get_session,
142 player_get_sleep_timer,
143 player_get_status,
144 player_get_streaming_qualities,
145 player_get_video_settings,
146 player_local_media_path,
148 player_move_in_queue,
149 player_next,
150 player_on_playback_ended,
151 player_pause,
152 player_play,
153 player_play_album_track,
154 player_play_item,
155 player_play_next_episode,
156 player_play_queue,
157 player_play_tracks,
158 player_preload_upcoming,
159 player_previous,
160 player_recover_stream,
161 player_remove_from_queue,
162 player_report_media_loaded,
163 player_report_position,
164 player_report_state,
166 player_seek,
167 player_seek_video,
168 player_set_audio_settings,
169 player_set_audio_track,
170 player_set_autoplay_settings,
171 player_set_cache_config,
172 player_set_sleep_timer,
174 player_set_stream_quality,
175 player_set_subtitle_track,
176 player_set_video_settings,
177 player_set_volume,
178 player_skip_to,
179 player_stop,
180 player_switch_audio_track,
181 player_toggle,
182 player_toggle_mute,
183 player_toggle_shuffle,
184 playlist_add_items,
185 playlist_create,
187 playlist_delete,
188 playlist_get_items,
189 playlist_move_item,
190 playlist_remove_items,
191 playlist_rename,
192 remote_play_on_session,
194 remote_send_command,
195 remote_session_seek,
196 remote_session_set_volume,
197 remote_session_toggle_mute,
198 repository_clear_watch_history,
200 repository_create,
201 repository_destroy,
202 repository_get_audio_only_stream_url_for_video,
203 repository_get_audio_stream_url,
204 repository_get_channels,
205 repository_get_download_disk_usage,
206 repository_get_downloaded_items,
207 repository_get_downloaded_libraries,
208 repository_get_favorites,
209 repository_get_genres,
210 repository_get_image_url,
211 repository_get_item,
212 repository_get_items,
213 repository_get_items_by_person,
214 repository_get_latest_items,
215 repository_get_libraries,
216 repository_get_live_tv_channels,
217 repository_get_next_up_episodes,
218 repository_get_person,
219 repository_get_playback_info,
220 repository_get_recently_played_audio,
221 repository_get_rediscover_albums,
222 repository_get_resume_items,
223 repository_get_resume_movies,
224 repository_get_series_current_episode,
225 repository_get_series_episodes,
226 repository_get_similar_items,
227 repository_get_subtitle_url,
228 repository_get_video_download_url,
229 repository_get_video_stream_url,
230 repository_jray_actors_at,
231 repository_mark_favorite,
232 repository_open_live_stream,
233 repository_report_playback_progress,
234 repository_report_playback_start,
235 repository_report_playback_stopped,
236 repository_search,
237 repository_unmark_favorite,
238 resume_download,
239 resume_queued_downloads,
240 sessions_poll_now,
241 sessions_set_polling_hint,
243 set_max_concurrent_downloads,
244 set_network_state,
245 set_show_server_catalog,
246 start_download,
247 storage_delete_server,
249 storage_delete_user,
250 storage_get_access_token,
251 storage_get_active_session,
252 storage_get_active_user,
253 storage_get_item,
254 storage_get_item_people,
255 storage_get_items,
256 storage_get_libraries,
258 storage_get_path,
259 storage_get_pending_sync_count,
260 storage_get_person,
261 storage_get_playback_progress,
262 storage_get_security_status,
263 storage_get_series_audio_preference,
264 storage_get_servers,
265 storage_get_size,
266 storage_get_users,
267 storage_init,
268 storage_mark_played,
269 storage_mark_synced,
270 storage_save_item,
271 storage_save_item_people,
272 storage_save_library,
273 storage_save_person,
275 storage_save_series_audio_preference,
277 storage_save_server,
278 storage_save_user,
279 storage_search_items,
280 storage_set_active_user,
281 storage_set_watched,
282 storage_toggle_favorite,
283 storage_update_playback_context,
284 storage_update_playback_progress,
285 sync_cleanup_completed,
286 sync_clear_user,
287 sync_full_catalog,
288 sync_get_pending,
289 sync_get_pending_count,
290 sync_mark_completed,
291 sync_mark_failed,
292 sync_mark_processing,
293 sync_process_pending,
294 sync_queue_mutation,
296 thumbnail_clear_cache,
297 thumbnail_delete_item,
298 thumbnail_get_cached,
300 thumbnail_get_stats,
301 thumbnail_save,
302 thumbnail_set_limit,
303 unpin_item,
304 update_smart_cache_config,
305 AuthManagerWrapper,
306 ConnectivityMonitorWrapper,
307 CredentialStoreWrapper,
308 DatabaseWrapper,
309 DownloadManagerWrapper,
310 MediaSessionManagerWrapper,
311 PlaybackModeManagerWrapper,
312 PlaybackReporterWrapper,
313 PlayerStateWrapper,
314 RepositoryManagerWrapper,
315 SessionPollerWrapper,
316 SessionVerifierWrapper,
317 SmartCacheWrapper,
318 ThumbnailCacheWrapper,
319 VideoSettingsWrapper,
320};
321use connectivity::ConnectivityMonitor;
322use credentials::CredentialStore;
323use download::cache::{CacheConfig as SmartCacheConfig, SmartCache};
324use download::DownloadManager;
325use jellyfin::{HttpClient, HttpConfig};
326#[cfg(target_os = "android")]
327use playback_mode::PlaybackModeManager;
328#[cfg(target_os = "android")]
331use player::seek::{resolve_skip_action, SkipAction};
332use player::{MediaSessionManager, PlayerBackend, PlayerController, TauriEventEmitter};
333use player::NullBackend;
337
338#[cfg(target_os = "linux")]
339use player::MpvBackend;
340use settings::VideoSettings;
341use storage::Database;
342use thumbnail::{CacheConfig as ThumbnailCacheConfig, ThumbnailCache};
343
344#[cfg(target_os = "android")]
345use credentials::initialize_secure_storage;
346
347#[cfg(target_os = "android")]
348use player::ExoPlayerBackend;
349
350#[cfg(target_os = "android")]
351use player::{
352 set_media_command_handler, set_remote_volume_handler, MediaCommandHandler, RemoteVolumeHandler,
353};
354
355#[cfg(target_os = "android")]
363struct MediaSessionHandler {
364 player: Arc<TokioMutex<PlayerController>>,
365 playback_mode: Arc<PlaybackModeManager>,
366 event_emitter: Arc<TauriEventEmitter>,
367}
368
369#[cfg(target_os = "android")]
370impl MediaSessionHandler {
371 fn handle_remote_command(&self, command: &str, session_id: String) {
376 use crate::player::{PlayerEventEmitter, PlayerStatusEvent};
377
378 if command == "stop" {
382 self.event_emitter
383 .emit(PlayerStatusEvent::RemoteDisconnectRequested);
384 return;
385 }
386
387 let jellyfin_client = {
388 let player = self.player.blocking_lock();
389 player.jellyfin_client()
390 };
391 let command = command.to_string();
392
393 tauri::async_runtime::spawn(async move {
394 let client = {
395 let guard = match jellyfin_client.lock() {
396 Ok(g) => g,
397 Err(e) => {
398 error!("[MediaSession] Failed to lock Jellyfin client: {}", e);
399 return;
400 }
401 };
402 match guard.as_ref() {
403 Some(c) => c.clone(),
404 None => {
405 warn!("[MediaSession] No Jellyfin client for remote command");
406 return;
407 }
408 }
409 };
410
411 let result = match command.as_str() {
413 "play" => client.send_session_command(session_id, "Unpause").await,
414 "pause" => client.send_session_command(session_id, "Pause").await,
415 "next" => client.send_session_command(session_id, "NextTrack").await,
416 "previous" => {
417 client
418 .send_session_command(session_id, "PreviousTrack")
419 .await
420 }
421 cmd if cmd.starts_with("seek:") => match cmd[5..].parse::<f64>() {
422 Ok(seconds) => {
423 let ticks = (seconds * 10_000_000.0) as i64;
424 client.session_seek(session_id, ticks).await
425 }
426 Err(_) => {
427 warn!("[MediaSession] Bad seek command: {}", command);
428 Ok(())
429 }
430 },
431 _ => {
432 warn!("[MediaSession] Unknown remote command: {}", command);
433 Ok(())
434 }
435 };
436
437 if let Err(e) = result {
438 error!("[MediaSession] Remote command '{}' failed: {}", command, e);
439 }
440 });
441 }
442
443 fn handle_local_command(&self, command: &str) {
445 if let Some(raw) = command.strip_prefix("seek:") {
452 match raw.parse::<f64>() {
453 Ok(position) => {
454 let player = self.player.clone();
455 tokio::spawn(async move {
456 let controller = player.lock().await;
457 if let Err(e) = controller.seek_absolute(position).await {
458 error!("[MediaSession] Seek to {:.1}s failed: {}", position, e);
459 }
460 });
461 }
462 Err(_) => warn!("[MediaSession] Bad seek command: {}", command),
463 }
464 return;
465 }
466
467 if command == "next" || command == "previous" {
477 let is_next = command == "next";
478 let player = self.player.clone();
479 tokio::spawn(async move {
480 let controller = player.lock().await;
481 let action = resolve_skip_action(
482 is_next,
483 controller.is_background_audio_active(),
484 controller.position(),
485 controller.duration(),
486 );
487 let label = if is_next { "next" } else { "previous" };
488 let result: Result<(), String> = match action {
489 SkipAction::Advance => if is_next {
490 controller.next()
491 } else {
492 controller.previous()
493 }
494 .map_err(|e| e.to_string()),
495 SkipAction::SeekTo(position) => {
496 info!(
497 "[MediaSession] Background audio: '{}' scrubs to {:.1}s",
498 label, position
499 );
500 controller.seek_absolute(position).await
501 }
502 };
503 if let Err(e) = result {
504 error!("[MediaSession] Skip '{}' failed: {}", label, e);
505 }
506 });
507 return;
508 }
509
510 let controller = self.player.blocking_lock();
512
513 let result = match command {
514 "play" => controller.play(),
515 "pause" => controller.pause(),
516 "stop" => controller.stop(),
517 _ => {
518 warn!("[MediaSession] Unknown command: {}", command);
519 Ok(())
520 }
521 };
522
523 if let Err(e) = result {
524 error!("[MediaSession] Command '{}' failed: {}", command, e);
525 }
526 }
527}
528
529#[cfg(target_os = "android")]
530impl MediaCommandHandler for MediaSessionHandler {
531 fn on_command(&self, command: &str) {
532 match self.playback_mode.get_mode() {
533 playback_mode::PlaybackMode::Remote { session_id } => {
534 self.handle_remote_command(command, session_id);
535 }
536 _ => self.handle_local_command(command),
537 }
538 }
539}
540
541#[cfg(target_os = "android")]
545struct RemoteVolumeSessionHandler {
546 playback_mode: Arc<PlaybackModeManager>,
547}
548
549#[cfg(target_os = "android")]
550impl RemoteVolumeHandler for RemoteVolumeSessionHandler {
551 fn on_remote_volume_change(&self, command: &str, volume: i32) {
552 log::info!("[RemoteVolume] Command: {}, Volume: {}", command, volume);
553
554 let playback_mode = Arc::clone(&self.playback_mode);
556 let command_str = command.to_string();
557
558 log::info!("[RemoteVolume] Spawning async task to send volume command...");
561 tauri::async_runtime::spawn(async move {
562 log::info!("[RemoteVolume] Async task started, calling send_remote_volume_command...");
563 match playback_mode
564 .send_remote_volume_command(&command_str, volume)
565 .await
566 {
567 Ok(_) => log::info!("[RemoteVolume] Volume command completed successfully"),
568 Err(e) => log::error!("[RemoteVolume] Failed to send volume command: {}", e),
569 }
570 log::info!("[RemoteVolume] Async task completed");
571 });
572 log::info!("[RemoteVolume] Async task spawned, returning from JNI callback");
573 }
574}
575
576#[derive(Clone, serde::Serialize)]
579struct BackendInitError {
580 platform: &'static str,
581 backend: &'static str,
582 message: String,
583}
584
585fn emit_backend_init_failed(app_handle: &tauri::AppHandle, backend: &'static str, message: String) {
588 error!(
589 "[INIT] Player backend '{}' failed to initialize: {}. Falling back to NullBackend (playback disabled).",
590 backend, message
591 );
592 let _ = app_handle.emit(
593 "backend-init-failed",
594 BackendInitError {
595 platform: std::env::consts::OS,
596 backend,
597 message,
598 },
599 );
600}
601
602#[cfg_attr(
606 not(any(target_os = "linux", target_os = "android")),
607 allow(unused_variables)
608)]
609fn create_player_backend(
610 app_handle: tauri::AppHandle,
611 playback_reporter: Arc<tokio::sync::Mutex<Option<playback_reporting::PlaybackReporter>>>,
612 position_throttler: Arc<playback_reporting::EventThrottler>,
613) -> Box<dyn PlayerBackend> {
614 let _event_emitter = Arc::new(TauriEventEmitter::new(app_handle.clone()));
615
616 #[cfg(target_os = "android")]
617 {
618 info!("Android platform detected - initializing ExoPlayer backend");
619
620 if let Err(e) = crate::android_context::ensure_initialized() {
625 log::error!("[INIT] Android context unavailable for the player: {e}");
626 }
627
628 let ctx = ndk_context::android_context();
630
631 let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) };
633
634 match vm {
635 Ok(java_vm) => {
636 match java_vm.attach_current_thread() {
637 Ok(mut env) => {
638 let context_obj =
639 unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) };
640
641 match ExoPlayerBackend::new(
642 &mut env,
643 &context_obj,
644 _event_emitter.clone(),
645 playback_reporter.clone(),
646 position_throttler.clone(),
647 ) {
648 Ok(backend) => {
649 info!("Successfully initialized ExoPlayer backend for Android");
650 return Box::new(backend);
651 }
652 Err(e) => {
653 emit_backend_init_failed(&app_handle, "exoplayer", e.to_string());
655 return Box::new(NullBackend::new());
656 }
657 }
658 }
659 Err(e) => {
660 emit_backend_init_failed(
661 &app_handle,
662 "exoplayer",
663 format!("attach JNI thread failed: {}", e),
664 );
665 return Box::new(NullBackend::new());
666 }
667 }
668 }
669 Err(e) => {
670 emit_backend_init_failed(
671 &app_handle,
672 "exoplayer",
673 format!("create JavaVM failed: {}", e),
674 );
675 return Box::new(NullBackend::new());
676 }
677 }
678 }
679
680 #[cfg(target_os = "linux")]
682 {
683 info!("Linux platform detected - initializing MPV backend for audio");
684 match MpvBackend::new(Some(_event_emitter), playback_reporter, position_throttler) {
685 Ok(backend) => {
686 info!("Successfully initialized MPV backend for Linux");
687 Box::new(backend)
688 }
689 Err(e) => {
690 error!("\n========================================");
691 error!("FATAL ERROR: Failed to initialize MPV backend");
692 error!("========================================");
693 error!("Error: {}", e);
694 error!("\nCommon causes:");
695 error!(" 1. MPV is not installed");
696 error!(" Solution: Install MPV using your package manager");
697 error!(" - Arch/CachyOS: sudo pacman -S mpv");
698 error!(" - Ubuntu/Debian: sudo apt install mpv libmpv-dev");
699 error!(" - Fedora: sudo dnf install mpv mpv-libs-devel");
700 error!("\n 2. MPV version mismatch (app was built with different libmpv version)");
701 error!(" Solution: Rebuild the application");
702 error!(" - cd src-tauri && cargo clean && cargo build --release");
703 error!("\n 3. Audio system not working");
704 error!(" Solution: Verify audio works with: pactl info");
705 error!("\nAudio playback will NOT work until this is fixed.");
706 error!("========================================\n");
707
708 emit_backend_init_failed(&app_handle, "mpv", e.to_string());
712 Box::new(NullBackend::new())
713 }
714 }
715 }
716
717 #[cfg(not(any(target_os = "linux", target_os = "android")))]
721 {
722 info!("No native audio backend for this platform - using webview <audio> backend");
723 match player::WebviewAudioBackend::new(_event_emitter) {
724 Ok(backend) => Box::new(backend),
725 Err(e) => {
726 emit_backend_init_failed(&app_handle, "webview-audio", e.to_string());
727 Box::new(NullBackend::new())
728 }
729 }
730 }
731}
732
733fn specta_builder() -> Builder<tauri::Wry> {
736 Builder::<tauri::Wry>::new()
737 .error_handling(tauri_specta::ErrorHandlingMode::Throw)
740 .events(tauri_specta::collect_events![
741 crate::player::events::PlayerStatusEvent
742 ])
743 .commands(tauri_specta::collect_commands![
744 player_play_item,
746 player_background_action,
747 player_enter_background_audio,
748 player_exit_background_audio,
749 player_play_queue,
750 player_play_album_track,
751 player_play_tracks,
752 player_play,
753 player_pause,
754 player_toggle,
755 player_stop,
756 player_next,
757 player_previous,
758 player_seek,
759 player_seek_video,
760 player_set_volume,
761 player_toggle_mute,
762 player_set_audio_track,
763 player_switch_audio_track,
764 player_set_subtitle_track,
765 player_toggle_shuffle,
766 player_cycle_repeat,
767 player_get_status,
768 player_get_queue,
769 player_get_capabilities,
770 player_add_to_queue,
771 player_add_track_by_id,
772 player_add_tracks_by_ids,
773 player_remove_from_queue,
774 player_move_in_queue,
775 player_skip_to,
776 player_set_audio_settings,
777 player_get_audio_settings,
778 player_get_eq_presets,
779 player_set_video_settings,
780 player_get_video_settings,
781 player_get_streaming_qualities,
782 player_set_stream_quality,
783 player_set_sleep_timer,
785 player_cancel_sleep_timer,
786 player_get_sleep_timer,
787 player_get_autoplay_settings,
788 player_set_autoplay_settings,
789 player_cancel_autoplay_countdown,
790 player_play_next_episode,
791 player_on_playback_ended,
792 player_recover_stream,
793 player_report_state,
794 player_report_position,
795 player_report_media_loaded,
796 player_local_media_path,
798 player_preload_upcoming,
799 player_set_cache_config,
800 player_get_cache_config,
801 player_configure_jellyfin,
803 player_disable_jellyfin,
804 player_get_session,
806 player_dismiss_session,
807 remote_play_on_session,
809 remote_send_command,
810 remote_session_seek,
811 remote_session_set_volume,
812 remote_session_toggle_mute,
813 lms_get_sync_groups,
815 lms_create_sync_group,
816 lms_unsync_player,
817 lms_dissolve_sync_group,
818 sessions_set_polling_hint,
820 sessions_poll_now,
821 playback_mode_get_current,
823 playback_mode_set,
824 playback_mode_is_transferring,
825 playback_mode_transfer_to_remote,
826 playback_mode_get_remote_status,
827 playback_mode_transfer_to_local,
828 playback_mode_set_transferring,
829 playback_reporter_init,
831 playback_reporter_destroy,
832 playback_report_start,
833 playback_report_progress,
834 playback_report_stopped,
835 playback_mark_played,
836 auth_initialize,
838 auth_connect_to_server,
839 auth_login,
840 auth_verify_session,
841 auth_logout,
842 auth_get_session,
843 auth_set_session,
844 auth_start_verification,
845 auth_stop_verification,
846 auth_reauthenticate,
847 device_get_id,
849 device_set_id,
850 connectivity_check_server,
852 connectivity_set_server_url,
853 connectivity_get_status,
854 connectivity_start_monitoring,
855 connectivity_stop_monitoring,
856 connectivity_mark_reachable,
857 connectivity_mark_unreachable,
858 storage_init,
860 storage_get_path,
861 storage_get_size,
862 storage_get_security_status,
863 storage_save_server,
864 storage_get_servers,
865 storage_delete_server,
866 storage_save_user,
867 storage_get_users,
868 storage_set_active_user,
869 storage_get_active_user,
870 storage_get_active_session,
871 storage_get_access_token,
872 storage_delete_user,
873 storage_update_playback_progress,
875 storage_update_playback_context,
876 storage_mark_played,
877 storage_set_watched,
878 storage_get_playback_progress,
879 storage_mark_synced,
880 storage_toggle_favorite,
881 download_item,
883 download_item_and_start,
884 download_album,
885 download_video,
886 download_series,
887 download_season,
888 get_downloads,
889 pause_download,
890 resume_download,
891 cancel_download,
892 delete_download,
893 delete_all_downloads,
894 delete_album_downloads,
895 delete_downloads_under,
896 clear_stale_downloads,
897 get_download_storage_stats,
898 mark_download_completed,
899 mark_download_failed,
900 media_local_url,
901 start_download,
902 enqueue_download,
903 enqueue_video_downloads,
904 sync_full_catalog,
905 catalog_sync_status,
906 set_show_server_catalog,
907 library_get_settings,
909 library_set_settings,
910 library_get_exclusion_candidates,
911 resume_queued_downloads,
912 get_download_manager_stats,
913 set_max_concurrent_downloads,
914 get_smart_cache_stats,
915 update_smart_cache_config,
916 get_smart_cache_config,
917 set_network_state,
919 get_downloads_allowed,
920 get_album_recommendations,
921 get_album_affinity_status,
922 pin_item,
924 unpin_item,
925 is_item_pinned,
926 offline_is_available,
928 offline_get_items,
929 offline_search,
930 storage_get_libraries,
932 storage_get_items,
933 storage_get_item,
934 storage_search_items,
935 storage_save_library,
936 storage_save_item,
937 storage_get_pending_sync_count,
938 sync_queue_mutation,
940 sync_get_pending,
941 sync_mark_processing,
942 sync_mark_completed,
943 sync_mark_failed,
944 sync_get_pending_count,
945 sync_process_pending,
946 sync_cleanup_completed,
947 sync_clear_user,
948 thumbnail_get_cached,
950 thumbnail_save,
951 thumbnail_get_stats,
952 thumbnail_set_limit,
953 thumbnail_clear_cache,
954 thumbnail_delete_item,
955 image_get_url,
956 storage_save_person,
958 storage_get_person,
959 storage_save_item_people,
960 storage_get_item_people,
961 storage_save_series_audio_preference,
963 storage_get_series_audio_preference,
964 repository_create,
966 repository_destroy,
967 repository_get_libraries,
968 repository_get_items,
969 repository_get_item,
970 repository_get_downloaded_libraries,
971 repository_get_downloaded_items,
972 repository_get_download_disk_usage,
973 repository_jray_actors_at,
974 repository_get_latest_items,
975 repository_get_resume_items,
976 repository_get_next_up_episodes,
977 repository_get_series_episodes,
978 repository_get_series_current_episode,
979 repository_clear_watch_history,
980 repository_get_recently_played_audio,
981 repository_get_resume_movies,
982 repository_get_rediscover_albums,
983 repository_get_genres,
984 repository_search,
985 repository_get_playback_info,
986 repository_get_video_stream_url,
987 repository_get_audio_stream_url,
988 repository_get_audio_only_stream_url_for_video,
989 repository_get_live_tv_channels,
990 repository_get_channels,
991 repository_open_live_stream,
992 repository_report_playback_start,
993 repository_report_playback_progress,
994 repository_report_playback_stopped,
995 repository_get_image_url,
996 repository_mark_favorite,
997 repository_unmark_favorite,
998 repository_get_favorites,
999 repository_get_person,
1000 repository_get_items_by_person,
1001 repository_get_similar_items,
1002 repository_get_subtitle_url,
1003 repository_get_video_download_url,
1004 playlist_create,
1006 playlist_delete,
1007 playlist_rename,
1008 playlist_get_items,
1009 playlist_add_items,
1010 playlist_remove_items,
1011 playlist_move_item,
1012 diagnostics_get_info,
1015 diagnostics_set_level,
1016 diagnostics_export,
1017 format_time_seconds,
1019 format_time_seconds_long,
1020 convert_ticks_to_seconds,
1021 calc_progress,
1022 convert_percent_to_volume,
1023 ])
1024}
1025
1026#[cfg(target_os = "linux")]
1035fn enable_linux_hardware_video_decoding() {
1036 let rank_overrides = "vah264dec:MAX,vah265dec:MAX,vavp9dec:MAX,vaav1dec:MAX,\
1041 vampeg2dec:MAX,vavp8dec:MAX";
1042
1043 set_env_if_unset("GST_PLUGIN_FEATURE_RANK", rank_overrides);
1044
1045 set_env_if_unset("WEBKIT_GST_ENABLE_HW_VIDEO_DECODER", "1");
1048
1049 info!("[INIT] Linux hardware video decoding (VAAPI) enabled where supported");
1050
1051 log_available_vaapi_decoders();
1052}
1053
1054#[cfg(target_os = "linux")]
1059fn log_available_vaapi_decoders() {
1060 const HW_DECODERS: &[&str] = &[
1061 "vah264dec",
1062 "vah265dec",
1063 "vavp9dec",
1064 "vaav1dec",
1065 "vampeg2dec",
1066 "vavp8dec",
1067 ];
1068
1069 let available: Vec<&str> = HW_DECODERS
1070 .iter()
1071 .copied()
1072 .filter(|name| {
1073 std::process::Command::new("gst-inspect-1.0")
1074 .arg(name)
1075 .stdout(std::process::Stdio::null())
1076 .stderr(std::process::Stdio::null())
1077 .status()
1078 .map(|s| s.success())
1079 .unwrap_or(false)
1080 })
1081 .collect();
1082
1083 if available.is_empty() {
1084 log::warn!(
1085 "[INIT] No VAAPI hardware video decoders found via gst-inspect-1.0; \
1086 video will use software decoding. Install the GStreamer 'va' plugin \
1087 (gst-plugins-bad) and a VAAPI driver to enable hardware decoding."
1088 );
1089 } else {
1090 info!(
1091 "[INIT] VAAPI hardware video decoders available to GStreamer: {}",
1092 available.join(", ")
1093 );
1094 }
1095}
1096
1097#[cfg(target_os = "linux")]
1098fn set_env_if_unset(key: &str, value: &str) {
1099 if std::env::var_os(key).is_none() {
1100 std::env::set_var(key, value);
1103 }
1104}
1105
1106fn build_log_plugin() -> tauri::plugin::TauriPlugin<tauri::Wry> {
1145 use tauri_plugin_log::{Target, TargetKind};
1146
1147 let mut targets = vec![
1148 Target::new(TargetKind::Stdout),
1149 Target::new(TargetKind::LogDir {
1150 file_name: Some("jellytau".to_string()),
1151 }),
1152 ];
1153
1154 if cfg!(debug_assertions) {
1158 targets.push(Target::new(TargetKind::Webview));
1159 }
1160
1161 tauri_plugin_log::Builder::new()
1162 .targets(targets)
1163 .level(log::LevelFilter::Info)
1164 .max_file_size(5 * 1024 * 1024)
1165 .rotation_strategy(tauri_plugin_log::RotationStrategy::KeepOne)
1166 .format(|out, message, record| {
1167 out.finish(format_args!(
1168 "[{}][{}] {}",
1169 record.level(),
1170 record.target(),
1171 crate::utils::diagnostics::redact(&message.to_string())
1172 ))
1173 })
1174 .build()
1175}
1176
1177#[cfg_attr(mobile, tauri::mobile_entry_point)]
1178pub fn run() {
1179 crate::utils::diagnostics::install_panic_hook();
1184
1185 #[cfg(target_os = "linux")]
1190 enable_linux_hardware_video_decoding();
1191
1192 let builder = specta_builder();
1197 let invoke_handler = builder.invoke_handler();
1198
1199 tauri::Builder::default()
1200 .plugin(build_log_plugin())
1201 .plugin(tauri_plugin_opener::init())
1202 .plugin(tauri_plugin_os::init())
1203 .invoke_handler(invoke_handler)
1204 .setup(move |app| {
1205 builder.mount_events(app);
1208
1209 if let Ok(config_dir) = app.path().app_config_dir() {
1226 if let Some(level) = crate::commands::diagnostics::stored_level(&config_dir) {
1227 log::set_max_level(level);
1228 log::info!("[DIAG] restored log level from settings: {level}");
1229 }
1230 }
1231
1232 #[cfg(desktop)]
1234 {
1235 app.handle()
1236 .plugin(tauri_plugin_updater::Builder::new().build())?;
1237 app.handle().plugin(tauri_plugin_process::init())?;
1238 }
1239
1240 let db_path = if let Ok(test_data_dir) = std::env::var("JELLYTAU_DATA_DIR") {
1243 let test_path = std::path::PathBuf::from(test_data_dir);
1244 info!("[INIT] Using test data directory: {:?}", test_path);
1245 test_path.join("jellytau.db")
1246 } else {
1247 app
1248 .path()
1249 .app_data_dir()
1250 .expect("Failed to get app data directory")
1251 .join("jellytau.db")
1252 };
1253
1254 info!("[INIT] Initializing database at: {:?}", db_path);
1255
1256 if let Some(parent) = db_path.parent() {
1258 info!("[INIT] Creating database directory: {:?}", parent);
1259 match std::fs::create_dir_all(parent) {
1260 Ok(_) => info!("[INIT] Database directory ready"),
1261 Err(e) => {
1262 error!("[INIT ERROR] Failed to create database directory: {}", e);
1263 panic!("Failed to create database directory: {}", e);
1264 }
1265 }
1266 }
1267
1268 info!("[INIT] Opening database...");
1269 let database = match Database::open(&db_path) {
1270 Ok(db) => {
1271 info!("[INIT] Database initialized successfully");
1272 db
1273 }
1274 Err(e) => {
1275 error!("[INIT ERROR] Failed to initialize database: {}", e);
1276 panic!("Failed to initialize database: {}", e);
1277 }
1278 };
1279 let db_wrapper = DatabaseWrapper(Mutex::new(database));
1280 app.manage(db_wrapper);
1281
1282 #[cfg(target_os = "android")]
1284 {
1285 if let Err(e) = crate::android_context::ensure_initialized() {
1296 log::error!("[INIT] Android context unavailable: {e}");
1297 log::error!("[INIT] Secure credential storage will fall back to the encrypted file.");
1298 }
1299
1300 info!("[INIT] Initializing Android SecureStorage for credentials...");
1301 let ctx = ndk_context::android_context();
1302 let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) };
1303
1304 match vm {
1305 Ok(java_vm) => {
1306 match java_vm.attach_current_thread() {
1307 Ok(mut env) => {
1308 let context_obj = unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) };
1309
1310 match initialize_secure_storage(&mut env, &context_obj) {
1311 Ok(()) => {
1312 info!("[INIT] Android SecureStorage initialized successfully");
1313 }
1314 Err(e) => {
1315 warn!("[INIT WARNING] Failed to initialize SecureStorage: {}. Credentials will use encrypted file fallback.", e);
1316 }
1317 }
1318 }
1319 Err(e) => {
1320 warn!("[INIT WARNING] Failed to attach JNI thread: {}. Credentials will use encrypted file fallback.", e);
1321 }
1322 }
1323 }
1324 Err(e) => {
1325 warn!("[INIT WARNING] Failed to create JavaVM: {}. Credentials will use encrypted file fallback.", e);
1326 }
1327 }
1328 }
1329
1330 info!("[INIT] Initializing credential store...");
1332 let credential_store = CredentialStore::new();
1333 let creds_wrapper = CredentialStoreWrapper(Mutex::new(credential_store));
1334 app.manage(creds_wrapper);
1335
1336 info!("[INIT] Creating shared playback reporting infrastructure...");
1338 let playback_reporter = Arc::new(tokio::sync::Mutex::new(None));
1339 let position_throttler = Arc::new(playback_reporting::EventThrottler::new());
1340
1341 info!("[INIT] Creating player backend...");
1343 let backend = create_player_backend(
1344 app.handle().clone(),
1345 playback_reporter.clone(),
1346 position_throttler.clone(),
1347 );
1348 let player_controller = PlayerController::new(
1349 backend,
1350 playback_reporter.clone(),
1351 position_throttler.clone(),
1352 );
1353
1354 let event_emitter = Arc::new(TauriEventEmitter::new(app.handle().clone()));
1356 player_controller.set_event_emitter(event_emitter.clone());
1357
1358 let player_arc = Arc::new(TokioMutex::new(player_controller));
1359
1360 #[cfg(target_os = "android")]
1364 {
1365 player::android::set_player_controller(player_arc.clone());
1366 }
1367
1368 let player_state = PlayerStateWrapper(player_arc.clone());
1369 app.manage(player_state);
1370
1371 info!("[INIT] Initializing media session manager...");
1373 let session_manager = MediaSessionManager::new();
1374 let session_wrapper = MediaSessionManagerWrapper(Mutex::new(session_manager));
1375 app.manage(session_wrapper);
1376
1377 info!("[INIT] Initializing playback mode manager...");
1379 let jellyfin_client = {
1380 let player = player_arc.blocking_lock();
1381 player.jellyfin_client()
1382 };
1383 let playback_mode_manager = playback_mode::PlaybackModeManager::new(
1384 jellyfin_client.clone(),
1385 player_arc.clone(),
1386 );
1387 let playback_mode_arc = Arc::new(playback_mode_manager);
1388 playback_mode_arc.set_event_emitter(event_emitter.clone());
1391 let playback_mode_wrapper = PlaybackModeManagerWrapper(playback_mode_arc.clone());
1392 app.manage(playback_mode_wrapper);
1393
1394 info!("[INIT] Initializing session poller manager...");
1396 let session_poller = session_poller::SessionPollerManager::new(
1397 jellyfin_client,
1398 playback_mode_arc.clone(),
1399 );
1400 session_poller.set_event_emitter(event_emitter.clone());
1401 let session_poller_arc = Arc::new(session_poller);
1405 let session_poller_wrapper = SessionPollerWrapper(session_poller_arc.clone());
1406 app.manage(session_poller_wrapper);
1407
1408 #[cfg(target_os = "android")]
1412 {
1413 info!("[INIT] Setting up MediaSession handler for lockscreen controls...");
1414 let media_handler = Arc::new(MediaSessionHandler {
1415 player: player_arc.clone(),
1416 playback_mode: playback_mode_arc.clone(),
1417 event_emitter: event_emitter.clone(),
1418 });
1419 set_media_command_handler(media_handler);
1420
1421 info!("[INIT] Setting up remote volume handler for Android...");
1422 let handler = Arc::new(RemoteVolumeSessionHandler {
1423 playback_mode: playback_mode_arc.clone(),
1424 });
1425 set_remote_volume_handler(handler);
1426 }
1427
1428 let video_settings = VideoSettingsWrapper(Mutex::new(VideoSettings::default()));
1430 app.manage(video_settings);
1431
1432 {
1440 let handle = app.handle().clone();
1441 tauri::async_runtime::spawn(async move {
1442 crate::commands::restore_streaming_quality(&handle).await;
1443 });
1444 }
1445
1446 {
1452 let handle = app.handle().clone();
1453 tauri::async_runtime::spawn(async move {
1454 crate::commands::restore_library_settings(&handle).await;
1455 });
1456 }
1457
1458 info!("[INIT] Initializing thumbnail cache...");
1460 let app_data_dir = if let Ok(test_data_dir) = std::env::var("JELLYTAU_DATA_DIR") {
1461 std::path::PathBuf::from(test_data_dir)
1462 } else {
1463 app
1464 .path()
1465 .app_data_dir()
1466 .expect("Failed to get app data directory")
1467 };
1468 let thumbnail_cache = ThumbnailCache::new(app_data_dir.clone(), ThumbnailCacheConfig::default());
1469 let thumbnail_wrapper = ThumbnailCacheWrapper(Arc::new(thumbnail_cache));
1470 app.manage(thumbnail_wrapper);
1471
1472 info!("[INIT] Initializing smart cache...");
1474 let smart_cache = SmartCache::new(SmartCacheConfig::default());
1475 let smart_cache_wrapper = SmartCacheWrapper(Mutex::new(smart_cache));
1476 app.manage(smart_cache_wrapper);
1477
1478 info!("[INIT] Starting local media server...");
1483 let media_server = match media_server::start(app_data_dir.clone()) {
1484 Ok(s) => Some(s),
1485 Err(e) => {
1486 error!("[INIT ERROR] Local media server failed to start: {}", e);
1489 None
1490 }
1491 };
1492 app.manage(media_server::MediaServerWrapper(media_server));
1493
1494 info!("[INIT] Initializing download manager...");
1496 let download_dir = app_data_dir.join("downloads");
1497 let download_manager = DownloadManager::new(download_dir);
1498 let download_manager_wrapper = DownloadManagerWrapper(Mutex::new(download_manager));
1499 app.manage(download_manager_wrapper);
1500
1501 app.manage(commands::download::NetworkStateWrapper(
1505 download::network::NetworkStateHandle::new(),
1506 ));
1507
1508 info!("[INIT] Initializing connectivity monitor...");
1510 let http_config = HttpConfig::default();
1511 let http_client = HttpClient::new(http_config)
1512 .expect("Failed to create HTTP client");
1513 let mut connectivity_monitor = ConnectivityMonitor::new(http_client);
1514 connectivity_monitor.set_app_handle(app.handle().clone());
1515
1516 session_poller_arc.set_connectivity_reporter(connectivity_monitor.reporter());
1520 session_poller_arc.start();
1521
1522 let connectivity_arc = Arc::new(tokio::sync::Mutex::new(connectivity_monitor));
1524 let connectivity_wrapper = ConnectivityMonitorWrapper(connectivity_arc.clone());
1525 app.manage(connectivity_wrapper);
1526
1527 info!("[INIT] Initializing auth manager...");
1529 let auth_http_config = HttpConfig::default();
1530 let auth_http_client = HttpClient::new(auth_http_config)
1531 .expect("Failed to create HTTP client for auth");
1532 let mut auth_manager = AuthManager::new(auth_http_client);
1533
1534 auth_manager.set_connectivity_monitor(connectivity_arc.clone());
1536
1537 let auth_manager_wrapper = AuthManagerWrapper(Arc::new(auth_manager));
1538 app.manage(auth_manager_wrapper);
1539
1540 info!("[INIT] Initializing session verifier wrapper...");
1542 let session_verifier_wrapper = SessionVerifierWrapper(Arc::new(tokio::sync::Mutex::new(None)));
1543 app.manage(session_verifier_wrapper);
1544
1545 info!("[INIT] Initializing repository manager...");
1547 let repository_manager = commands::RepositoryManager::new();
1548 let repository_manager_wrapper = RepositoryManagerWrapper(repository_manager);
1549 app.manage(repository_manager_wrapper);
1550
1551 info!("[INIT] Initializing playback reporter wrapper...");
1557 let playback_reporter_wrapper = PlaybackReporterWrapper(playback_reporter.clone());
1558 app.manage(playback_reporter_wrapper);
1559
1560 info!("[INIT] Starting background catalog indexer...");
1566 commands::catalog::spawn_catalog_indexer(app.handle().clone());
1567
1568 info!("[INIT] Starting favourites drain...");
1573 commands::favorites::spawn_favorites_drain(app.handle().clone());
1574
1575 info!("[INIT] Starting sync-queue drain...");
1580 commands::sync_drain::spawn_sync_queue_drain(app.handle().clone());
1581
1582 info!("[INIT] Application setup completed successfully");
1583 Ok(())
1584 })
1585 .run(tauri::generate_context!())
1586 .expect("error while running tauri application");
1587}
1588
1589#[cfg(test)]
1590mod specta_bindings {
1591 #[test]
1593 fn export_typescript_bindings() {
1594 super::specta_builder()
1595 .export(
1596 specta_typescript::Typescript::default()
1597 .bigint(specta_typescript::BigIntExportBehavior::Number),
1598 "../src/lib/api/bindings.ts",
1599 )
1600 .expect("failed to export typescript bindings");
1601 }
1602}