MediaRole auto-detection + manual role API + sync wiring (#7)
role_detector: classify items as Interstitial by collection name/tag patterns.
Wire chapter extraction + role detection into sync adapter (worker + presentation).
SQLite: persist/read role column, migration.
PUT /library/items/{id}/role endpoint for manual override.
This commit is contained in:
@@ -17,6 +17,7 @@ local-files = ["dep:adapter-local-files"]
|
||||
domain = { workspace = true }
|
||||
application = { workspace = true }
|
||||
infra-wiring = { workspace = true }
|
||||
adapter-common = { workspace = true }
|
||||
adapter-sqlite = { workspace = true, optional = true }
|
||||
adapter-auth = { workspace = true }
|
||||
adapter-jellyfin = { workspace = true, optional = true }
|
||||
|
||||
@@ -341,10 +341,16 @@ impl IProviderRegistry for SimpleProviderRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_item_to_library_item(item: domain::MediaItem, provider_id: &str) -> domain::MediaItem {
|
||||
fn provider_item_to_library_item(
|
||||
item: domain::MediaItem,
|
||||
provider_id: &str,
|
||||
role_config: &adapter_common::role_detector::RoleDetectionConfig,
|
||||
) -> domain::MediaItem {
|
||||
let external_id = item.id().value().to_string();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
let role = adapter_common::role_detector::detect_role(&item, role_config);
|
||||
|
||||
domain::MediaItem::from_persistence(domain::MediaItemRow {
|
||||
id: domain::MediaItemId::new(format!("{}::{}", provider_id, external_id)),
|
||||
provider_id: provider_id.to_string(),
|
||||
@@ -360,22 +366,26 @@ fn provider_item_to_library_item(item: domain::MediaItem, provider_id: &str) ->
|
||||
genres: item.genres().to_vec(),
|
||||
tags: item.tags().to_vec(),
|
||||
collection_id: item.collection_id().map(|s| s.to_string()),
|
||||
collection_name: None,
|
||||
collection_type: None,
|
||||
collection_name: item.collection_name().map(|s| s.to_string()),
|
||||
collection_type: item.collection_type().map(|s| s.to_string()),
|
||||
thumbnail_url: item.thumbnail_url().map(|s| s.to_string()),
|
||||
synced_at: Some(now),
|
||||
role: domain::MediaRole::default(),
|
||||
role,
|
||||
chapters: item.chapters().to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
struct SimpleSyncAdapter {
|
||||
library_command: Arc<dyn domain::ports::LibraryCommand>,
|
||||
role_config: adapter_common::role_detector::RoleDetectionConfig,
|
||||
}
|
||||
|
||||
impl SimpleSyncAdapter {
|
||||
fn new(library_command: Arc<dyn domain::ports::LibraryCommand>) -> Self {
|
||||
Self { library_command }
|
||||
Self {
|
||||
library_command,
|
||||
role_config: adapter_common::role_detector::RoleDetectionConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,6 +396,7 @@ impl domain::ports::LibrarySyncAdapter for SimpleSyncAdapter {
|
||||
provider: &dyn IMediaProvider,
|
||||
provider_id: &str,
|
||||
) -> domain::LibrarySyncResult {
|
||||
use adapter_common::ffprobe;
|
||||
use std::time::Instant;
|
||||
|
||||
let start = Instant::now();
|
||||
@@ -426,11 +437,20 @@ impl domain::ports::LibrarySyncAdapter for SimpleSyncAdapter {
|
||||
return result;
|
||||
}
|
||||
|
||||
let library_items: Vec<domain::MediaItem> = items
|
||||
let mut library_items: Vec<domain::MediaItem> = items
|
||||
.into_iter()
|
||||
.map(|item| provider_item_to_library_item(item, provider_id))
|
||||
.map(|item| provider_item_to_library_item(item, provider_id, &self.role_config))
|
||||
.collect();
|
||||
|
||||
for item in &mut library_items {
|
||||
if ffprobe::should_probe_chapters(item.content_type(), item.duration_secs()) {
|
||||
if let Ok(uri) = provider.get_source_uri(item.id()).await {
|
||||
let chapters = ffprobe::extract_chapters(&uri).await;
|
||||
item.set_chapters(chapters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = self
|
||||
.library_command
|
||||
.upsert_items(provider_id, library_items)
|
||||
|
||||
Reference in New Issue
Block a user