Files
k-tv/crates/presentation/src/background/library_sync.rs
Gabriel Kaszewski 56d742a74c presentation: HTTP server crate w/ handlers, routes, background tasks
Axum binary that wires all clean-arch crates together:
- AppState holds pre-built Deps structs (auth, channels, schedule, library, etc.)
- JWT extractors (CurrentUser, AdminUser, OptionalCurrentUser)
- Handlers delegate to application use cases, map to api-types DTOs
- Routes: auth, channels, schedule, library, admin, providers, config, iptv
- Background: auto-scheduler, broadcast poller, webhook consumer, library sync
- Factory builds everything from Config + DbPool
- SimpleProviderRegistry impl of IProviderRegistry trait
- NoopMediaProvider fallback
2026-07-12 03:23:20 +02:00

126 lines
4.0 KiB
Rust

//! 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<dyn LibrarySyncAdapter>,
provider_registry: Arc<dyn IProviderRegistry>,
settings_repo: Arc<dyn AppSettingsRepository>,
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<dyn AppSettingsRepository>) -> u64 {
repo.get("library_sync_interval_hours")
.await
.ok()
.flatten()
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(DEFAULT_INTERVAL_HOURS)
}
async fn do_sync(
sync_adapter: &Arc<dyn LibrarySyncAdapter>,
registry: &Arc<dyn IProviderRegistry>,
) {
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<dyn IProviderRegistry>,
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<Vec<domain::MediaItem>> {
self.registry.fetch_items(&self.provider_id, filter).await
}
async fn fetch_by_id(
&self,
item_id: &domain::MediaItemId,
) -> domain::DomainResult<Option<domain::MediaItem>> {
self.registry.fetch_by_id(item_id).await
}
async fn get_stream_url(
&self,
item_id: &domain::MediaItemId,
quality: &domain::ports::StreamQuality,
) -> domain::DomainResult<String> {
self.registry.get_stream_url(item_id, quality).await
}
}