//! Background library sync task. //! //! Fires 10 seconds after startup, then every N hours (read from app_settings). //! Can be triggered on-demand via the sync_trigger watch channel. use std::sync::Arc; use std::time::Duration; use domain::ports::{AppSettingsRepository, IProviderRegistry, LibrarySyncAdapter}; use tokio::sync::watch; const STARTUP_DELAY_SECS: u64 = 10; const DEFAULT_INTERVAL_HOURS: u64 = 6; pub async fn run( sync_adapter: Arc, provider_registry: Arc, settings_repo: Arc, mut trigger_rx: watch::Receiver<()>, ) { tokio::time::sleep(Duration::from_secs(STARTUP_DELAY_SECS)).await; loop { do_sync(&sync_adapter, &provider_registry).await; let interval_hours = load_interval_hours(&settings_repo).await; let sleep = tokio::time::sleep(Duration::from_secs(interval_hours * 3600)); tokio::select! { _ = sleep => {} _ = trigger_rx.changed() => { tracing::info!("library-sync: triggered manually"); } } } } async fn load_interval_hours(repo: &Arc) -> u64 { repo.get("library_sync_interval_hours") .await .ok() .flatten() .and_then(|v| v.parse::().ok()) .unwrap_or(DEFAULT_INTERVAL_HOURS) } async fn do_sync( sync_adapter: &Arc, registry: &Arc, ) { let provider_ids = registry.provider_ids(); for provider_id in provider_ids { // We need a &dyn IMediaProvider, but IProviderRegistry doesn't expose one. // The sync adapter will use the registry's fetch_items internally via its // own stored reference to the provider. For now, we create a thin adapter. tracing::info!("library-sync: syncing provider '{}'", provider_id); let wrapper = RegistryProviderAdapter { registry: registry.clone(), provider_id: provider_id.clone(), }; let result = sync_adapter.sync_provider(&wrapper, &provider_id).await; if let Some(err) = result.error() { tracing::warn!("library-sync: provider '{}' failed: {}", provider_id, err); } else { tracing::info!( "library-sync: provider '{}' done — {} items in {}ms", provider_id, result.items_found(), result.duration_ms() ); } } } /// Adapter that wraps IProviderRegistry calls for a specific provider_id, /// implementing IMediaProvider so it can be passed to LibrarySyncAdapter. struct RegistryProviderAdapter { registry: Arc, provider_id: String, } #[async_trait::async_trait] impl domain::ports::IMediaProvider for RegistryProviderAdapter { fn capabilities(&self) -> domain::ports::ProviderCapabilities { self.registry .capabilities(&self.provider_id) .unwrap_or(domain::ports::ProviderCapabilities { collections: false, series: false, genres: false, tags: false, decade: false, search: false, streaming_protocol: domain::ports::StreamingProtocol::DirectFile, rescan: false, transcode: false, }) } async fn fetch_items( &self, filter: &domain::MediaFilter, ) -> domain::DomainResult> { self.registry.fetch_items(&self.provider_id, filter).await } async fn fetch_by_id( &self, item_id: &domain::MediaItemId, ) -> domain::DomainResult> { self.registry.fetch_by_id(item_id).await } async fn get_stream_url( &self, item_id: &domain::MediaItemId, quality: &domain::ports::StreamQuality, ) -> domain::DomainResult { self.registry.get_stream_url(item_id, quality).await } }