kill LibraryItem, unify to MediaItem; decouple schedule engine from providers

ADR-0001: MediaItem absorbs LibraryItem fields (provider_id, external_id,
collection_name, collection_type, synced_at, role/MediaRole).
LibraryItem + LibraryItemRow deleted. All ports/adapters/tests updated.

ADR-0002: schedule engine takes LibraryQuery instead of IProviderRegistry.
Algorithmic blocks query library via search(), manual blocks via get_by_id().
get_stream_url removed from engine; provider_registry moved to ScheduleDeps
for playback-time stream URL resolution in application layer.

BlockContent provider_id field removed (meaningless when querying library).
This commit is contained in:
2026-07-12 07:02:08 +02:00
parent 773e228e21
commit a6558e15b2
30 changed files with 324 additions and 354 deletions

View File

@@ -4,9 +4,10 @@ use sqlx::SqlitePool;
use adapter_common::{content_type_str, parse_content_type, parse_genres_blob};
use domain::{
ports::library::{LibraryCommand, LibraryQuery},
ContentType, DomainError, DomainResult, LibraryCollection, LibraryItem,
LibraryItemRow as DomainLibraryItemRow, LibrarySearchFilter, LibrarySyncLogEntry,
LibrarySyncResult, SeasonSummary, ShowSummary,
ContentType, DomainError, DomainResult, LibraryCollection,
LibrarySearchFilter, LibrarySyncLogEntry,
LibrarySyncResult, MediaItem, MediaItemRow as DomainMediaItemRow,
MediaRole, SeasonSummary, ShowSummary,
};
pub struct SqliteLibraryRepository {
@@ -41,14 +42,15 @@ struct LibraryItemRow {
}
impl LibraryItemRow {
fn into_library_item(self) -> LibraryItem {
LibraryItem::from_persistence(DomainLibraryItemRow {
id: self.id,
fn into_media_item(self) -> MediaItem {
MediaItem::from_persistence(DomainMediaItemRow {
id: domain::MediaItemId::new(&self.id),
provider_id: self.provider_id,
external_id: self.external_id,
title: self.title,
content_type: parse_content_type(&self.content_type),
duration_secs: self.duration_secs as u32,
description: None,
series_name: self.series_name,
season_number: self.season_number.map(|n| n as u32),
episode_number: self.episode_number.map(|n| n as u32),
@@ -59,7 +61,8 @@ impl LibraryItemRow {
collection_name: self.collection_name,
collection_type: self.collection_type,
thumbnail_url: self.thumbnail_url,
synced_at: self.synced_at,
synced_at: Some(self.synced_at),
role: MediaRole::default(),
})
}
}
@@ -93,7 +96,7 @@ struct SeasonSummaryRow {
#[async_trait]
impl LibraryCommand for SqliteLibraryRepository {
async fn upsert_items(&self, _provider_id: &str, items: Vec<LibraryItem>) -> DomainResult<()> {
async fn upsert_items(&self, _provider_id: &str, items: Vec<MediaItem>) -> DomainResult<()> {
let mut tx = self
.pool
.begin()
@@ -108,7 +111,7 @@ impl LibraryCommand for SqliteLibraryRepository {
collection_id, collection_name, collection_type, thumbnail_url, synced_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
)
.bind(item.id())
.bind(item.id().value())
.bind(item.provider_id())
.bind(item.external_id())
.bind(item.title())
@@ -124,7 +127,7 @@ impl LibraryCommand for SqliteLibraryRepository {
.bind(item.collection_name())
.bind(item.collection_type())
.bind(item.thumbnail_url())
.bind(item.synced_at())
.bind(item.synced_at().unwrap_or(""))
.execute(&mut *tx)
.await
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
@@ -187,7 +190,7 @@ impl LibraryQuery for SqliteLibraryRepository {
async fn search(
&self,
filter: &LibrarySearchFilter,
) -> DomainResult<(Vec<LibraryItem>, u32)> {
) -> DomainResult<(Vec<MediaItem>, u32)> {
let mut conditions: Vec<String> = vec![];
if let Some(p) = filter.provider_id() {
@@ -263,19 +266,19 @@ impl LibraryQuery for SqliteLibraryRepository {
Ok((
rows.into_iter()
.map(LibraryItemRow::into_library_item)
.map(LibraryItemRow::into_media_item)
.collect(),
total as u32,
))
}
async fn get_by_id(&self, id: &str) -> DomainResult<Option<LibraryItem>> {
async fn get_by_id(&self, id: &str) -> DomainResult<Option<MediaItem>> {
let row = sqlx::query_as::<_, LibraryItemRow>("SELECT * FROM library_items WHERE id = ?")
.bind(id)
.fetch_optional(&self.pool)
.await
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
Ok(row.map(LibraryItemRow::into_library_item))
Ok(row.map(LibraryItemRow::into_media_item))
}
async fn list_collections(