From c0e685a4ee0802c2ceb80a8e056ef8dc4755e959 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sun, 12 Jul 2026 05:00:49 +0200 Subject: [PATCH] refactor(domain): Row structs for from_persistence, ID newtypes, kill clippy.toml - delete clippy.toml (too-many-arguments-threshold=20 hack) - ChannelRow/MediaItemRow/LibraryItemRow structs for from_persistence - SnapshotId/ActivityEventId/PlaybackRecordId newtypes - DomainError variants use ChannelId/UserId instead of Uuid - ActivityEvent.channel_id: Option not Option - InMemory repos key on newtype IDs - AlgorithmicParams struct for schedule engine - update all adapters/application/presentation callers --- crates/adapters/jellyfin/src/mapping.rs | 28 ++--- crates/adapters/local-files/src/provider.rs | 28 ++--- crates/adapters/postgres/src/activity.rs | 8 +- crates/adapters/postgres/src/channel.rs | 65 +++++----- crates/adapters/postgres/src/library.rs | 43 +++---- crates/adapters/postgres/src/schedule.rs | 4 +- crates/adapters/sqlite/src/activity.rs | 8 +- crates/adapters/sqlite/src/channel.rs | 46 +++---- crates/adapters/sqlite/src/library.rs | 43 +++---- crates/adapters/sqlite/src/schedule.rs | 4 +- crates/api-types/src/admin.rs | 4 +- crates/api-types/src/channels.rs | 2 +- crates/application/src/auth/tests/login.rs | 4 +- crates/application/src/auth/tests/register.rs | 2 +- crates/application/src/channels/commands.rs | 14 +-- crates/application/src/channels/create.rs | 4 +- crates/application/src/channels/delete.rs | 10 +- crates/application/src/channels/get.rs | 4 +- .../application/src/channels/list_by_owner.rs | 4 +- crates/application/src/channels/mod.rs | 3 +- crates/application/src/channels/queries.rs | 6 +- .../application/src/channels/tests/create.rs | 4 +- .../application/src/channels/tests/delete.rs | 18 +-- crates/application/src/channels/tests/get.rs | 8 +- crates/application/src/channels/tests/list.rs | 2 +- .../src/channels/tests/list_by_owner.rs | 8 +- .../application/src/channels/tests/update.rs | 40 +++--- crates/application/src/channels/update.rs | 8 +- .../src/config_snapshots/commands.rs | 12 +- .../application/src/config_snapshots/get.rs | 4 +- .../application/src/config_snapshots/list.rs | 4 +- .../src/config_snapshots/patch_label.rs | 5 +- .../src/config_snapshots/queries.rs | 8 +- .../src/config_snapshots/restore.rs | 9 +- .../application/src/config_snapshots/save.rs | 7 +- .../src/config_snapshots/tests/list.rs | 8 +- .../src/config_snapshots/tests/save.rs | 8 +- crates/application/src/iptv/tests/m3u.rs | 4 +- .../src/library/tests/list_collections.rs | 78 ++++++------ .../src/library/tests/list_genres.rs | 78 ++++++------ .../src/library/tests/list_seasons.rs | 40 +++--- .../src/library/tests/list_shows.rs | 40 +++--- .../application/src/library/tests/search.rs | 116 +++++++++--------- .../src/schedule/tests/delete_after.rs | 4 +- .../src/schedule/tests/generate.rs | 4 +- crates/domain/clippy.toml | 1 - crates/domain/src/errors/mod.rs | 9 +- crates/domain/src/models/activity.rs | 19 +-- crates/domain/src/models/channel.rs | 82 +++++++------ crates/domain/src/models/config_snapshot.rs | 11 +- crates/domain/src/models/library.rs | 74 +++++------ crates/domain/src/models/media.rs | 69 ++++++----- crates/domain/src/models/mod.rs | 8 +- crates/domain/src/models/tests/activity.rs | 7 +- .../src/models/tests/config_snapshot.rs | 3 +- crates/domain/src/models/tests/library.rs | 38 +++--- crates/domain/src/models/tests/media.rs | 33 ++--- crates/domain/src/ports/channel.rs | 7 +- crates/domain/src/services/schedule/mod.rs | 42 ++++--- crates/domain/src/testing/in_memory.rs | 34 +++-- crates/domain/src/value_objects/ids.rs | 3 + crates/mcp/src/tools/channels.rs | 18 +-- crates/presentation/src/factory.rs | 34 ++--- crates/presentation/src/handlers/channels.rs | 34 ++--- crates/presentation/src/handlers/schedule.rs | 2 +- 65 files changed, 684 insertions(+), 695 deletions(-) delete mode 100644 crates/domain/clippy.toml diff --git a/crates/adapters/jellyfin/src/mapping.rs b/crates/adapters/jellyfin/src/mapping.rs index 6ac5fa0..f0ad08b 100644 --- a/crates/adapters/jellyfin/src/mapping.rs +++ b/crates/adapters/jellyfin/src/mapping.rs @@ -1,4 +1,4 @@ -use domain::{ContentType, MediaItem, MediaItemId}; +use domain::{ContentType, MediaItem, MediaItemId, MediaItemRow}; use crate::models::JellyfinItem; @@ -16,19 +16,19 @@ pub(crate) fn map_jellyfin_item(item: JellyfinItem) -> Option { .map(|t| (t / TICKS_PER_SEC) as u32) .unwrap_or(0); - Some(MediaItem::from_persistence( - MediaItemId::new(item.id), - item.name, + Some(MediaItem::from_persistence(MediaItemRow { + id: MediaItemId::new(item.id), + title: item.name, content_type, duration_secs, - item.overview, - item.genres.unwrap_or_default(), - item.production_year, - item.tags.unwrap_or_default(), - item.series_name, - item.parent_index_number, - item.index_number, - None, - None, - )) + description: item.overview, + genres: item.genres.unwrap_or_default(), + year: item.production_year, + tags: item.tags.unwrap_or_default(), + series_name: item.series_name, + season_number: item.parent_index_number, + episode_number: item.index_number, + thumbnail_url: None, + collection_id: None, + })) } diff --git a/crates/adapters/local-files/src/provider.rs b/crates/adapters/local-files/src/provider.rs index 72bb15e..5666e79 100644 --- a/crates/adapters/local-files/src/provider.rs +++ b/crates/adapters/local-files/src/provider.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use domain::ports::{ Collection, IMediaProvider, ProviderCapabilities, StreamQuality, StreamingProtocol, }; -use domain::{ContentType, DomainError, DomainResult, MediaFilter, MediaItem, MediaItemId}; +use domain::{ContentType, DomainError, DomainResult, MediaFilter, MediaItem, MediaItemId, MediaItemRow}; use crate::config::LocalFilesConfig; use crate::index::{decode_id, LocalIndex}; @@ -40,21 +40,21 @@ fn to_media_item(id: MediaItemId, item: &LocalFileItem) -> MediaItem { } else { ContentType::Movie }; - MediaItem::from_persistence( + MediaItem::from_persistence(MediaItemRow { id, - item.title.clone(), + title: item.title.clone(), content_type, - item.duration_secs, - None, - vec![], - item.year, - item.tags.clone(), - None, - None, - None, - None, - None, - ) + duration_secs: item.duration_secs, + description: None, + genres: vec![], + year: item.year, + tags: item.tags.clone(), + series_name: None, + season_number: None, + episode_number: None, + thumbnail_url: None, + collection_id: None, + }) } #[async_trait] diff --git a/crates/adapters/postgres/src/activity.rs b/crates/adapters/postgres/src/activity.rs index b4f7a3b..2ca5731 100644 --- a/crates/adapters/postgres/src/activity.rs +++ b/crates/adapters/postgres/src/activity.rs @@ -6,7 +6,7 @@ use uuid::Uuid; use adapter_common::{map_sqlx_error, parse_dt, parse_uuid}; use domain::{ ports::activity::{ActivityLogCommand, ActivityLogQuery}, - ActivityEvent, ChannelId, DomainResult, + ActivityEvent, ActivityEventId, ChannelId, DomainResult, }; pub struct PgActivityLog { @@ -66,9 +66,11 @@ impl ActivityLogQuery for PgActivityLog { let Ok(timestamp) = parse_dt(&ts_str) else { continue; }; - let channel_id = channel_id_str.and_then(|s| Uuid::parse_str(&s).ok()); + let channel_id = channel_id_str + .and_then(|s| Uuid::parse_str(&s).ok()) + .map(ChannelId::from_uuid); events.push(ActivityEvent::from_persistence( - id, + ActivityEventId::from_uuid(id), timestamp, event_type, detail, diff --git a/crates/adapters/postgres/src/channel.rs b/crates/adapters/postgres/src/channel.rs index 3b6b68f..a620dc7 100644 --- a/crates/adapters/postgres/src/channel.rs +++ b/crates/adapters/postgres/src/channel.rs @@ -9,8 +9,8 @@ use adapter_common::{ }; use domain::{ ports::channel::{ChannelCommand, ChannelQuery}, - AccessMode, Channel, ChannelConfigSnapshot, ChannelId, DomainError, DomainResult, LogoPosition, - ScheduleConfig, UserId, + Channel, ChannelConfigSnapshot, ChannelId, ChannelRow as DomainChannelRow, DomainError, + DomainResult, ScheduleConfig, SnapshotId, UserId, }; pub struct PgChannelRepository { @@ -50,34 +50,27 @@ struct ChannelRow { impl ChannelRow { fn into_channel(self) -> DomainResult { - let id = ChannelId::from_uuid(parse_uuid(&self.id, "channel id")?); - let owner_id = UserId::from_uuid(parse_uuid(&self.owner_id, "owner id")?); - let schedule_config = parse_schedule_config(&self.schedule_config)?; - let recycle_policy = parse_recycle_policy(&self.recycle_policy)?; - let access_mode: AccessMode = parse_enum_or_default(self.access_mode); - let logo_position: LogoPosition = parse_enum_or_default(self.logo_position); - - Ok(Channel::from_persistence( - id, - owner_id, - self.name, - self.description, - self.timezone, - schedule_config, - recycle_policy, - self.auto_schedule, - access_mode, - self.access_password_hash, - self.logo, - logo_position, - self.logo_opacity, - self.webhook_url, - self.webhook_poll_interval_secs as u32, - self.webhook_body_template, - self.webhook_headers, - parse_dt(&self.created_at)?, - parse_dt(&self.updated_at)?, - )) + Ok(Channel::from_persistence(DomainChannelRow { + id: ChannelId::from_uuid(parse_uuid(&self.id, "channel id")?), + owner_id: UserId::from_uuid(parse_uuid(&self.owner_id, "owner id")?), + name: self.name, + description: self.description, + timezone: self.timezone, + schedule_config: parse_schedule_config(&self.schedule_config)?, + recycle_policy: parse_recycle_policy(&self.recycle_policy)?, + auto_schedule: self.auto_schedule, + access_mode: parse_enum_or_default(self.access_mode), + access_password_hash: self.access_password_hash, + logo: self.logo, + logo_position: parse_enum_or_default(self.logo_position), + logo_opacity: self.logo_opacity, + webhook_url: self.webhook_url, + webhook_poll_interval_secs: self.webhook_poll_interval_secs as u32, + webhook_body_template: self.webhook_body_template, + webhook_headers: self.webhook_headers, + created_at: parse_dt(&self.created_at)?, + updated_at: parse_dt(&self.updated_at)?, + })) } } @@ -86,7 +79,7 @@ fn map_snapshot_row( channel_id: ChannelId, ) -> DomainResult { let id_str: String = row.get("id"); - let id = parse_uuid(&id_str, "snapshot id")?; + let id = SnapshotId::from_uuid(parse_uuid(&id_str, "snapshot id")?); let config_json: String = row.get("config_json"); let config = parse_schedule_config(&config_json)?; let version_num: i64 = row.get("version_num"); @@ -214,7 +207,7 @@ impl ChannelCommand for PgChannelRepository { tx.commit().await.map_err(map_sqlx_error)?; Ok(ChannelConfigSnapshot::from_persistence( - id, + SnapshotId::from_uuid(id), channel_id, config.clone(), version_num, @@ -226,14 +219,14 @@ impl ChannelCommand for PgChannelRepository { async fn patch_config_snapshot_label( &self, channel_id: ChannelId, - snapshot_id: Uuid, + snapshot_id: SnapshotId, label: Option, ) -> DomainResult> { let updated = sqlx::query( "UPDATE channel_config_snapshots SET label = $1 WHERE id = $2 AND channel_id = $3 RETURNING id", ) .bind(&label) - .bind(snapshot_id.to_string()) + .bind(snapshot_id.value().to_string()) .bind(channel_id.value().to_string()) .fetch_optional(&self.pool) .await @@ -316,13 +309,13 @@ impl ChannelQuery for PgChannelRepository { async fn get_config_snapshot( &self, channel_id: ChannelId, - snapshot_id: Uuid, + snapshot_id: SnapshotId, ) -> DomainResult> { let row = sqlx::query( "SELECT id, config_json, version_num, label, created_at FROM channel_config_snapshots WHERE id = $1 AND channel_id = $2", ) - .bind(snapshot_id.to_string()) + .bind(snapshot_id.value().to_string()) .bind(channel_id.value().to_string()) .fetch_optional(&self.pool) .await diff --git a/crates/adapters/postgres/src/library.rs b/crates/adapters/postgres/src/library.rs index 4ccc637..673335e 100644 --- a/crates/adapters/postgres/src/library.rs +++ b/crates/adapters/postgres/src/library.rs @@ -4,8 +4,9 @@ use sqlx::PgPool; use adapter_common::{content_type_str, parse_content_type, parse_genres_blob}; use domain::{ ports::library::{LibraryCommand, LibraryQuery}, - ContentType, DomainError, DomainResult, LibraryCollection, LibraryItem, LibrarySearchFilter, - LibrarySyncLogEntry, LibrarySyncResult, SeasonSummary, ShowSummary, + ContentType, DomainError, DomainResult, LibraryCollection, LibraryItem, + LibraryItemRow as DomainLibraryItemRow, LibrarySearchFilter, LibrarySyncLogEntry, + LibrarySyncResult, SeasonSummary, ShowSummary, }; pub struct PgLibraryRepository { @@ -41,25 +42,25 @@ struct LibraryItemRow { impl LibraryItemRow { fn into_library_item(self) -> LibraryItem { - LibraryItem::from_persistence( - self.id, - self.provider_id, - self.external_id, - self.title, - parse_content_type(&self.content_type), - self.duration_secs as u32, - self.series_name, - self.season_number.map(|n| n as u32), - self.episode_number.map(|n| n as u32), - self.year.map(|n| n as u16), - serde_json::from_str(&self.genres).unwrap_or_default(), - serde_json::from_str(&self.tags).unwrap_or_default(), - self.collection_id, - self.collection_name, - self.collection_type, - self.thumbnail_url, - self.synced_at, - ) + LibraryItem::from_persistence(DomainLibraryItemRow { + id: 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, + 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), + year: self.year.map(|n| n as u16), + genres: serde_json::from_str(&self.genres).unwrap_or_default(), + tags: serde_json::from_str(&self.tags).unwrap_or_default(), + collection_id: self.collection_id, + collection_name: self.collection_name, + collection_type: self.collection_type, + thumbnail_url: self.thumbnail_url, + synced_at: self.synced_at, + }) } } diff --git a/crates/adapters/postgres/src/schedule.rs b/crates/adapters/postgres/src/schedule.rs index 28534b9..0256a10 100644 --- a/crates/adapters/postgres/src/schedule.rs +++ b/crates/adapters/postgres/src/schedule.rs @@ -7,7 +7,7 @@ use adapter_common::{map_sqlx_error, parse_dt, parse_json, parse_uuid}; use domain::{ ports::schedule::{ScheduleCommand, ScheduleQuery}, BlockId, ChannelId, DomainError, DomainResult, GeneratedSchedule, MediaItem, MediaItemId, - PlaybackRecord, ScheduleId, ScheduledSlot, SlotId, + PlaybackRecord, PlaybackRecordId, ScheduleId, ScheduledSlot, SlotId, }; pub struct PgScheduleRepository { @@ -85,7 +85,7 @@ fn map_schedule(row: ScheduleRow, slot_rows: Vec) -> DomainResult DomainResult { - let id = parse_uuid(&row.id, "playback record id")?; + let id = PlaybackRecordId::from_uuid(parse_uuid(&row.id, "playback record id")?); let channel_id = ChannelId::from_uuid(parse_uuid(&row.channel_id, "channel id")?); Ok(PlaybackRecord::from_persistence( diff --git a/crates/adapters/sqlite/src/activity.rs b/crates/adapters/sqlite/src/activity.rs index 82ce074..a7c1407 100644 --- a/crates/adapters/sqlite/src/activity.rs +++ b/crates/adapters/sqlite/src/activity.rs @@ -6,7 +6,7 @@ use uuid::Uuid; use adapter_common::{map_sqlx_error, parse_dt, parse_uuid}; use domain::{ ports::activity::{ActivityLogCommand, ActivityLogQuery}, - ActivityEvent, ChannelId, DomainResult, + ActivityEvent, ActivityEventId, ChannelId, DomainResult, }; pub struct SqliteActivityLog { @@ -66,9 +66,11 @@ impl ActivityLogQuery for SqliteActivityLog { let Ok(timestamp) = parse_dt(&ts_str) else { continue; }; - let channel_id = channel_id_str.and_then(|s| Uuid::parse_str(&s).ok()); + let channel_id = channel_id_str + .and_then(|s| Uuid::parse_str(&s).ok()) + .map(ChannelId::from_uuid); events.push(ActivityEvent::from_persistence( - id, + ActivityEventId::from_uuid(id), timestamp, event_type, detail, diff --git a/crates/adapters/sqlite/src/channel.rs b/crates/adapters/sqlite/src/channel.rs index 880c917..0103ae7 100644 --- a/crates/adapters/sqlite/src/channel.rs +++ b/crates/adapters/sqlite/src/channel.rs @@ -9,8 +9,8 @@ use adapter_common::{ }; use domain::{ ports::channel::{ChannelCommand, ChannelQuery}, - AccessMode, Channel, ChannelConfigSnapshot, ChannelId, DomainError, DomainResult, LogoPosition, - ScheduleConfig, UserId, + AccessMode, Channel, ChannelConfigSnapshot, ChannelId, ChannelRow as DomainChannelRow, + DomainError, DomainResult, LogoPosition, ScheduleConfig, SnapshotId, UserId, }; pub struct SqliteChannelRepository { @@ -57,27 +57,27 @@ impl ChannelRow { let access_mode: AccessMode = parse_enum_or_default(self.access_mode); let logo_position: LogoPosition = parse_enum_or_default(self.logo_position); - Ok(Channel::from_persistence( + Ok(Channel::from_persistence(DomainChannelRow { id, owner_id, - self.name, - self.description, - self.timezone, + name: self.name, + description: self.description, + timezone: self.timezone, schedule_config, recycle_policy, - self.auto_schedule != 0, + auto_schedule: self.auto_schedule != 0, access_mode, - self.access_password_hash, - self.logo, + access_password_hash: self.access_password_hash, + logo: self.logo, logo_position, - self.logo_opacity, - self.webhook_url, - self.webhook_poll_interval_secs as u32, - self.webhook_body_template, - self.webhook_headers, - parse_dt(&self.created_at)?, - parse_dt(&self.updated_at)?, - )) + logo_opacity: self.logo_opacity, + webhook_url: self.webhook_url, + webhook_poll_interval_secs: self.webhook_poll_interval_secs as u32, + webhook_body_template: self.webhook_body_template, + webhook_headers: self.webhook_headers, + created_at: parse_dt(&self.created_at)?, + updated_at: parse_dt(&self.updated_at)?, + })) } } @@ -86,7 +86,7 @@ fn map_snapshot_row( channel_id: ChannelId, ) -> DomainResult { let id_str: String = row.get("id"); - let id = parse_uuid(&id_str, "snapshot id")?; + let id = SnapshotId::from_uuid(parse_uuid(&id_str, "snapshot id")?); let config_json: String = row.get("config_json"); let config = parse_schedule_config(&config_json)?; let version_num: i64 = row.get("version_num"); @@ -214,7 +214,7 @@ impl ChannelCommand for SqliteChannelRepository { tx.commit().await.map_err(map_sqlx_error)?; Ok(ChannelConfigSnapshot::from_persistence( - id, + SnapshotId::from_uuid(id), channel_id, config.clone(), version_num, @@ -226,14 +226,14 @@ impl ChannelCommand for SqliteChannelRepository { async fn patch_config_snapshot_label( &self, channel_id: ChannelId, - snapshot_id: Uuid, + snapshot_id: SnapshotId, label: Option, ) -> DomainResult> { let updated = sqlx::query( "UPDATE channel_config_snapshots SET label = ? WHERE id = ? AND channel_id = ? RETURNING id", ) .bind(&label) - .bind(snapshot_id.to_string()) + .bind(snapshot_id.value().to_string()) .bind(channel_id.value().to_string()) .fetch_optional(&self.pool) .await @@ -316,13 +316,13 @@ impl ChannelQuery for SqliteChannelRepository { async fn get_config_snapshot( &self, channel_id: ChannelId, - snapshot_id: Uuid, + snapshot_id: SnapshotId, ) -> DomainResult> { let row = sqlx::query( "SELECT id, config_json, version_num, label, created_at FROM channel_config_snapshots WHERE id = ? AND channel_id = ?", ) - .bind(snapshot_id.to_string()) + .bind(snapshot_id.value().to_string()) .bind(channel_id.value().to_string()) .fetch_optional(&self.pool) .await diff --git a/crates/adapters/sqlite/src/library.rs b/crates/adapters/sqlite/src/library.rs index d1a9eb4..1743859 100644 --- a/crates/adapters/sqlite/src/library.rs +++ b/crates/adapters/sqlite/src/library.rs @@ -4,8 +4,9 @@ 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, LibrarySearchFilter, - LibrarySyncLogEntry, LibrarySyncResult, SeasonSummary, ShowSummary, + ContentType, DomainError, DomainResult, LibraryCollection, LibraryItem, + LibraryItemRow as DomainLibraryItemRow, LibrarySearchFilter, LibrarySyncLogEntry, + LibrarySyncResult, SeasonSummary, ShowSummary, }; pub struct SqliteLibraryRepository { @@ -41,25 +42,25 @@ struct LibraryItemRow { impl LibraryItemRow { fn into_library_item(self) -> LibraryItem { - LibraryItem::from_persistence( - self.id, - self.provider_id, - self.external_id, - self.title, - parse_content_type(&self.content_type), - self.duration_secs as u32, - self.series_name, - self.season_number.map(|n| n as u32), - self.episode_number.map(|n| n as u32), - self.year.map(|n| n as u16), - serde_json::from_str(&self.genres).unwrap_or_default(), - serde_json::from_str(&self.tags).unwrap_or_default(), - self.collection_id, - self.collection_name, - self.collection_type, - self.thumbnail_url, - self.synced_at, - ) + LibraryItem::from_persistence(DomainLibraryItemRow { + id: 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, + 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), + year: self.year.map(|n| n as u16), + genres: serde_json::from_str(&self.genres).unwrap_or_default(), + tags: serde_json::from_str(&self.tags).unwrap_or_default(), + collection_id: self.collection_id, + collection_name: self.collection_name, + collection_type: self.collection_type, + thumbnail_url: self.thumbnail_url, + synced_at: self.synced_at, + }) } } diff --git a/crates/adapters/sqlite/src/schedule.rs b/crates/adapters/sqlite/src/schedule.rs index 07aab55..48d2100 100644 --- a/crates/adapters/sqlite/src/schedule.rs +++ b/crates/adapters/sqlite/src/schedule.rs @@ -7,7 +7,7 @@ use adapter_common::{map_sqlx_error, parse_dt, parse_json, parse_uuid}; use domain::{ ports::schedule::{ScheduleCommand, ScheduleQuery}, BlockId, ChannelId, DomainError, DomainResult, GeneratedSchedule, MediaItem, MediaItemId, - PlaybackRecord, ScheduleId, ScheduledSlot, SlotId, + PlaybackRecord, PlaybackRecordId, ScheduleId, ScheduledSlot, SlotId, }; pub struct SqliteScheduleRepository { @@ -84,7 +84,7 @@ fn map_schedule(row: ScheduleRow, slot_rows: Vec) -> DomainResult DomainResult { - let id = parse_uuid(&row.id, "playback record id")?; + let id = PlaybackRecordId::from_uuid(parse_uuid(&row.id, "playback record id")?); let channel_id = ChannelId::from_uuid(parse_uuid(&row.channel_id, "channel id")?); Ok(PlaybackRecord::from_persistence( diff --git a/crates/api-types/src/admin.rs b/crates/api-types/src/admin.rs index e06d314..2385ce3 100644 --- a/crates/api-types/src/admin.rs +++ b/crates/api-types/src/admin.rs @@ -20,11 +20,11 @@ pub struct ActivityEventResponse { impl From for ActivityEventResponse { fn from(e: domain::ActivityEvent) -> Self { Self { - id: e.id(), + id: e.id().value(), timestamp: e.timestamp(), event_type: e.event_type().to_string(), detail: e.detail().to_string(), - channel_id: e.channel_id(), + channel_id: e.channel_id().map(|id| id.value()), } } } diff --git a/crates/api-types/src/channels.rs b/crates/api-types/src/channels.rs index e375069..3e09e5e 100644 --- a/crates/api-types/src/channels.rs +++ b/crates/api-types/src/channels.rs @@ -95,7 +95,7 @@ pub struct ConfigSnapshotResponse { impl From for ConfigSnapshotResponse { fn from(s: domain::ChannelConfigSnapshot) -> Self { Self { - id: s.id(), + id: s.id().value(), version_num: s.version_num(), label: s.label().map(|s| s.to_string()), created_at: s.created_at(), diff --git a/crates/application/src/auth/tests/login.rs b/crates/application/src/auth/tests/login.rs index 967e4bc..ddfe1d9 100644 --- a/crates/application/src/auth/tests/login.rs +++ b/crates/application/src/auth/tests/login.rs @@ -34,7 +34,7 @@ fn make_deps_with_user( repo.store .lock() .unwrap() - .insert(user.id().value(), user); + .insert(user.id(), user); let deps = AuthDeps { user_command: repo.clone(), @@ -112,7 +112,7 @@ async fn login_fails_for_oidc_only_user() { repo.store .lock() .unwrap() - .insert(user.id().value(), user); + .insert(user.id(), user); let deps = AuthDeps { user_command: repo.clone(), diff --git a/crates/application/src/auth/tests/register.rs b/crates/application/src/auth/tests/register.rs index 69f27f9..cc77b01 100644 --- a/crates/application/src/auth/tests/register.rs +++ b/crates/application/src/auth/tests/register.rs @@ -103,7 +103,7 @@ async fn register_fails_for_duplicate_email() { repo.store .lock() .unwrap() - .insert(existing.id().value(), existing); + .insert(existing.id(), existing); let result = register::execute( &deps, diff --git a/crates/application/src/channels/commands.rs b/crates/application/src/channels/commands.rs index ad2f217..56f4624 100644 --- a/crates/application/src/channels/commands.rs +++ b/crates/application/src/channels/commands.rs @@ -1,17 +1,15 @@ -use uuid::Uuid; - use domain::models::ScheduleConfig; -use domain::value_objects::RecyclePolicy; +use domain::value_objects::{ChannelId, RecyclePolicy, UserId}; pub struct CreateChannelCommand { - pub owner_id: Uuid, + pub owner_id: UserId, pub name: String, pub timezone: String, } pub struct UpdateChannelCommand { - pub channel_id: Uuid, - pub owner_id: Uuid, + pub channel_id: ChannelId, + pub owner_id: UserId, pub name: Option, pub description: Option>, pub timezone: Option, @@ -21,6 +19,6 @@ pub struct UpdateChannelCommand { } pub struct DeleteChannelCommand { - pub channel_id: Uuid, - pub owner_id: Uuid, + pub channel_id: ChannelId, + pub owner_id: UserId, } diff --git a/crates/application/src/channels/create.rs b/crates/application/src/channels/create.rs index 52d6f65..b909115 100644 --- a/crates/application/src/channels/create.rs +++ b/crates/application/src/channels/create.rs @@ -1,14 +1,12 @@ use domain::events::DomainEvent; use domain::models::Channel; -use domain::value_objects::UserId; use domain::DomainResult; use super::commands::CreateChannelCommand; use super::deps::ChannelCommandDeps; pub async fn execute(deps: &ChannelCommandDeps, cmd: CreateChannelCommand) -> DomainResult { - let owner_id = UserId::from(cmd.owner_id); - let channel = Channel::new(owner_id, cmd.name, cmd.timezone); + let channel = Channel::new(cmd.owner_id, cmd.name, cmd.timezone); deps.channel_command.save(&channel).await?; diff --git a/crates/application/src/channels/delete.rs b/crates/application/src/channels/delete.rs index e2025af..3ed3fa1 100644 --- a/crates/application/src/channels/delete.rs +++ b/crates/application/src/channels/delete.rs @@ -1,5 +1,4 @@ use domain::events::DomainEvent; -use domain::value_objects::{ChannelId, UserId}; use domain::DomainResult; use super::commands::DeleteChannelCommand; @@ -7,15 +6,12 @@ use super::deps::ChannelCommandDeps; use super::find_owned_channel; pub async fn execute(deps: &ChannelCommandDeps, cmd: DeleteChannelCommand) -> DomainResult<()> { - let channel_id = ChannelId::from(cmd.channel_id); - let owner_id = UserId::from(cmd.owner_id); + find_owned_channel(deps.channel_query.as_ref(), cmd.channel_id, cmd.owner_id).await?; - find_owned_channel(deps.channel_query.as_ref(), channel_id, owner_id, cmd.channel_id).await?; - - deps.channel_command.delete(channel_id).await?; + deps.channel_command.delete(cmd.channel_id).await?; deps.event_publisher - .publish(DomainEvent::ChannelDeleted { channel_id }) + .publish(DomainEvent::ChannelDeleted { channel_id: cmd.channel_id }) .await?; Ok(()) diff --git a/crates/application/src/channels/get.rs b/crates/application/src/channels/get.rs index 4eb9529..51adda9 100644 --- a/crates/application/src/channels/get.rs +++ b/crates/application/src/channels/get.rs @@ -1,13 +1,11 @@ use domain::models::Channel; -use domain::value_objects::ChannelId; use domain::DomainResult; use super::deps::ChannelQueryDeps; use super::queries::GetChannelQuery; pub async fn execute(deps: &ChannelQueryDeps, query: GetChannelQuery) -> DomainResult> { - let channel_id = ChannelId::from(query.channel_id); - deps.channel_query.find_by_id(channel_id).await + deps.channel_query.find_by_id(query.channel_id).await } #[cfg(test)] diff --git a/crates/application/src/channels/list_by_owner.rs b/crates/application/src/channels/list_by_owner.rs index a2bb7e9..e4bba83 100644 --- a/crates/application/src/channels/list_by_owner.rs +++ b/crates/application/src/channels/list_by_owner.rs @@ -1,13 +1,11 @@ use domain::models::Channel; -use domain::value_objects::UserId; use domain::DomainResult; use super::deps::ChannelQueryDeps; use super::queries::ListByOwnerQuery; pub async fn execute(deps: &ChannelQueryDeps, query: ListByOwnerQuery) -> DomainResult> { - let owner_id = UserId::from(query.owner_id); - deps.channel_query.find_by_owner(owner_id).await + deps.channel_query.find_by_owner(query.owner_id).await } #[cfg(test)] diff --git a/crates/application/src/channels/mod.rs b/crates/application/src/channels/mod.rs index ef67bd7..41d5bfe 100644 --- a/crates/application/src/channels/mod.rs +++ b/crates/application/src/channels/mod.rs @@ -22,12 +22,11 @@ pub(crate) async fn find_owned_channel( query: &dyn domain::ports::ChannelQuery, channel_id: ChannelId, owner_id: UserId, - raw_channel_id: uuid::Uuid, ) -> DomainResult { let channel = query .find_by_id(channel_id) .await? - .ok_or(DomainError::ChannelNotFound(raw_channel_id))?; + .ok_or(DomainError::ChannelNotFound(channel_id))?; if channel.owner_id() != owner_id { return Err(DomainError::forbidden(OWNERSHIP_DENIED)); diff --git a/crates/application/src/channels/queries.rs b/crates/application/src/channels/queries.rs index ec5c443..618892c 100644 --- a/crates/application/src/channels/queries.rs +++ b/crates/application/src/channels/queries.rs @@ -1,11 +1,11 @@ -use uuid::Uuid; +use domain::value_objects::{ChannelId, UserId}; pub struct GetChannelQuery { - pub channel_id: Uuid, + pub channel_id: ChannelId, } pub struct ListChannelsQuery; pub struct ListByOwnerQuery { - pub owner_id: Uuid, + pub owner_id: UserId, } diff --git a/crates/application/src/channels/tests/create.rs b/crates/application/src/channels/tests/create.rs index 354d4bf..877ae2e 100644 --- a/crates/application/src/channels/tests/create.rs +++ b/crates/application/src/channels/tests/create.rs @@ -25,7 +25,7 @@ async fn creates_channel_successfully() { let channel = create::execute( &deps, CreateChannelCommand { - owner_id: owner.value(), + owner_id: owner, name: "Movie Night".into(), timezone: "America/New_York".into(), }, @@ -51,7 +51,7 @@ async fn create_returns_default_config() { let channel = create::execute( &deps, CreateChannelCommand { - owner_id: UserId::generate().value(), + owner_id: UserId::generate(), name: "Defaults".into(), timezone: "UTC".into(), }, diff --git a/crates/application/src/channels/tests/delete.rs b/crates/application/src/channels/tests/delete.rs index 4cf3b37..6f2da12 100644 --- a/crates/application/src/channels/tests/delete.rs +++ b/crates/application/src/channels/tests/delete.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use domain::testing::{InMemoryChannelRepository, NoopEventPublisher}; -use domain::value_objects::UserId; +use domain::value_objects::{ChannelId, UserId}; use domain::DomainError; use crate::channels::commands::{CreateChannelCommand, DeleteChannelCommand}; @@ -26,7 +26,7 @@ async fn deletes_channel_by_owner() { let channel = create::execute( &deps, CreateChannelCommand { - owner_id: owner.value(), + owner_id: owner, name: "Doomed".into(), timezone: "UTC".into(), }, @@ -37,8 +37,8 @@ async fn deletes_channel_by_owner() { delete::execute( &deps, DeleteChannelCommand { - channel_id: channel.id().value(), - owner_id: owner.value(), + channel_id: channel.id(), + owner_id: owner, }, ) .await @@ -56,7 +56,7 @@ async fn delete_fails_if_not_owner() { let channel = create::execute( &deps, CreateChannelCommand { - owner_id: owner.value(), + owner_id: owner, name: "Protected".into(), timezone: "UTC".into(), }, @@ -67,8 +67,8 @@ async fn delete_fails_if_not_owner() { let result = delete::execute( &deps, DeleteChannelCommand { - channel_id: channel.id().value(), - owner_id: stranger.value(), + channel_id: channel.id(), + owner_id: stranger, }, ) .await; @@ -87,8 +87,8 @@ async fn delete_nonexistent_channel_returns_not_found() { let result = delete::execute( &deps, DeleteChannelCommand { - channel_id: uuid::Uuid::new_v4(), - owner_id: uuid::Uuid::new_v4(), + channel_id: ChannelId::generate(), + owner_id: UserId::generate(), }, ) .await; diff --git a/crates/application/src/channels/tests/get.rs b/crates/application/src/channels/tests/get.rs index 9c078cd..06f099c 100644 --- a/crates/application/src/channels/tests/get.rs +++ b/crates/application/src/channels/tests/get.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use domain::testing::{InMemoryChannelRepository, NoopEventPublisher}; -use domain::value_objects::UserId; +use domain::value_objects::{ChannelId, UserId}; use crate::channels::commands::CreateChannelCommand; use crate::channels::deps::{ChannelCommandDeps, ChannelQueryDeps}; @@ -28,7 +28,7 @@ async fn get_existing_channel() { let channel = create::execute( &cmd_deps, CreateChannelCommand { - owner_id: UserId::generate().value(), + owner_id: UserId::generate(), name: "Findable".into(), timezone: "UTC".into(), }, @@ -39,7 +39,7 @@ async fn get_existing_channel() { let found = get::execute( &query_deps, GetChannelQuery { - channel_id: channel.id().value(), + channel_id: channel.id(), }, ) .await @@ -56,7 +56,7 @@ async fn get_nonexistent_returns_none() { let found = get::execute( &query_deps, GetChannelQuery { - channel_id: uuid::Uuid::new_v4(), + channel_id: ChannelId::generate(), }, ) .await diff --git a/crates/application/src/channels/tests/list.rs b/crates/application/src/channels/tests/list.rs index 26ddf19..e5391b2 100644 --- a/crates/application/src/channels/tests/list.rs +++ b/crates/application/src/channels/tests/list.rs @@ -37,7 +37,7 @@ async fn list_returns_all_channels() { create::execute( &cmd_deps, CreateChannelCommand { - owner_id: UserId::generate().value(), + owner_id: UserId::generate(), name: name.into(), timezone: "UTC".into(), }, diff --git a/crates/application/src/channels/tests/list_by_owner.rs b/crates/application/src/channels/tests/list_by_owner.rs index ebce587..bd9641d 100644 --- a/crates/application/src/channels/tests/list_by_owner.rs +++ b/crates/application/src/channels/tests/list_by_owner.rs @@ -32,7 +32,7 @@ async fn filters_by_owner() { create::execute( &cmd_deps, CreateChannelCommand { - owner_id: alice.value(), + owner_id: alice, name: name.into(), timezone: "UTC".into(), }, @@ -45,7 +45,7 @@ async fn filters_by_owner() { create::execute( &cmd_deps, CreateChannelCommand { - owner_id: bob.value(), + owner_id: bob, name: "Bob-1".into(), timezone: "UTC".into(), }, @@ -56,7 +56,7 @@ async fn filters_by_owner() { let alice_channels = list_by_owner::execute( &query_deps, ListByOwnerQuery { - owner_id: alice.value(), + owner_id: alice, }, ) .await @@ -73,7 +73,7 @@ async fn no_channels_returns_empty() { let channels = list_by_owner::execute( &query_deps, ListByOwnerQuery { - owner_id: UserId::generate().value(), + owner_id: UserId::generate(), }, ) .await diff --git a/crates/application/src/channels/tests/update.rs b/crates/application/src/channels/tests/update.rs index 3522add..5866823 100644 --- a/crates/application/src/channels/tests/update.rs +++ b/crates/application/src/channels/tests/update.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use domain::testing::{InMemoryChannelRepository, NoopEventPublisher}; -use domain::value_objects::UserId; +use domain::value_objects::{ChannelId, UserId}; use domain::DomainError; use crate::channels::commands::{CreateChannelCommand, UpdateChannelCommand}; @@ -26,7 +26,7 @@ async fn updates_channel_name() { let channel = create::execute( &deps, CreateChannelCommand { - owner_id: owner.value(), + owner_id: owner, name: "Original".into(), timezone: "UTC".into(), }, @@ -37,8 +37,8 @@ async fn updates_channel_name() { let updated = update::execute( &deps, UpdateChannelCommand { - channel_id: channel.id().value(), - owner_id: owner.value(), + channel_id: channel.id(), + owner_id: owner, name: Some("Renamed".into()), description: None, timezone: None, @@ -63,7 +63,7 @@ async fn update_fails_if_not_owner() { let channel = create::execute( &deps, CreateChannelCommand { - owner_id: owner.value(), + owner_id: owner, name: "Protected".into(), timezone: "UTC".into(), }, @@ -74,8 +74,8 @@ async fn update_fails_if_not_owner() { let result = update::execute( &deps, UpdateChannelCommand { - channel_id: channel.id().value(), - owner_id: stranger.value(), + channel_id: channel.id(), + owner_id: stranger, name: Some("Hacked".into()), description: None, timezone: None, @@ -100,8 +100,8 @@ async fn update_nonexistent_channel_returns_not_found() { let result = update::execute( &deps, UpdateChannelCommand { - channel_id: uuid::Uuid::new_v4(), - owner_id: uuid::Uuid::new_v4(), + channel_id: ChannelId::generate(), + owner_id: UserId::generate(), name: Some("Ghost".into()), description: None, timezone: None, @@ -127,7 +127,7 @@ async fn update_config_creates_snapshot() { let channel = create::execute( &deps, CreateChannelCommand { - owner_id: owner.value(), + owner_id: owner, name: "Snapshotted".into(), timezone: "UTC".into(), }, @@ -140,8 +140,8 @@ async fn update_config_creates_snapshot() { update::execute( &deps, UpdateChannelCommand { - channel_id: channel.id().value(), - owner_id: owner.value(), + channel_id: channel.id(), + owner_id: owner, name: None, description: None, timezone: None, @@ -167,7 +167,7 @@ async fn update_without_config_skips_snapshot() { let channel = create::execute( &deps, CreateChannelCommand { - owner_id: owner.value(), + owner_id: owner, name: "NoSnapshot".into(), timezone: "UTC".into(), }, @@ -179,8 +179,8 @@ async fn update_without_config_skips_snapshot() { update::execute( &deps, UpdateChannelCommand { - channel_id: channel.id().value(), - owner_id: owner.value(), + channel_id: channel.id(), + owner_id: owner, name: Some("Renamed".into()), description: None, timezone: None, @@ -205,7 +205,7 @@ async fn update_description_clear() { let channel = create::execute( &deps, CreateChannelCommand { - owner_id: owner.value(), + owner_id: owner, name: "Desc Test".into(), timezone: "UTC".into(), }, @@ -217,8 +217,8 @@ async fn update_description_clear() { let updated = update::execute( &deps, UpdateChannelCommand { - channel_id: channel.id().value(), - owner_id: owner.value(), + channel_id: channel.id(), + owner_id: owner, name: None, description: Some(Some("A description".into())), timezone: None, @@ -235,8 +235,8 @@ async fn update_description_clear() { let cleared = update::execute( &deps, UpdateChannelCommand { - channel_id: channel.id().value(), - owner_id: owner.value(), + channel_id: channel.id(), + owner_id: owner, name: None, description: Some(None), timezone: None, diff --git a/crates/application/src/channels/update.rs b/crates/application/src/channels/update.rs index 7fe33ee..7b1acf9 100644 --- a/crates/application/src/channels/update.rs +++ b/crates/application/src/channels/update.rs @@ -1,6 +1,5 @@ use domain::events::DomainEvent; use domain::models::Channel; -use domain::value_objects::{ChannelId, UserId}; use domain::DomainResult; use super::commands::UpdateChannelCommand; @@ -8,16 +7,13 @@ use super::deps::ChannelCommandDeps; use super::find_owned_channel; pub async fn execute(deps: &ChannelCommandDeps, cmd: UpdateChannelCommand) -> DomainResult { - let channel_id = ChannelId::from(cmd.channel_id); - let owner_id = UserId::from(cmd.owner_id); - let mut channel = - find_owned_channel(deps.channel_query.as_ref(), channel_id, owner_id, cmd.channel_id) + find_owned_channel(deps.channel_query.as_ref(), cmd.channel_id, cmd.owner_id) .await?; if cmd.schedule_config.is_some() { deps.channel_command - .save_config_snapshot(channel_id, channel.schedule_config(), None) + .save_config_snapshot(cmd.channel_id, channel.schedule_config(), None) .await?; } diff --git a/crates/application/src/config_snapshots/commands.rs b/crates/application/src/config_snapshots/commands.rs index c847166..560277c 100644 --- a/crates/application/src/config_snapshots/commands.rs +++ b/crates/application/src/config_snapshots/commands.rs @@ -1,17 +1,17 @@ -use uuid::Uuid; +use domain::value_objects::{ChannelId, SnapshotId}; pub struct SaveSnapshotCommand { - pub channel_id: Uuid, + pub channel_id: ChannelId, pub label: Option, } pub struct PatchLabelCommand { - pub channel_id: Uuid, - pub snapshot_id: Uuid, + pub channel_id: ChannelId, + pub snapshot_id: SnapshotId, pub label: Option, } pub struct RestoreSnapshotCommand { - pub channel_id: Uuid, - pub snapshot_id: Uuid, + pub channel_id: ChannelId, + pub snapshot_id: SnapshotId, } diff --git a/crates/application/src/config_snapshots/get.rs b/crates/application/src/config_snapshots/get.rs index 7aec656..d6a7425 100644 --- a/crates/application/src/config_snapshots/get.rs +++ b/crates/application/src/config_snapshots/get.rs @@ -1,5 +1,4 @@ use domain::models::ChannelConfigSnapshot; -use domain::value_objects::ChannelId; use domain::DomainResult; use super::deps::ConfigSnapshotDeps; @@ -9,8 +8,7 @@ pub async fn execute( deps: &ConfigSnapshotDeps, query: GetSnapshotQuery, ) -> DomainResult> { - let channel_id = ChannelId::from(query.channel_id); deps.channel_query - .get_config_snapshot(channel_id, query.snapshot_id) + .get_config_snapshot(query.channel_id, query.snapshot_id) .await } diff --git a/crates/application/src/config_snapshots/list.rs b/crates/application/src/config_snapshots/list.rs index d06cb9c..f28272a 100644 --- a/crates/application/src/config_snapshots/list.rs +++ b/crates/application/src/config_snapshots/list.rs @@ -1,5 +1,4 @@ use domain::models::ChannelConfigSnapshot; -use domain::value_objects::ChannelId; use domain::DomainResult; use super::deps::ConfigSnapshotDeps; @@ -9,8 +8,7 @@ pub async fn execute( deps: &ConfigSnapshotDeps, query: ListSnapshotsQuery, ) -> DomainResult> { - let channel_id = ChannelId::from(query.channel_id); - deps.channel_query.list_config_snapshots(channel_id).await + deps.channel_query.list_config_snapshots(query.channel_id).await } #[cfg(test)] diff --git a/crates/application/src/config_snapshots/patch_label.rs b/crates/application/src/config_snapshots/patch_label.rs index 2f20dc2..cc97ee8 100644 --- a/crates/application/src/config_snapshots/patch_label.rs +++ b/crates/application/src/config_snapshots/patch_label.rs @@ -1,5 +1,4 @@ use domain::models::ChannelConfigSnapshot; -use domain::value_objects::ChannelId; use domain::DomainResult; use super::commands::PatchLabelCommand; @@ -9,9 +8,7 @@ pub async fn execute( deps: &ConfigSnapshotDeps, cmd: PatchLabelCommand, ) -> DomainResult> { - let channel_id = ChannelId::from(cmd.channel_id); - deps.channel_command - .patch_config_snapshot_label(channel_id, cmd.snapshot_id, cmd.label) + .patch_config_snapshot_label(cmd.channel_id, cmd.snapshot_id, cmd.label) .await } diff --git a/crates/application/src/config_snapshots/queries.rs b/crates/application/src/config_snapshots/queries.rs index 1a0060e..13bc7f4 100644 --- a/crates/application/src/config_snapshots/queries.rs +++ b/crates/application/src/config_snapshots/queries.rs @@ -1,10 +1,10 @@ -use uuid::Uuid; +use domain::value_objects::{ChannelId, SnapshotId}; pub struct ListSnapshotsQuery { - pub channel_id: Uuid, + pub channel_id: ChannelId, } pub struct GetSnapshotQuery { - pub channel_id: Uuid, - pub snapshot_id: Uuid, + pub channel_id: ChannelId, + pub snapshot_id: SnapshotId, } diff --git a/crates/application/src/config_snapshots/restore.rs b/crates/application/src/config_snapshots/restore.rs index 0b0e19a..90eb1c3 100644 --- a/crates/application/src/config_snapshots/restore.rs +++ b/crates/application/src/config_snapshots/restore.rs @@ -1,5 +1,4 @@ use domain::models::Channel; -use domain::value_objects::ChannelId; use domain::{DomainError, DomainResult}; use super::commands::RestoreSnapshotCommand; @@ -9,11 +8,9 @@ pub async fn execute( deps: &ConfigSnapshotDeps, cmd: RestoreSnapshotCommand, ) -> DomainResult { - let channel_id = ChannelId::from(cmd.channel_id); - let snapshot = deps .channel_query - .get_config_snapshot(channel_id, cmd.snapshot_id) + .get_config_snapshot(cmd.channel_id, cmd.snapshot_id) .await? .ok_or(DomainError::ValidationError(format!( "Snapshot {} not found", @@ -22,12 +19,12 @@ pub async fn execute( let mut channel = deps .channel_query - .find_by_id(channel_id) + .find_by_id(cmd.channel_id) .await? .ok_or(DomainError::ChannelNotFound(cmd.channel_id))?; deps.channel_command - .save_config_snapshot(channel_id, channel.schedule_config(), None) + .save_config_snapshot(cmd.channel_id, channel.schedule_config(), None) .await?; channel.set_schedule_config(snapshot.config().clone()); diff --git a/crates/application/src/config_snapshots/save.rs b/crates/application/src/config_snapshots/save.rs index ad12f1a..efe2321 100644 --- a/crates/application/src/config_snapshots/save.rs +++ b/crates/application/src/config_snapshots/save.rs @@ -1,5 +1,4 @@ use domain::models::ChannelConfigSnapshot; -use domain::value_objects::ChannelId; use domain::{DomainError, DomainResult}; use super::commands::SaveSnapshotCommand; @@ -9,16 +8,14 @@ pub async fn execute( deps: &ConfigSnapshotDeps, cmd: SaveSnapshotCommand, ) -> DomainResult { - let channel_id = ChannelId::from(cmd.channel_id); - let channel = deps .channel_query - .find_by_id(channel_id) + .find_by_id(cmd.channel_id) .await? .ok_or(DomainError::ChannelNotFound(cmd.channel_id))?; deps.channel_command - .save_config_snapshot(channel_id, channel.schedule_config(), cmd.label) + .save_config_snapshot(cmd.channel_id, channel.schedule_config(), cmd.label) .await } diff --git a/crates/application/src/config_snapshots/tests/list.rs b/crates/application/src/config_snapshots/tests/list.rs index b5a7614..067ae58 100644 --- a/crates/application/src/config_snapshots/tests/list.rs +++ b/crates/application/src/config_snapshots/tests/list.rs @@ -23,7 +23,7 @@ async fn seed_channel(repo: &InMemoryChannelRepository) -> Channel { repo.channels .lock() .unwrap() - .insert(channel.id().value(), channel.clone()); + .insert(channel.id(), channel.clone()); channel } @@ -35,7 +35,7 @@ async fn list_empty() { let snaps = list::execute( &deps, ListSnapshotsQuery { - channel_id: channel.id().value(), + channel_id: channel.id(), }, ) .await @@ -53,7 +53,7 @@ async fn list_returns_saved_snapshots() { save::execute( &deps, SaveSnapshotCommand { - channel_id: channel.id().value(), + channel_id: channel.id(), label: Some(label.into()), }, ) @@ -64,7 +64,7 @@ async fn list_returns_saved_snapshots() { let snaps = list::execute( &deps, ListSnapshotsQuery { - channel_id: channel.id().value(), + channel_id: channel.id(), }, ) .await diff --git a/crates/application/src/config_snapshots/tests/save.rs b/crates/application/src/config_snapshots/tests/save.rs index 1f8e71c..5058db0 100644 --- a/crates/application/src/config_snapshots/tests/save.rs +++ b/crates/application/src/config_snapshots/tests/save.rs @@ -22,7 +22,7 @@ async fn seed_channel(repo: &InMemoryChannelRepository) -> Channel { repo.channels .lock() .unwrap() - .insert(channel.id().value(), channel.clone()); + .insert(channel.id(), channel.clone()); channel } @@ -34,7 +34,7 @@ async fn save_creates_snapshot() { let snap = save::execute( &deps, SaveSnapshotCommand { - channel_id: channel.id().value(), + channel_id: channel.id(), label: Some("v1".into()), }, ) @@ -54,7 +54,7 @@ async fn save_increments_version() { save::execute( &deps, SaveSnapshotCommand { - channel_id: channel.id().value(), + channel_id: channel.id(), label: None, }, ) @@ -64,7 +64,7 @@ async fn save_increments_version() { let snap2 = save::execute( &deps, SaveSnapshotCommand { - channel_id: channel.id().value(), + channel_id: channel.id(), label: None, }, ) diff --git a/crates/application/src/iptv/tests/m3u.rs b/crates/application/src/iptv/tests/m3u.rs index 2adc350..634c1a6 100644 --- a/crates/application/src/iptv/tests/m3u.rs +++ b/crates/application/src/iptv/tests/m3u.rs @@ -43,7 +43,7 @@ async fn m3u_includes_channels() { repo.channels .lock() .unwrap() - .insert(ch.id().value(), ch.clone()); + .insert(ch.id(), ch.clone()); let result = m3u::execute( &deps, @@ -68,7 +68,7 @@ async fn m3u_no_token() { repo.channels .lock() .unwrap() - .insert(ch.id().value(), ch); + .insert(ch.id(), ch); let result = m3u::execute( &deps, diff --git a/crates/application/src/library/tests/list_collections.rs b/crates/application/src/library/tests/list_collections.rs index fae8b3f..c91f7ca 100644 --- a/crates/application/src/library/tests/list_collections.rs +++ b/crates/application/src/library/tests/list_collections.rs @@ -1,4 +1,4 @@ -use domain::models::LibraryItem; +use domain::models::{LibraryItem, LibraryItemRow}; use domain::value_objects::ContentType; use crate::library::list_collections; @@ -10,46 +10,46 @@ mod helpers; fn seed_with_collections(repo: &std::sync::Arc) { let mut store = repo.items.lock().unwrap(); - let item = LibraryItem::from_persistence( - "test::m1".into(), - "test".into(), - "m1".into(), - "Die Hard".into(), - ContentType::Movie, - 7800, - None, - None, - None, - None, - vec![], - vec![], - Some("col-1".into()), - Some("Movies".into()), - Some("movies".into()), - None, - "2026-01-01".into(), - ); + let item = LibraryItem::from_persistence(LibraryItemRow { + id: "test::m1".into(), + provider_id: "test".into(), + external_id: "m1".into(), + title: "Die Hard".into(), + content_type: ContentType::Movie, + duration_secs: 7800, + series_name: None, + season_number: None, + episode_number: None, + year: None, + genres: vec![], + tags: vec![], + collection_id: Some("col-1".into()), + collection_name: Some("Movies".into()), + collection_type: Some("movies".into()), + thumbnail_url: None, + synced_at: "2026-01-01".into(), + }); store.insert(item.id().to_string(), item); - let item2 = LibraryItem::from_persistence( - "test::e1".into(), - "test".into(), - "e1".into(), - "BB S01E01".into(), - ContentType::Episode, - 2700, - Some("Breaking Bad".into()), - Some(1), - Some(1), - None, - vec![], - vec![], - Some("col-2".into()), - Some("TV Shows".into()), - Some("tvshows".into()), - None, - "2026-01-01".into(), - ); + let item2 = LibraryItem::from_persistence(LibraryItemRow { + id: "test::e1".into(), + provider_id: "test".into(), + external_id: "e1".into(), + title: "BB S01E01".into(), + content_type: ContentType::Episode, + duration_secs: 2700, + series_name: Some("Breaking Bad".into()), + season_number: Some(1), + episode_number: Some(1), + year: None, + genres: vec![], + tags: vec![], + collection_id: Some("col-2".into()), + collection_name: Some("TV Shows".into()), + collection_type: Some("tvshows".into()), + thumbnail_url: None, + synced_at: "2026-01-01".into(), + }); store.insert(item2.id().to_string(), item2); } diff --git a/crates/application/src/library/tests/list_genres.rs b/crates/application/src/library/tests/list_genres.rs index 12f3f0a..ec8dfdb 100644 --- a/crates/application/src/library/tests/list_genres.rs +++ b/crates/application/src/library/tests/list_genres.rs @@ -1,4 +1,4 @@ -use domain::models::LibraryItem; +use domain::models::{LibraryItem, LibraryItemRow}; use domain::value_objects::ContentType; use crate::library::list_genres; @@ -10,44 +10,44 @@ mod helpers; fn seed_with_genres(repo: &std::sync::Arc) { let mut store = repo.items.lock().unwrap(); - let item1 = LibraryItem::from_persistence( - "test::m1".into(), - "test".into(), - "m1".into(), - "Die Hard".into(), - ContentType::Movie, - 7800, - None, - None, - None, - None, - vec!["Action".into(), "Thriller".into()], - vec![], - None, - None, - None, - None, - "2026-01-01".into(), - ); - let item2 = LibraryItem::from_persistence( - "test::m2".into(), - "test".into(), - "m2".into(), - "Alien".into(), - ContentType::Movie, - 7020, - None, - None, - None, - None, - vec!["Sci-Fi".into(), "Action".into()], - vec![], - None, - None, - None, - None, - "2026-01-01".into(), - ); + let item1 = LibraryItem::from_persistence(LibraryItemRow { + id: "test::m1".into(), + provider_id: "test".into(), + external_id: "m1".into(), + title: "Die Hard".into(), + content_type: ContentType::Movie, + duration_secs: 7800, + series_name: None, + season_number: None, + episode_number: None, + year: None, + genres: vec!["Action".into(), "Thriller".into()], + tags: vec![], + collection_id: None, + collection_name: None, + collection_type: None, + thumbnail_url: None, + synced_at: "2026-01-01".into(), + }); + let item2 = LibraryItem::from_persistence(LibraryItemRow { + id: "test::m2".into(), + provider_id: "test".into(), + external_id: "m2".into(), + title: "Alien".into(), + content_type: ContentType::Movie, + duration_secs: 7020, + series_name: None, + season_number: None, + episode_number: None, + year: None, + genres: vec!["Sci-Fi".into(), "Action".into()], + tags: vec![], + collection_id: None, + collection_name: None, + collection_type: None, + thumbnail_url: None, + synced_at: "2026-01-01".into(), + }); store.insert(item1.id().to_string(), item1); store.insert(item2.id().to_string(), item2); diff --git a/crates/application/src/library/tests/list_seasons.rs b/crates/application/src/library/tests/list_seasons.rs index 70687d6..548aa69 100644 --- a/crates/application/src/library/tests/list_seasons.rs +++ b/crates/application/src/library/tests/list_seasons.rs @@ -1,4 +1,4 @@ -use domain::models::LibraryItem; +use domain::models::{LibraryItem, LibraryItemRow}; use domain::value_objects::ContentType; use crate::library::list_seasons; @@ -11,25 +11,25 @@ fn seed_episodes(repo: &std::sync::Arc) fn seed_items_with_genres(repo: &std::sync::Arc) { let mut store = repo.items.lock().unwrap(); - let action = LibraryItem::from_persistence( - "test::m1".into(), - "test".into(), - "m1".into(), - "Die Hard".into(), - ContentType::Movie, - 7800, - None, - None, - None, - Some(1988), - vec!["Action".into(), "Thriller".into()], - vec![], - None, - None, - None, - None, - "2026-01-01".into(), - ); - let scifi = LibraryItem::from_persistence( - "test::m2".into(), - "test".into(), - "m2".into(), - "Alien".into(), - ContentType::Movie, - 7020, - None, - None, - None, - Some(1979), - vec!["Sci-Fi".into(), "Horror".into()], - vec![], - None, - None, - None, - None, - "2026-01-01".into(), - ); - let comedy = LibraryItem::from_persistence( - "test::m3".into(), - "test".into(), - "m3".into(), - "Airplane!".into(), - ContentType::Movie, - 5280, - None, - None, - None, - Some(1980), - vec!["Comedy".into()], - vec![], - None, - None, - None, - None, - "2026-01-01".into(), - ); + let action = LibraryItem::from_persistence(LibraryItemRow { + id: "test::m1".into(), + provider_id: "test".into(), + external_id: "m1".into(), + title: "Die Hard".into(), + content_type: ContentType::Movie, + duration_secs: 7800, + series_name: None, + season_number: None, + episode_number: None, + year: Some(1988), + genres: vec!["Action".into(), "Thriller".into()], + tags: vec![], + collection_id: None, + collection_name: None, + collection_type: None, + thumbnail_url: None, + synced_at: "2026-01-01".into(), + }); + let scifi = LibraryItem::from_persistence(LibraryItemRow { + id: "test::m2".into(), + provider_id: "test".into(), + external_id: "m2".into(), + title: "Alien".into(), + content_type: ContentType::Movie, + duration_secs: 7020, + series_name: None, + season_number: None, + episode_number: None, + year: Some(1979), + genres: vec!["Sci-Fi".into(), "Horror".into()], + tags: vec![], + collection_id: None, + collection_name: None, + collection_type: None, + thumbnail_url: None, + synced_at: "2026-01-01".into(), + }); + let comedy = LibraryItem::from_persistence(LibraryItemRow { + id: "test::m3".into(), + provider_id: "test".into(), + external_id: "m3".into(), + title: "Airplane!".into(), + content_type: ContentType::Movie, + duration_secs: 5280, + series_name: None, + season_number: None, + episode_number: None, + year: Some(1980), + genres: vec!["Comedy".into()], + tags: vec![], + collection_id: None, + collection_name: None, + collection_type: None, + thumbnail_url: None, + synced_at: "2026-01-01".into(), + }); store.insert(action.id().to_string(), action); store.insert(scifi.id().to_string(), scifi); diff --git a/crates/application/src/schedule/tests/delete_after.rs b/crates/application/src/schedule/tests/delete_after.rs index 140d7ed..e6a0e47 100644 --- a/crates/application/src/schedule/tests/delete_after.rs +++ b/crates/application/src/schedule/tests/delete_after.rs @@ -20,7 +20,7 @@ async fn delete_after_removes_later_generations() { .channels .lock() .unwrap() - .insert(channel_id.value(), channel); + .insert(channel_id, channel); // Manually insert schedules with different generations. let now = chrono::Utc::now(); @@ -36,7 +36,7 @@ async fn delete_after_removes_later_generations() { .schedules .lock() .unwrap() - .insert(sched.id().value(), sched); + .insert(sched.id(), sched); } // Delete generations > 1. diff --git a/crates/application/src/schedule/tests/generate.rs b/crates/application/src/schedule/tests/generate.rs index 0938eca..7d0ae9d 100644 --- a/crates/application/src/schedule/tests/generate.rs +++ b/crates/application/src/schedule/tests/generate.rs @@ -18,7 +18,7 @@ async fn generate_produces_empty_schedule_for_channel_with_no_blocks() { .channels .lock() .unwrap() - .insert(channel.id().value(), channel.clone()); + .insert(channel.id(), channel.clone()); let schedule = generate::execute( &deps, @@ -58,7 +58,7 @@ async fn generate_increments_generation() { .channels .lock() .unwrap() - .insert(channel.id().value(), channel.clone()); + .insert(channel.id(), channel.clone()); let first = generate::execute( &deps, diff --git a/crates/domain/clippy.toml b/crates/domain/clippy.toml deleted file mode 100644 index 0d4e02f..0000000 --- a/crates/domain/clippy.toml +++ /dev/null @@ -1 +0,0 @@ -too-many-arguments-threshold = 20 diff --git a/crates/domain/src/errors/mod.rs b/crates/domain/src/errors/mod.rs index 800cbdf..84fb3d4 100644 --- a/crates/domain/src/errors/mod.rs +++ b/crates/domain/src/errors/mod.rs @@ -1,20 +1,21 @@ use thiserror::Error; -use uuid::Uuid; + +use crate::value_objects::{ChannelId, UserId}; #[derive(Debug, Error)] #[non_exhaustive] pub enum DomainError { #[error("User not found: {0}")] - UserNotFound(Uuid), + UserNotFound(UserId), #[error("User already exists: {0}")] UserAlreadyExists(String), #[error("Channel not found: {0}")] - ChannelNotFound(Uuid), + ChannelNotFound(ChannelId), #[error("No active schedule for channel: {0}")] - NoActiveSchedule(Uuid), + NoActiveSchedule(ChannelId), #[error("Validation error: {0}")] ValidationError(String), diff --git a/crates/domain/src/models/activity.rs b/crates/domain/src/models/activity.rs index 934e6df..ca694ab 100644 --- a/crates/domain/src/models/activity.rs +++ b/crates/domain/src/models/activity.rs @@ -1,23 +1,24 @@ use chrono::{DateTime, Utc}; -use uuid::Uuid; + +use crate::value_objects::{ActivityEventId, ChannelId}; #[derive(Debug, Clone)] pub struct ActivityEvent { - id: Uuid, + id: ActivityEventId, timestamp: DateTime, event_type: String, detail: String, - channel_id: Option, + channel_id: Option, } impl ActivityEvent { pub fn new( event_type: impl Into, detail: impl Into, - channel_id: Option, + channel_id: Option, ) -> Self { Self { - id: Uuid::new_v4(), + id: ActivityEventId::generate(), timestamp: Utc::now(), event_type: event_type.into(), detail: detail.into(), @@ -26,11 +27,11 @@ impl ActivityEvent { } pub fn from_persistence( - id: Uuid, + id: ActivityEventId, timestamp: DateTime, event_type: String, detail: String, - channel_id: Option, + channel_id: Option, ) -> Self { Self { id, @@ -41,7 +42,7 @@ impl ActivityEvent { } } - pub fn id(&self) -> Uuid { + pub fn id(&self) -> ActivityEventId { self.id } @@ -57,7 +58,7 @@ impl ActivityEvent { &self.detail } - pub fn channel_id(&self) -> Option { + pub fn channel_id(&self) -> Option { self.channel_id } } diff --git a/crates/domain/src/models/channel.rs b/crates/domain/src/models/channel.rs index 1e9357c..b0bbb76 100644 --- a/crates/domain/src/models/channel.rs +++ b/crates/domain/src/models/channel.rs @@ -34,6 +34,28 @@ pub struct Channel { updated_at: DateTime, } +pub struct ChannelRow { + pub id: ChannelId, + pub owner_id: UserId, + pub name: String, + pub description: Option, + pub timezone: String, + pub schedule_config: ScheduleConfig, + pub recycle_policy: RecyclePolicy, + pub auto_schedule: bool, + pub access_mode: AccessMode, + pub access_password_hash: Option, + pub logo: Option, + pub logo_position: LogoPosition, + pub logo_opacity: f32, + pub webhook_url: Option, + pub webhook_poll_interval_secs: u32, + pub webhook_body_template: Option, + pub webhook_headers: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} + impl Channel { pub fn new( owner_id: UserId, @@ -64,47 +86,27 @@ impl Channel { } } - pub fn from_persistence( - id: ChannelId, - owner_id: UserId, - name: String, - description: Option, - timezone: String, - schedule_config: ScheduleConfig, - recycle_policy: RecyclePolicy, - auto_schedule: bool, - access_mode: AccessMode, - access_password_hash: Option, - logo: Option, - logo_position: LogoPosition, - logo_opacity: f32, - webhook_url: Option, - webhook_poll_interval_secs: u32, - webhook_body_template: Option, - webhook_headers: Option, - created_at: DateTime, - updated_at: DateTime, - ) -> Self { + pub fn from_persistence(row: ChannelRow) -> Self { Self { - id, - owner_id, - name, - description, - timezone, - schedule_config, - recycle_policy, - auto_schedule, - access_mode, - access_password_hash, - logo, - logo_position, - logo_opacity, - webhook_url, - webhook_poll_interval_secs, - webhook_body_template, - webhook_headers, - created_at, - updated_at, + id: row.id, + owner_id: row.owner_id, + name: row.name, + description: row.description, + timezone: row.timezone, + schedule_config: row.schedule_config, + recycle_policy: row.recycle_policy, + auto_schedule: row.auto_schedule, + access_mode: row.access_mode, + access_password_hash: row.access_password_hash, + logo: row.logo, + logo_position: row.logo_position, + logo_opacity: row.logo_opacity, + webhook_url: row.webhook_url, + webhook_poll_interval_secs: row.webhook_poll_interval_secs, + webhook_body_template: row.webhook_body_template, + webhook_headers: row.webhook_headers, + created_at: row.created_at, + updated_at: row.updated_at, } } diff --git a/crates/domain/src/models/config_snapshot.rs b/crates/domain/src/models/config_snapshot.rs index ac29348..d3b255d 100644 --- a/crates/domain/src/models/config_snapshot.rs +++ b/crates/domain/src/models/config_snapshot.rs @@ -1,14 +1,13 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use uuid::Uuid; -use crate::value_objects::ChannelId; +use crate::value_objects::{ChannelId, SnapshotId}; use super::ScheduleConfig; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChannelConfigSnapshot { - id: Uuid, + id: SnapshotId, channel_id: ChannelId, config: ScheduleConfig, version_num: i64, @@ -23,7 +22,7 @@ impl ChannelConfigSnapshot { version_num: i64, ) -> Self { Self { - id: Uuid::new_v4(), + id: SnapshotId::generate(), channel_id, config, version_num, @@ -33,7 +32,7 @@ impl ChannelConfigSnapshot { } pub fn from_persistence( - id: Uuid, + id: SnapshotId, channel_id: ChannelId, config: ScheduleConfig, version_num: i64, @@ -50,7 +49,7 @@ impl ChannelConfigSnapshot { } } - pub fn id(&self) -> Uuid { + pub fn id(&self) -> SnapshotId { self.id } diff --git a/crates/domain/src/models/library.rs b/crates/domain/src/models/library.rs index 2cd7b30..f205f60 100644 --- a/crates/domain/src/models/library.rs +++ b/crates/domain/src/models/library.rs @@ -23,6 +23,26 @@ pub struct LibraryItem { synced_at: String, } +pub struct LibraryItemRow { + pub id: String, + pub provider_id: String, + pub external_id: String, + pub title: String, + pub content_type: ContentType, + pub duration_secs: u32, + pub series_name: Option, + pub season_number: Option, + pub episode_number: Option, + pub year: Option, + pub genres: Vec, + pub tags: Vec, + pub collection_id: Option, + pub collection_name: Option, + pub collection_type: Option, + pub thumbnail_url: Option, + pub synced_at: String, +} + impl LibraryItem { pub fn new( provider_id: impl Into, @@ -56,43 +76,25 @@ impl LibraryItem { } } - pub fn from_persistence( - id: String, - provider_id: String, - external_id: String, - title: String, - content_type: ContentType, - duration_secs: u32, - series_name: Option, - season_number: Option, - episode_number: Option, - year: Option, - genres: Vec, - tags: Vec, - collection_id: Option, - collection_name: Option, - collection_type: Option, - thumbnail_url: Option, - synced_at: String, - ) -> Self { + pub fn from_persistence(row: LibraryItemRow) -> Self { Self { - 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, + id: row.id, + provider_id: row.provider_id, + external_id: row.external_id, + title: row.title, + content_type: row.content_type, + duration_secs: row.duration_secs, + series_name: row.series_name, + season_number: row.season_number, + episode_number: row.episode_number, + year: row.year, + genres: row.genres, + tags: row.tags, + collection_id: row.collection_id, + collection_name: row.collection_name, + collection_type: row.collection_type, + thumbnail_url: row.thumbnail_url, + synced_at: row.synced_at, } } diff --git a/crates/domain/src/models/media.rs b/crates/domain/src/models/media.rs index 91dba1f..bc615b2 100644 --- a/crates/domain/src/models/media.rs +++ b/crates/domain/src/models/media.rs @@ -1,8 +1,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use uuid::Uuid; -use crate::value_objects::{ChannelId, ContentType, MediaItemId}; +use crate::value_objects::{ChannelId, ContentType, MediaItemId, PlaybackRecordId}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MediaItem { @@ -21,6 +20,22 @@ pub struct MediaItem { collection_id: Option, } +pub struct MediaItemRow { + pub id: MediaItemId, + pub title: String, + pub content_type: ContentType, + pub duration_secs: u32, + pub description: Option, + pub genres: Vec, + pub year: Option, + pub tags: Vec, + pub series_name: Option, + pub season_number: Option, + pub episode_number: Option, + pub thumbnail_url: Option, + pub collection_id: Option, +} + impl MediaItem { pub fn new( id: MediaItemId, @@ -45,35 +60,21 @@ impl MediaItem { } } - pub fn from_persistence( - id: MediaItemId, - title: String, - content_type: ContentType, - duration_secs: u32, - description: Option, - genres: Vec, - year: Option, - tags: Vec, - series_name: Option, - season_number: Option, - episode_number: Option, - thumbnail_url: Option, - collection_id: Option, - ) -> Self { + pub fn from_persistence(row: MediaItemRow) -> Self { Self { - id, - title, - content_type, - duration_secs, - description, - genres, - year, - tags, - series_name, - season_number, - episode_number, - thumbnail_url, - collection_id, + id: row.id, + title: row.title, + content_type: row.content_type, + duration_secs: row.duration_secs, + description: row.description, + genres: row.genres, + year: row.year, + tags: row.tags, + series_name: row.series_name, + season_number: row.season_number, + episode_number: row.episode_number, + thumbnail_url: row.thumbnail_url, + collection_id: row.collection_id, } } @@ -132,7 +133,7 @@ impl MediaItem { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PlaybackRecord { - id: Uuid, + id: PlaybackRecordId, channel_id: ChannelId, item_id: MediaItemId, played_at: DateTime, @@ -142,7 +143,7 @@ pub struct PlaybackRecord { impl PlaybackRecord { pub fn new(channel_id: ChannelId, item_id: MediaItemId, generation: u32) -> Self { Self { - id: Uuid::new_v4(), + id: PlaybackRecordId::generate(), channel_id, item_id, played_at: Utc::now(), @@ -151,7 +152,7 @@ impl PlaybackRecord { } pub fn from_persistence( - id: Uuid, + id: PlaybackRecordId, channel_id: ChannelId, item_id: MediaItemId, played_at: DateTime, @@ -166,7 +167,7 @@ impl PlaybackRecord { } } - pub fn id(&self) -> Uuid { + pub fn id(&self) -> PlaybackRecordId { self.id } diff --git a/crates/domain/src/models/mod.rs b/crates/domain/src/models/mod.rs index ac6fd0d..136b9c9 100644 --- a/crates/domain/src/models/mod.rs +++ b/crates/domain/src/models/mod.rs @@ -10,16 +10,16 @@ mod user; pub use activity::ActivityEvent; pub use channel::{ - BlockContent, Channel, OldScheduleConfig, ProgrammingBlock, ScheduleConfig, + BlockContent, Channel, ChannelRow, OldScheduleConfig, ProgrammingBlock, ScheduleConfig, ScheduleConfigCompat, }; pub use collections::{PageParams, Paginated}; pub use config_snapshot::ChannelConfigSnapshot; pub use library::{ - LibraryCollection, LibraryItem, LibrarySyncLogEntry, LibrarySyncResult, SeasonSummary, - ShowSummary, + LibraryCollection, LibraryItem, LibraryItemRow, LibrarySyncLogEntry, LibrarySyncResult, + SeasonSummary, ShowSummary, }; -pub use media::{MediaItem, PlaybackRecord}; +pub use media::{MediaItem, MediaItemRow, PlaybackRecord}; pub use provider_config::ProviderConfigRow; pub use schedule::{CurrentBroadcast, GeneratedSchedule, ScheduledSlot}; pub use user::User; diff --git a/crates/domain/src/models/tests/activity.rs b/crates/domain/src/models/tests/activity.rs index 290d458..ebc0ebb 100644 --- a/crates/domain/src/models/tests/activity.rs +++ b/crates/domain/src/models/tests/activity.rs @@ -1,4 +1,5 @@ use super::*; +use crate::value_objects::ActivityEventId; #[test] fn new_generates_id_and_timestamp() { @@ -10,15 +11,15 @@ fn new_generates_id_and_timestamp() { #[test] fn new_with_channel_id() { - let ch_id = Uuid::new_v4(); + let ch_id = ChannelId::generate(); let event = ActivityEvent::new("schedule_generated", "Gen #5", Some(ch_id)); assert_eq!(event.channel_id(), Some(ch_id)); } #[test] fn from_persistence_round_trip() { - let id = Uuid::new_v4(); - let ch_id = Uuid::new_v4(); + let id = ActivityEventId::generate(); + let ch_id = ChannelId::generate(); let now = Utc::now(); let event = ActivityEvent::from_persistence( id, diff --git a/crates/domain/src/models/tests/config_snapshot.rs b/crates/domain/src/models/tests/config_snapshot.rs index 1fd825c..a24988d 100644 --- a/crates/domain/src/models/tests/config_snapshot.rs +++ b/crates/domain/src/models/tests/config_snapshot.rs @@ -1,4 +1,5 @@ use super::*; +use crate::value_objects::SnapshotId; #[test] fn new_generates_id_and_timestamp() { @@ -11,7 +12,7 @@ fn new_generates_id_and_timestamp() { #[test] fn from_persistence_round_trip() { - let id = Uuid::new_v4(); + let id = SnapshotId::generate(); let ch_id = ChannelId::generate(); let now = Utc::now(); let snap = ChannelConfigSnapshot::from_persistence( diff --git a/crates/domain/src/models/tests/library.rs b/crates/domain/src/models/tests/library.rs index 2624763..bb8c969 100644 --- a/crates/domain/src/models/tests/library.rs +++ b/crates/domain/src/models/tests/library.rs @@ -21,25 +21,25 @@ fn library_item_new_defaults_optional_fields() { #[test] fn library_item_from_persistence_all_fields() { - let item = LibraryItem::from_persistence( - "jf::abc".into(), - "jf".into(), - "abc".into(), - "Breaking Bad S01E01".into(), - ContentType::Episode, - 2700, - Some("Breaking Bad".into()), - Some(1), - Some(1), - Some(2008), - vec!["Drama".into()], - vec!["tv".into()], - Some("col-1".into()), - Some("TV Shows".into()), - Some("tvshows".into()), - Some("http://thumb.jpg".into()), - "2026-03-19T00:00:00Z".into(), - ); + let item = LibraryItem::from_persistence(LibraryItemRow { + id: "jf::abc".into(), + provider_id: "jf".into(), + external_id: "abc".into(), + title: "Breaking Bad S01E01".into(), + content_type: ContentType::Episode, + duration_secs: 2700, + series_name: Some("Breaking Bad".into()), + season_number: Some(1), + episode_number: Some(1), + year: Some(2008), + genres: vec!["Drama".into()], + tags: vec!["tv".into()], + collection_id: Some("col-1".into()), + collection_name: Some("TV Shows".into()), + collection_type: Some("tvshows".into()), + thumbnail_url: Some("http://thumb.jpg".into()), + synced_at: "2026-03-19T00:00:00Z".into(), + }); assert_eq!(item.series_name(), Some("Breaking Bad")); assert_eq!(item.season_number(), Some(1)); assert_eq!(item.year(), Some(2008)); diff --git a/crates/domain/src/models/tests/media.rs b/crates/domain/src/models/tests/media.rs index cb3d7eb..89477d3 100644 --- a/crates/domain/src/models/tests/media.rs +++ b/crates/domain/src/models/tests/media.rs @@ -1,4 +1,5 @@ use super::*; +use crate::value_objects::PlaybackRecordId; #[test] fn media_item_new_defaults() { @@ -18,21 +19,21 @@ fn media_item_new_defaults() { #[test] fn media_item_from_persistence_round_trip() { - let item = MediaItem::from_persistence( - MediaItemId::new("jf::abc"), - "Breaking Bad S01E01".into(), - ContentType::Episode, - 2700, - Some("Pilot episode".into()), - vec!["Drama".into()], - Some(2008), - vec!["tv".into()], - Some("Breaking Bad".into()), - Some(1), - Some(1), - Some("http://thumb.jpg".into()), - Some("col-1".into()), - ); + let item = MediaItem::from_persistence(MediaItemRow { + id: MediaItemId::new("jf::abc"), + title: "Breaking Bad S01E01".into(), + content_type: ContentType::Episode, + duration_secs: 2700, + description: Some("Pilot episode".into()), + genres: vec!["Drama".into()], + year: Some(2008), + tags: vec!["tv".into()], + series_name: Some("Breaking Bad".into()), + season_number: Some(1), + episode_number: Some(1), + thumbnail_url: Some("http://thumb.jpg".into()), + collection_id: Some("col-1".into()), + }); assert_eq!(item.title(), "Breaking Bad S01E01"); assert_eq!(item.series_name(), Some("Breaking Bad")); assert_eq!(item.season_number(), Some(1)); @@ -53,7 +54,7 @@ fn playback_record_new() { #[test] fn playback_record_from_persistence() { - let id = Uuid::new_v4(); + let id = PlaybackRecordId::generate(); let ch_id = ChannelId::generate(); let item_id = MediaItemId::new("test::2"); let now = Utc::now(); diff --git a/crates/domain/src/ports/channel.rs b/crates/domain/src/ports/channel.rs index 0ba3e0a..22402ff 100644 --- a/crates/domain/src/ports/channel.rs +++ b/crates/domain/src/ports/channel.rs @@ -1,9 +1,8 @@ use async_trait::async_trait; -use uuid::Uuid; use crate::errors::DomainResult; use crate::models::{Channel, ChannelConfigSnapshot, ScheduleConfig}; -use crate::value_objects::{ChannelId, UserId}; +use crate::value_objects::{ChannelId, SnapshotId, UserId}; #[async_trait] pub trait ChannelCommand: Send + Sync { @@ -21,7 +20,7 @@ pub trait ChannelCommand: Send + Sync { async fn patch_config_snapshot_label( &self, channel_id: ChannelId, - snapshot_id: Uuid, + snapshot_id: SnapshotId, label: Option, ) -> DomainResult>; } @@ -44,6 +43,6 @@ pub trait ChannelQuery: Send + Sync { async fn get_config_snapshot( &self, channel_id: ChannelId, - snapshot_id: Uuid, + snapshot_id: SnapshotId, ) -> DomainResult>; } diff --git a/crates/domain/src/services/schedule/mod.rs b/crates/domain/src/services/schedule/mod.rs index 1612155..21c3324 100644 --- a/crates/domain/src/services/schedule/mod.rs +++ b/crates/domain/src/services/schedule/mod.rs @@ -21,6 +21,15 @@ struct BlockTimeWindow { end: DateTime, } +struct AlgorithmicParams<'a> { + provider_id: &'a str, + filter: &'a MediaFilter, + strategy: &'a FillStrategy, + block_id: BlockId, + loop_on_finish: bool, + ignore_recycle_policy: bool, +} + struct RecycleContext<'a> { history: &'a [PlaybackRecord], policy: &'a RecyclePolicy, @@ -59,7 +68,7 @@ impl ScheduleEngineService { .channel_query .find_by_id(channel_id) .await? - .ok_or(DomainError::ChannelNotFound(channel_id.value()))?; + .ok_or(DomainError::ChannelNotFound(channel_id))?; let tz: Tz = channel .timezone() @@ -259,14 +268,16 @@ impl ScheduleEngineService { provider_id, } => { self.resolve_algorithmic( - provider_id, - filter, - strategy, + AlgorithmicParams { + provider_id, + filter, + strategy, + block_id: block.id(), + loop_on_finish: block.loop_on_finish(), + ignore_recycle_policy: block.ignore_recycle_policy(), + }, window, recycle, - block.id(), - block.loop_on_finish(), - block.ignore_recycle_policy(), ) .await } @@ -300,25 +311,20 @@ impl ScheduleEngineService { async fn resolve_algorithmic( &self, - provider_id: &str, - filter: &MediaFilter, - strategy: &FillStrategy, + params: AlgorithmicParams<'_>, window: BlockTimeWindow, recycle: RecycleContext<'_>, - block_id: BlockId, - loop_on_finish: bool, - ignore_recycle_policy: bool, ) -> DomainResult> { let candidates = self .provider_registry - .fetch_items(provider_id, filter) + .fetch_items(params.provider_id, params.filter) .await?; if candidates.is_empty() { return Ok(vec![]); } - let pool = if ignore_recycle_policy { + let pool = if params.ignore_recycle_policy { candidates.clone() } else { recycle::apply_recycle_policy(&candidates, recycle.history, recycle.policy, recycle.generation) @@ -328,9 +334,9 @@ impl ScheduleEngineService { &candidates, &pool, target_secs, - strategy, + params.strategy, recycle.last_item_id, - loop_on_finish, + params.loop_on_finish, ); let mut slots = Vec::new(); @@ -342,7 +348,7 @@ impl ScheduleEngineService { } let item_end = (cursor + Duration::seconds(item.duration_secs() as i64)).min(window.end); - slots.push(ScheduledSlot::new(cursor, item_end, item.clone(), block_id)); + slots.push(ScheduledSlot::new(cursor, item_end, item.clone(), params.block_id)); cursor = item_end; } diff --git a/crates/domain/src/testing/in_memory.rs b/crates/domain/src/testing/in_memory.rs index 4e5e3b1..c27173f 100644 --- a/crates/domain/src/testing/in_memory.rs +++ b/crates/domain/src/testing/in_memory.rs @@ -4,7 +4,6 @@ use std::sync::Mutex; use async_trait::async_trait; use chrono::{DateTime, Utc}; -use uuid::Uuid; use crate::errors::DomainResult; use crate::models::{ @@ -18,11 +17,12 @@ use crate::ports::{ ScheduleQuery, TranscodeSettingsRepository, UserCommand, UserQuery, }; use crate::value_objects::{ - BlockId, ChannelId, ContentType, LibrarySearchFilter, MediaItemId, ScheduleId, UserId, + BlockId, ChannelId, ContentType, LibrarySearchFilter, MediaItemId, ScheduleId, SnapshotId, + UserId, }; pub struct InMemoryUserRepository { - pub store: Mutex>, + pub store: Mutex>, } impl InMemoryUserRepository { @@ -45,12 +45,12 @@ impl UserCommand for InMemoryUserRepository { self.store .lock() .unwrap() - .insert(user.id().value(), user.clone()); + .insert(user.id(), user.clone()); Ok(()) } async fn delete(&self, id: UserId) -> DomainResult<()> { - self.store.lock().unwrap().remove(&id.value()); + self.store.lock().unwrap().remove(&id); Ok(()) } } @@ -58,7 +58,7 @@ impl UserCommand for InMemoryUserRepository { #[async_trait] impl UserQuery for InMemoryUserRepository { async fn find_by_id(&self, id: UserId) -> DomainResult> { - Ok(self.store.lock().unwrap().get(&id.value()).cloned()) + Ok(self.store.lock().unwrap().get(&id).cloned()) } async fn find_by_subject(&self, subject: &str) -> DomainResult> { @@ -80,7 +80,7 @@ impl UserQuery for InMemoryUserRepository { } pub struct InMemoryChannelRepository { - pub channels: Mutex>, + pub channels: Mutex>, pub snapshots: Mutex>, } @@ -105,12 +105,12 @@ impl ChannelCommand for InMemoryChannelRepository { self.channels .lock() .unwrap() - .insert(channel.id().value(), channel.clone()); + .insert(channel.id(), channel.clone()); Ok(()) } async fn delete(&self, id: ChannelId) -> DomainResult<()> { - self.channels.lock().unwrap().remove(&id.value()); + self.channels.lock().unwrap().remove(&id); Ok(()) } @@ -148,7 +148,7 @@ impl ChannelCommand for InMemoryChannelRepository { async fn patch_config_snapshot_label( &self, channel_id: ChannelId, - snapshot_id: Uuid, + snapshot_id: SnapshotId, label: Option, ) -> DomainResult> { let mut snaps = self.snapshots.lock().unwrap(); @@ -176,7 +176,7 @@ impl ChannelCommand for InMemoryChannelRepository { #[async_trait] impl ChannelQuery for InMemoryChannelRepository { async fn find_by_id(&self, id: ChannelId) -> DomainResult> { - Ok(self.channels.lock().unwrap().get(&id.value()).cloned()) + Ok(self.channels.lock().unwrap().get(&id).cloned()) } async fn find_by_owner(&self, owner_id: UserId) -> DomainResult> { @@ -218,7 +218,7 @@ impl ChannelQuery for InMemoryChannelRepository { async fn get_config_snapshot( &self, channel_id: ChannelId, - snapshot_id: Uuid, + snapshot_id: SnapshotId, ) -> DomainResult> { let snaps = self.snapshots.lock().unwrap(); Ok(snaps @@ -229,7 +229,7 @@ impl ChannelQuery for InMemoryChannelRepository { } pub struct InMemoryScheduleRepository { - pub schedules: Mutex>, + pub schedules: Mutex>, pub playback_records: Mutex>, } @@ -254,7 +254,7 @@ impl ScheduleCommand for InMemoryScheduleRepository { self.schedules .lock() .unwrap() - .insert(schedule.id().value(), schedule.clone()); + .insert(schedule.id(), schedule.clone()); Ok(()) } @@ -644,11 +644,7 @@ impl ActivityLogCommand for InMemoryActivityLog { detail: &str, channel_id: Option, ) -> DomainResult<()> { - let event = ActivityEvent::new( - event_type, - detail, - channel_id.map(|c| c.value()), - ); + let event = ActivityEvent::new(event_type, detail, channel_id); self.events.lock().unwrap().push(event); Ok(()) } diff --git a/crates/domain/src/value_objects/ids.rs b/crates/domain/src/value_objects/ids.rs index 1b161a1..114a2b1 100644 --- a/crates/domain/src/value_objects/ids.rs +++ b/crates/domain/src/value_objects/ids.rs @@ -45,6 +45,9 @@ uuid_id!(ChannelId); uuid_id!(SlotId); uuid_id!(BlockId); uuid_id!(ScheduleId); +uuid_id!(SnapshotId); +uuid_id!(ActivityEventId); +uuid_id!(PlaybackRecordId); #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct MediaItemId(String); diff --git a/crates/mcp/src/tools/channels.rs b/crates/mcp/src/tools/channels.rs index 685ef63..c8cd654 100644 --- a/crates/mcp/src/tools/channels.rs +++ b/crates/mcp/src/tools/channels.rs @@ -12,7 +12,9 @@ pub async fn list_channels( query_deps: &Arc, owner_id: Uuid, ) -> String { - let query = ListByOwnerQuery { owner_id }; + let query = ListByOwnerQuery { + owner_id: owner_id.into(), + }; match application::channels::list_by_owner::execute(query_deps, query).await { Ok(channels) => ok_json(&channels), Err(e) => domain_err(e), @@ -20,7 +22,9 @@ pub async fn list_channels( } pub async fn get_channel(query_deps: &Arc, id: Uuid) -> String { - let query = GetChannelQuery { channel_id: id }; + let query = GetChannelQuery { + channel_id: id.into(), + }; match application::channels::get::execute(query_deps, query).await { Ok(Some(channel)) => ok_json(&channel), Ok(None) => serde_json::json!({"error": "Channel not found"}).to_string(), @@ -35,7 +39,7 @@ pub async fn create_channel( timezone: &str, ) -> String { let cmd = CreateChannelCommand { - owner_id, + owner_id: owner_id.into(), name: name.to_string(), timezone: timezone.to_string(), }; @@ -55,8 +59,8 @@ pub async fn update_channel( schedule_config: Option, ) -> String { let cmd = UpdateChannelCommand { - channel_id, - owner_id, + channel_id: channel_id.into(), + owner_id: owner_id.into(), name, description: description.map(Some), timezone, @@ -76,8 +80,8 @@ pub async fn delete_channel( owner_id: Uuid, ) -> String { let cmd = DeleteChannelCommand { - channel_id, - owner_id, + channel_id: channel_id.into(), + owner_id: owner_id.into(), }; match application::channels::delete::execute(cmd_deps, cmd).await { Ok(()) => serde_json::json!({"deleted": channel_id}).to_string(), diff --git a/crates/presentation/src/factory.rs b/crates/presentation/src/factory.rs index 159478e..9bd1db5 100644 --- a/crates/presentation/src/factory.rs +++ b/crates/presentation/src/factory.rs @@ -475,25 +475,25 @@ fn media_item_to_library_item(item: domain::MediaItem, provider_id: &str) -> dom let id = format!("{}::{}", provider_id, external_id); let now = chrono::Utc::now().to_rfc3339(); - domain::LibraryItem::from_persistence( + domain::LibraryItem::from_persistence(domain::LibraryItemRow { id, - provider_id.to_string(), + provider_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, - None, - item.thumbnail_url().map(|s| s.to_string()), - now, - ) + title: item.title().to_string(), + content_type: item.content_type().clone(), + duration_secs: item.duration_secs(), + series_name: item.series_name().map(|s| s.to_string()), + season_number: item.season_number(), + episode_number: item.episode_number(), + year: item.year(), + 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, + thumbnail_url: item.thumbnail_url().map(|s| s.to_string()), + synced_at: now, + }) } struct SimpleSyncAdapter { diff --git a/crates/presentation/src/handlers/channels.rs b/crates/presentation/src/handlers/channels.rs index d287186..501fee4 100644 --- a/crates/presentation/src/handlers/channels.rs +++ b/crates/presentation/src/handlers/channels.rs @@ -32,7 +32,7 @@ pub async fn list_my_channels( CurrentUser(user): CurrentUser, ) -> Result>, ApiError> { let query = ListByOwnerQuery { - owner_id: user.id().value(), + owner_id: user.id(), }; let channels = application::channels::list_by_owner::execute(&state.channel_query_deps, query).await?; @@ -45,7 +45,7 @@ pub async fn create_channel( Json(req): Json, ) -> Result, ApiError> { let cmd = CreateChannelCommand { - owner_id: user.id().value(), + owner_id: user.id(), name: req.name, timezone: req.timezone, }; @@ -58,7 +58,9 @@ pub async fn get_channel( CurrentUser(_user): CurrentUser, Path(id): Path, ) -> Result, ApiError> { - let query = GetChannelQuery { channel_id: id }; + let query = GetChannelQuery { + channel_id: id.into(), + }; let channel = application::channels::get::execute(&state.channel_query_deps, query) .await? .ok_or_else(|| ApiError::not_found(format!("Channel {id} not found")))?; @@ -88,8 +90,8 @@ pub async fn update_channel( .transpose()?; let cmd = UpdateChannelCommand { - channel_id: id, - owner_id: user.id().value(), + channel_id: id.into(), + owner_id: user.id(), name: req.name, description: req.description.map(Some), timezone: req.timezone, @@ -107,8 +109,8 @@ pub async fn delete_channel( Path(id): Path, ) -> Result { let cmd = DeleteChannelCommand { - channel_id: id, - owner_id: user.id().value(), + channel_id: id.into(), + owner_id: user.id(), }; application::channels::delete::execute(&state.channel_command_deps, cmd).await?; Ok(axum::http::StatusCode::NO_CONTENT) @@ -120,7 +122,7 @@ pub async fn save_snapshot( Path(id): Path, ) -> Result, ApiError> { let cmd = SaveSnapshotCommand { - channel_id: id, + channel_id: id.into(), label: None, }; let snap = application::config_snapshots::save::execute(&state.config_snapshot_deps, cmd).await?; @@ -132,7 +134,9 @@ pub async fn list_snapshots( CurrentUser(_user): CurrentUser, Path(id): Path, ) -> Result>, ApiError> { - let query = ListSnapshotsQuery { channel_id: id }; + let query = ListSnapshotsQuery { + channel_id: id.into(), + }; let snaps = application::config_snapshots::list::execute(&state.config_snapshot_deps, query).await?; Ok(Json(snaps.into_iter().map(ConfigSnapshotResponse::from).collect())) } @@ -143,8 +147,8 @@ pub async fn get_snapshot( Path((id, snapshot_id)): Path<(uuid::Uuid, uuid::Uuid)>, ) -> Result, ApiError> { let query = GetSnapshotQuery { - channel_id: id, - snapshot_id, + channel_id: id.into(), + snapshot_id: snapshot_id.into(), }; let snap = application::config_snapshots::get::execute(&state.config_snapshot_deps, query) .await? @@ -159,8 +163,8 @@ pub async fn patch_snapshot( Json(req): Json, ) -> Result, ApiError> { let cmd = PatchLabelCommand { - channel_id: id, - snapshot_id, + channel_id: id.into(), + snapshot_id: snapshot_id.into(), label: req.label, }; let snap = @@ -176,8 +180,8 @@ pub async fn restore_snapshot( Path((id, snapshot_id)): Path<(uuid::Uuid, uuid::Uuid)>, ) -> Result, ApiError> { let cmd = RestoreSnapshotCommand { - channel_id: id, - snapshot_id, + channel_id: id.into(), + snapshot_id: snapshot_id.into(), }; let channel = application::config_snapshots::restore::execute(&state.config_snapshot_deps, cmd).await?; diff --git a/crates/presentation/src/handlers/schedule.rs b/crates/presentation/src/handlers/schedule.rs index f0fc5ac..584dfd0 100644 --- a/crates/presentation/src/handlers/schedule.rs +++ b/crates/presentation/src/handlers/schedule.rs @@ -45,7 +45,7 @@ pub async fn get_current_broadcast( match application::schedule::get_current_broadcast::execute(&state.schedule_deps, query).await? { Some(broadcast) => { - let channel_query = application::channels::GetChannelQuery { channel_id: id }; + let channel_query = application::channels::GetChannelQuery { channel_id: id.into() }; let channel = application::channels::get::execute(&state.channel_query_deps, channel_query).await?; let slot_response = match &channel {