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

@@ -1,4 +1,4 @@
use domain::models::LibraryItem;
use domain::models::MediaItem;
use domain::DomainResult;
use super::deps::LibraryQueryDeps;
@@ -7,7 +7,7 @@ use super::queries::GetItemQuery;
pub async fn execute(
deps: &LibraryQueryDeps,
query: GetItemQuery,
) -> DomainResult<Option<LibraryItem>> {
) -> DomainResult<Option<MediaItem>> {
deps.library_query.get_by_id(&query.item_id).await
}

View File

@@ -1,5 +1,5 @@
use domain::DomainResult;
use domain::models::LibraryItem;
use domain::models::MediaItem;
use domain::value_objects::LibrarySearchFilter;
use super::deps::LibraryQueryDeps;
@@ -9,7 +9,7 @@ use super::queries::SearchItemsQuery;
pub async fn execute(
deps: &LibraryQueryDeps,
query: SearchItemsQuery,
) -> DomainResult<(Vec<LibraryItem>, u32)> {
) -> DomainResult<(Vec<MediaItem>, u32)> {
let content_type = query
.content_type
.as_deref()

View File

@@ -1,4 +1,4 @@
use domain::models::LibraryItem;
use domain::models::MediaItem;
use domain::value_objects::ContentType;
use crate::library::get_item;
@@ -8,11 +8,11 @@ use crate::library::queries::GetItemQuery;
mod helpers;
fn seed_item(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepository>) {
let item = LibraryItem::new("test", "m1", "Die Hard", ContentType::Movie, 7800, "2026-01-01");
let item = MediaItem::new_library("test", "m1", "Die Hard", ContentType::Movie, 7800, "2026-01-01");
repo.items
.lock()
.unwrap()
.insert(item.id().to_string(), item);
.insert(item.id().value().to_string(), item);
}
#[tokio::test]

View File

@@ -1,5 +1,5 @@
use domain::models::{LibraryItem, LibraryItemRow};
use domain::value_objects::ContentType;
use domain::models::{MediaItem, MediaItemRow};
use domain::value_objects::{ContentType, MediaItemId, MediaRole};
use crate::library::list_collections;
use crate::library::queries::ListCollectionsQuery;
@@ -10,13 +10,14 @@ mod helpers;
fn seed_with_collections(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepository>) {
let mut store = repo.items.lock().unwrap();
let item = LibraryItem::from_persistence(LibraryItemRow {
id: "test::m1".into(),
let item = MediaItem::from_persistence(MediaItemRow {
id: MediaItemId::new("test::m1"),
provider_id: "test".into(),
external_id: "m1".into(),
title: "Die Hard".into(),
content_type: ContentType::Movie,
duration_secs: 7800,
description: None,
series_name: None,
season_number: None,
episode_number: None,
@@ -27,17 +28,19 @@ fn seed_with_collections(repo: &std::sync::Arc<domain::testing::InMemoryLibraryR
collection_name: Some("Movies".into()),
collection_type: Some("movies".into()),
thumbnail_url: None,
synced_at: "2026-01-01".into(),
synced_at: Some("2026-01-01".into()),
role: MediaRole::default(),
});
store.insert(item.id().to_string(), item);
store.insert(item.id().value().to_string(), item);
let item2 = LibraryItem::from_persistence(LibraryItemRow {
id: "test::e1".into(),
let item2 = MediaItem::from_persistence(MediaItemRow {
id: MediaItemId::new("test::e1"),
provider_id: "test".into(),
external_id: "e1".into(),
title: "BB S01E01".into(),
content_type: ContentType::Episode,
duration_secs: 2700,
description: None,
series_name: Some("Breaking Bad".into()),
season_number: Some(1),
episode_number: Some(1),
@@ -48,9 +51,10 @@ fn seed_with_collections(repo: &std::sync::Arc<domain::testing::InMemoryLibraryR
collection_name: Some("TV Shows".into()),
collection_type: Some("tvshows".into()),
thumbnail_url: None,
synced_at: "2026-01-01".into(),
synced_at: Some("2026-01-01".into()),
role: MediaRole::default(),
});
store.insert(item2.id().to_string(), item2);
store.insert(item2.id().value().to_string(), item2);
}
#[tokio::test]

View File

@@ -1,5 +1,5 @@
use domain::models::{LibraryItem, LibraryItemRow};
use domain::value_objects::ContentType;
use domain::models::{MediaItem, MediaItemRow};
use domain::value_objects::{ContentType, MediaItemId, MediaRole};
use crate::library::list_genres;
use crate::library::queries::ListGenresQuery;
@@ -10,13 +10,14 @@ mod helpers;
fn seed_with_genres(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepository>) {
let mut store = repo.items.lock().unwrap();
let item1 = LibraryItem::from_persistence(LibraryItemRow {
id: "test::m1".into(),
let item1 = MediaItem::from_persistence(MediaItemRow {
id: MediaItemId::new("test::m1"),
provider_id: "test".into(),
external_id: "m1".into(),
title: "Die Hard".into(),
content_type: ContentType::Movie,
duration_secs: 7800,
description: None,
series_name: None,
season_number: None,
episode_number: None,
@@ -27,15 +28,17 @@ fn seed_with_genres(repo: &std::sync::Arc<domain::testing::InMemoryLibraryReposi
collection_name: None,
collection_type: None,
thumbnail_url: None,
synced_at: "2026-01-01".into(),
synced_at: Some("2026-01-01".into()),
role: MediaRole::default(),
});
let item2 = LibraryItem::from_persistence(LibraryItemRow {
id: "test::m2".into(),
let item2 = MediaItem::from_persistence(MediaItemRow {
id: MediaItemId::new("test::m2"),
provider_id: "test".into(),
external_id: "m2".into(),
title: "Alien".into(),
content_type: ContentType::Movie,
duration_secs: 7020,
description: None,
series_name: None,
season_number: None,
episode_number: None,
@@ -46,11 +49,12 @@ fn seed_with_genres(repo: &std::sync::Arc<domain::testing::InMemoryLibraryReposi
collection_name: None,
collection_type: None,
thumbnail_url: None,
synced_at: "2026-01-01".into(),
synced_at: Some("2026-01-01".into()),
role: MediaRole::default(),
});
store.insert(item1.id().to_string(), item1);
store.insert(item2.id().to_string(), item2);
store.insert(item1.id().value().to_string(), item1);
store.insert(item2.id().value().to_string(), item2);
}
#[tokio::test]

View File

@@ -1,5 +1,5 @@
use domain::models::{LibraryItem, LibraryItemRow};
use domain::value_objects::ContentType;
use domain::models::{MediaItem, MediaItemRow};
use domain::value_objects::{ContentType, MediaItemId, MediaRole};
use crate::library::list_seasons;
use crate::library::queries::ListSeasonsQuery;
@@ -11,13 +11,14 @@ fn seed_episodes(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepositor
let mut store = repo.items.lock().unwrap();
for (i, season) in [1u32, 1, 2, 2, 2, 3].iter().enumerate() {
let item = LibraryItem::from_persistence(LibraryItemRow {
id: format!("test::e{i}"),
let item = MediaItem::from_persistence(MediaItemRow {
id: MediaItemId::new(format!("test::e{i}")),
provider_id: "test".into(),
external_id: format!("e{i}"),
title: format!("BB S{season:02}E{:02}", i + 1),
content_type: ContentType::Episode,
duration_secs: 2700,
description: None,
series_name: Some("Breaking Bad".into()),
season_number: Some(*season),
episode_number: Some(i as u32 + 1),
@@ -28,9 +29,10 @@ fn seed_episodes(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepositor
collection_name: None,
collection_type: None,
thumbnail_url: None,
synced_at: "2026-01-01".into(),
synced_at: Some("2026-01-01".into()),
role: MediaRole::default(),
});
store.insert(item.id().to_string(), item);
store.insert(item.id().value().to_string(), item);
}
}

View File

@@ -1,5 +1,5 @@
use domain::models::{LibraryItem, LibraryItemRow};
use domain::value_objects::ContentType;
use domain::models::{MediaItem, MediaItemRow};
use domain::value_objects::{ContentType, MediaItemId, MediaRole};
use crate::library::list_shows;
use crate::library::queries::ListShowsQuery;
@@ -20,13 +20,14 @@ fn seed_episodes(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepositor
.iter()
.enumerate()
{
let item = LibraryItem::from_persistence(LibraryItemRow {
id: format!("test::e{i}"),
let item = MediaItem::from_persistence(MediaItemRow {
id: MediaItemId::new(format!("test::e{i}")),
provider_id: "test".into(),
external_id: format!("e{i}"),
title: format!("{series} S{season:02}E{i:02}"),
content_type: ContentType::Episode,
duration_secs: 2700,
description: None,
series_name: Some(series.to_string()),
season_number: Some(*season),
episode_number: Some(i as u32 + 1),
@@ -37,9 +38,10 @@ fn seed_episodes(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepositor
collection_name: None,
collection_type: None,
thumbnail_url: None,
synced_at: "2026-01-01".into(),
synced_at: Some("2026-01-01".into()),
role: MediaRole::default(),
});
store.insert(item.id().to_string(), item);
store.insert(item.id().value().to_string(), item);
}
}

View File

@@ -1,5 +1,5 @@
use domain::models::{LibraryItem, LibraryItemRow};
use domain::value_objects::ContentType;
use domain::models::{MediaItem, MediaItemRow};
use domain::value_objects::{ContentType, MediaItemId, MediaRole};
use crate::library::queries::SearchItemsQuery;
use crate::library::search;
@@ -10,25 +10,26 @@ mod helpers;
fn seed_items(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepository>) {
let mut store = repo.items.lock().unwrap();
let items = vec![
LibraryItem::new("test", "m1", "Die Hard", ContentType::Movie, 7800, "2026-01-01"),
LibraryItem::new("test", "m2", "Alien", ContentType::Movie, 7020, "2026-01-01"),
LibraryItem::new("test", "e1", "BB S01E01", ContentType::Episode, 2700, "2026-01-01"),
MediaItem::new_library("test", "m1", "Die Hard", ContentType::Movie, 7800, "2026-01-01"),
MediaItem::new_library("test", "m2", "Alien", ContentType::Movie, 7020, "2026-01-01"),
MediaItem::new_library("test", "e1", "BB S01E01", ContentType::Episode, 2700, "2026-01-01"),
];
for item in items {
store.insert(item.id().to_string(), item);
store.insert(item.id().value().to_string(), item);
}
}
fn seed_items_with_genres(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepository>) {
let mut store = repo.items.lock().unwrap();
let action = LibraryItem::from_persistence(LibraryItemRow {
id: "test::m1".into(),
let action = MediaItem::from_persistence(MediaItemRow {
id: MediaItemId::new("test::m1"),
provider_id: "test".into(),
external_id: "m1".into(),
title: "Die Hard".into(),
content_type: ContentType::Movie,
duration_secs: 7800,
description: None,
series_name: None,
season_number: None,
episode_number: None,
@@ -39,15 +40,17 @@ fn seed_items_with_genres(repo: &std::sync::Arc<domain::testing::InMemoryLibrary
collection_name: None,
collection_type: None,
thumbnail_url: None,
synced_at: "2026-01-01".into(),
synced_at: Some("2026-01-01".into()),
role: MediaRole::default(),
});
let scifi = LibraryItem::from_persistence(LibraryItemRow {
id: "test::m2".into(),
let scifi = MediaItem::from_persistence(MediaItemRow {
id: MediaItemId::new("test::m2"),
provider_id: "test".into(),
external_id: "m2".into(),
title: "Alien".into(),
content_type: ContentType::Movie,
duration_secs: 7020,
description: None,
series_name: None,
season_number: None,
episode_number: None,
@@ -58,15 +61,17 @@ fn seed_items_with_genres(repo: &std::sync::Arc<domain::testing::InMemoryLibrary
collection_name: None,
collection_type: None,
thumbnail_url: None,
synced_at: "2026-01-01".into(),
synced_at: Some("2026-01-01".into()),
role: MediaRole::default(),
});
let comedy = LibraryItem::from_persistence(LibraryItemRow {
id: "test::m3".into(),
let comedy = MediaItem::from_persistence(MediaItemRow {
id: MediaItemId::new("test::m3"),
provider_id: "test".into(),
external_id: "m3".into(),
title: "Airplane!".into(),
content_type: ContentType::Movie,
duration_secs: 5280,
description: None,
series_name: None,
season_number: None,
episode_number: None,
@@ -77,12 +82,13 @@ fn seed_items_with_genres(repo: &std::sync::Arc<domain::testing::InMemoryLibrary
collection_name: None,
collection_type: None,
thumbnail_url: None,
synced_at: "2026-01-01".into(),
synced_at: Some("2026-01-01".into()),
role: MediaRole::default(),
});
store.insert(action.id().to_string(), action);
store.insert(scifi.id().to_string(), scifi);
store.insert(comedy.id().to_string(), comedy);
store.insert(action.id().value().to_string(), action);
store.insert(scifi.id().value().to_string(), scifi);
store.insert(comedy.id().value().to_string(), comedy);
}
#[tokio::test]