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:
@@ -1,7 +1,7 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use adapter_common::{content_type_str, parse_content_type, parse_genres_blob};
|
||||
use adapter_common::{content_type_str, parse_content_type, parse_enum_or_default, parse_genres_blob, serialize_enum_as_string};
|
||||
use domain::{
|
||||
ports::library::{LibraryCommand, LibraryQuery},
|
||||
ContentType, DomainError, DomainResult, LibraryCollection,
|
||||
@@ -40,10 +40,15 @@ struct LibraryItemRow {
|
||||
thumbnail_url: Option<String>,
|
||||
synced_at: String,
|
||||
chapters: Option<String>,
|
||||
role: Option<String>,
|
||||
}
|
||||
|
||||
impl LibraryItemRow {
|
||||
fn into_media_item(self) -> MediaItem {
|
||||
let role: MediaRole = self
|
||||
.role
|
||||
.map(parse_enum_or_default)
|
||||
.unwrap_or_default();
|
||||
MediaItem::from_persistence(DomainMediaItemRow {
|
||||
id: domain::MediaItemId::new(&self.id),
|
||||
provider_id: self.provider_id,
|
||||
@@ -63,7 +68,7 @@ impl LibraryItemRow {
|
||||
collection_type: self.collection_type,
|
||||
thumbnail_url: self.thumbnail_url,
|
||||
synced_at: Some(self.synced_at),
|
||||
role: MediaRole::default(),
|
||||
role,
|
||||
chapters: self
|
||||
.chapters
|
||||
.as_deref()
|
||||
@@ -116,12 +121,14 @@ impl LibraryCommand for SqliteLibraryRepository {
|
||||
Some(serde_json::to_string(item.chapters()).unwrap_or_default())
|
||||
};
|
||||
|
||||
let role_str = serialize_enum_as_string(item.role(), "program");
|
||||
|
||||
sqlx::query(
|
||||
"INSERT OR REPLACE INTO library_items
|
||||
(id, provider_id, external_id, title, content_type, duration_secs,
|
||||
series_name, season_number, episode_number, year, genres, tags,
|
||||
collection_id, collection_name, collection_type, thumbnail_url, synced_at, chapters)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
collection_id, collection_name, collection_type, thumbnail_url, synced_at, chapters, role)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
)
|
||||
.bind(item.id().value())
|
||||
.bind(item.provider_id())
|
||||
@@ -141,6 +148,7 @@ impl LibraryCommand for SqliteLibraryRepository {
|
||||
.bind(item.thumbnail_url())
|
||||
.bind(item.synced_at().unwrap_or(""))
|
||||
.bind(&chapters_json)
|
||||
.bind(&role_str)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
@@ -151,6 +159,22 @@ impl LibraryCommand for SqliteLibraryRepository {
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))
|
||||
}
|
||||
|
||||
async fn update_role(&self, item_id: &str, role: MediaRole) -> DomainResult<()> {
|
||||
let role_str = serialize_enum_as_string(&role, "program");
|
||||
let rows = sqlx::query("UPDATE library_items SET role = ? WHERE id = ?")
|
||||
.bind(&role_str)
|
||||
.bind(item_id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
|
||||
if rows.rows_affected() == 0 {
|
||||
return Err(DomainError::NotFound(format!(
|
||||
"Library item {item_id} not found"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn clear_provider(&self, provider_id: &str) -> DomainResult<()> {
|
||||
sqlx::query("DELETE FROM library_items WHERE provider_id = ?")
|
||||
.bind(provider_id)
|
||||
|
||||
Reference in New Issue
Block a user