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<ChannelId> not Option<Uuid>
- InMemory repos key on newtype IDs
- AlgorithmicParams struct for schedule engine
- update all adapters/application/presentation callers
This commit is contained in:
2026-07-12 05:00:49 +02:00
parent 031cba5cfb
commit c0e685a4ee
65 changed files with 684 additions and 695 deletions

View File

@@ -1,4 +1,4 @@
use domain::{ContentType, MediaItem, MediaItemId}; use domain::{ContentType, MediaItem, MediaItemId, MediaItemRow};
use crate::models::JellyfinItem; use crate::models::JellyfinItem;
@@ -16,19 +16,19 @@ pub(crate) fn map_jellyfin_item(item: JellyfinItem) -> Option<MediaItem> {
.map(|t| (t / TICKS_PER_SEC) as u32) .map(|t| (t / TICKS_PER_SEC) as u32)
.unwrap_or(0); .unwrap_or(0);
Some(MediaItem::from_persistence( Some(MediaItem::from_persistence(MediaItemRow {
MediaItemId::new(item.id), id: MediaItemId::new(item.id),
item.name, title: item.name,
content_type, content_type,
duration_secs, duration_secs,
item.overview, description: item.overview,
item.genres.unwrap_or_default(), genres: item.genres.unwrap_or_default(),
item.production_year, year: item.production_year,
item.tags.unwrap_or_default(), tags: item.tags.unwrap_or_default(),
item.series_name, series_name: item.series_name,
item.parent_index_number, season_number: item.parent_index_number,
item.index_number, episode_number: item.index_number,
None, thumbnail_url: None,
None, collection_id: None,
)) }))
} }

View File

@@ -4,7 +4,7 @@ use async_trait::async_trait;
use domain::ports::{ use domain::ports::{
Collection, IMediaProvider, ProviderCapabilities, StreamQuality, StreamingProtocol, 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::config::LocalFilesConfig;
use crate::index::{decode_id, LocalIndex}; use crate::index::{decode_id, LocalIndex};
@@ -40,21 +40,21 @@ fn to_media_item(id: MediaItemId, item: &LocalFileItem) -> MediaItem {
} else { } else {
ContentType::Movie ContentType::Movie
}; };
MediaItem::from_persistence( MediaItem::from_persistence(MediaItemRow {
id, id,
item.title.clone(), title: item.title.clone(),
content_type, content_type,
item.duration_secs, duration_secs: item.duration_secs,
None, description: None,
vec![], genres: vec![],
item.year, year: item.year,
item.tags.clone(), tags: item.tags.clone(),
None, series_name: None,
None, season_number: None,
None, episode_number: None,
None, thumbnail_url: None,
None, collection_id: None,
) })
} }
#[async_trait] #[async_trait]

View File

@@ -6,7 +6,7 @@ use uuid::Uuid;
use adapter_common::{map_sqlx_error, parse_dt, parse_uuid}; use adapter_common::{map_sqlx_error, parse_dt, parse_uuid};
use domain::{ use domain::{
ports::activity::{ActivityLogCommand, ActivityLogQuery}, ports::activity::{ActivityLogCommand, ActivityLogQuery},
ActivityEvent, ChannelId, DomainResult, ActivityEvent, ActivityEventId, ChannelId, DomainResult,
}; };
pub struct PgActivityLog { pub struct PgActivityLog {
@@ -66,9 +66,11 @@ impl ActivityLogQuery for PgActivityLog {
let Ok(timestamp) = parse_dt(&ts_str) else { let Ok(timestamp) = parse_dt(&ts_str) else {
continue; 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( events.push(ActivityEvent::from_persistence(
id, ActivityEventId::from_uuid(id),
timestamp, timestamp,
event_type, event_type,
detail, detail,

View File

@@ -9,8 +9,8 @@ use adapter_common::{
}; };
use domain::{ use domain::{
ports::channel::{ChannelCommand, ChannelQuery}, ports::channel::{ChannelCommand, ChannelQuery},
AccessMode, Channel, ChannelConfigSnapshot, ChannelId, DomainError, DomainResult, LogoPosition, Channel, ChannelConfigSnapshot, ChannelId, ChannelRow as DomainChannelRow, DomainError,
ScheduleConfig, UserId, DomainResult, ScheduleConfig, SnapshotId, UserId,
}; };
pub struct PgChannelRepository { pub struct PgChannelRepository {
@@ -50,34 +50,27 @@ struct ChannelRow {
impl ChannelRow { impl ChannelRow {
fn into_channel(self) -> DomainResult<Channel> { fn into_channel(self) -> DomainResult<Channel> {
let id = ChannelId::from_uuid(parse_uuid(&self.id, "channel id")?); Ok(Channel::from_persistence(DomainChannelRow {
let owner_id = UserId::from_uuid(parse_uuid(&self.owner_id, "owner id")?); id: ChannelId::from_uuid(parse_uuid(&self.id, "channel id")?),
let schedule_config = parse_schedule_config(&self.schedule_config)?; owner_id: UserId::from_uuid(parse_uuid(&self.owner_id, "owner id")?),
let recycle_policy = parse_recycle_policy(&self.recycle_policy)?; name: self.name,
let access_mode: AccessMode = parse_enum_or_default(self.access_mode); description: self.description,
let logo_position: LogoPosition = parse_enum_or_default(self.logo_position); timezone: self.timezone,
schedule_config: parse_schedule_config(&self.schedule_config)?,
Ok(Channel::from_persistence( recycle_policy: parse_recycle_policy(&self.recycle_policy)?,
id, auto_schedule: self.auto_schedule,
owner_id, access_mode: parse_enum_or_default(self.access_mode),
self.name, access_password_hash: self.access_password_hash,
self.description, logo: self.logo,
self.timezone, logo_position: parse_enum_or_default(self.logo_position),
schedule_config, logo_opacity: self.logo_opacity,
recycle_policy, webhook_url: self.webhook_url,
self.auto_schedule, webhook_poll_interval_secs: self.webhook_poll_interval_secs as u32,
access_mode, webhook_body_template: self.webhook_body_template,
self.access_password_hash, webhook_headers: self.webhook_headers,
self.logo, created_at: parse_dt(&self.created_at)?,
logo_position, updated_at: parse_dt(&self.updated_at)?,
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)?,
))
} }
} }
@@ -86,7 +79,7 @@ fn map_snapshot_row(
channel_id: ChannelId, channel_id: ChannelId,
) -> DomainResult<ChannelConfigSnapshot> { ) -> DomainResult<ChannelConfigSnapshot> {
let id_str: String = row.get("id"); 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_json: String = row.get("config_json");
let config = parse_schedule_config(&config_json)?; let config = parse_schedule_config(&config_json)?;
let version_num: i64 = row.get("version_num"); let version_num: i64 = row.get("version_num");
@@ -214,7 +207,7 @@ impl ChannelCommand for PgChannelRepository {
tx.commit().await.map_err(map_sqlx_error)?; tx.commit().await.map_err(map_sqlx_error)?;
Ok(ChannelConfigSnapshot::from_persistence( Ok(ChannelConfigSnapshot::from_persistence(
id, SnapshotId::from_uuid(id),
channel_id, channel_id,
config.clone(), config.clone(),
version_num, version_num,
@@ -226,14 +219,14 @@ impl ChannelCommand for PgChannelRepository {
async fn patch_config_snapshot_label( async fn patch_config_snapshot_label(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
snapshot_id: Uuid, snapshot_id: SnapshotId,
label: Option<String>, label: Option<String>,
) -> DomainResult<Option<ChannelConfigSnapshot>> { ) -> DomainResult<Option<ChannelConfigSnapshot>> {
let updated = sqlx::query( let updated = sqlx::query(
"UPDATE channel_config_snapshots SET label = $1 WHERE id = $2 AND channel_id = $3 RETURNING id", "UPDATE channel_config_snapshots SET label = $1 WHERE id = $2 AND channel_id = $3 RETURNING id",
) )
.bind(&label) .bind(&label)
.bind(snapshot_id.to_string()) .bind(snapshot_id.value().to_string())
.bind(channel_id.value().to_string()) .bind(channel_id.value().to_string())
.fetch_optional(&self.pool) .fetch_optional(&self.pool)
.await .await
@@ -316,13 +309,13 @@ impl ChannelQuery for PgChannelRepository {
async fn get_config_snapshot( async fn get_config_snapshot(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
snapshot_id: Uuid, snapshot_id: SnapshotId,
) -> DomainResult<Option<ChannelConfigSnapshot>> { ) -> DomainResult<Option<ChannelConfigSnapshot>> {
let row = sqlx::query( let row = sqlx::query(
"SELECT id, config_json, version_num, label, created_at "SELECT id, config_json, version_num, label, created_at
FROM channel_config_snapshots WHERE id = $1 AND channel_id = $2", 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()) .bind(channel_id.value().to_string())
.fetch_optional(&self.pool) .fetch_optional(&self.pool)
.await .await

View File

@@ -4,8 +4,9 @@ use sqlx::PgPool;
use adapter_common::{content_type_str, parse_content_type, parse_genres_blob}; use adapter_common::{content_type_str, parse_content_type, parse_genres_blob};
use domain::{ use domain::{
ports::library::{LibraryCommand, LibraryQuery}, ports::library::{LibraryCommand, LibraryQuery},
ContentType, DomainError, DomainResult, LibraryCollection, LibraryItem, LibrarySearchFilter, ContentType, DomainError, DomainResult, LibraryCollection, LibraryItem,
LibrarySyncLogEntry, LibrarySyncResult, SeasonSummary, ShowSummary, LibraryItemRow as DomainLibraryItemRow, LibrarySearchFilter, LibrarySyncLogEntry,
LibrarySyncResult, SeasonSummary, ShowSummary,
}; };
pub struct PgLibraryRepository { pub struct PgLibraryRepository {
@@ -41,25 +42,25 @@ struct LibraryItemRow {
impl LibraryItemRow { impl LibraryItemRow {
fn into_library_item(self) -> LibraryItem { fn into_library_item(self) -> LibraryItem {
LibraryItem::from_persistence( LibraryItem::from_persistence(DomainLibraryItemRow {
self.id, id: self.id,
self.provider_id, provider_id: self.provider_id,
self.external_id, external_id: self.external_id,
self.title, title: self.title,
parse_content_type(&self.content_type), content_type: parse_content_type(&self.content_type),
self.duration_secs as u32, duration_secs: self.duration_secs as u32,
self.series_name, series_name: self.series_name,
self.season_number.map(|n| n as u32), season_number: self.season_number.map(|n| n as u32),
self.episode_number.map(|n| n as u32), episode_number: self.episode_number.map(|n| n as u32),
self.year.map(|n| n as u16), year: self.year.map(|n| n as u16),
serde_json::from_str(&self.genres).unwrap_or_default(), genres: serde_json::from_str(&self.genres).unwrap_or_default(),
serde_json::from_str(&self.tags).unwrap_or_default(), tags: serde_json::from_str(&self.tags).unwrap_or_default(),
self.collection_id, collection_id: self.collection_id,
self.collection_name, collection_name: self.collection_name,
self.collection_type, collection_type: self.collection_type,
self.thumbnail_url, thumbnail_url: self.thumbnail_url,
self.synced_at, synced_at: self.synced_at,
) })
} }
} }

View File

@@ -7,7 +7,7 @@ use adapter_common::{map_sqlx_error, parse_dt, parse_json, parse_uuid};
use domain::{ use domain::{
ports::schedule::{ScheduleCommand, ScheduleQuery}, ports::schedule::{ScheduleCommand, ScheduleQuery},
BlockId, ChannelId, DomainError, DomainResult, GeneratedSchedule, MediaItem, MediaItemId, BlockId, ChannelId, DomainError, DomainResult, GeneratedSchedule, MediaItem, MediaItemId,
PlaybackRecord, ScheduleId, ScheduledSlot, SlotId, PlaybackRecord, PlaybackRecordId, ScheduleId, ScheduledSlot, SlotId,
}; };
pub struct PgScheduleRepository { pub struct PgScheduleRepository {
@@ -85,7 +85,7 @@ fn map_schedule(row: ScheduleRow, slot_rows: Vec<SlotRow>) -> DomainResult<Gener
} }
fn map_playback_row(row: PlaybackRecordRow) -> DomainResult<PlaybackRecord> { fn map_playback_row(row: PlaybackRecordRow) -> DomainResult<PlaybackRecord> {
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")?); let channel_id = ChannelId::from_uuid(parse_uuid(&row.channel_id, "channel id")?);
Ok(PlaybackRecord::from_persistence( Ok(PlaybackRecord::from_persistence(

View File

@@ -6,7 +6,7 @@ use uuid::Uuid;
use adapter_common::{map_sqlx_error, parse_dt, parse_uuid}; use adapter_common::{map_sqlx_error, parse_dt, parse_uuid};
use domain::{ use domain::{
ports::activity::{ActivityLogCommand, ActivityLogQuery}, ports::activity::{ActivityLogCommand, ActivityLogQuery},
ActivityEvent, ChannelId, DomainResult, ActivityEvent, ActivityEventId, ChannelId, DomainResult,
}; };
pub struct SqliteActivityLog { pub struct SqliteActivityLog {
@@ -66,9 +66,11 @@ impl ActivityLogQuery for SqliteActivityLog {
let Ok(timestamp) = parse_dt(&ts_str) else { let Ok(timestamp) = parse_dt(&ts_str) else {
continue; 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( events.push(ActivityEvent::from_persistence(
id, ActivityEventId::from_uuid(id),
timestamp, timestamp,
event_type, event_type,
detail, detail,

View File

@@ -9,8 +9,8 @@ use adapter_common::{
}; };
use domain::{ use domain::{
ports::channel::{ChannelCommand, ChannelQuery}, ports::channel::{ChannelCommand, ChannelQuery},
AccessMode, Channel, ChannelConfigSnapshot, ChannelId, DomainError, DomainResult, LogoPosition, AccessMode, Channel, ChannelConfigSnapshot, ChannelId, ChannelRow as DomainChannelRow,
ScheduleConfig, UserId, DomainError, DomainResult, LogoPosition, ScheduleConfig, SnapshotId, UserId,
}; };
pub struct SqliteChannelRepository { pub struct SqliteChannelRepository {
@@ -57,27 +57,27 @@ impl ChannelRow {
let access_mode: AccessMode = parse_enum_or_default(self.access_mode); let access_mode: AccessMode = parse_enum_or_default(self.access_mode);
let logo_position: LogoPosition = parse_enum_or_default(self.logo_position); let logo_position: LogoPosition = parse_enum_or_default(self.logo_position);
Ok(Channel::from_persistence( Ok(Channel::from_persistence(DomainChannelRow {
id, id,
owner_id, owner_id,
self.name, name: self.name,
self.description, description: self.description,
self.timezone, timezone: self.timezone,
schedule_config, schedule_config,
recycle_policy, recycle_policy,
self.auto_schedule != 0, auto_schedule: self.auto_schedule != 0,
access_mode, access_mode,
self.access_password_hash, access_password_hash: self.access_password_hash,
self.logo, logo: self.logo,
logo_position, logo_position,
self.logo_opacity, logo_opacity: self.logo_opacity,
self.webhook_url, webhook_url: self.webhook_url,
self.webhook_poll_interval_secs as u32, webhook_poll_interval_secs: self.webhook_poll_interval_secs as u32,
self.webhook_body_template, webhook_body_template: self.webhook_body_template,
self.webhook_headers, webhook_headers: self.webhook_headers,
parse_dt(&self.created_at)?, created_at: parse_dt(&self.created_at)?,
parse_dt(&self.updated_at)?, updated_at: parse_dt(&self.updated_at)?,
)) }))
} }
} }
@@ -86,7 +86,7 @@ fn map_snapshot_row(
channel_id: ChannelId, channel_id: ChannelId,
) -> DomainResult<ChannelConfigSnapshot> { ) -> DomainResult<ChannelConfigSnapshot> {
let id_str: String = row.get("id"); 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_json: String = row.get("config_json");
let config = parse_schedule_config(&config_json)?; let config = parse_schedule_config(&config_json)?;
let version_num: i64 = row.get("version_num"); let version_num: i64 = row.get("version_num");
@@ -214,7 +214,7 @@ impl ChannelCommand for SqliteChannelRepository {
tx.commit().await.map_err(map_sqlx_error)?; tx.commit().await.map_err(map_sqlx_error)?;
Ok(ChannelConfigSnapshot::from_persistence( Ok(ChannelConfigSnapshot::from_persistence(
id, SnapshotId::from_uuid(id),
channel_id, channel_id,
config.clone(), config.clone(),
version_num, version_num,
@@ -226,14 +226,14 @@ impl ChannelCommand for SqliteChannelRepository {
async fn patch_config_snapshot_label( async fn patch_config_snapshot_label(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
snapshot_id: Uuid, snapshot_id: SnapshotId,
label: Option<String>, label: Option<String>,
) -> DomainResult<Option<ChannelConfigSnapshot>> { ) -> DomainResult<Option<ChannelConfigSnapshot>> {
let updated = sqlx::query( let updated = sqlx::query(
"UPDATE channel_config_snapshots SET label = ? WHERE id = ? AND channel_id = ? RETURNING id", "UPDATE channel_config_snapshots SET label = ? WHERE id = ? AND channel_id = ? RETURNING id",
) )
.bind(&label) .bind(&label)
.bind(snapshot_id.to_string()) .bind(snapshot_id.value().to_string())
.bind(channel_id.value().to_string()) .bind(channel_id.value().to_string())
.fetch_optional(&self.pool) .fetch_optional(&self.pool)
.await .await
@@ -316,13 +316,13 @@ impl ChannelQuery for SqliteChannelRepository {
async fn get_config_snapshot( async fn get_config_snapshot(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
snapshot_id: Uuid, snapshot_id: SnapshotId,
) -> DomainResult<Option<ChannelConfigSnapshot>> { ) -> DomainResult<Option<ChannelConfigSnapshot>> {
let row = sqlx::query( let row = sqlx::query(
"SELECT id, config_json, version_num, label, created_at "SELECT id, config_json, version_num, label, created_at
FROM channel_config_snapshots WHERE id = ? AND channel_id = ?", 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()) .bind(channel_id.value().to_string())
.fetch_optional(&self.pool) .fetch_optional(&self.pool)
.await .await

View File

@@ -4,8 +4,9 @@ use sqlx::SqlitePool;
use adapter_common::{content_type_str, parse_content_type, parse_genres_blob}; use adapter_common::{content_type_str, parse_content_type, parse_genres_blob};
use domain::{ use domain::{
ports::library::{LibraryCommand, LibraryQuery}, ports::library::{LibraryCommand, LibraryQuery},
ContentType, DomainError, DomainResult, LibraryCollection, LibraryItem, LibrarySearchFilter, ContentType, DomainError, DomainResult, LibraryCollection, LibraryItem,
LibrarySyncLogEntry, LibrarySyncResult, SeasonSummary, ShowSummary, LibraryItemRow as DomainLibraryItemRow, LibrarySearchFilter, LibrarySyncLogEntry,
LibrarySyncResult, SeasonSummary, ShowSummary,
}; };
pub struct SqliteLibraryRepository { pub struct SqliteLibraryRepository {
@@ -41,25 +42,25 @@ struct LibraryItemRow {
impl LibraryItemRow { impl LibraryItemRow {
fn into_library_item(self) -> LibraryItem { fn into_library_item(self) -> LibraryItem {
LibraryItem::from_persistence( LibraryItem::from_persistence(DomainLibraryItemRow {
self.id, id: self.id,
self.provider_id, provider_id: self.provider_id,
self.external_id, external_id: self.external_id,
self.title, title: self.title,
parse_content_type(&self.content_type), content_type: parse_content_type(&self.content_type),
self.duration_secs as u32, duration_secs: self.duration_secs as u32,
self.series_name, series_name: self.series_name,
self.season_number.map(|n| n as u32), season_number: self.season_number.map(|n| n as u32),
self.episode_number.map(|n| n as u32), episode_number: self.episode_number.map(|n| n as u32),
self.year.map(|n| n as u16), year: self.year.map(|n| n as u16),
serde_json::from_str(&self.genres).unwrap_or_default(), genres: serde_json::from_str(&self.genres).unwrap_or_default(),
serde_json::from_str(&self.tags).unwrap_or_default(), tags: serde_json::from_str(&self.tags).unwrap_or_default(),
self.collection_id, collection_id: self.collection_id,
self.collection_name, collection_name: self.collection_name,
self.collection_type, collection_type: self.collection_type,
self.thumbnail_url, thumbnail_url: self.thumbnail_url,
self.synced_at, synced_at: self.synced_at,
) })
} }
} }

View File

@@ -7,7 +7,7 @@ use adapter_common::{map_sqlx_error, parse_dt, parse_json, parse_uuid};
use domain::{ use domain::{
ports::schedule::{ScheduleCommand, ScheduleQuery}, ports::schedule::{ScheduleCommand, ScheduleQuery},
BlockId, ChannelId, DomainError, DomainResult, GeneratedSchedule, MediaItem, MediaItemId, BlockId, ChannelId, DomainError, DomainResult, GeneratedSchedule, MediaItem, MediaItemId,
PlaybackRecord, ScheduleId, ScheduledSlot, SlotId, PlaybackRecord, PlaybackRecordId, ScheduleId, ScheduledSlot, SlotId,
}; };
pub struct SqliteScheduleRepository { pub struct SqliteScheduleRepository {
@@ -84,7 +84,7 @@ fn map_schedule(row: ScheduleRow, slot_rows: Vec<SlotRow>) -> DomainResult<Gener
} }
fn map_playback_row(row: PlaybackRecordRow) -> DomainResult<PlaybackRecord> { fn map_playback_row(row: PlaybackRecordRow) -> DomainResult<PlaybackRecord> {
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")?); let channel_id = ChannelId::from_uuid(parse_uuid(&row.channel_id, "channel id")?);
Ok(PlaybackRecord::from_persistence( Ok(PlaybackRecord::from_persistence(

View File

@@ -20,11 +20,11 @@ pub struct ActivityEventResponse {
impl From<domain::ActivityEvent> for ActivityEventResponse { impl From<domain::ActivityEvent> for ActivityEventResponse {
fn from(e: domain::ActivityEvent) -> Self { fn from(e: domain::ActivityEvent) -> Self {
Self { Self {
id: e.id(), id: e.id().value(),
timestamp: e.timestamp(), timestamp: e.timestamp(),
event_type: e.event_type().to_string(), event_type: e.event_type().to_string(),
detail: e.detail().to_string(), detail: e.detail().to_string(),
channel_id: e.channel_id(), channel_id: e.channel_id().map(|id| id.value()),
} }
} }
} }

View File

@@ -95,7 +95,7 @@ pub struct ConfigSnapshotResponse {
impl From<domain::ChannelConfigSnapshot> for ConfigSnapshotResponse { impl From<domain::ChannelConfigSnapshot> for ConfigSnapshotResponse {
fn from(s: domain::ChannelConfigSnapshot) -> Self { fn from(s: domain::ChannelConfigSnapshot) -> Self {
Self { Self {
id: s.id(), id: s.id().value(),
version_num: s.version_num(), version_num: s.version_num(),
label: s.label().map(|s| s.to_string()), label: s.label().map(|s| s.to_string()),
created_at: s.created_at(), created_at: s.created_at(),

View File

@@ -34,7 +34,7 @@ fn make_deps_with_user(
repo.store repo.store
.lock() .lock()
.unwrap() .unwrap()
.insert(user.id().value(), user); .insert(user.id(), user);
let deps = AuthDeps { let deps = AuthDeps {
user_command: repo.clone(), user_command: repo.clone(),
@@ -112,7 +112,7 @@ async fn login_fails_for_oidc_only_user() {
repo.store repo.store
.lock() .lock()
.unwrap() .unwrap()
.insert(user.id().value(), user); .insert(user.id(), user);
let deps = AuthDeps { let deps = AuthDeps {
user_command: repo.clone(), user_command: repo.clone(),

View File

@@ -103,7 +103,7 @@ async fn register_fails_for_duplicate_email() {
repo.store repo.store
.lock() .lock()
.unwrap() .unwrap()
.insert(existing.id().value(), existing); .insert(existing.id(), existing);
let result = register::execute( let result = register::execute(
&deps, &deps,

View File

@@ -1,17 +1,15 @@
use uuid::Uuid;
use domain::models::ScheduleConfig; use domain::models::ScheduleConfig;
use domain::value_objects::RecyclePolicy; use domain::value_objects::{ChannelId, RecyclePolicy, UserId};
pub struct CreateChannelCommand { pub struct CreateChannelCommand {
pub owner_id: Uuid, pub owner_id: UserId,
pub name: String, pub name: String,
pub timezone: String, pub timezone: String,
} }
pub struct UpdateChannelCommand { pub struct UpdateChannelCommand {
pub channel_id: Uuid, pub channel_id: ChannelId,
pub owner_id: Uuid, pub owner_id: UserId,
pub name: Option<String>, pub name: Option<String>,
pub description: Option<Option<String>>, pub description: Option<Option<String>>,
pub timezone: Option<String>, pub timezone: Option<String>,
@@ -21,6 +19,6 @@ pub struct UpdateChannelCommand {
} }
pub struct DeleteChannelCommand { pub struct DeleteChannelCommand {
pub channel_id: Uuid, pub channel_id: ChannelId,
pub owner_id: Uuid, pub owner_id: UserId,
} }

View File

@@ -1,14 +1,12 @@
use domain::events::DomainEvent; use domain::events::DomainEvent;
use domain::models::Channel; use domain::models::Channel;
use domain::value_objects::UserId;
use domain::DomainResult; use domain::DomainResult;
use super::commands::CreateChannelCommand; use super::commands::CreateChannelCommand;
use super::deps::ChannelCommandDeps; use super::deps::ChannelCommandDeps;
pub async fn execute(deps: &ChannelCommandDeps, cmd: CreateChannelCommand) -> DomainResult<Channel> { pub async fn execute(deps: &ChannelCommandDeps, cmd: CreateChannelCommand) -> DomainResult<Channel> {
let owner_id = UserId::from(cmd.owner_id); let channel = Channel::new(cmd.owner_id, cmd.name, cmd.timezone);
let channel = Channel::new(owner_id, cmd.name, cmd.timezone);
deps.channel_command.save(&channel).await?; deps.channel_command.save(&channel).await?;

View File

@@ -1,5 +1,4 @@
use domain::events::DomainEvent; use domain::events::DomainEvent;
use domain::value_objects::{ChannelId, UserId};
use domain::DomainResult; use domain::DomainResult;
use super::commands::DeleteChannelCommand; use super::commands::DeleteChannelCommand;
@@ -7,15 +6,12 @@ use super::deps::ChannelCommandDeps;
use super::find_owned_channel; use super::find_owned_channel;
pub async fn execute(deps: &ChannelCommandDeps, cmd: DeleteChannelCommand) -> DomainResult<()> { pub async fn execute(deps: &ChannelCommandDeps, cmd: DeleteChannelCommand) -> DomainResult<()> {
let channel_id = ChannelId::from(cmd.channel_id); find_owned_channel(deps.channel_query.as_ref(), cmd.channel_id, cmd.owner_id).await?;
let owner_id = UserId::from(cmd.owner_id);
find_owned_channel(deps.channel_query.as_ref(), channel_id, owner_id, cmd.channel_id).await?; deps.channel_command.delete(cmd.channel_id).await?;
deps.channel_command.delete(channel_id).await?;
deps.event_publisher deps.event_publisher
.publish(DomainEvent::ChannelDeleted { channel_id }) .publish(DomainEvent::ChannelDeleted { channel_id: cmd.channel_id })
.await?; .await?;
Ok(()) Ok(())

View File

@@ -1,13 +1,11 @@
use domain::models::Channel; use domain::models::Channel;
use domain::value_objects::ChannelId;
use domain::DomainResult; use domain::DomainResult;
use super::deps::ChannelQueryDeps; use super::deps::ChannelQueryDeps;
use super::queries::GetChannelQuery; use super::queries::GetChannelQuery;
pub async fn execute(deps: &ChannelQueryDeps, query: GetChannelQuery) -> DomainResult<Option<Channel>> { pub async fn execute(deps: &ChannelQueryDeps, query: GetChannelQuery) -> DomainResult<Option<Channel>> {
let channel_id = ChannelId::from(query.channel_id); deps.channel_query.find_by_id(query.channel_id).await
deps.channel_query.find_by_id(channel_id).await
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -1,13 +1,11 @@
use domain::models::Channel; use domain::models::Channel;
use domain::value_objects::UserId;
use domain::DomainResult; use domain::DomainResult;
use super::deps::ChannelQueryDeps; use super::deps::ChannelQueryDeps;
use super::queries::ListByOwnerQuery; use super::queries::ListByOwnerQuery;
pub async fn execute(deps: &ChannelQueryDeps, query: ListByOwnerQuery) -> DomainResult<Vec<Channel>> { pub async fn execute(deps: &ChannelQueryDeps, query: ListByOwnerQuery) -> DomainResult<Vec<Channel>> {
let owner_id = UserId::from(query.owner_id); deps.channel_query.find_by_owner(query.owner_id).await
deps.channel_query.find_by_owner(owner_id).await
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -22,12 +22,11 @@ pub(crate) async fn find_owned_channel(
query: &dyn domain::ports::ChannelQuery, query: &dyn domain::ports::ChannelQuery,
channel_id: ChannelId, channel_id: ChannelId,
owner_id: UserId, owner_id: UserId,
raw_channel_id: uuid::Uuid,
) -> DomainResult<Channel> { ) -> DomainResult<Channel> {
let channel = query let channel = query
.find_by_id(channel_id) .find_by_id(channel_id)
.await? .await?
.ok_or(DomainError::ChannelNotFound(raw_channel_id))?; .ok_or(DomainError::ChannelNotFound(channel_id))?;
if channel.owner_id() != owner_id { if channel.owner_id() != owner_id {
return Err(DomainError::forbidden(OWNERSHIP_DENIED)); return Err(DomainError::forbidden(OWNERSHIP_DENIED));

View File

@@ -1,11 +1,11 @@
use uuid::Uuid; use domain::value_objects::{ChannelId, UserId};
pub struct GetChannelQuery { pub struct GetChannelQuery {
pub channel_id: Uuid, pub channel_id: ChannelId,
} }
pub struct ListChannelsQuery; pub struct ListChannelsQuery;
pub struct ListByOwnerQuery { pub struct ListByOwnerQuery {
pub owner_id: Uuid, pub owner_id: UserId,
} }

View File

@@ -25,7 +25,7 @@ async fn creates_channel_successfully() {
let channel = create::execute( let channel = create::execute(
&deps, &deps,
CreateChannelCommand { CreateChannelCommand {
owner_id: owner.value(), owner_id: owner,
name: "Movie Night".into(), name: "Movie Night".into(),
timezone: "America/New_York".into(), timezone: "America/New_York".into(),
}, },
@@ -51,7 +51,7 @@ async fn create_returns_default_config() {
let channel = create::execute( let channel = create::execute(
&deps, &deps,
CreateChannelCommand { CreateChannelCommand {
owner_id: UserId::generate().value(), owner_id: UserId::generate(),
name: "Defaults".into(), name: "Defaults".into(),
timezone: "UTC".into(), timezone: "UTC".into(),
}, },

View File

@@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use domain::testing::{InMemoryChannelRepository, NoopEventPublisher}; use domain::testing::{InMemoryChannelRepository, NoopEventPublisher};
use domain::value_objects::UserId; use domain::value_objects::{ChannelId, UserId};
use domain::DomainError; use domain::DomainError;
use crate::channels::commands::{CreateChannelCommand, DeleteChannelCommand}; use crate::channels::commands::{CreateChannelCommand, DeleteChannelCommand};
@@ -26,7 +26,7 @@ async fn deletes_channel_by_owner() {
let channel = create::execute( let channel = create::execute(
&deps, &deps,
CreateChannelCommand { CreateChannelCommand {
owner_id: owner.value(), owner_id: owner,
name: "Doomed".into(), name: "Doomed".into(),
timezone: "UTC".into(), timezone: "UTC".into(),
}, },
@@ -37,8 +37,8 @@ async fn deletes_channel_by_owner() {
delete::execute( delete::execute(
&deps, &deps,
DeleteChannelCommand { DeleteChannelCommand {
channel_id: channel.id().value(), channel_id: channel.id(),
owner_id: owner.value(), owner_id: owner,
}, },
) )
.await .await
@@ -56,7 +56,7 @@ async fn delete_fails_if_not_owner() {
let channel = create::execute( let channel = create::execute(
&deps, &deps,
CreateChannelCommand { CreateChannelCommand {
owner_id: owner.value(), owner_id: owner,
name: "Protected".into(), name: "Protected".into(),
timezone: "UTC".into(), timezone: "UTC".into(),
}, },
@@ -67,8 +67,8 @@ async fn delete_fails_if_not_owner() {
let result = delete::execute( let result = delete::execute(
&deps, &deps,
DeleteChannelCommand { DeleteChannelCommand {
channel_id: channel.id().value(), channel_id: channel.id(),
owner_id: stranger.value(), owner_id: stranger,
}, },
) )
.await; .await;
@@ -87,8 +87,8 @@ async fn delete_nonexistent_channel_returns_not_found() {
let result = delete::execute( let result = delete::execute(
&deps, &deps,
DeleteChannelCommand { DeleteChannelCommand {
channel_id: uuid::Uuid::new_v4(), channel_id: ChannelId::generate(),
owner_id: uuid::Uuid::new_v4(), owner_id: UserId::generate(),
}, },
) )
.await; .await;

View File

@@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use domain::testing::{InMemoryChannelRepository, NoopEventPublisher}; use domain::testing::{InMemoryChannelRepository, NoopEventPublisher};
use domain::value_objects::UserId; use domain::value_objects::{ChannelId, UserId};
use crate::channels::commands::CreateChannelCommand; use crate::channels::commands::CreateChannelCommand;
use crate::channels::deps::{ChannelCommandDeps, ChannelQueryDeps}; use crate::channels::deps::{ChannelCommandDeps, ChannelQueryDeps};
@@ -28,7 +28,7 @@ async fn get_existing_channel() {
let channel = create::execute( let channel = create::execute(
&cmd_deps, &cmd_deps,
CreateChannelCommand { CreateChannelCommand {
owner_id: UserId::generate().value(), owner_id: UserId::generate(),
name: "Findable".into(), name: "Findable".into(),
timezone: "UTC".into(), timezone: "UTC".into(),
}, },
@@ -39,7 +39,7 @@ async fn get_existing_channel() {
let found = get::execute( let found = get::execute(
&query_deps, &query_deps,
GetChannelQuery { GetChannelQuery {
channel_id: channel.id().value(), channel_id: channel.id(),
}, },
) )
.await .await
@@ -56,7 +56,7 @@ async fn get_nonexistent_returns_none() {
let found = get::execute( let found = get::execute(
&query_deps, &query_deps,
GetChannelQuery { GetChannelQuery {
channel_id: uuid::Uuid::new_v4(), channel_id: ChannelId::generate(),
}, },
) )
.await .await

View File

@@ -37,7 +37,7 @@ async fn list_returns_all_channels() {
create::execute( create::execute(
&cmd_deps, &cmd_deps,
CreateChannelCommand { CreateChannelCommand {
owner_id: UserId::generate().value(), owner_id: UserId::generate(),
name: name.into(), name: name.into(),
timezone: "UTC".into(), timezone: "UTC".into(),
}, },

View File

@@ -32,7 +32,7 @@ async fn filters_by_owner() {
create::execute( create::execute(
&cmd_deps, &cmd_deps,
CreateChannelCommand { CreateChannelCommand {
owner_id: alice.value(), owner_id: alice,
name: name.into(), name: name.into(),
timezone: "UTC".into(), timezone: "UTC".into(),
}, },
@@ -45,7 +45,7 @@ async fn filters_by_owner() {
create::execute( create::execute(
&cmd_deps, &cmd_deps,
CreateChannelCommand { CreateChannelCommand {
owner_id: bob.value(), owner_id: bob,
name: "Bob-1".into(), name: "Bob-1".into(),
timezone: "UTC".into(), timezone: "UTC".into(),
}, },
@@ -56,7 +56,7 @@ async fn filters_by_owner() {
let alice_channels = list_by_owner::execute( let alice_channels = list_by_owner::execute(
&query_deps, &query_deps,
ListByOwnerQuery { ListByOwnerQuery {
owner_id: alice.value(), owner_id: alice,
}, },
) )
.await .await
@@ -73,7 +73,7 @@ async fn no_channels_returns_empty() {
let channels = list_by_owner::execute( let channels = list_by_owner::execute(
&query_deps, &query_deps,
ListByOwnerQuery { ListByOwnerQuery {
owner_id: UserId::generate().value(), owner_id: UserId::generate(),
}, },
) )
.await .await

View File

@@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use domain::testing::{InMemoryChannelRepository, NoopEventPublisher}; use domain::testing::{InMemoryChannelRepository, NoopEventPublisher};
use domain::value_objects::UserId; use domain::value_objects::{ChannelId, UserId};
use domain::DomainError; use domain::DomainError;
use crate::channels::commands::{CreateChannelCommand, UpdateChannelCommand}; use crate::channels::commands::{CreateChannelCommand, UpdateChannelCommand};
@@ -26,7 +26,7 @@ async fn updates_channel_name() {
let channel = create::execute( let channel = create::execute(
&deps, &deps,
CreateChannelCommand { CreateChannelCommand {
owner_id: owner.value(), owner_id: owner,
name: "Original".into(), name: "Original".into(),
timezone: "UTC".into(), timezone: "UTC".into(),
}, },
@@ -37,8 +37,8 @@ async fn updates_channel_name() {
let updated = update::execute( let updated = update::execute(
&deps, &deps,
UpdateChannelCommand { UpdateChannelCommand {
channel_id: channel.id().value(), channel_id: channel.id(),
owner_id: owner.value(), owner_id: owner,
name: Some("Renamed".into()), name: Some("Renamed".into()),
description: None, description: None,
timezone: None, timezone: None,
@@ -63,7 +63,7 @@ async fn update_fails_if_not_owner() {
let channel = create::execute( let channel = create::execute(
&deps, &deps,
CreateChannelCommand { CreateChannelCommand {
owner_id: owner.value(), owner_id: owner,
name: "Protected".into(), name: "Protected".into(),
timezone: "UTC".into(), timezone: "UTC".into(),
}, },
@@ -74,8 +74,8 @@ async fn update_fails_if_not_owner() {
let result = update::execute( let result = update::execute(
&deps, &deps,
UpdateChannelCommand { UpdateChannelCommand {
channel_id: channel.id().value(), channel_id: channel.id(),
owner_id: stranger.value(), owner_id: stranger,
name: Some("Hacked".into()), name: Some("Hacked".into()),
description: None, description: None,
timezone: None, timezone: None,
@@ -100,8 +100,8 @@ async fn update_nonexistent_channel_returns_not_found() {
let result = update::execute( let result = update::execute(
&deps, &deps,
UpdateChannelCommand { UpdateChannelCommand {
channel_id: uuid::Uuid::new_v4(), channel_id: ChannelId::generate(),
owner_id: uuid::Uuid::new_v4(), owner_id: UserId::generate(),
name: Some("Ghost".into()), name: Some("Ghost".into()),
description: None, description: None,
timezone: None, timezone: None,
@@ -127,7 +127,7 @@ async fn update_config_creates_snapshot() {
let channel = create::execute( let channel = create::execute(
&deps, &deps,
CreateChannelCommand { CreateChannelCommand {
owner_id: owner.value(), owner_id: owner,
name: "Snapshotted".into(), name: "Snapshotted".into(),
timezone: "UTC".into(), timezone: "UTC".into(),
}, },
@@ -140,8 +140,8 @@ async fn update_config_creates_snapshot() {
update::execute( update::execute(
&deps, &deps,
UpdateChannelCommand { UpdateChannelCommand {
channel_id: channel.id().value(), channel_id: channel.id(),
owner_id: owner.value(), owner_id: owner,
name: None, name: None,
description: None, description: None,
timezone: None, timezone: None,
@@ -167,7 +167,7 @@ async fn update_without_config_skips_snapshot() {
let channel = create::execute( let channel = create::execute(
&deps, &deps,
CreateChannelCommand { CreateChannelCommand {
owner_id: owner.value(), owner_id: owner,
name: "NoSnapshot".into(), name: "NoSnapshot".into(),
timezone: "UTC".into(), timezone: "UTC".into(),
}, },
@@ -179,8 +179,8 @@ async fn update_without_config_skips_snapshot() {
update::execute( update::execute(
&deps, &deps,
UpdateChannelCommand { UpdateChannelCommand {
channel_id: channel.id().value(), channel_id: channel.id(),
owner_id: owner.value(), owner_id: owner,
name: Some("Renamed".into()), name: Some("Renamed".into()),
description: None, description: None,
timezone: None, timezone: None,
@@ -205,7 +205,7 @@ async fn update_description_clear() {
let channel = create::execute( let channel = create::execute(
&deps, &deps,
CreateChannelCommand { CreateChannelCommand {
owner_id: owner.value(), owner_id: owner,
name: "Desc Test".into(), name: "Desc Test".into(),
timezone: "UTC".into(), timezone: "UTC".into(),
}, },
@@ -217,8 +217,8 @@ async fn update_description_clear() {
let updated = update::execute( let updated = update::execute(
&deps, &deps,
UpdateChannelCommand { UpdateChannelCommand {
channel_id: channel.id().value(), channel_id: channel.id(),
owner_id: owner.value(), owner_id: owner,
name: None, name: None,
description: Some(Some("A description".into())), description: Some(Some("A description".into())),
timezone: None, timezone: None,
@@ -235,8 +235,8 @@ async fn update_description_clear() {
let cleared = update::execute( let cleared = update::execute(
&deps, &deps,
UpdateChannelCommand { UpdateChannelCommand {
channel_id: channel.id().value(), channel_id: channel.id(),
owner_id: owner.value(), owner_id: owner,
name: None, name: None,
description: Some(None), description: Some(None),
timezone: None, timezone: None,

View File

@@ -1,6 +1,5 @@
use domain::events::DomainEvent; use domain::events::DomainEvent;
use domain::models::Channel; use domain::models::Channel;
use domain::value_objects::{ChannelId, UserId};
use domain::DomainResult; use domain::DomainResult;
use super::commands::UpdateChannelCommand; use super::commands::UpdateChannelCommand;
@@ -8,16 +7,13 @@ use super::deps::ChannelCommandDeps;
use super::find_owned_channel; use super::find_owned_channel;
pub async fn execute(deps: &ChannelCommandDeps, cmd: UpdateChannelCommand) -> DomainResult<Channel> { pub async fn execute(deps: &ChannelCommandDeps, cmd: UpdateChannelCommand) -> DomainResult<Channel> {
let channel_id = ChannelId::from(cmd.channel_id);
let owner_id = UserId::from(cmd.owner_id);
let mut channel = 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?; .await?;
if cmd.schedule_config.is_some() { if cmd.schedule_config.is_some() {
deps.channel_command deps.channel_command
.save_config_snapshot(channel_id, channel.schedule_config(), None) .save_config_snapshot(cmd.channel_id, channel.schedule_config(), None)
.await?; .await?;
} }

View File

@@ -1,17 +1,17 @@
use uuid::Uuid; use domain::value_objects::{ChannelId, SnapshotId};
pub struct SaveSnapshotCommand { pub struct SaveSnapshotCommand {
pub channel_id: Uuid, pub channel_id: ChannelId,
pub label: Option<String>, pub label: Option<String>,
} }
pub struct PatchLabelCommand { pub struct PatchLabelCommand {
pub channel_id: Uuid, pub channel_id: ChannelId,
pub snapshot_id: Uuid, pub snapshot_id: SnapshotId,
pub label: Option<String>, pub label: Option<String>,
} }
pub struct RestoreSnapshotCommand { pub struct RestoreSnapshotCommand {
pub channel_id: Uuid, pub channel_id: ChannelId,
pub snapshot_id: Uuid, pub snapshot_id: SnapshotId,
} }

View File

@@ -1,5 +1,4 @@
use domain::models::ChannelConfigSnapshot; use domain::models::ChannelConfigSnapshot;
use domain::value_objects::ChannelId;
use domain::DomainResult; use domain::DomainResult;
use super::deps::ConfigSnapshotDeps; use super::deps::ConfigSnapshotDeps;
@@ -9,8 +8,7 @@ pub async fn execute(
deps: &ConfigSnapshotDeps, deps: &ConfigSnapshotDeps,
query: GetSnapshotQuery, query: GetSnapshotQuery,
) -> DomainResult<Option<ChannelConfigSnapshot>> { ) -> DomainResult<Option<ChannelConfigSnapshot>> {
let channel_id = ChannelId::from(query.channel_id);
deps.channel_query deps.channel_query
.get_config_snapshot(channel_id, query.snapshot_id) .get_config_snapshot(query.channel_id, query.snapshot_id)
.await .await
} }

View File

@@ -1,5 +1,4 @@
use domain::models::ChannelConfigSnapshot; use domain::models::ChannelConfigSnapshot;
use domain::value_objects::ChannelId;
use domain::DomainResult; use domain::DomainResult;
use super::deps::ConfigSnapshotDeps; use super::deps::ConfigSnapshotDeps;
@@ -9,8 +8,7 @@ pub async fn execute(
deps: &ConfigSnapshotDeps, deps: &ConfigSnapshotDeps,
query: ListSnapshotsQuery, query: ListSnapshotsQuery,
) -> DomainResult<Vec<ChannelConfigSnapshot>> { ) -> DomainResult<Vec<ChannelConfigSnapshot>> {
let channel_id = ChannelId::from(query.channel_id); deps.channel_query.list_config_snapshots(query.channel_id).await
deps.channel_query.list_config_snapshots(channel_id).await
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -1,5 +1,4 @@
use domain::models::ChannelConfigSnapshot; use domain::models::ChannelConfigSnapshot;
use domain::value_objects::ChannelId;
use domain::DomainResult; use domain::DomainResult;
use super::commands::PatchLabelCommand; use super::commands::PatchLabelCommand;
@@ -9,9 +8,7 @@ pub async fn execute(
deps: &ConfigSnapshotDeps, deps: &ConfigSnapshotDeps,
cmd: PatchLabelCommand, cmd: PatchLabelCommand,
) -> DomainResult<Option<ChannelConfigSnapshot>> { ) -> DomainResult<Option<ChannelConfigSnapshot>> {
let channel_id = ChannelId::from(cmd.channel_id);
deps.channel_command 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 .await
} }

View File

@@ -1,10 +1,10 @@
use uuid::Uuid; use domain::value_objects::{ChannelId, SnapshotId};
pub struct ListSnapshotsQuery { pub struct ListSnapshotsQuery {
pub channel_id: Uuid, pub channel_id: ChannelId,
} }
pub struct GetSnapshotQuery { pub struct GetSnapshotQuery {
pub channel_id: Uuid, pub channel_id: ChannelId,
pub snapshot_id: Uuid, pub snapshot_id: SnapshotId,
} }

View File

@@ -1,5 +1,4 @@
use domain::models::Channel; use domain::models::Channel;
use domain::value_objects::ChannelId;
use domain::{DomainError, DomainResult}; use domain::{DomainError, DomainResult};
use super::commands::RestoreSnapshotCommand; use super::commands::RestoreSnapshotCommand;
@@ -9,11 +8,9 @@ pub async fn execute(
deps: &ConfigSnapshotDeps, deps: &ConfigSnapshotDeps,
cmd: RestoreSnapshotCommand, cmd: RestoreSnapshotCommand,
) -> DomainResult<Channel> { ) -> DomainResult<Channel> {
let channel_id = ChannelId::from(cmd.channel_id);
let snapshot = deps let snapshot = deps
.channel_query .channel_query
.get_config_snapshot(channel_id, cmd.snapshot_id) .get_config_snapshot(cmd.channel_id, cmd.snapshot_id)
.await? .await?
.ok_or(DomainError::ValidationError(format!( .ok_or(DomainError::ValidationError(format!(
"Snapshot {} not found", "Snapshot {} not found",
@@ -22,12 +19,12 @@ pub async fn execute(
let mut channel = deps let mut channel = deps
.channel_query .channel_query
.find_by_id(channel_id) .find_by_id(cmd.channel_id)
.await? .await?
.ok_or(DomainError::ChannelNotFound(cmd.channel_id))?; .ok_or(DomainError::ChannelNotFound(cmd.channel_id))?;
deps.channel_command deps.channel_command
.save_config_snapshot(channel_id, channel.schedule_config(), None) .save_config_snapshot(cmd.channel_id, channel.schedule_config(), None)
.await?; .await?;
channel.set_schedule_config(snapshot.config().clone()); channel.set_schedule_config(snapshot.config().clone());

View File

@@ -1,5 +1,4 @@
use domain::models::ChannelConfigSnapshot; use domain::models::ChannelConfigSnapshot;
use domain::value_objects::ChannelId;
use domain::{DomainError, DomainResult}; use domain::{DomainError, DomainResult};
use super::commands::SaveSnapshotCommand; use super::commands::SaveSnapshotCommand;
@@ -9,16 +8,14 @@ pub async fn execute(
deps: &ConfigSnapshotDeps, deps: &ConfigSnapshotDeps,
cmd: SaveSnapshotCommand, cmd: SaveSnapshotCommand,
) -> DomainResult<ChannelConfigSnapshot> { ) -> DomainResult<ChannelConfigSnapshot> {
let channel_id = ChannelId::from(cmd.channel_id);
let channel = deps let channel = deps
.channel_query .channel_query
.find_by_id(channel_id) .find_by_id(cmd.channel_id)
.await? .await?
.ok_or(DomainError::ChannelNotFound(cmd.channel_id))?; .ok_or(DomainError::ChannelNotFound(cmd.channel_id))?;
deps.channel_command 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 .await
} }

View File

@@ -23,7 +23,7 @@ async fn seed_channel(repo: &InMemoryChannelRepository) -> Channel {
repo.channels repo.channels
.lock() .lock()
.unwrap() .unwrap()
.insert(channel.id().value(), channel.clone()); .insert(channel.id(), channel.clone());
channel channel
} }
@@ -35,7 +35,7 @@ async fn list_empty() {
let snaps = list::execute( let snaps = list::execute(
&deps, &deps,
ListSnapshotsQuery { ListSnapshotsQuery {
channel_id: channel.id().value(), channel_id: channel.id(),
}, },
) )
.await .await
@@ -53,7 +53,7 @@ async fn list_returns_saved_snapshots() {
save::execute( save::execute(
&deps, &deps,
SaveSnapshotCommand { SaveSnapshotCommand {
channel_id: channel.id().value(), channel_id: channel.id(),
label: Some(label.into()), label: Some(label.into()),
}, },
) )
@@ -64,7 +64,7 @@ async fn list_returns_saved_snapshots() {
let snaps = list::execute( let snaps = list::execute(
&deps, &deps,
ListSnapshotsQuery { ListSnapshotsQuery {
channel_id: channel.id().value(), channel_id: channel.id(),
}, },
) )
.await .await

View File

@@ -22,7 +22,7 @@ async fn seed_channel(repo: &InMemoryChannelRepository) -> Channel {
repo.channels repo.channels
.lock() .lock()
.unwrap() .unwrap()
.insert(channel.id().value(), channel.clone()); .insert(channel.id(), channel.clone());
channel channel
} }
@@ -34,7 +34,7 @@ async fn save_creates_snapshot() {
let snap = save::execute( let snap = save::execute(
&deps, &deps,
SaveSnapshotCommand { SaveSnapshotCommand {
channel_id: channel.id().value(), channel_id: channel.id(),
label: Some("v1".into()), label: Some("v1".into()),
}, },
) )
@@ -54,7 +54,7 @@ async fn save_increments_version() {
save::execute( save::execute(
&deps, &deps,
SaveSnapshotCommand { SaveSnapshotCommand {
channel_id: channel.id().value(), channel_id: channel.id(),
label: None, label: None,
}, },
) )
@@ -64,7 +64,7 @@ async fn save_increments_version() {
let snap2 = save::execute( let snap2 = save::execute(
&deps, &deps,
SaveSnapshotCommand { SaveSnapshotCommand {
channel_id: channel.id().value(), channel_id: channel.id(),
label: None, label: None,
}, },
) )

View File

@@ -43,7 +43,7 @@ async fn m3u_includes_channels() {
repo.channels repo.channels
.lock() .lock()
.unwrap() .unwrap()
.insert(ch.id().value(), ch.clone()); .insert(ch.id(), ch.clone());
let result = m3u::execute( let result = m3u::execute(
&deps, &deps,
@@ -68,7 +68,7 @@ async fn m3u_no_token() {
repo.channels repo.channels
.lock() .lock()
.unwrap() .unwrap()
.insert(ch.id().value(), ch); .insert(ch.id(), ch);
let result = m3u::execute( let result = m3u::execute(
&deps, &deps,

View File

@@ -1,4 +1,4 @@
use domain::models::LibraryItem; use domain::models::{LibraryItem, LibraryItemRow};
use domain::value_objects::ContentType; use domain::value_objects::ContentType;
use crate::library::list_collections; use crate::library::list_collections;
@@ -10,46 +10,46 @@ mod helpers;
fn seed_with_collections(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepository>) { fn seed_with_collections(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepository>) {
let mut store = repo.items.lock().unwrap(); let mut store = repo.items.lock().unwrap();
let item = LibraryItem::from_persistence( let item = LibraryItem::from_persistence(LibraryItemRow {
"test::m1".into(), id: "test::m1".into(),
"test".into(), provider_id: "test".into(),
"m1".into(), external_id: "m1".into(),
"Die Hard".into(), title: "Die Hard".into(),
ContentType::Movie, content_type: ContentType::Movie,
7800, duration_secs: 7800,
None, series_name: None,
None, season_number: None,
None, episode_number: None,
None, year: None,
vec![], genres: vec![],
vec![], tags: vec![],
Some("col-1".into()), collection_id: Some("col-1".into()),
Some("Movies".into()), collection_name: Some("Movies".into()),
Some("movies".into()), collection_type: Some("movies".into()),
None, thumbnail_url: None,
"2026-01-01".into(), synced_at: "2026-01-01".into(),
); });
store.insert(item.id().to_string(), item); store.insert(item.id().to_string(), item);
let item2 = LibraryItem::from_persistence( let item2 = LibraryItem::from_persistence(LibraryItemRow {
"test::e1".into(), id: "test::e1".into(),
"test".into(), provider_id: "test".into(),
"e1".into(), external_id: "e1".into(),
"BB S01E01".into(), title: "BB S01E01".into(),
ContentType::Episode, content_type: ContentType::Episode,
2700, duration_secs: 2700,
Some("Breaking Bad".into()), series_name: Some("Breaking Bad".into()),
Some(1), season_number: Some(1),
Some(1), episode_number: Some(1),
None, year: None,
vec![], genres: vec![],
vec![], tags: vec![],
Some("col-2".into()), collection_id: Some("col-2".into()),
Some("TV Shows".into()), collection_name: Some("TV Shows".into()),
Some("tvshows".into()), collection_type: Some("tvshows".into()),
None, thumbnail_url: None,
"2026-01-01".into(), synced_at: "2026-01-01".into(),
); });
store.insert(item2.id().to_string(), item2); store.insert(item2.id().to_string(), item2);
} }

View File

@@ -1,4 +1,4 @@
use domain::models::LibraryItem; use domain::models::{LibraryItem, LibraryItemRow};
use domain::value_objects::ContentType; use domain::value_objects::ContentType;
use crate::library::list_genres; use crate::library::list_genres;
@@ -10,44 +10,44 @@ mod helpers;
fn seed_with_genres(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepository>) { fn seed_with_genres(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepository>) {
let mut store = repo.items.lock().unwrap(); let mut store = repo.items.lock().unwrap();
let item1 = LibraryItem::from_persistence( let item1 = LibraryItem::from_persistence(LibraryItemRow {
"test::m1".into(), id: "test::m1".into(),
"test".into(), provider_id: "test".into(),
"m1".into(), external_id: "m1".into(),
"Die Hard".into(), title: "Die Hard".into(),
ContentType::Movie, content_type: ContentType::Movie,
7800, duration_secs: 7800,
None, series_name: None,
None, season_number: None,
None, episode_number: None,
None, year: None,
vec!["Action".into(), "Thriller".into()], genres: vec!["Action".into(), "Thriller".into()],
vec![], tags: vec![],
None, collection_id: None,
None, collection_name: None,
None, collection_type: None,
None, thumbnail_url: None,
"2026-01-01".into(), synced_at: "2026-01-01".into(),
); });
let item2 = LibraryItem::from_persistence( let item2 = LibraryItem::from_persistence(LibraryItemRow {
"test::m2".into(), id: "test::m2".into(),
"test".into(), provider_id: "test".into(),
"m2".into(), external_id: "m2".into(),
"Alien".into(), title: "Alien".into(),
ContentType::Movie, content_type: ContentType::Movie,
7020, duration_secs: 7020,
None, series_name: None,
None, season_number: None,
None, episode_number: None,
None, year: None,
vec!["Sci-Fi".into(), "Action".into()], genres: vec!["Sci-Fi".into(), "Action".into()],
vec![], tags: vec![],
None, collection_id: None,
None, collection_name: None,
None, collection_type: None,
None, thumbnail_url: None,
"2026-01-01".into(), synced_at: "2026-01-01".into(),
); });
store.insert(item1.id().to_string(), item1); store.insert(item1.id().to_string(), item1);
store.insert(item2.id().to_string(), item2); store.insert(item2.id().to_string(), item2);

View File

@@ -1,4 +1,4 @@
use domain::models::LibraryItem; use domain::models::{LibraryItem, LibraryItemRow};
use domain::value_objects::ContentType; use domain::value_objects::ContentType;
use crate::library::list_seasons; use crate::library::list_seasons;
@@ -11,25 +11,25 @@ fn seed_episodes(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepositor
let mut store = repo.items.lock().unwrap(); let mut store = repo.items.lock().unwrap();
for (i, season) in [1u32, 1, 2, 2, 2, 3].iter().enumerate() { for (i, season) in [1u32, 1, 2, 2, 2, 3].iter().enumerate() {
let item = LibraryItem::from_persistence( let item = LibraryItem::from_persistence(LibraryItemRow {
format!("test::e{i}"), id: format!("test::e{i}"),
"test".into(), provider_id: "test".into(),
format!("e{i}"), external_id: format!("e{i}"),
format!("BB S{season:02}E{:02}", i + 1), title: format!("BB S{season:02}E{:02}", i + 1),
ContentType::Episode, content_type: ContentType::Episode,
2700, duration_secs: 2700,
Some("Breaking Bad".into()), series_name: Some("Breaking Bad".into()),
Some(*season), season_number: Some(*season),
Some(i as u32 + 1), episode_number: Some(i as u32 + 1),
None, year: None,
vec![], genres: vec![],
vec![], tags: vec![],
None, collection_id: None,
None, collection_name: None,
None, collection_type: None,
None, thumbnail_url: None,
"2026-01-01".into(), synced_at: "2026-01-01".into(),
); });
store.insert(item.id().to_string(), item); store.insert(item.id().to_string(), item);
} }
} }

View File

@@ -1,4 +1,4 @@
use domain::models::LibraryItem; use domain::models::{LibraryItem, LibraryItemRow};
use domain::value_objects::ContentType; use domain::value_objects::ContentType;
use crate::library::list_shows; use crate::library::list_shows;
@@ -20,25 +20,25 @@ fn seed_episodes(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepositor
.iter() .iter()
.enumerate() .enumerate()
{ {
let item = LibraryItem::from_persistence( let item = LibraryItem::from_persistence(LibraryItemRow {
format!("test::e{i}"), id: format!("test::e{i}"),
"test".into(), provider_id: "test".into(),
format!("e{i}"), external_id: format!("e{i}"),
format!("{series} S{season:02}E{i:02}"), title: format!("{series} S{season:02}E{i:02}"),
ContentType::Episode, content_type: ContentType::Episode,
2700, duration_secs: 2700,
Some(series.to_string()), series_name: Some(series.to_string()),
Some(*season), season_number: Some(*season),
Some(i as u32 + 1), episode_number: Some(i as u32 + 1),
None, year: None,
vec![], genres: vec![],
vec![], tags: vec![],
None, collection_id: None,
None, collection_name: None,
None, collection_type: None,
None, thumbnail_url: None,
"2026-01-01".into(), synced_at: "2026-01-01".into(),
); });
store.insert(item.id().to_string(), item); store.insert(item.id().to_string(), item);
} }
} }

View File

@@ -1,4 +1,4 @@
use domain::models::LibraryItem; use domain::models::{LibraryItem, LibraryItemRow};
use domain::value_objects::ContentType; use domain::value_objects::ContentType;
use crate::library::queries::SearchItemsQuery; use crate::library::queries::SearchItemsQuery;
@@ -22,63 +22,63 @@ fn seed_items(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepository>)
fn seed_items_with_genres(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepository>) { fn seed_items_with_genres(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepository>) {
let mut store = repo.items.lock().unwrap(); let mut store = repo.items.lock().unwrap();
let action = LibraryItem::from_persistence( let action = LibraryItem::from_persistence(LibraryItemRow {
"test::m1".into(), id: "test::m1".into(),
"test".into(), provider_id: "test".into(),
"m1".into(), external_id: "m1".into(),
"Die Hard".into(), title: "Die Hard".into(),
ContentType::Movie, content_type: ContentType::Movie,
7800, duration_secs: 7800,
None, series_name: None,
None, season_number: None,
None, episode_number: None,
Some(1988), year: Some(1988),
vec!["Action".into(), "Thriller".into()], genres: vec!["Action".into(), "Thriller".into()],
vec![], tags: vec![],
None, collection_id: None,
None, collection_name: None,
None, collection_type: None,
None, thumbnail_url: None,
"2026-01-01".into(), synced_at: "2026-01-01".into(),
); });
let scifi = LibraryItem::from_persistence( let scifi = LibraryItem::from_persistence(LibraryItemRow {
"test::m2".into(), id: "test::m2".into(),
"test".into(), provider_id: "test".into(),
"m2".into(), external_id: "m2".into(),
"Alien".into(), title: "Alien".into(),
ContentType::Movie, content_type: ContentType::Movie,
7020, duration_secs: 7020,
None, series_name: None,
None, season_number: None,
None, episode_number: None,
Some(1979), year: Some(1979),
vec!["Sci-Fi".into(), "Horror".into()], genres: vec!["Sci-Fi".into(), "Horror".into()],
vec![], tags: vec![],
None, collection_id: None,
None, collection_name: None,
None, collection_type: None,
None, thumbnail_url: None,
"2026-01-01".into(), synced_at: "2026-01-01".into(),
); });
let comedy = LibraryItem::from_persistence( let comedy = LibraryItem::from_persistence(LibraryItemRow {
"test::m3".into(), id: "test::m3".into(),
"test".into(), provider_id: "test".into(),
"m3".into(), external_id: "m3".into(),
"Airplane!".into(), title: "Airplane!".into(),
ContentType::Movie, content_type: ContentType::Movie,
5280, duration_secs: 5280,
None, series_name: None,
None, season_number: None,
None, episode_number: None,
Some(1980), year: Some(1980),
vec!["Comedy".into()], genres: vec!["Comedy".into()],
vec![], tags: vec![],
None, collection_id: None,
None, collection_name: None,
None, collection_type: None,
None, thumbnail_url: None,
"2026-01-01".into(), synced_at: "2026-01-01".into(),
); });
store.insert(action.id().to_string(), action); store.insert(action.id().to_string(), action);
store.insert(scifi.id().to_string(), scifi); store.insert(scifi.id().to_string(), scifi);

View File

@@ -20,7 +20,7 @@ async fn delete_after_removes_later_generations() {
.channels .channels
.lock() .lock()
.unwrap() .unwrap()
.insert(channel_id.value(), channel); .insert(channel_id, channel);
// Manually insert schedules with different generations. // Manually insert schedules with different generations.
let now = chrono::Utc::now(); let now = chrono::Utc::now();
@@ -36,7 +36,7 @@ async fn delete_after_removes_later_generations() {
.schedules .schedules
.lock() .lock()
.unwrap() .unwrap()
.insert(sched.id().value(), sched); .insert(sched.id(), sched);
} }
// Delete generations > 1. // Delete generations > 1.

View File

@@ -18,7 +18,7 @@ async fn generate_produces_empty_schedule_for_channel_with_no_blocks() {
.channels .channels
.lock() .lock()
.unwrap() .unwrap()
.insert(channel.id().value(), channel.clone()); .insert(channel.id(), channel.clone());
let schedule = generate::execute( let schedule = generate::execute(
&deps, &deps,
@@ -58,7 +58,7 @@ async fn generate_increments_generation() {
.channels .channels
.lock() .lock()
.unwrap() .unwrap()
.insert(channel.id().value(), channel.clone()); .insert(channel.id(), channel.clone());
let first = generate::execute( let first = generate::execute(
&deps, &deps,

View File

@@ -1 +0,0 @@
too-many-arguments-threshold = 20

View File

@@ -1,20 +1,21 @@
use thiserror::Error; use thiserror::Error;
use uuid::Uuid;
use crate::value_objects::{ChannelId, UserId};
#[derive(Debug, Error)] #[derive(Debug, Error)]
#[non_exhaustive] #[non_exhaustive]
pub enum DomainError { pub enum DomainError {
#[error("User not found: {0}")] #[error("User not found: {0}")]
UserNotFound(Uuid), UserNotFound(UserId),
#[error("User already exists: {0}")] #[error("User already exists: {0}")]
UserAlreadyExists(String), UserAlreadyExists(String),
#[error("Channel not found: {0}")] #[error("Channel not found: {0}")]
ChannelNotFound(Uuid), ChannelNotFound(ChannelId),
#[error("No active schedule for channel: {0}")] #[error("No active schedule for channel: {0}")]
NoActiveSchedule(Uuid), NoActiveSchedule(ChannelId),
#[error("Validation error: {0}")] #[error("Validation error: {0}")]
ValidationError(String), ValidationError(String),

View File

@@ -1,23 +1,24 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use uuid::Uuid;
use crate::value_objects::{ActivityEventId, ChannelId};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ActivityEvent { pub struct ActivityEvent {
id: Uuid, id: ActivityEventId,
timestamp: DateTime<Utc>, timestamp: DateTime<Utc>,
event_type: String, event_type: String,
detail: String, detail: String,
channel_id: Option<Uuid>, channel_id: Option<ChannelId>,
} }
impl ActivityEvent { impl ActivityEvent {
pub fn new( pub fn new(
event_type: impl Into<String>, event_type: impl Into<String>,
detail: impl Into<String>, detail: impl Into<String>,
channel_id: Option<Uuid>, channel_id: Option<ChannelId>,
) -> Self { ) -> Self {
Self { Self {
id: Uuid::new_v4(), id: ActivityEventId::generate(),
timestamp: Utc::now(), timestamp: Utc::now(),
event_type: event_type.into(), event_type: event_type.into(),
detail: detail.into(), detail: detail.into(),
@@ -26,11 +27,11 @@ impl ActivityEvent {
} }
pub fn from_persistence( pub fn from_persistence(
id: Uuid, id: ActivityEventId,
timestamp: DateTime<Utc>, timestamp: DateTime<Utc>,
event_type: String, event_type: String,
detail: String, detail: String,
channel_id: Option<Uuid>, channel_id: Option<ChannelId>,
) -> Self { ) -> Self {
Self { Self {
id, id,
@@ -41,7 +42,7 @@ impl ActivityEvent {
} }
} }
pub fn id(&self) -> Uuid { pub fn id(&self) -> ActivityEventId {
self.id self.id
} }
@@ -57,7 +58,7 @@ impl ActivityEvent {
&self.detail &self.detail
} }
pub fn channel_id(&self) -> Option<Uuid> { pub fn channel_id(&self) -> Option<ChannelId> {
self.channel_id self.channel_id
} }
} }

View File

@@ -34,6 +34,28 @@ pub struct Channel {
updated_at: DateTime<Utc>, updated_at: DateTime<Utc>,
} }
pub struct ChannelRow {
pub id: ChannelId,
pub owner_id: UserId,
pub name: String,
pub description: Option<String>,
pub timezone: String,
pub schedule_config: ScheduleConfig,
pub recycle_policy: RecyclePolicy,
pub auto_schedule: bool,
pub access_mode: AccessMode,
pub access_password_hash: Option<String>,
pub logo: Option<String>,
pub logo_position: LogoPosition,
pub logo_opacity: f32,
pub webhook_url: Option<String>,
pub webhook_poll_interval_secs: u32,
pub webhook_body_template: Option<String>,
pub webhook_headers: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Channel { impl Channel {
pub fn new( pub fn new(
owner_id: UserId, owner_id: UserId,
@@ -64,47 +86,27 @@ impl Channel {
} }
} }
pub fn from_persistence( pub fn from_persistence(row: ChannelRow) -> Self {
id: ChannelId,
owner_id: UserId,
name: String,
description: Option<String>,
timezone: String,
schedule_config: ScheduleConfig,
recycle_policy: RecyclePolicy,
auto_schedule: bool,
access_mode: AccessMode,
access_password_hash: Option<String>,
logo: Option<String>,
logo_position: LogoPosition,
logo_opacity: f32,
webhook_url: Option<String>,
webhook_poll_interval_secs: u32,
webhook_body_template: Option<String>,
webhook_headers: Option<String>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
) -> Self {
Self { Self {
id, id: row.id,
owner_id, owner_id: row.owner_id,
name, name: row.name,
description, description: row.description,
timezone, timezone: row.timezone,
schedule_config, schedule_config: row.schedule_config,
recycle_policy, recycle_policy: row.recycle_policy,
auto_schedule, auto_schedule: row.auto_schedule,
access_mode, access_mode: row.access_mode,
access_password_hash, access_password_hash: row.access_password_hash,
logo, logo: row.logo,
logo_position, logo_position: row.logo_position,
logo_opacity, logo_opacity: row.logo_opacity,
webhook_url, webhook_url: row.webhook_url,
webhook_poll_interval_secs, webhook_poll_interval_secs: row.webhook_poll_interval_secs,
webhook_body_template, webhook_body_template: row.webhook_body_template,
webhook_headers, webhook_headers: row.webhook_headers,
created_at, created_at: row.created_at,
updated_at, updated_at: row.updated_at,
} }
} }

View File

@@ -1,14 +1,13 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::value_objects::ChannelId; use crate::value_objects::{ChannelId, SnapshotId};
use super::ScheduleConfig; use super::ScheduleConfig;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelConfigSnapshot { pub struct ChannelConfigSnapshot {
id: Uuid, id: SnapshotId,
channel_id: ChannelId, channel_id: ChannelId,
config: ScheduleConfig, config: ScheduleConfig,
version_num: i64, version_num: i64,
@@ -23,7 +22,7 @@ impl ChannelConfigSnapshot {
version_num: i64, version_num: i64,
) -> Self { ) -> Self {
Self { Self {
id: Uuid::new_v4(), id: SnapshotId::generate(),
channel_id, channel_id,
config, config,
version_num, version_num,
@@ -33,7 +32,7 @@ impl ChannelConfigSnapshot {
} }
pub fn from_persistence( pub fn from_persistence(
id: Uuid, id: SnapshotId,
channel_id: ChannelId, channel_id: ChannelId,
config: ScheduleConfig, config: ScheduleConfig,
version_num: i64, version_num: i64,
@@ -50,7 +49,7 @@ impl ChannelConfigSnapshot {
} }
} }
pub fn id(&self) -> Uuid { pub fn id(&self) -> SnapshotId {
self.id self.id
} }

View File

@@ -23,6 +23,26 @@ pub struct LibraryItem {
synced_at: String, 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<String>,
pub season_number: Option<u32>,
pub episode_number: Option<u32>,
pub year: Option<u16>,
pub genres: Vec<String>,
pub tags: Vec<String>,
pub collection_id: Option<String>,
pub collection_name: Option<String>,
pub collection_type: Option<String>,
pub thumbnail_url: Option<String>,
pub synced_at: String,
}
impl LibraryItem { impl LibraryItem {
pub fn new( pub fn new(
provider_id: impl Into<String>, provider_id: impl Into<String>,
@@ -56,43 +76,25 @@ impl LibraryItem {
} }
} }
pub fn from_persistence( pub fn from_persistence(row: LibraryItemRow) -> Self {
id: String,
provider_id: String,
external_id: String,
title: String,
content_type: ContentType,
duration_secs: u32,
series_name: Option<String>,
season_number: Option<u32>,
episode_number: Option<u32>,
year: Option<u16>,
genres: Vec<String>,
tags: Vec<String>,
collection_id: Option<String>,
collection_name: Option<String>,
collection_type: Option<String>,
thumbnail_url: Option<String>,
synced_at: String,
) -> Self {
Self { Self {
id, id: row.id,
provider_id, provider_id: row.provider_id,
external_id, external_id: row.external_id,
title, title: row.title,
content_type, content_type: row.content_type,
duration_secs, duration_secs: row.duration_secs,
series_name, series_name: row.series_name,
season_number, season_number: row.season_number,
episode_number, episode_number: row.episode_number,
year, year: row.year,
genres, genres: row.genres,
tags, tags: row.tags,
collection_id, collection_id: row.collection_id,
collection_name, collection_name: row.collection_name,
collection_type, collection_type: row.collection_type,
thumbnail_url, thumbnail_url: row.thumbnail_url,
synced_at, synced_at: row.synced_at,
} }
} }

View File

@@ -1,8 +1,7 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; 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)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MediaItem { pub struct MediaItem {
@@ -21,6 +20,22 @@ pub struct MediaItem {
collection_id: Option<String>, collection_id: Option<String>,
} }
pub struct MediaItemRow {
pub id: MediaItemId,
pub title: String,
pub content_type: ContentType,
pub duration_secs: u32,
pub description: Option<String>,
pub genres: Vec<String>,
pub year: Option<u16>,
pub tags: Vec<String>,
pub series_name: Option<String>,
pub season_number: Option<u32>,
pub episode_number: Option<u32>,
pub thumbnail_url: Option<String>,
pub collection_id: Option<String>,
}
impl MediaItem { impl MediaItem {
pub fn new( pub fn new(
id: MediaItemId, id: MediaItemId,
@@ -45,35 +60,21 @@ impl MediaItem {
} }
} }
pub fn from_persistence( pub fn from_persistence(row: MediaItemRow) -> Self {
id: MediaItemId,
title: String,
content_type: ContentType,
duration_secs: u32,
description: Option<String>,
genres: Vec<String>,
year: Option<u16>,
tags: Vec<String>,
series_name: Option<String>,
season_number: Option<u32>,
episode_number: Option<u32>,
thumbnail_url: Option<String>,
collection_id: Option<String>,
) -> Self {
Self { Self {
id, id: row.id,
title, title: row.title,
content_type, content_type: row.content_type,
duration_secs, duration_secs: row.duration_secs,
description, description: row.description,
genres, genres: row.genres,
year, year: row.year,
tags, tags: row.tags,
series_name, series_name: row.series_name,
season_number, season_number: row.season_number,
episode_number, episode_number: row.episode_number,
thumbnail_url, thumbnail_url: row.thumbnail_url,
collection_id, collection_id: row.collection_id,
} }
} }
@@ -132,7 +133,7 @@ impl MediaItem {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlaybackRecord { pub struct PlaybackRecord {
id: Uuid, id: PlaybackRecordId,
channel_id: ChannelId, channel_id: ChannelId,
item_id: MediaItemId, item_id: MediaItemId,
played_at: DateTime<Utc>, played_at: DateTime<Utc>,
@@ -142,7 +143,7 @@ pub struct PlaybackRecord {
impl PlaybackRecord { impl PlaybackRecord {
pub fn new(channel_id: ChannelId, item_id: MediaItemId, generation: u32) -> Self { pub fn new(channel_id: ChannelId, item_id: MediaItemId, generation: u32) -> Self {
Self { Self {
id: Uuid::new_v4(), id: PlaybackRecordId::generate(),
channel_id, channel_id,
item_id, item_id,
played_at: Utc::now(), played_at: Utc::now(),
@@ -151,7 +152,7 @@ impl PlaybackRecord {
} }
pub fn from_persistence( pub fn from_persistence(
id: Uuid, id: PlaybackRecordId,
channel_id: ChannelId, channel_id: ChannelId,
item_id: MediaItemId, item_id: MediaItemId,
played_at: DateTime<Utc>, played_at: DateTime<Utc>,
@@ -166,7 +167,7 @@ impl PlaybackRecord {
} }
} }
pub fn id(&self) -> Uuid { pub fn id(&self) -> PlaybackRecordId {
self.id self.id
} }

View File

@@ -10,16 +10,16 @@ mod user;
pub use activity::ActivityEvent; pub use activity::ActivityEvent;
pub use channel::{ pub use channel::{
BlockContent, Channel, OldScheduleConfig, ProgrammingBlock, ScheduleConfig, BlockContent, Channel, ChannelRow, OldScheduleConfig, ProgrammingBlock, ScheduleConfig,
ScheduleConfigCompat, ScheduleConfigCompat,
}; };
pub use collections::{PageParams, Paginated}; pub use collections::{PageParams, Paginated};
pub use config_snapshot::ChannelConfigSnapshot; pub use config_snapshot::ChannelConfigSnapshot;
pub use library::{ pub use library::{
LibraryCollection, LibraryItem, LibrarySyncLogEntry, LibrarySyncResult, SeasonSummary, LibraryCollection, LibraryItem, LibraryItemRow, LibrarySyncLogEntry, LibrarySyncResult,
ShowSummary, SeasonSummary, ShowSummary,
}; };
pub use media::{MediaItem, PlaybackRecord}; pub use media::{MediaItem, MediaItemRow, PlaybackRecord};
pub use provider_config::ProviderConfigRow; pub use provider_config::ProviderConfigRow;
pub use schedule::{CurrentBroadcast, GeneratedSchedule, ScheduledSlot}; pub use schedule::{CurrentBroadcast, GeneratedSchedule, ScheduledSlot};
pub use user::User; pub use user::User;

View File

@@ -1,4 +1,5 @@
use super::*; use super::*;
use crate::value_objects::ActivityEventId;
#[test] #[test]
fn new_generates_id_and_timestamp() { fn new_generates_id_and_timestamp() {
@@ -10,15 +11,15 @@ fn new_generates_id_and_timestamp() {
#[test] #[test]
fn new_with_channel_id() { 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)); let event = ActivityEvent::new("schedule_generated", "Gen #5", Some(ch_id));
assert_eq!(event.channel_id(), Some(ch_id)); assert_eq!(event.channel_id(), Some(ch_id));
} }
#[test] #[test]
fn from_persistence_round_trip() { fn from_persistence_round_trip() {
let id = Uuid::new_v4(); let id = ActivityEventId::generate();
let ch_id = Uuid::new_v4(); let ch_id = ChannelId::generate();
let now = Utc::now(); let now = Utc::now();
let event = ActivityEvent::from_persistence( let event = ActivityEvent::from_persistence(
id, id,

View File

@@ -1,4 +1,5 @@
use super::*; use super::*;
use crate::value_objects::SnapshotId;
#[test] #[test]
fn new_generates_id_and_timestamp() { fn new_generates_id_and_timestamp() {
@@ -11,7 +12,7 @@ fn new_generates_id_and_timestamp() {
#[test] #[test]
fn from_persistence_round_trip() { fn from_persistence_round_trip() {
let id = Uuid::new_v4(); let id = SnapshotId::generate();
let ch_id = ChannelId::generate(); let ch_id = ChannelId::generate();
let now = Utc::now(); let now = Utc::now();
let snap = ChannelConfigSnapshot::from_persistence( let snap = ChannelConfigSnapshot::from_persistence(

View File

@@ -21,25 +21,25 @@ fn library_item_new_defaults_optional_fields() {
#[test] #[test]
fn library_item_from_persistence_all_fields() { fn library_item_from_persistence_all_fields() {
let item = LibraryItem::from_persistence( let item = LibraryItem::from_persistence(LibraryItemRow {
"jf::abc".into(), id: "jf::abc".into(),
"jf".into(), provider_id: "jf".into(),
"abc".into(), external_id: "abc".into(),
"Breaking Bad S01E01".into(), title: "Breaking Bad S01E01".into(),
ContentType::Episode, content_type: ContentType::Episode,
2700, duration_secs: 2700,
Some("Breaking Bad".into()), series_name: Some("Breaking Bad".into()),
Some(1), season_number: Some(1),
Some(1), episode_number: Some(1),
Some(2008), year: Some(2008),
vec!["Drama".into()], genres: vec!["Drama".into()],
vec!["tv".into()], tags: vec!["tv".into()],
Some("col-1".into()), collection_id: Some("col-1".into()),
Some("TV Shows".into()), collection_name: Some("TV Shows".into()),
Some("tvshows".into()), collection_type: Some("tvshows".into()),
Some("http://thumb.jpg".into()), thumbnail_url: Some("http://thumb.jpg".into()),
"2026-03-19T00:00:00Z".into(), synced_at: "2026-03-19T00:00:00Z".into(),
); });
assert_eq!(item.series_name(), Some("Breaking Bad")); assert_eq!(item.series_name(), Some("Breaking Bad"));
assert_eq!(item.season_number(), Some(1)); assert_eq!(item.season_number(), Some(1));
assert_eq!(item.year(), Some(2008)); assert_eq!(item.year(), Some(2008));

View File

@@ -1,4 +1,5 @@
use super::*; use super::*;
use crate::value_objects::PlaybackRecordId;
#[test] #[test]
fn media_item_new_defaults() { fn media_item_new_defaults() {
@@ -18,21 +19,21 @@ fn media_item_new_defaults() {
#[test] #[test]
fn media_item_from_persistence_round_trip() { fn media_item_from_persistence_round_trip() {
let item = MediaItem::from_persistence( let item = MediaItem::from_persistence(MediaItemRow {
MediaItemId::new("jf::abc"), id: MediaItemId::new("jf::abc"),
"Breaking Bad S01E01".into(), title: "Breaking Bad S01E01".into(),
ContentType::Episode, content_type: ContentType::Episode,
2700, duration_secs: 2700,
Some("Pilot episode".into()), description: Some("Pilot episode".into()),
vec!["Drama".into()], genres: vec!["Drama".into()],
Some(2008), year: Some(2008),
vec!["tv".into()], tags: vec!["tv".into()],
Some("Breaking Bad".into()), series_name: Some("Breaking Bad".into()),
Some(1), season_number: Some(1),
Some(1), episode_number: Some(1),
Some("http://thumb.jpg".into()), thumbnail_url: Some("http://thumb.jpg".into()),
Some("col-1".into()), collection_id: Some("col-1".into()),
); });
assert_eq!(item.title(), "Breaking Bad S01E01"); assert_eq!(item.title(), "Breaking Bad S01E01");
assert_eq!(item.series_name(), Some("Breaking Bad")); assert_eq!(item.series_name(), Some("Breaking Bad"));
assert_eq!(item.season_number(), Some(1)); assert_eq!(item.season_number(), Some(1));
@@ -53,7 +54,7 @@ fn playback_record_new() {
#[test] #[test]
fn playback_record_from_persistence() { fn playback_record_from_persistence() {
let id = Uuid::new_v4(); let id = PlaybackRecordId::generate();
let ch_id = ChannelId::generate(); let ch_id = ChannelId::generate();
let item_id = MediaItemId::new("test::2"); let item_id = MediaItemId::new("test::2");
let now = Utc::now(); let now = Utc::now();

View File

@@ -1,9 +1,8 @@
use async_trait::async_trait; use async_trait::async_trait;
use uuid::Uuid;
use crate::errors::DomainResult; use crate::errors::DomainResult;
use crate::models::{Channel, ChannelConfigSnapshot, ScheduleConfig}; use crate::models::{Channel, ChannelConfigSnapshot, ScheduleConfig};
use crate::value_objects::{ChannelId, UserId}; use crate::value_objects::{ChannelId, SnapshotId, UserId};
#[async_trait] #[async_trait]
pub trait ChannelCommand: Send + Sync { pub trait ChannelCommand: Send + Sync {
@@ -21,7 +20,7 @@ pub trait ChannelCommand: Send + Sync {
async fn patch_config_snapshot_label( async fn patch_config_snapshot_label(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
snapshot_id: Uuid, snapshot_id: SnapshotId,
label: Option<String>, label: Option<String>,
) -> DomainResult<Option<ChannelConfigSnapshot>>; ) -> DomainResult<Option<ChannelConfigSnapshot>>;
} }
@@ -44,6 +43,6 @@ pub trait ChannelQuery: Send + Sync {
async fn get_config_snapshot( async fn get_config_snapshot(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
snapshot_id: Uuid, snapshot_id: SnapshotId,
) -> DomainResult<Option<ChannelConfigSnapshot>>; ) -> DomainResult<Option<ChannelConfigSnapshot>>;
} }

View File

@@ -21,6 +21,15 @@ struct BlockTimeWindow {
end: DateTime<Utc>, end: DateTime<Utc>,
} }
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> { struct RecycleContext<'a> {
history: &'a [PlaybackRecord], history: &'a [PlaybackRecord],
policy: &'a RecyclePolicy, policy: &'a RecyclePolicy,
@@ -59,7 +68,7 @@ impl ScheduleEngineService {
.channel_query .channel_query
.find_by_id(channel_id) .find_by_id(channel_id)
.await? .await?
.ok_or(DomainError::ChannelNotFound(channel_id.value()))?; .ok_or(DomainError::ChannelNotFound(channel_id))?;
let tz: Tz = channel let tz: Tz = channel
.timezone() .timezone()
@@ -259,14 +268,16 @@ impl ScheduleEngineService {
provider_id, provider_id,
} => { } => {
self.resolve_algorithmic( self.resolve_algorithmic(
provider_id, AlgorithmicParams {
filter, provider_id,
strategy, filter,
strategy,
block_id: block.id(),
loop_on_finish: block.loop_on_finish(),
ignore_recycle_policy: block.ignore_recycle_policy(),
},
window, window,
recycle, recycle,
block.id(),
block.loop_on_finish(),
block.ignore_recycle_policy(),
) )
.await .await
} }
@@ -300,25 +311,20 @@ impl ScheduleEngineService {
async fn resolve_algorithmic( async fn resolve_algorithmic(
&self, &self,
provider_id: &str, params: AlgorithmicParams<'_>,
filter: &MediaFilter,
strategy: &FillStrategy,
window: BlockTimeWindow, window: BlockTimeWindow,
recycle: RecycleContext<'_>, recycle: RecycleContext<'_>,
block_id: BlockId,
loop_on_finish: bool,
ignore_recycle_policy: bool,
) -> DomainResult<Vec<ScheduledSlot>> { ) -> DomainResult<Vec<ScheduledSlot>> {
let candidates = self let candidates = self
.provider_registry .provider_registry
.fetch_items(provider_id, filter) .fetch_items(params.provider_id, params.filter)
.await?; .await?;
if candidates.is_empty() { if candidates.is_empty() {
return Ok(vec![]); return Ok(vec![]);
} }
let pool = if ignore_recycle_policy { let pool = if params.ignore_recycle_policy {
candidates.clone() candidates.clone()
} else { } else {
recycle::apply_recycle_policy(&candidates, recycle.history, recycle.policy, recycle.generation) recycle::apply_recycle_policy(&candidates, recycle.history, recycle.policy, recycle.generation)
@@ -328,9 +334,9 @@ impl ScheduleEngineService {
&candidates, &candidates,
&pool, &pool,
target_secs, target_secs,
strategy, params.strategy,
recycle.last_item_id, recycle.last_item_id,
loop_on_finish, params.loop_on_finish,
); );
let mut slots = Vec::new(); let mut slots = Vec::new();
@@ -342,7 +348,7 @@ impl ScheduleEngineService {
} }
let item_end = let item_end =
(cursor + Duration::seconds(item.duration_secs() as i64)).min(window.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; cursor = item_end;
} }

View File

@@ -4,7 +4,6 @@ use std::sync::Mutex;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use uuid::Uuid;
use crate::errors::DomainResult; use crate::errors::DomainResult;
use crate::models::{ use crate::models::{
@@ -18,11 +17,12 @@ use crate::ports::{
ScheduleQuery, TranscodeSettingsRepository, UserCommand, UserQuery, ScheduleQuery, TranscodeSettingsRepository, UserCommand, UserQuery,
}; };
use crate::value_objects::{ use crate::value_objects::{
BlockId, ChannelId, ContentType, LibrarySearchFilter, MediaItemId, ScheduleId, UserId, BlockId, ChannelId, ContentType, LibrarySearchFilter, MediaItemId, ScheduleId, SnapshotId,
UserId,
}; };
pub struct InMemoryUserRepository { pub struct InMemoryUserRepository {
pub store: Mutex<HashMap<Uuid, crate::models::User>>, pub store: Mutex<HashMap<UserId, crate::models::User>>,
} }
impl InMemoryUserRepository { impl InMemoryUserRepository {
@@ -45,12 +45,12 @@ impl UserCommand for InMemoryUserRepository {
self.store self.store
.lock() .lock()
.unwrap() .unwrap()
.insert(user.id().value(), user.clone()); .insert(user.id(), user.clone());
Ok(()) Ok(())
} }
async fn delete(&self, id: UserId) -> DomainResult<()> { async fn delete(&self, id: UserId) -> DomainResult<()> {
self.store.lock().unwrap().remove(&id.value()); self.store.lock().unwrap().remove(&id);
Ok(()) Ok(())
} }
} }
@@ -58,7 +58,7 @@ impl UserCommand for InMemoryUserRepository {
#[async_trait] #[async_trait]
impl UserQuery for InMemoryUserRepository { impl UserQuery for InMemoryUserRepository {
async fn find_by_id(&self, id: UserId) -> DomainResult<Option<crate::models::User>> { async fn find_by_id(&self, id: UserId) -> DomainResult<Option<crate::models::User>> {
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<Option<crate::models::User>> { async fn find_by_subject(&self, subject: &str) -> DomainResult<Option<crate::models::User>> {
@@ -80,7 +80,7 @@ impl UserQuery for InMemoryUserRepository {
} }
pub struct InMemoryChannelRepository { pub struct InMemoryChannelRepository {
pub channels: Mutex<HashMap<Uuid, Channel>>, pub channels: Mutex<HashMap<ChannelId, Channel>>,
pub snapshots: Mutex<Vec<ChannelConfigSnapshot>>, pub snapshots: Mutex<Vec<ChannelConfigSnapshot>>,
} }
@@ -105,12 +105,12 @@ impl ChannelCommand for InMemoryChannelRepository {
self.channels self.channels
.lock() .lock()
.unwrap() .unwrap()
.insert(channel.id().value(), channel.clone()); .insert(channel.id(), channel.clone());
Ok(()) Ok(())
} }
async fn delete(&self, id: ChannelId) -> DomainResult<()> { async fn delete(&self, id: ChannelId) -> DomainResult<()> {
self.channels.lock().unwrap().remove(&id.value()); self.channels.lock().unwrap().remove(&id);
Ok(()) Ok(())
} }
@@ -148,7 +148,7 @@ impl ChannelCommand for InMemoryChannelRepository {
async fn patch_config_snapshot_label( async fn patch_config_snapshot_label(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
snapshot_id: Uuid, snapshot_id: SnapshotId,
label: Option<String>, label: Option<String>,
) -> DomainResult<Option<ChannelConfigSnapshot>> { ) -> DomainResult<Option<ChannelConfigSnapshot>> {
let mut snaps = self.snapshots.lock().unwrap(); let mut snaps = self.snapshots.lock().unwrap();
@@ -176,7 +176,7 @@ impl ChannelCommand for InMemoryChannelRepository {
#[async_trait] #[async_trait]
impl ChannelQuery for InMemoryChannelRepository { impl ChannelQuery for InMemoryChannelRepository {
async fn find_by_id(&self, id: ChannelId) -> DomainResult<Option<Channel>> { async fn find_by_id(&self, id: ChannelId) -> DomainResult<Option<Channel>> {
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<Vec<Channel>> { async fn find_by_owner(&self, owner_id: UserId) -> DomainResult<Vec<Channel>> {
@@ -218,7 +218,7 @@ impl ChannelQuery for InMemoryChannelRepository {
async fn get_config_snapshot( async fn get_config_snapshot(
&self, &self,
channel_id: ChannelId, channel_id: ChannelId,
snapshot_id: Uuid, snapshot_id: SnapshotId,
) -> DomainResult<Option<ChannelConfigSnapshot>> { ) -> DomainResult<Option<ChannelConfigSnapshot>> {
let snaps = self.snapshots.lock().unwrap(); let snaps = self.snapshots.lock().unwrap();
Ok(snaps Ok(snaps
@@ -229,7 +229,7 @@ impl ChannelQuery for InMemoryChannelRepository {
} }
pub struct InMemoryScheduleRepository { pub struct InMemoryScheduleRepository {
pub schedules: Mutex<HashMap<Uuid, GeneratedSchedule>>, pub schedules: Mutex<HashMap<ScheduleId, GeneratedSchedule>>,
pub playback_records: Mutex<Vec<PlaybackRecord>>, pub playback_records: Mutex<Vec<PlaybackRecord>>,
} }
@@ -254,7 +254,7 @@ impl ScheduleCommand for InMemoryScheduleRepository {
self.schedules self.schedules
.lock() .lock()
.unwrap() .unwrap()
.insert(schedule.id().value(), schedule.clone()); .insert(schedule.id(), schedule.clone());
Ok(()) Ok(())
} }
@@ -644,11 +644,7 @@ impl ActivityLogCommand for InMemoryActivityLog {
detail: &str, detail: &str,
channel_id: Option<ChannelId>, channel_id: Option<ChannelId>,
) -> DomainResult<()> { ) -> DomainResult<()> {
let event = ActivityEvent::new( let event = ActivityEvent::new(event_type, detail, channel_id);
event_type,
detail,
channel_id.map(|c| c.value()),
);
self.events.lock().unwrap().push(event); self.events.lock().unwrap().push(event);
Ok(()) Ok(())
} }

View File

@@ -45,6 +45,9 @@ uuid_id!(ChannelId);
uuid_id!(SlotId); uuid_id!(SlotId);
uuid_id!(BlockId); uuid_id!(BlockId);
uuid_id!(ScheduleId); uuid_id!(ScheduleId);
uuid_id!(SnapshotId);
uuid_id!(ActivityEventId);
uuid_id!(PlaybackRecordId);
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct MediaItemId(String); pub struct MediaItemId(String);

View File

@@ -12,7 +12,9 @@ pub async fn list_channels(
query_deps: &Arc<ChannelQueryDeps>, query_deps: &Arc<ChannelQueryDeps>,
owner_id: Uuid, owner_id: Uuid,
) -> String { ) -> 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 { match application::channels::list_by_owner::execute(query_deps, query).await {
Ok(channels) => ok_json(&channels), Ok(channels) => ok_json(&channels),
Err(e) => domain_err(e), Err(e) => domain_err(e),
@@ -20,7 +22,9 @@ pub async fn list_channels(
} }
pub async fn get_channel(query_deps: &Arc<ChannelQueryDeps>, id: Uuid) -> String { pub async fn get_channel(query_deps: &Arc<ChannelQueryDeps>, 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 { match application::channels::get::execute(query_deps, query).await {
Ok(Some(channel)) => ok_json(&channel), Ok(Some(channel)) => ok_json(&channel),
Ok(None) => serde_json::json!({"error": "Channel not found"}).to_string(), Ok(None) => serde_json::json!({"error": "Channel not found"}).to_string(),
@@ -35,7 +39,7 @@ pub async fn create_channel(
timezone: &str, timezone: &str,
) -> String { ) -> String {
let cmd = CreateChannelCommand { let cmd = CreateChannelCommand {
owner_id, owner_id: owner_id.into(),
name: name.to_string(), name: name.to_string(),
timezone: timezone.to_string(), timezone: timezone.to_string(),
}; };
@@ -55,8 +59,8 @@ pub async fn update_channel(
schedule_config: Option<domain::ScheduleConfig>, schedule_config: Option<domain::ScheduleConfig>,
) -> String { ) -> String {
let cmd = UpdateChannelCommand { let cmd = UpdateChannelCommand {
channel_id, channel_id: channel_id.into(),
owner_id, owner_id: owner_id.into(),
name, name,
description: description.map(Some), description: description.map(Some),
timezone, timezone,
@@ -76,8 +80,8 @@ pub async fn delete_channel(
owner_id: Uuid, owner_id: Uuid,
) -> String { ) -> String {
let cmd = DeleteChannelCommand { let cmd = DeleteChannelCommand {
channel_id, channel_id: channel_id.into(),
owner_id, owner_id: owner_id.into(),
}; };
match application::channels::delete::execute(cmd_deps, cmd).await { match application::channels::delete::execute(cmd_deps, cmd).await {
Ok(()) => serde_json::json!({"deleted": channel_id}).to_string(), Ok(()) => serde_json::json!({"deleted": channel_id}).to_string(),

View File

@@ -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 id = format!("{}::{}", provider_id, external_id);
let now = chrono::Utc::now().to_rfc3339(); let now = chrono::Utc::now().to_rfc3339();
domain::LibraryItem::from_persistence( domain::LibraryItem::from_persistence(domain::LibraryItemRow {
id, id,
provider_id.to_string(), provider_id: provider_id.to_string(),
external_id, external_id,
item.title().to_string(), title: item.title().to_string(),
item.content_type().clone(), content_type: item.content_type().clone(),
item.duration_secs(), duration_secs: item.duration_secs(),
item.series_name().map(|s| s.to_string()), series_name: item.series_name().map(|s| s.to_string()),
item.season_number(), season_number: item.season_number(),
item.episode_number(), episode_number: item.episode_number(),
item.year(), year: item.year(),
item.genres().to_vec(), genres: item.genres().to_vec(),
item.tags().to_vec(), tags: item.tags().to_vec(),
item.collection_id().map(|s| s.to_string()), collection_id: item.collection_id().map(|s| s.to_string()),
None, collection_name: None,
None, collection_type: None,
item.thumbnail_url().map(|s| s.to_string()), thumbnail_url: item.thumbnail_url().map(|s| s.to_string()),
now, synced_at: now,
) })
} }
struct SimpleSyncAdapter { struct SimpleSyncAdapter {

View File

@@ -32,7 +32,7 @@ pub async fn list_my_channels(
CurrentUser(user): CurrentUser, CurrentUser(user): CurrentUser,
) -> Result<Json<Vec<ChannelResponse>>, ApiError> { ) -> Result<Json<Vec<ChannelResponse>>, ApiError> {
let query = ListByOwnerQuery { let query = ListByOwnerQuery {
owner_id: user.id().value(), owner_id: user.id(),
}; };
let channels = let channels =
application::channels::list_by_owner::execute(&state.channel_query_deps, query).await?; application::channels::list_by_owner::execute(&state.channel_query_deps, query).await?;
@@ -45,7 +45,7 @@ pub async fn create_channel(
Json(req): Json<CreateChannelRequest>, Json(req): Json<CreateChannelRequest>,
) -> Result<Json<ChannelResponse>, ApiError> { ) -> Result<Json<ChannelResponse>, ApiError> {
let cmd = CreateChannelCommand { let cmd = CreateChannelCommand {
owner_id: user.id().value(), owner_id: user.id(),
name: req.name, name: req.name,
timezone: req.timezone, timezone: req.timezone,
}; };
@@ -58,7 +58,9 @@ pub async fn get_channel(
CurrentUser(_user): CurrentUser, CurrentUser(_user): CurrentUser,
Path(id): Path<uuid::Uuid>, Path(id): Path<uuid::Uuid>,
) -> Result<Json<ChannelResponse>, ApiError> { ) -> Result<Json<ChannelResponse>, 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) let channel = application::channels::get::execute(&state.channel_query_deps, query)
.await? .await?
.ok_or_else(|| ApiError::not_found(format!("Channel {id} not found")))?; .ok_or_else(|| ApiError::not_found(format!("Channel {id} not found")))?;
@@ -88,8 +90,8 @@ pub async fn update_channel(
.transpose()?; .transpose()?;
let cmd = UpdateChannelCommand { let cmd = UpdateChannelCommand {
channel_id: id, channel_id: id.into(),
owner_id: user.id().value(), owner_id: user.id(),
name: req.name, name: req.name,
description: req.description.map(Some), description: req.description.map(Some),
timezone: req.timezone, timezone: req.timezone,
@@ -107,8 +109,8 @@ pub async fn delete_channel(
Path(id): Path<uuid::Uuid>, Path(id): Path<uuid::Uuid>,
) -> Result<axum::http::StatusCode, ApiError> { ) -> Result<axum::http::StatusCode, ApiError> {
let cmd = DeleteChannelCommand { let cmd = DeleteChannelCommand {
channel_id: id, channel_id: id.into(),
owner_id: user.id().value(), owner_id: user.id(),
}; };
application::channels::delete::execute(&state.channel_command_deps, cmd).await?; application::channels::delete::execute(&state.channel_command_deps, cmd).await?;
Ok(axum::http::StatusCode::NO_CONTENT) Ok(axum::http::StatusCode::NO_CONTENT)
@@ -120,7 +122,7 @@ pub async fn save_snapshot(
Path(id): Path<uuid::Uuid>, Path(id): Path<uuid::Uuid>,
) -> Result<Json<ConfigSnapshotResponse>, ApiError> { ) -> Result<Json<ConfigSnapshotResponse>, ApiError> {
let cmd = SaveSnapshotCommand { let cmd = SaveSnapshotCommand {
channel_id: id, channel_id: id.into(),
label: None, label: None,
}; };
let snap = application::config_snapshots::save::execute(&state.config_snapshot_deps, cmd).await?; 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, CurrentUser(_user): CurrentUser,
Path(id): Path<uuid::Uuid>, Path(id): Path<uuid::Uuid>,
) -> Result<Json<Vec<ConfigSnapshotResponse>>, ApiError> { ) -> Result<Json<Vec<ConfigSnapshotResponse>>, 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?; let snaps = application::config_snapshots::list::execute(&state.config_snapshot_deps, query).await?;
Ok(Json(snaps.into_iter().map(ConfigSnapshotResponse::from).collect())) 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)>, Path((id, snapshot_id)): Path<(uuid::Uuid, uuid::Uuid)>,
) -> Result<Json<ConfigSnapshotResponse>, ApiError> { ) -> Result<Json<ConfigSnapshotResponse>, ApiError> {
let query = GetSnapshotQuery { let query = GetSnapshotQuery {
channel_id: id, channel_id: id.into(),
snapshot_id, snapshot_id: snapshot_id.into(),
}; };
let snap = application::config_snapshots::get::execute(&state.config_snapshot_deps, query) let snap = application::config_snapshots::get::execute(&state.config_snapshot_deps, query)
.await? .await?
@@ -159,8 +163,8 @@ pub async fn patch_snapshot(
Json(req): Json<PatchSnapshotRequest>, Json(req): Json<PatchSnapshotRequest>,
) -> Result<Json<ConfigSnapshotResponse>, ApiError> { ) -> Result<Json<ConfigSnapshotResponse>, ApiError> {
let cmd = PatchLabelCommand { let cmd = PatchLabelCommand {
channel_id: id, channel_id: id.into(),
snapshot_id, snapshot_id: snapshot_id.into(),
label: req.label, label: req.label,
}; };
let snap = let snap =
@@ -176,8 +180,8 @@ pub async fn restore_snapshot(
Path((id, snapshot_id)): Path<(uuid::Uuid, uuid::Uuid)>, Path((id, snapshot_id)): Path<(uuid::Uuid, uuid::Uuid)>,
) -> Result<Json<ChannelResponse>, ApiError> { ) -> Result<Json<ChannelResponse>, ApiError> {
let cmd = RestoreSnapshotCommand { let cmd = RestoreSnapshotCommand {
channel_id: id, channel_id: id.into(),
snapshot_id, snapshot_id: snapshot_id.into(),
}; };
let channel = let channel =
application::config_snapshots::restore::execute(&state.config_snapshot_deps, cmd).await?; application::config_snapshots::restore::execute(&state.config_snapshot_deps, cmd).await?;

View File

@@ -45,7 +45,7 @@ pub async fn get_current_broadcast(
match application::schedule::get_current_broadcast::execute(&state.schedule_deps, query).await? match application::schedule::get_current_broadcast::execute(&state.schedule_deps, query).await?
{ {
Some(broadcast) => { 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 channel = application::channels::get::execute(&state.channel_query_deps, channel_query).await?;
let slot_response = match &channel { let slot_response = match &channel {