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:
@@ -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,
|
||||||
))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
too-many-arguments-threshold = 20
|
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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?;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user