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
This commit is contained in:
623
crates/presentation/src/factory.rs
Normal file
623
crates/presentation/src/factory.rs
Normal file
@@ -0,0 +1,623 @@
|
||||
//! Factory — builds AppState from Config + DbPool.
|
||||
//!
|
||||
//! Connects to the database, runs migrations, creates all adapter instances,
|
||||
//! constructs Deps structs, and returns a fully-wired AppState.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use application::{
|
||||
admin::AdminDeps,
|
||||
auth::AuthDeps,
|
||||
channels::{ChannelCommandDeps, ChannelQueryDeps},
|
||||
config_snapshots::ConfigSnapshotDeps,
|
||||
iptv::IptvDeps,
|
||||
library::{LibraryCommandDeps, LibraryQueryDeps},
|
||||
providers::ProviderDeps,
|
||||
schedule::ScheduleDeps,
|
||||
};
|
||||
use domain::ports::{IMediaProvider, IProviderRegistry, ProviderCapabilities, StreamingProtocol};
|
||||
use domain::{DomainError, ScheduleEngineService};
|
||||
use infra_wiring::{Config, ConfigSource, DbPool};
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
/// Build a fully-wired AppState ready for the HTTP server.
|
||||
pub async fn build_app_state(config: Config) -> anyhow::Result<AppState> {
|
||||
// Connect to database
|
||||
let pool = DbPool::connect(&config.database_url).await?;
|
||||
pool.run_migrations().await?;
|
||||
|
||||
// Wire up all repositories from the database pool
|
||||
let wire_output = wire_repositories(&pool)?;
|
||||
|
||||
// Auth service
|
||||
let auth_service: Arc<dyn domain::ports::AuthService> =
|
||||
Arc::new(adapter_auth::PasswordAuthService);
|
||||
|
||||
// Event bus
|
||||
let event_bus = Arc::new(adapter_event_publisher::ChannelEventBus::new(64));
|
||||
let event_publisher: Arc<dyn domain::ports::EventPublisher> = event_bus.clone();
|
||||
|
||||
// Provider registry
|
||||
let provider_registry = build_provider_registry(&config).await;
|
||||
|
||||
// Library sync adapter — uses the LibraryCommand port internally
|
||||
let library_sync: Arc<dyn domain::ports::LibrarySyncAdapter> =
|
||||
build_library_sync(wire_output.library_command.clone());
|
||||
|
||||
// Schedule engine
|
||||
let schedule_engine = Arc::new(ScheduleEngineService::new(
|
||||
provider_registry.clone(),
|
||||
wire_output.channel_query.clone(),
|
||||
wire_output.schedule_query.clone(),
|
||||
wire_output.schedule_command.clone(),
|
||||
));
|
||||
|
||||
// JWT validator
|
||||
#[cfg(feature = "auth-jwt")]
|
||||
let jwt_validator = build_jwt_validator(&config)?;
|
||||
|
||||
// Sync trigger channel
|
||||
let (sync_tx, sync_rx) = tokio::sync::watch::channel(());
|
||||
|
||||
// Build all deps structs
|
||||
let auth_deps = Arc::new(AuthDeps {
|
||||
user_command: wire_output.user_command.clone(),
|
||||
user_query: wire_output.user_query.clone(),
|
||||
auth_service,
|
||||
event_publisher: event_publisher.clone(),
|
||||
});
|
||||
|
||||
let channel_command_deps = Arc::new(ChannelCommandDeps {
|
||||
channel_command: wire_output.channel_command.clone(),
|
||||
channel_query: wire_output.channel_query.clone(),
|
||||
event_publisher: event_publisher.clone(),
|
||||
});
|
||||
|
||||
let channel_query_deps = Arc::new(ChannelQueryDeps {
|
||||
channel_query: wire_output.channel_query.clone(),
|
||||
});
|
||||
|
||||
let config_snapshot_deps = Arc::new(ConfigSnapshotDeps {
|
||||
channel_command: wire_output.channel_command.clone(),
|
||||
channel_query: wire_output.channel_query.clone(),
|
||||
});
|
||||
|
||||
let schedule_deps = Arc::new(ScheduleDeps {
|
||||
schedule_engine: schedule_engine.clone(),
|
||||
channel_query: wire_output.channel_query.clone(),
|
||||
schedule_query: wire_output.schedule_query.clone(),
|
||||
schedule_command: wire_output.schedule_command.clone(),
|
||||
event_publisher: event_publisher.clone(),
|
||||
});
|
||||
|
||||
let library_command_deps = Arc::new(LibraryCommandDeps {
|
||||
library_command: wire_output.library_command.clone(),
|
||||
library_query: wire_output.library_query.clone(),
|
||||
library_sync: library_sync.clone(),
|
||||
provider_registry: provider_registry.clone(),
|
||||
event_publisher: event_publisher.clone(),
|
||||
});
|
||||
|
||||
let library_query_deps = Arc::new(LibraryQueryDeps {
|
||||
library_query: wire_output.library_query.clone(),
|
||||
});
|
||||
|
||||
let admin_deps = Arc::new(AdminDeps {
|
||||
settings_repo: wire_output.settings.clone(),
|
||||
activity_query: wire_output.activity_query.clone(),
|
||||
});
|
||||
|
||||
let iptv_deps = Arc::new(IptvDeps {
|
||||
channel_query: wire_output.channel_query.clone(),
|
||||
schedule_query: wire_output.schedule_query.clone(),
|
||||
});
|
||||
|
||||
let provider_deps = Arc::new(ProviderDeps {
|
||||
provider_config_command: wire_output.provider_config_command.clone(),
|
||||
provider_config_query: wire_output.provider_config_query.clone(),
|
||||
});
|
||||
|
||||
let config_arc = Arc::new(config);
|
||||
|
||||
// Spawn background tasks
|
||||
let bg_schedule_deps = schedule_deps.clone();
|
||||
tokio::spawn(crate::background::auto_scheduler::run(bg_schedule_deps));
|
||||
|
||||
let bg_schedule_deps2 = schedule_deps.clone();
|
||||
let bg_event_publisher = event_publisher.clone();
|
||||
tokio::spawn(crate::background::broadcast_poller::run(
|
||||
bg_schedule_deps2,
|
||||
bg_event_publisher,
|
||||
));
|
||||
|
||||
let webhook_rx = event_bus.subscriber();
|
||||
let webhook_channel_query = wire_output.channel_query.clone();
|
||||
tokio::spawn(crate::background::webhook_consumer::run(
|
||||
webhook_rx,
|
||||
webhook_channel_query,
|
||||
reqwest::Client::new(),
|
||||
));
|
||||
|
||||
let bg_sync = library_sync.clone();
|
||||
let bg_registry = provider_registry.clone();
|
||||
let bg_settings = wire_output.settings.clone();
|
||||
tokio::spawn(crate::background::library_sync::run(
|
||||
bg_sync,
|
||||
bg_registry,
|
||||
bg_settings,
|
||||
sync_rx,
|
||||
));
|
||||
|
||||
Ok(AppState {
|
||||
auth_deps,
|
||||
channel_command_deps,
|
||||
channel_query_deps,
|
||||
config_snapshot_deps,
|
||||
schedule_deps,
|
||||
library_command_deps,
|
||||
library_query_deps,
|
||||
admin_deps,
|
||||
iptv_deps,
|
||||
provider_deps,
|
||||
#[cfg(feature = "auth-jwt")]
|
||||
jwt_validator,
|
||||
provider_registry,
|
||||
library_sync,
|
||||
settings_repo: wire_output.settings,
|
||||
event_bus,
|
||||
config: config_arc,
|
||||
sync_trigger: sync_tx,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Repository wiring output — trait objects ready for dependency injection.
|
||||
struct WireOutput {
|
||||
user_command: Arc<dyn domain::ports::UserCommand>,
|
||||
user_query: Arc<dyn domain::ports::UserQuery>,
|
||||
channel_command: Arc<dyn domain::ports::ChannelCommand>,
|
||||
channel_query: Arc<dyn domain::ports::ChannelQuery>,
|
||||
schedule_command: Arc<dyn domain::ports::ScheduleCommand>,
|
||||
schedule_query: Arc<dyn domain::ports::ScheduleQuery>,
|
||||
library_command: Arc<dyn domain::ports::LibraryCommand>,
|
||||
library_query: Arc<dyn domain::ports::LibraryQuery>,
|
||||
activity_query: Arc<dyn domain::ports::ActivityLogQuery>,
|
||||
settings: Arc<dyn domain::ports::AppSettingsRepository>,
|
||||
provider_config_command: Arc<dyn domain::ports::ProviderConfigCommand>,
|
||||
provider_config_query: Arc<dyn domain::ports::ProviderConfigQuery>,
|
||||
}
|
||||
|
||||
fn wire_repositories(pool: &DbPool) -> anyhow::Result<WireOutput> {
|
||||
match pool {
|
||||
#[cfg(feature = "sqlite")]
|
||||
DbPool::Sqlite(sqlite_pool) => {
|
||||
let w = adapter_sqlite::wire(sqlite_pool.clone());
|
||||
Ok(WireOutput {
|
||||
user_command: w.user_command,
|
||||
user_query: w.user_query,
|
||||
channel_command: w.channel_command,
|
||||
channel_query: w.channel_query,
|
||||
schedule_command: w.schedule_command,
|
||||
schedule_query: w.schedule_query,
|
||||
library_command: w.library_command,
|
||||
library_query: w.library_query,
|
||||
activity_query: w.activity_query,
|
||||
settings: w.settings,
|
||||
provider_config_command: w.provider_config_command,
|
||||
provider_config_query: w.provider_config_query,
|
||||
})
|
||||
}
|
||||
#[cfg(feature = "postgres")]
|
||||
DbPool::Postgres(pg_pool) => {
|
||||
let w = adapter_postgres::wire(pg_pool.clone());
|
||||
Ok(WireOutput {
|
||||
user_command: w.user_command,
|
||||
user_query: w.user_query,
|
||||
channel_command: w.channel_command,
|
||||
channel_query: w.channel_query,
|
||||
schedule_command: w.schedule_command,
|
||||
schedule_query: w.schedule_query,
|
||||
library_command: w.library_command,
|
||||
library_query: w.library_query,
|
||||
activity_query: w.activity_query,
|
||||
settings: w.settings,
|
||||
provider_config_command: w.provider_config_command,
|
||||
provider_config_query: w.provider_config_query,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_provider_registry(config: &Config) -> Arc<dyn IProviderRegistry> {
|
||||
// Build a concrete registry that routes to configured providers.
|
||||
let mut providers: Vec<(String, Arc<dyn IMediaProvider>)> = Vec::new();
|
||||
|
||||
match config.config_source {
|
||||
ConfigSource::Env => {
|
||||
#[cfg(feature = "jellyfin")]
|
||||
if let (Some(url), Some(api_key), Some(user_id)) = (
|
||||
&config.jellyfin_url,
|
||||
&config.jellyfin_api_key,
|
||||
&config.jellyfin_user_id,
|
||||
) {
|
||||
tracing::info!("Media provider: Jellyfin at {}", url);
|
||||
providers.push((
|
||||
"jellyfin".to_string(),
|
||||
Arc::new(adapter_jellyfin::JellyfinMediaProvider::new(
|
||||
adapter_jellyfin::JellyfinConfig {
|
||||
base_url: url.clone(),
|
||||
api_key: api_key.clone(),
|
||||
user_id: user_id.clone(),
|
||||
},
|
||||
)),
|
||||
));
|
||||
}
|
||||
}
|
||||
ConfigSource::Db => {
|
||||
// DB-based provider configs loaded elsewhere at runtime.
|
||||
// For now, fall through to noop if nothing configured via env.
|
||||
tracing::info!("CONFIG_SOURCE=db: provider configs loaded from database at runtime");
|
||||
}
|
||||
}
|
||||
|
||||
if providers.is_empty() {
|
||||
tracing::warn!("No media provider configured — using NoopMediaProvider");
|
||||
providers.push(("noop".to_string(), Arc::new(NoopMediaProvider)));
|
||||
}
|
||||
|
||||
Arc::new(SimpleProviderRegistry::new(providers))
|
||||
}
|
||||
|
||||
fn build_library_sync(
|
||||
library_command: Arc<dyn domain::ports::LibraryCommand>,
|
||||
) -> Arc<dyn domain::ports::LibrarySyncAdapter> {
|
||||
Arc::new(SimpleSyncAdapter::new(library_command))
|
||||
}
|
||||
|
||||
#[cfg(feature = "auth-jwt")]
|
||||
fn build_jwt_validator(config: &Config) -> anyhow::Result<Option<Arc<adapter_auth::JwtValidator>>> {
|
||||
let secret = match &config.jwt_secret {
|
||||
Some(s) if !s.is_empty() => s.clone(),
|
||||
_ => {
|
||||
if config.is_production {
|
||||
anyhow::bail!("JWT_SECRET is required in production");
|
||||
}
|
||||
tracing::warn!("JWT_SECRET not set — using insecure development secret");
|
||||
"k-template-dev-secret-not-for-production-use-only".to_string()
|
||||
}
|
||||
};
|
||||
|
||||
let jwt_config = adapter_auth::JwtConfig::new(
|
||||
secret,
|
||||
config.jwt_issuer.clone(),
|
||||
config.jwt_audience.clone(),
|
||||
Some(config.jwt_expiry_hours),
|
||||
Some(config.jwt_refresh_expiry_days),
|
||||
config.is_production,
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("JWT config error: {}", e))?;
|
||||
|
||||
Ok(Some(Arc::new(adapter_auth::JwtValidator::new(jwt_config))))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NoopMediaProvider — fallback when nothing is configured
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct NoopMediaProvider;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl IMediaProvider for NoopMediaProvider {
|
||||
fn capabilities(&self) -> ProviderCapabilities {
|
||||
ProviderCapabilities {
|
||||
collections: false,
|
||||
series: false,
|
||||
genres: false,
|
||||
tags: false,
|
||||
decade: false,
|
||||
search: false,
|
||||
streaming_protocol: StreamingProtocol::DirectFile,
|
||||
rescan: false,
|
||||
transcode: false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_items(
|
||||
&self,
|
||||
_: &domain::MediaFilter,
|
||||
) -> domain::DomainResult<Vec<domain::MediaItem>> {
|
||||
Err(DomainError::InfrastructureError(
|
||||
"No media provider configured. Set JELLYFIN_BASE_URL or LOCAL_FILES_DIR.".into(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn fetch_by_id(
|
||||
&self,
|
||||
_: &domain::MediaItemId,
|
||||
) -> domain::DomainResult<Option<domain::MediaItem>> {
|
||||
Err(DomainError::InfrastructureError(
|
||||
"No media provider configured.".into(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn get_stream_url(
|
||||
&self,
|
||||
_: &domain::MediaItemId,
|
||||
_: &domain::ports::StreamQuality,
|
||||
) -> domain::DomainResult<String> {
|
||||
Err(DomainError::InfrastructureError(
|
||||
"No media provider configured.".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SimpleProviderRegistry — implements IProviderRegistry for N providers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct SimpleProviderRegistry {
|
||||
providers: Vec<(String, Arc<dyn IMediaProvider>)>,
|
||||
}
|
||||
|
||||
impl SimpleProviderRegistry {
|
||||
fn new(providers: Vec<(String, Arc<dyn IMediaProvider>)>) -> Self {
|
||||
Self { providers }
|
||||
}
|
||||
|
||||
fn get(&self, id: &str) -> Option<&Arc<dyn IMediaProvider>> {
|
||||
self.providers.iter().find(|(k, _)| k == id).map(|(_, v)| v)
|
||||
}
|
||||
|
||||
fn primary(&self) -> Option<&Arc<dyn IMediaProvider>> {
|
||||
self.providers.first().map(|(_, v)| v)
|
||||
}
|
||||
|
||||
/// Extract provider_id from a prefixed item ID (e.g. "jellyfin::abc123" → "jellyfin").
|
||||
fn extract_provider_id(item_id: &str) -> Option<&str> {
|
||||
item_id.find("::").map(|pos| &item_id[..pos])
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl IProviderRegistry for SimpleProviderRegistry {
|
||||
async fn fetch_items(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
filter: &domain::MediaFilter,
|
||||
) -> domain::DomainResult<Vec<domain::MediaItem>> {
|
||||
let id = if provider_id.is_empty() {
|
||||
self.providers.first().map(|(k, _)| k.as_str()).unwrap_or("")
|
||||
} else {
|
||||
provider_id
|
||||
};
|
||||
let provider = self
|
||||
.get(id)
|
||||
.ok_or_else(|| DomainError::InfrastructureError(format!("Unknown provider: {id}")))?;
|
||||
provider.fetch_items(filter).await
|
||||
}
|
||||
|
||||
async fn fetch_by_id(
|
||||
&self,
|
||||
item_id: &domain::MediaItemId,
|
||||
) -> domain::DomainResult<Option<domain::MediaItem>> {
|
||||
let id_str = item_id.value();
|
||||
if let Some(pid) = Self::extract_provider_id(id_str) {
|
||||
if let Some(provider) = self.get(pid) {
|
||||
return provider.fetch_by_id(item_id).await;
|
||||
}
|
||||
}
|
||||
// Fall back to primary
|
||||
if let Some(provider) = self.primary() {
|
||||
provider.fetch_by_id(item_id).await
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_stream_url(
|
||||
&self,
|
||||
item_id: &domain::MediaItemId,
|
||||
quality: &domain::ports::StreamQuality,
|
||||
) -> domain::DomainResult<String> {
|
||||
let id_str = item_id.value();
|
||||
if let Some(pid) = Self::extract_provider_id(id_str) {
|
||||
if let Some(provider) = self.get(pid) {
|
||||
return provider.get_stream_url(item_id, quality).await;
|
||||
}
|
||||
}
|
||||
if let Some(provider) = self.primary() {
|
||||
provider.get_stream_url(item_id, quality).await
|
||||
} else {
|
||||
Err(DomainError::InfrastructureError(
|
||||
"No provider available".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_ids(&self) -> Vec<String> {
|
||||
self.providers.iter().map(|(k, _)| k.clone()).collect()
|
||||
}
|
||||
|
||||
fn primary_id(&self) -> &str {
|
||||
self.providers
|
||||
.first()
|
||||
.map(|(k, _)| k.as_str())
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
fn capabilities(&self, provider_id: &str) -> Option<ProviderCapabilities> {
|
||||
self.get(provider_id).map(|p| p.capabilities())
|
||||
}
|
||||
|
||||
async fn list_collections(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
) -> domain::DomainResult<Vec<domain::ports::Collection>> {
|
||||
let id = if provider_id.is_empty() {
|
||||
self.primary_id()
|
||||
} else {
|
||||
provider_id
|
||||
};
|
||||
let provider = self
|
||||
.get(id)
|
||||
.ok_or_else(|| DomainError::InfrastructureError(format!("Unknown provider: {id}")))?;
|
||||
provider.list_collections().await
|
||||
}
|
||||
|
||||
async fn list_series(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
collection_id: Option<&str>,
|
||||
) -> domain::DomainResult<Vec<domain::ports::SeriesSummary>> {
|
||||
let id = if provider_id.is_empty() {
|
||||
self.primary_id()
|
||||
} else {
|
||||
provider_id
|
||||
};
|
||||
let provider = self
|
||||
.get(id)
|
||||
.ok_or_else(|| DomainError::InfrastructureError(format!("Unknown provider: {id}")))?;
|
||||
provider.list_series(collection_id).await
|
||||
}
|
||||
|
||||
async fn list_genres(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
content_type: Option<&domain::ContentType>,
|
||||
) -> domain::DomainResult<Vec<String>> {
|
||||
let id = if provider_id.is_empty() {
|
||||
self.primary_id()
|
||||
} else {
|
||||
provider_id
|
||||
};
|
||||
let provider = self
|
||||
.get(id)
|
||||
.ok_or_else(|| DomainError::InfrastructureError(format!("Unknown provider: {id}")))?;
|
||||
provider.list_genres(content_type).await
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SimpleSyncAdapter — wraps LibraryCommand for sync operations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Convert a MediaItem from a provider into a LibraryItem for persistence.
|
||||
fn media_item_to_library_item(item: domain::MediaItem, provider_id: &str) -> domain::LibraryItem {
|
||||
let external_id = item.id().value().to_string();
|
||||
let id = format!("{}::{}", provider_id, external_id);
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
domain::LibraryItem::from_persistence(
|
||||
id,
|
||||
provider_id.to_string(),
|
||||
external_id,
|
||||
item.title().to_string(),
|
||||
item.content_type().clone(),
|
||||
item.duration_secs(),
|
||||
item.series_name().map(|s| s.to_string()),
|
||||
item.season_number(),
|
||||
item.episode_number(),
|
||||
item.year(),
|
||||
item.genres().to_vec(),
|
||||
item.tags().to_vec(),
|
||||
item.collection_id().map(|s| s.to_string()),
|
||||
None, // collection_name not in MediaItem
|
||||
None, // collection_type not in MediaItem
|
||||
item.thumbnail_url().map(|s| s.to_string()),
|
||||
now,
|
||||
)
|
||||
}
|
||||
|
||||
struct SimpleSyncAdapter {
|
||||
library_command: Arc<dyn domain::ports::LibraryCommand>,
|
||||
}
|
||||
|
||||
impl SimpleSyncAdapter {
|
||||
fn new(library_command: Arc<dyn domain::ports::LibraryCommand>) -> Self {
|
||||
Self { library_command }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl domain::ports::LibrarySyncAdapter for SimpleSyncAdapter {
|
||||
async fn sync_provider(
|
||||
&self,
|
||||
provider: &dyn IMediaProvider,
|
||||
provider_id: &str,
|
||||
) -> domain::LibrarySyncResult {
|
||||
use std::time::Instant;
|
||||
|
||||
let start = Instant::now();
|
||||
let log_id = match self.library_command.log_sync_start(provider_id).await {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
return domain::LibrarySyncResult::with_error(
|
||||
provider_id,
|
||||
0,
|
||||
format!("Failed to log sync start: {e}"),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch all items from provider
|
||||
let filter = domain::MediaFilter::default();
|
||||
let items = match provider.fetch_items(&filter).await {
|
||||
Ok(items) => items,
|
||||
Err(e) => {
|
||||
let result = domain::LibrarySyncResult::with_error(
|
||||
provider_id,
|
||||
start.elapsed().as_millis() as u64,
|
||||
format!("Failed to fetch items: {e}"),
|
||||
);
|
||||
let _ = self.library_command.log_sync_finish(log_id, &result).await;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
let items_found = items.len() as u32;
|
||||
|
||||
// Clear + insert (items are MediaItem; LibrarySyncAdapter implementations
|
||||
// typically handle the conversion. Here we delegate to library_command directly.)
|
||||
if let Err(e) = self.library_command.clear_provider(provider_id).await {
|
||||
let result = domain::LibrarySyncResult::with_error(
|
||||
provider_id,
|
||||
start.elapsed().as_millis() as u64,
|
||||
format!("Failed to clear provider items: {e}"),
|
||||
);
|
||||
let _ = self.library_command.log_sync_finish(log_id, &result).await;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Convert MediaItems to LibraryItems for storage
|
||||
let library_items: Vec<domain::LibraryItem> = items
|
||||
.into_iter()
|
||||
.map(|item| media_item_to_library_item(item, provider_id))
|
||||
.collect();
|
||||
|
||||
if let Err(e) = self
|
||||
.library_command
|
||||
.upsert_items(provider_id, library_items)
|
||||
.await
|
||||
{
|
||||
let result = domain::LibrarySyncResult::with_error(
|
||||
provider_id,
|
||||
start.elapsed().as_millis() as u64,
|
||||
format!("Failed to upsert items: {e}"),
|
||||
);
|
||||
let _ = self.library_command.log_sync_finish(log_id, &result).await;
|
||||
return result;
|
||||
}
|
||||
|
||||
let result = domain::LibrarySyncResult::new(
|
||||
provider_id,
|
||||
items_found,
|
||||
start.elapsed().as_millis() as u64,
|
||||
);
|
||||
let _ = self.library_command.log_sync_finish(log_id, &result).await;
|
||||
result
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user