refactor(domain): Row structs for from_persistence, ID newtypes, kill clippy.toml

- delete clippy.toml (too-many-arguments-threshold=20 hack)
- ChannelRow/MediaItemRow/LibraryItemRow structs for from_persistence
- SnapshotId/ActivityEventId/PlaybackRecordId newtypes
- DomainError variants use ChannelId/UserId instead of Uuid
- ActivityEvent.channel_id: Option<ChannelId> not Option<Uuid>
- InMemory repos key on newtype IDs
- AlgorithmicParams struct for schedule engine
- update all adapters/application/presentation callers
This commit is contained in:
2026-07-12 05:00:49 +02:00
parent 031cba5cfb
commit c0e685a4ee
65 changed files with 684 additions and 695 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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