refactor(application): strip comments, DRY ownership check + parse_content_type

This commit is contained in:
2026-07-12 04:10:58 +02:00
parent 98a54245b1
commit 9dcd169689
62 changed files with 56 additions and 203 deletions

View File

@@ -4,7 +4,6 @@ use domain::DomainResult;
use super::deps::AdminDeps; use super::deps::AdminDeps;
use super::queries::GetActivityLogQuery; use super::queries::GetActivityLogQuery;
/// Get recent activity log entries.
pub async fn execute( pub async fn execute(
deps: &AdminDeps, deps: &AdminDeps,
query: GetActivityLogQuery, query: GetActivityLogQuery,

View File

@@ -1,4 +1,3 @@
/// Update one or more admin settings (key-value pairs).
pub struct UpdateSettingsCommand { pub struct UpdateSettingsCommand {
pub settings: Vec<(String, String)>, pub settings: Vec<(String, String)>,
} }

View File

@@ -2,7 +2,6 @@ use std::sync::Arc;
use domain::ports::{ActivityLogQuery, AppSettingsRepository}; use domain::ports::{ActivityLogQuery, AppSettingsRepository};
/// Dependencies for admin use cases.
pub struct AdminDeps { pub struct AdminDeps {
pub settings_repo: Arc<dyn AppSettingsRepository>, pub settings_repo: Arc<dyn AppSettingsRepository>,
pub activity_query: Arc<dyn ActivityLogQuery>, pub activity_query: Arc<dyn ActivityLogQuery>,

View File

@@ -3,7 +3,6 @@ use domain::DomainResult;
use super::deps::AdminDeps; use super::deps::AdminDeps;
use super::queries::GetSettingsQuery; use super::queries::GetSettingsQuery;
/// Get all admin settings as key-value pairs.
pub async fn execute( pub async fn execute(
deps: &AdminDeps, deps: &AdminDeps,
_query: GetSettingsQuery, _query: GetSettingsQuery,

View File

@@ -1,7 +1,5 @@
/// Get all admin settings.
pub struct GetSettingsQuery; pub struct GetSettingsQuery;
/// Get recent activity log entries.
pub struct GetActivityLogQuery { pub struct GetActivityLogQuery {
pub limit: u32, pub limit: u32,
} }

View File

@@ -3,9 +3,6 @@ use domain::DomainResult;
use super::commands::UpdateSettingsCommand; use super::commands::UpdateSettingsCommand;
use super::deps::AdminDeps; use super::deps::AdminDeps;
/// Update one or more admin settings.
///
/// Iterates key/value pairs and upserts each one.
pub async fn execute(deps: &AdminDeps, cmd: UpdateSettingsCommand) -> DomainResult<()> { pub async fn execute(deps: &AdminDeps, cmd: UpdateSettingsCommand) -> DomainResult<()> {
for (key, value) in &cmd.settings { for (key, value) in &cmd.settings {
deps.settings_repo.set(key, value).await?; deps.settings_repo.set(key, value).await?;

View File

@@ -1,10 +1,8 @@
/// Register a new local user.
pub struct RegisterCommand { pub struct RegisterCommand {
pub email: String, pub email: String,
pub password: String, pub password: String,
} }
/// Log in with email + password.
pub struct LoginCommand { pub struct LoginCommand {
pub email: String, pub email: String,
pub password: String, pub password: String,

View File

@@ -2,10 +2,6 @@ use std::sync::Arc;
use domain::ports::{AuthService, EventPublisher, UserCommand, UserQuery}; use domain::ports::{AuthService, EventPublisher, UserCommand, UserQuery};
/// Dependencies for auth use cases.
///
/// Aggregates the ports required by register/login operations.
/// Built once at startup and shared via `Arc<AuthDeps>` or passed by reference.
pub struct AuthDeps { pub struct AuthDeps {
pub user_command: Arc<dyn UserCommand>, pub user_command: Arc<dyn UserCommand>,
pub user_query: Arc<dyn UserQuery>, pub user_query: Arc<dyn UserQuery>,

View File

@@ -4,30 +4,24 @@ use domain::{DomainError, DomainResult, Email};
use super::commands::LoginCommand; use super::commands::LoginCommand;
use super::deps::AuthDeps; use super::deps::AuthDeps;
/// Log in with email + password. const INVALID_CREDENTIALS: &str = "Invalid credentials";
///
/// Flow: validate email -> find user -> verify password -> return User.
/// JWT generation belongs in the presentation layer, not here.
pub async fn execute(deps: &AuthDeps, cmd: LoginCommand) -> DomainResult<User> { pub async fn execute(deps: &AuthDeps, cmd: LoginCommand) -> DomainResult<User> {
// Validate email format
let email = Email::new(&cmd.email)?; let email = Email::new(&cmd.email)?;
// Find user
let user = deps let user = deps
.user_query .user_query
.find_by_email(email.as_ref()) .find_by_email(email.as_ref())
.await? .await?
.ok_or_else(|| DomainError::unauthenticated("Invalid credentials"))?; .ok_or_else(|| DomainError::unauthenticated(INVALID_CREDENTIALS))?;
// Must have a password hash (not an OIDC-only user)
let hash = user let hash = user
.password_hash() .password_hash()
.ok_or_else(|| DomainError::unauthenticated("Invalid credentials"))?; .ok_or_else(|| DomainError::unauthenticated(INVALID_CREDENTIALS))?;
// Verify password
let valid = deps.auth_service.verify_password(&cmd.password, hash)?; let valid = deps.auth_service.verify_password(&cmd.password, hash)?;
if !valid { if !valid {
return Err(DomainError::unauthenticated("Invalid credentials")); return Err(DomainError::unauthenticated(INVALID_CREDENTIALS));
} }
Ok(user) Ok(user)

View File

@@ -1 +0,0 @@
// Auth queries (reserved for future use, e.g. GetCurrentUserQuery).

View File

@@ -5,33 +5,23 @@ use domain::{DomainResult, Email, Password};
use super::commands::RegisterCommand; use super::commands::RegisterCommand;
use super::deps::AuthDeps; use super::deps::AuthDeps;
/// Register a new local user.
///
/// Flow: validate email/password -> check duplicate -> hash password ->
/// create User (first user gets admin) -> save -> publish event -> return User.
pub async fn execute(deps: &AuthDeps, cmd: RegisterCommand) -> DomainResult<User> { pub async fn execute(deps: &AuthDeps, cmd: RegisterCommand) -> DomainResult<User> {
// Validate inputs via domain value objects
let email = Email::new(&cmd.email)?; let email = Email::new(&cmd.email)?;
let password = Password::new(&cmd.password)?; let password = Password::new(&cmd.password)?;
// Check for duplicate
if deps.user_query.find_by_email(email.as_ref()).await?.is_some() { if deps.user_query.find_by_email(email.as_ref()).await?.is_some() {
return Err(domain::DomainError::UserAlreadyExists(cmd.email)); return Err(domain::DomainError::UserAlreadyExists(cmd.email));
} }
// Hash password
let hash = deps.auth_service.hash_password(password.as_ref())?; let hash = deps.auth_service.hash_password(password.as_ref())?;
// Create user; first user gets admin
let mut user = User::new_local(email, hash); let mut user = User::new_local(email, hash);
if deps.user_query.count_users().await? == 0 { if deps.user_query.count_users().await? == 0 {
user.promote_to_admin(); user.promote_to_admin();
} }
// Persist
deps.user_command.save(&user).await?; deps.user_command.save(&user).await?;
// Publish event
deps.event_publisher deps.event_publisher
.publish(DomainEvent::UserRegistered { .publish(DomainEvent::UserRegistered {
user_id: user.id(), user_id: user.id(),

View File

@@ -3,20 +3,16 @@ use uuid::Uuid;
use domain::models::ScheduleConfig; use domain::models::ScheduleConfig;
use domain::value_objects::RecyclePolicy; use domain::value_objects::RecyclePolicy;
/// Create a new channel.
pub struct CreateChannelCommand { pub struct CreateChannelCommand {
pub owner_id: Uuid, pub owner_id: Uuid,
pub name: String, pub name: String,
pub timezone: String, pub timezone: String,
} }
/// Update an existing channel (partial — only `Some` fields are applied).
pub struct UpdateChannelCommand { pub struct UpdateChannelCommand {
pub channel_id: Uuid, pub channel_id: Uuid,
/// Used for ownership check.
pub owner_id: Uuid, pub owner_id: Uuid,
pub name: Option<String>, pub name: Option<String>,
/// `Some(None)` clears the description; `None` leaves it unchanged.
pub description: Option<Option<String>>, pub description: Option<Option<String>>,
pub timezone: Option<String>, pub timezone: Option<String>,
pub schedule_config: Option<ScheduleConfig>, pub schedule_config: Option<ScheduleConfig>,
@@ -24,9 +20,7 @@ pub struct UpdateChannelCommand {
pub auto_schedule: Option<bool>, pub auto_schedule: Option<bool>,
} }
/// Delete a channel.
pub struct DeleteChannelCommand { pub struct DeleteChannelCommand {
pub channel_id: Uuid, pub channel_id: Uuid,
/// Used for ownership check.
pub owner_id: Uuid, pub owner_id: Uuid,
} }

View File

@@ -6,9 +6,6 @@ use domain::DomainResult;
use super::commands::CreateChannelCommand; use super::commands::CreateChannelCommand;
use super::deps::ChannelCommandDeps; use super::deps::ChannelCommandDeps;
/// Create a new channel.
///
/// Flow: convert raw IDs -> build Channel -> save -> publish event -> return.
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 owner_id = UserId::from(cmd.owner_id);
let channel = Channel::new(owner_id, cmd.name, cmd.timezone); let channel = Channel::new(owner_id, cmd.name, cmd.timezone);

View File

@@ -1,26 +1,16 @@
use domain::events::DomainEvent; use domain::events::DomainEvent;
use domain::value_objects::{ChannelId, UserId}; use domain::value_objects::{ChannelId, UserId};
use domain::{DomainError, DomainResult}; use domain::DomainResult;
use super::commands::DeleteChannelCommand; use super::commands::DeleteChannelCommand;
use super::deps::ChannelCommandDeps; use super::deps::ChannelCommandDeps;
use super::find_owned_channel;
/// Delete a channel after verifying ownership.
///
/// Flow: find channel -> verify ownership -> delete -> publish event.
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); let channel_id = ChannelId::from(cmd.channel_id);
let owner_id = UserId::from(cmd.owner_id); let owner_id = UserId::from(cmd.owner_id);
let channel = deps find_owned_channel(deps.channel_query.as_ref(), channel_id, owner_id, cmd.channel_id).await?;
.channel_query
.find_by_id(channel_id)
.await?
.ok_or(DomainError::ChannelNotFound(cmd.channel_id))?;
if channel.owner_id() != owner_id {
return Err(DomainError::forbidden("You don't own this channel"));
}
deps.channel_command.delete(channel_id).await?; deps.channel_command.delete(channel_id).await?;

View File

@@ -2,14 +2,12 @@ use std::sync::Arc;
use domain::ports::{ChannelCommand, ChannelQuery, EventPublisher}; use domain::ports::{ChannelCommand, ChannelQuery, EventPublisher};
/// Dependencies for channel write use cases (create, update, delete).
pub struct ChannelCommandDeps { pub struct ChannelCommandDeps {
pub channel_command: Arc<dyn ChannelCommand>, pub channel_command: Arc<dyn ChannelCommand>,
pub channel_query: Arc<dyn ChannelQuery>, pub channel_query: Arc<dyn ChannelQuery>,
pub event_publisher: Arc<dyn EventPublisher>, pub event_publisher: Arc<dyn EventPublisher>,
} }
/// Dependencies for channel read use cases (get, list, list_by_owner).
pub struct ChannelQueryDeps { pub struct ChannelQueryDeps {
pub channel_query: Arc<dyn ChannelQuery>, pub channel_query: Arc<dyn ChannelQuery>,
} }

View File

@@ -5,7 +5,6 @@ use domain::DomainResult;
use super::deps::ChannelQueryDeps; use super::deps::ChannelQueryDeps;
use super::queries::GetChannelQuery; use super::queries::GetChannelQuery;
/// Get a single channel by ID.
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); let channel_id = ChannelId::from(query.channel_id);
deps.channel_query.find_by_id(channel_id).await deps.channel_query.find_by_id(channel_id).await

View File

@@ -4,7 +4,6 @@ use domain::DomainResult;
use super::deps::ChannelQueryDeps; use super::deps::ChannelQueryDeps;
use super::queries::ListChannelsQuery; use super::queries::ListChannelsQuery;
/// List all channels.
pub async fn execute(deps: &ChannelQueryDeps, _query: ListChannelsQuery) -> DomainResult<Vec<Channel>> { pub async fn execute(deps: &ChannelQueryDeps, _query: ListChannelsQuery) -> DomainResult<Vec<Channel>> {
deps.channel_query.find_all().await deps.channel_query.find_all().await
} }

View File

@@ -5,7 +5,6 @@ use domain::DomainResult;
use super::deps::ChannelQueryDeps; use super::deps::ChannelQueryDeps;
use super::queries::ListByOwnerQuery; use super::queries::ListByOwnerQuery;
/// List channels belonging to a specific owner.
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); let owner_id = UserId::from(query.owner_id);
deps.channel_query.find_by_owner(owner_id).await deps.channel_query.find_by_owner(owner_id).await

View File

@@ -11,3 +11,27 @@ pub mod update;
pub use commands::{CreateChannelCommand, DeleteChannelCommand, UpdateChannelCommand}; pub use commands::{CreateChannelCommand, DeleteChannelCommand, UpdateChannelCommand};
pub use deps::{ChannelCommandDeps, ChannelQueryDeps}; pub use deps::{ChannelCommandDeps, ChannelQueryDeps};
pub use queries::{GetChannelQuery, ListByOwnerQuery, ListChannelsQuery}; pub use queries::{GetChannelQuery, ListByOwnerQuery, ListChannelsQuery};
use domain::models::Channel;
use domain::value_objects::{ChannelId, UserId};
use domain::{DomainError, DomainResult};
const OWNERSHIP_DENIED: &str = "You don't own this channel";
pub(crate) async fn find_owned_channel(
query: &dyn domain::ports::ChannelQuery,
channel_id: ChannelId,
owner_id: UserId,
raw_channel_id: uuid::Uuid,
) -> DomainResult<Channel> {
let channel = query
.find_by_id(channel_id)
.await?
.ok_or(DomainError::ChannelNotFound(raw_channel_id))?;
if channel.owner_id() != owner_id {
return Err(DomainError::forbidden(OWNERSHIP_DENIED));
}
Ok(channel)
}

View File

@@ -1,14 +1,11 @@
use uuid::Uuid; use uuid::Uuid;
/// Fetch a single channel by ID.
pub struct GetChannelQuery { pub struct GetChannelQuery {
pub channel_id: Uuid, pub channel_id: Uuid,
} }
/// List all channels.
pub struct ListChannelsQuery; pub struct ListChannelsQuery;
/// List channels belonging to a specific owner.
pub struct ListByOwnerQuery { pub struct ListByOwnerQuery {
pub owner_id: Uuid, pub owner_id: Uuid,
} }

View File

@@ -1,38 +1,26 @@
use domain::events::DomainEvent; use domain::events::DomainEvent;
use domain::models::Channel; use domain::models::Channel;
use domain::value_objects::{ChannelId, UserId}; use domain::value_objects::{ChannelId, UserId};
use domain::{DomainError, DomainResult}; use domain::DomainResult;
use super::commands::UpdateChannelCommand; use super::commands::UpdateChannelCommand;
use super::deps::ChannelCommandDeps; use super::deps::ChannelCommandDeps;
use super::find_owned_channel;
/// Update an existing channel.
///
/// Flow: find channel -> verify ownership -> snapshot config if changed ->
/// apply updates -> save -> publish event -> return.
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 channel_id = ChannelId::from(cmd.channel_id);
let owner_id = UserId::from(cmd.owner_id); let owner_id = UserId::from(cmd.owner_id);
let mut channel = deps let mut channel =
.channel_query find_owned_channel(deps.channel_query.as_ref(), channel_id, owner_id, cmd.channel_id)
.find_by_id(channel_id) .await?;
.await?
.ok_or(DomainError::ChannelNotFound(cmd.channel_id))?;
// Ownership check
if channel.owner_id() != owner_id {
return Err(DomainError::forbidden("You don't own this channel"));
}
// Auto-snapshot the current config before overwriting
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(channel_id, channel.schedule_config(), None)
.await?; .await?;
} }
// Apply partial updates
if let Some(name) = cmd.name { if let Some(name) = cmd.name {
channel.set_name(name); channel.set_name(name);
} }

View File

@@ -1,19 +1,16 @@
use uuid::Uuid; use uuid::Uuid;
/// Save a snapshot of the channel's current config.
pub struct SaveSnapshotCommand { pub struct SaveSnapshotCommand {
pub channel_id: Uuid, pub channel_id: Uuid,
pub label: Option<String>, pub label: Option<String>,
} }
/// Update the label on an existing snapshot.
pub struct PatchLabelCommand { pub struct PatchLabelCommand {
pub channel_id: Uuid, pub channel_id: Uuid,
pub snapshot_id: Uuid, pub snapshot_id: Uuid,
pub label: Option<String>, pub label: Option<String>,
} }
/// Restore a channel's config from a snapshot.
pub struct RestoreSnapshotCommand { pub struct RestoreSnapshotCommand {
pub channel_id: Uuid, pub channel_id: Uuid,
pub snapshot_id: Uuid, pub snapshot_id: Uuid,

View File

@@ -2,7 +2,6 @@ use std::sync::Arc;
use domain::ports::{ChannelCommand, ChannelQuery}; use domain::ports::{ChannelCommand, ChannelQuery};
/// Dependencies for config snapshot use cases.
pub struct ConfigSnapshotDeps { pub struct ConfigSnapshotDeps {
pub channel_command: Arc<dyn ChannelCommand>, pub channel_command: Arc<dyn ChannelCommand>,
pub channel_query: Arc<dyn ChannelQuery>, pub channel_query: Arc<dyn ChannelQuery>,

View File

@@ -5,7 +5,6 @@ use domain::DomainResult;
use super::deps::ConfigSnapshotDeps; use super::deps::ConfigSnapshotDeps;
use super::queries::GetSnapshotQuery; use super::queries::GetSnapshotQuery;
/// Get a specific config snapshot by channel and snapshot ID.
pub async fn execute( pub async fn execute(
deps: &ConfigSnapshotDeps, deps: &ConfigSnapshotDeps,
query: GetSnapshotQuery, query: GetSnapshotQuery,

View File

@@ -5,7 +5,6 @@ use domain::DomainResult;
use super::deps::ConfigSnapshotDeps; use super::deps::ConfigSnapshotDeps;
use super::queries::ListSnapshotsQuery; use super::queries::ListSnapshotsQuery;
/// List all config snapshots for a channel, newest first.
pub async fn execute( pub async fn execute(
deps: &ConfigSnapshotDeps, deps: &ConfigSnapshotDeps,
query: ListSnapshotsQuery, query: ListSnapshotsQuery,

View File

@@ -5,7 +5,6 @@ use domain::DomainResult;
use super::commands::PatchLabelCommand; use super::commands::PatchLabelCommand;
use super::deps::ConfigSnapshotDeps; use super::deps::ConfigSnapshotDeps;
/// Update the label on an existing config snapshot.
pub async fn execute( pub async fn execute(
deps: &ConfigSnapshotDeps, deps: &ConfigSnapshotDeps,
cmd: PatchLabelCommand, cmd: PatchLabelCommand,

View File

@@ -1,11 +1,9 @@
use uuid::Uuid; use uuid::Uuid;
/// List all config snapshots for a channel (newest first).
pub struct ListSnapshotsQuery { pub struct ListSnapshotsQuery {
pub channel_id: Uuid, pub channel_id: Uuid,
} }
/// Get a specific config snapshot.
pub struct GetSnapshotQuery { pub struct GetSnapshotQuery {
pub channel_id: Uuid, pub channel_id: Uuid,
pub snapshot_id: Uuid, pub snapshot_id: Uuid,

View File

@@ -5,10 +5,6 @@ use domain::{DomainError, DomainResult};
use super::commands::RestoreSnapshotCommand; use super::commands::RestoreSnapshotCommand;
use super::deps::ConfigSnapshotDeps; use super::deps::ConfigSnapshotDeps;
/// Restore a channel's config from a snapshot.
///
/// Flow: find snapshot -> find channel -> snapshot current config (backup) ->
/// apply snapshot config to channel -> save channel -> return updated channel.
pub async fn execute( pub async fn execute(
deps: &ConfigSnapshotDeps, deps: &ConfigSnapshotDeps,
cmd: RestoreSnapshotCommand, cmd: RestoreSnapshotCommand,
@@ -30,12 +26,10 @@ pub async fn execute(
.await? .await?
.ok_or(DomainError::ChannelNotFound(cmd.channel_id))?; .ok_or(DomainError::ChannelNotFound(cmd.channel_id))?;
// Auto-snapshot the current config before overwriting
deps.channel_command deps.channel_command
.save_config_snapshot(channel_id, channel.schedule_config(), None) .save_config_snapshot(channel_id, channel.schedule_config(), None)
.await?; .await?;
// Apply the snapshot's config
channel.set_schedule_config(snapshot.config().clone()); channel.set_schedule_config(snapshot.config().clone());
deps.channel_command.save(&channel).await?; deps.channel_command.save(&channel).await?;

View File

@@ -5,9 +5,6 @@ use domain::{DomainError, DomainResult};
use super::commands::SaveSnapshotCommand; use super::commands::SaveSnapshotCommand;
use super::deps::ConfigSnapshotDeps; use super::deps::ConfigSnapshotDeps;
/// Save a snapshot of the channel's current schedule config.
///
/// Flow: find channel -> snapshot its current config -> return snapshot.
pub async fn execute( pub async fn execute(
deps: &ConfigSnapshotDeps, deps: &ConfigSnapshotDeps,
cmd: SaveSnapshotCommand, cmd: SaveSnapshotCommand,

View File

@@ -2,7 +2,6 @@ use std::sync::Arc;
use domain::ports::{ChannelQuery, ScheduleQuery}; use domain::ports::{ChannelQuery, ScheduleQuery};
/// Dependencies for IPTV export use cases.
pub struct IptvDeps { pub struct IptvDeps {
pub channel_query: Arc<dyn ChannelQuery>, pub channel_query: Arc<dyn ChannelQuery>,
pub schedule_query: Arc<dyn ScheduleQuery>, pub schedule_query: Arc<dyn ScheduleQuery>,

View File

@@ -4,9 +4,6 @@ use domain::DomainResult;
use super::deps::IptvDeps; use super::deps::IptvDeps;
use super::queries::GetM3uQuery; use super::queries::GetM3uQuery;
/// Generate an M3U playlist for all channels.
///
/// Flow: fetch all channels -> delegate to domain::generate_m3u -> return string.
pub async fn execute(deps: &IptvDeps, query: GetM3uQuery) -> DomainResult<String> { pub async fn execute(deps: &IptvDeps, query: GetM3uQuery) -> DomainResult<String> {
let channels = deps.channel_query.find_all().await?; let channels = deps.channel_query.find_all().await?;
let token = query.token.as_deref().unwrap_or(""); let token = query.token.as_deref().unwrap_or("");

View File

@@ -1,8 +1,6 @@
/// Generate an M3U playlist for all channels.
pub struct GetM3uQuery { pub struct GetM3uQuery {
pub base_url: String, pub base_url: String,
pub token: Option<String>, pub token: Option<String>,
} }
/// Generate an XMLTV EPG document for all channels.
pub struct GetXmltvQuery; pub struct GetXmltvQuery;

View File

@@ -8,10 +8,6 @@ use domain::DomainResult;
use super::deps::IptvDeps; use super::deps::IptvDeps;
use super::queries::GetXmltvQuery; use super::queries::GetXmltvQuery;
/// Generate an XMLTV EPG document for all channels with active schedules.
///
/// Flow: fetch all channels -> for each, find active schedule -> collect slots
/// -> delegate to domain::generate_xmltv -> return string.
pub async fn execute(deps: &IptvDeps, _query: GetXmltvQuery) -> DomainResult<String> { pub async fn execute(deps: &IptvDeps, _query: GetXmltvQuery) -> DomainResult<String> {
let channels = deps.channel_query.find_all().await?; let channels = deps.channel_query.find_all().await?;
let now = Utc::now(); let now = Utc::now();

View File

@@ -1,5 +1,3 @@
/// Trigger a library sync for one or all providers.
pub struct TriggerSyncCommand { pub struct TriggerSyncCommand {
/// Provider to sync. `None` means sync all registered providers.
pub provider_id: Option<String>, pub provider_id: Option<String>,
} }

View File

@@ -2,7 +2,6 @@ use std::sync::Arc;
use domain::ports::{EventPublisher, IProviderRegistry, LibraryCommand, LibraryQuery, LibrarySyncAdapter}; use domain::ports::{EventPublisher, IProviderRegistry, LibraryCommand, LibraryQuery, LibrarySyncAdapter};
/// Dependencies for library write use cases (trigger sync).
pub struct LibraryCommandDeps { pub struct LibraryCommandDeps {
pub library_command: Arc<dyn LibraryCommand>, pub library_command: Arc<dyn LibraryCommand>,
pub library_query: Arc<dyn LibraryQuery>, pub library_query: Arc<dyn LibraryQuery>,
@@ -11,7 +10,6 @@ pub struct LibraryCommandDeps {
pub event_publisher: Arc<dyn EventPublisher>, pub event_publisher: Arc<dyn EventPublisher>,
} }
/// Dependencies for library read use cases (search, list, get).
pub struct LibraryQueryDeps { pub struct LibraryQueryDeps {
pub library_query: Arc<dyn LibraryQuery>, pub library_query: Arc<dyn LibraryQuery>,
} }

View File

@@ -4,7 +4,6 @@ use domain::DomainResult;
use super::deps::LibraryQueryDeps; use super::deps::LibraryQueryDeps;
use super::queries::GetItemQuery; use super::queries::GetItemQuery;
/// Get a single library item by its composite ID.
pub async fn execute( pub async fn execute(
deps: &LibraryQueryDeps, deps: &LibraryQueryDeps,
query: GetItemQuery, query: GetItemQuery,

View File

@@ -4,7 +4,6 @@ use domain::DomainResult;
use super::deps::LibraryQueryDeps; use super::deps::LibraryQueryDeps;
use super::queries::GetSyncStatusQuery; use super::queries::GetSyncStatusQuery;
/// Get the latest sync status per provider.
pub async fn execute( pub async fn execute(
deps: &LibraryQueryDeps, deps: &LibraryQueryDeps,
_query: GetSyncStatusQuery, _query: GetSyncStatusQuery,

View File

@@ -4,7 +4,6 @@ use domain::DomainResult;
use super::deps::LibraryQueryDeps; use super::deps::LibraryQueryDeps;
use super::queries::ListCollectionsQuery; use super::queries::ListCollectionsQuery;
/// List library collections, optionally filtered by provider.
pub async fn execute( pub async fn execute(
deps: &LibraryQueryDeps, deps: &LibraryQueryDeps,
query: ListCollectionsQuery, query: ListCollectionsQuery,

View File

@@ -1,10 +1,9 @@
use domain::errors::{DomainError, DomainResult}; use domain::DomainResult;
use domain::value_objects::ContentType;
use super::deps::LibraryQueryDeps; use super::deps::LibraryQueryDeps;
use super::parse_content_type;
use super::queries::ListGenresQuery; use super::queries::ListGenresQuery;
/// List genres available in the library, optionally filtered.
pub async fn execute(deps: &LibraryQueryDeps, query: ListGenresQuery) -> DomainResult<Vec<String>> { pub async fn execute(deps: &LibraryQueryDeps, query: ListGenresQuery) -> DomainResult<Vec<String>> {
let content_type = query let content_type = query
.content_type .content_type
@@ -17,17 +16,6 @@ pub async fn execute(deps: &LibraryQueryDeps, query: ListGenresQuery) -> DomainR
.await .await
} }
fn parse_content_type(s: &str) -> DomainResult<ContentType> {
match s {
"movie" => Ok(ContentType::Movie),
"episode" => Ok(ContentType::Episode),
"short" => Ok(ContentType::Short),
other => Err(DomainError::ValidationError(format!(
"Unknown content type '{other}'. Use movie, episode, or short."
))),
}
}
#[cfg(test)] #[cfg(test)]
#[path = "tests/list_genres.rs"] #[path = "tests/list_genres.rs"]
mod tests; mod tests;

View File

@@ -4,7 +4,6 @@ use domain::DomainResult;
use super::deps::LibraryQueryDeps; use super::deps::LibraryQueryDeps;
use super::queries::ListSeasonsQuery; use super::queries::ListSeasonsQuery;
/// List season summaries for a specific series.
pub async fn execute( pub async fn execute(
deps: &LibraryQueryDeps, deps: &LibraryQueryDeps,
query: ListSeasonsQuery, query: ListSeasonsQuery,

View File

@@ -4,7 +4,6 @@ use domain::DomainResult;
use super::deps::LibraryQueryDeps; use super::deps::LibraryQueryDeps;
use super::queries::ListShowsQuery; use super::queries::ListShowsQuery;
/// List TV show summaries, optionally filtered.
pub async fn execute( pub async fn execute(
deps: &LibraryQueryDeps, deps: &LibraryQueryDeps,
query: ListShowsQuery, query: ListShowsQuery,

View File

@@ -16,3 +16,17 @@ pub use queries::{
GetItemQuery, GetSyncStatusQuery, ListCollectionsQuery, ListGenresQuery, ListSeasonsQuery, GetItemQuery, GetSyncStatusQuery, ListCollectionsQuery, ListGenresQuery, ListSeasonsQuery,
ListShowsQuery, SearchItemsQuery, ListShowsQuery, SearchItemsQuery,
}; };
use domain::errors::{DomainError, DomainResult};
use domain::value_objects::ContentType;
pub(crate) fn parse_content_type(s: &str) -> DomainResult<ContentType> {
match s {
"movie" => Ok(ContentType::Movie),
"episode" => Ok(ContentType::Episode),
"short" => Ok(ContentType::Short),
other => Err(DomainError::ValidationError(format!(
"Unknown content type '{other}'. Use movie, episode, or short."
))),
}
}

View File

@@ -1,4 +1,3 @@
/// Search library items with filters.
pub struct SearchItemsQuery { pub struct SearchItemsQuery {
pub provider_id: Option<String>, pub provider_id: Option<String>,
pub content_type: Option<String>, pub content_type: Option<String>,
@@ -12,34 +11,28 @@ pub struct SearchItemsQuery {
pub limit: u32, pub limit: u32,
} }
/// List library collections.
pub struct ListCollectionsQuery { pub struct ListCollectionsQuery {
pub provider_id: Option<String>, pub provider_id: Option<String>,
} }
/// List TV show summaries.
pub struct ListShowsQuery { pub struct ListShowsQuery {
pub provider_id: Option<String>, pub provider_id: Option<String>,
pub search_term: Option<String>, pub search_term: Option<String>,
pub genres: Vec<String>, pub genres: Vec<String>,
} }
/// List seasons for a specific series.
pub struct ListSeasonsQuery { pub struct ListSeasonsQuery {
pub series_name: String, pub series_name: String,
pub provider_id: Option<String>, pub provider_id: Option<String>,
} }
/// List genres available in the library.
pub struct ListGenresQuery { pub struct ListGenresQuery {
pub content_type: Option<String>, pub content_type: Option<String>,
pub provider_id: Option<String>, pub provider_id: Option<String>,
} }
/// Get a single library item by its composite ID.
pub struct GetItemQuery { pub struct GetItemQuery {
pub item_id: String, pub item_id: String,
} }
/// Get the latest sync status per provider.
pub struct GetSyncStatusQuery; pub struct GetSyncStatusQuery;

View File

@@ -1,11 +1,11 @@
use domain::errors::{DomainError, DomainResult}; use domain::DomainResult;
use domain::models::LibraryItem; use domain::models::LibraryItem;
use domain::value_objects::{ContentType, LibrarySearchFilter}; use domain::value_objects::LibrarySearchFilter;
use super::deps::LibraryQueryDeps; use super::deps::LibraryQueryDeps;
use super::parse_content_type;
use super::queries::SearchItemsQuery; use super::queries::SearchItemsQuery;
/// Search library items with filters. Returns `(items, total_count)`.
pub async fn execute( pub async fn execute(
deps: &LibraryQueryDeps, deps: &LibraryQueryDeps,
query: SearchItemsQuery, query: SearchItemsQuery,
@@ -48,17 +48,6 @@ pub async fn execute(
deps.library_query.search(&filter).await deps.library_query.search(&filter).await
} }
fn parse_content_type(s: &str) -> DomainResult<ContentType> {
match s {
"movie" => Ok(ContentType::Movie),
"episode" => Ok(ContentType::Episode),
"short" => Ok(ContentType::Short),
other => Err(DomainError::ValidationError(format!(
"Unknown content type '{other}'. Use movie, episode, or short."
))),
}
}
#[cfg(test)] #[cfg(test)]
#[path = "tests/search.rs"] #[path = "tests/search.rs"]
mod tests; mod tests;

View File

@@ -3,15 +3,6 @@ use domain::errors::{DomainError, DomainResult};
use super::commands::TriggerSyncCommand; use super::commands::TriggerSyncCommand;
use super::deps::LibraryCommandDeps; use super::deps::LibraryCommandDeps;
/// Validate and return provider IDs eligible for sync.
///
/// Checks that no sync is already running for the targeted provider(s).
/// Returns the list of provider IDs to sync. The caller (API layer) is
/// responsible for spawning the actual sync tasks, since `LibrarySyncAdapter`
/// requires `&dyn IMediaProvider` references that only the infra layer holds.
///
/// Returns `Err(ValidationError)` if any targeted provider is already syncing
/// (maps to 409 Conflict at the API layer).
pub async fn execute( pub async fn execute(
deps: &LibraryCommandDeps, deps: &LibraryCommandDeps,
cmd: TriggerSyncCommand, cmd: TriggerSyncCommand,

View File

@@ -1,4 +1,3 @@
/// Insert or update a provider configuration.
pub struct UpsertProviderCommand { pub struct UpsertProviderCommand {
pub id: String, pub id: String,
pub provider_type: String, pub provider_type: String,
@@ -6,7 +5,6 @@ pub struct UpsertProviderCommand {
pub enabled: bool, pub enabled: bool,
} }
/// Delete a provider configuration.
pub struct DeleteProviderCommand { pub struct DeleteProviderCommand {
pub id: String, pub id: String,
} }

View File

@@ -3,7 +3,6 @@ use domain::DomainResult;
use super::commands::DeleteProviderCommand; use super::commands::DeleteProviderCommand;
use super::deps::ProviderDeps; use super::deps::ProviderDeps;
/// Delete a provider configuration by ID.
pub async fn execute(deps: &ProviderDeps, cmd: DeleteProviderCommand) -> DomainResult<()> { pub async fn execute(deps: &ProviderDeps, cmd: DeleteProviderCommand) -> DomainResult<()> {
deps.provider_config_command.delete(&cmd.id).await deps.provider_config_command.delete(&cmd.id).await
} }

View File

@@ -2,7 +2,6 @@ use std::sync::Arc;
use domain::ports::{ProviderConfigCommand, ProviderConfigQuery}; use domain::ports::{ProviderConfigCommand, ProviderConfigQuery};
/// Dependencies for provider config use cases.
pub struct ProviderDeps { pub struct ProviderDeps {
pub provider_config_command: Arc<dyn ProviderConfigCommand>, pub provider_config_command: Arc<dyn ProviderConfigCommand>,
pub provider_config_query: Arc<dyn ProviderConfigQuery>, pub provider_config_query: Arc<dyn ProviderConfigQuery>,

View File

@@ -4,7 +4,6 @@ use domain::DomainResult;
use super::deps::ProviderDeps; use super::deps::ProviderDeps;
use super::queries::GetProviderQuery; use super::queries::GetProviderQuery;
/// Get a provider configuration by ID.
pub async fn execute( pub async fn execute(
deps: &ProviderDeps, deps: &ProviderDeps,
query: GetProviderQuery, query: GetProviderQuery,

View File

@@ -4,7 +4,6 @@ use domain::DomainResult;
use super::deps::ProviderDeps; use super::deps::ProviderDeps;
use super::queries::ListProvidersQuery; use super::queries::ListProvidersQuery;
/// List all provider configurations.
pub async fn execute( pub async fn execute(
deps: &ProviderDeps, deps: &ProviderDeps,
_query: ListProvidersQuery, _query: ListProvidersQuery,

View File

@@ -1,7 +1,5 @@
/// List all provider configurations.
pub struct ListProvidersQuery; pub struct ListProvidersQuery;
/// Get a provider configuration by ID.
pub struct GetProviderQuery { pub struct GetProviderQuery {
pub id: String, pub id: String,
} }

View File

@@ -4,7 +4,6 @@ use domain::DomainResult;
use super::commands::UpsertProviderCommand; use super::commands::UpsertProviderCommand;
use super::deps::ProviderDeps; use super::deps::ProviderDeps;
/// Insert or update a provider configuration.
pub async fn execute(deps: &ProviderDeps, cmd: UpsertProviderCommand) -> DomainResult<()> { pub async fn execute(deps: &ProviderDeps, cmd: UpsertProviderCommand) -> DomainResult<()> {
let row = ProviderConfigRow::from_persistence( let row = ProviderConfigRow::from_persistence(
cmd.id, cmd.id,

View File

@@ -1,11 +1,9 @@
use uuid::Uuid; use uuid::Uuid;
/// Generate a new schedule for a channel.
pub struct GenerateScheduleCommand { pub struct GenerateScheduleCommand {
pub channel_id: Uuid, pub channel_id: Uuid,
} }
/// Delete all schedules with generation > target_generation for a channel.
pub struct DeleteSchedulesAfterCommand { pub struct DeleteSchedulesAfterCommand {
pub channel_id: Uuid, pub channel_id: Uuid,
pub target_generation: u32, pub target_generation: u32,

View File

@@ -4,7 +4,6 @@ use domain::DomainResult;
use super::commands::DeleteSchedulesAfterCommand; use super::commands::DeleteSchedulesAfterCommand;
use super::deps::ScheduleDeps; use super::deps::ScheduleDeps;
/// Delete all schedules with generation > target_generation for a channel.
pub async fn execute( pub async fn execute(
deps: &ScheduleDeps, deps: &ScheduleDeps,
cmd: DeleteSchedulesAfterCommand, cmd: DeleteSchedulesAfterCommand,

View File

@@ -3,7 +3,6 @@ use std::sync::Arc;
use domain::ports::{ChannelQuery, EventPublisher, ScheduleCommand, ScheduleQuery}; use domain::ports::{ChannelQuery, EventPublisher, ScheduleCommand, ScheduleQuery};
use domain::ScheduleEngineService; use domain::ScheduleEngineService;
/// Dependencies for schedule use cases.
pub struct ScheduleDeps { pub struct ScheduleDeps {
pub schedule_engine: Arc<ScheduleEngineService>, pub schedule_engine: Arc<ScheduleEngineService>,
pub channel_query: Arc<dyn ChannelQuery>, pub channel_query: Arc<dyn ChannelQuery>,

View File

@@ -8,10 +8,6 @@ use domain::DomainResult;
use super::commands::GenerateScheduleCommand; use super::commands::GenerateScheduleCommand;
use super::deps::ScheduleDeps; use super::deps::ScheduleDeps;
/// Generate a new 7-day schedule for a channel.
///
/// Delegates the heavy lifting to `ScheduleEngineService::generate_schedule`,
/// then publishes a `ScheduleGenerated` domain event.
pub async fn execute( pub async fn execute(
deps: &ScheduleDeps, deps: &ScheduleDeps,
cmd: GenerateScheduleCommand, cmd: GenerateScheduleCommand,

View File

@@ -7,9 +7,6 @@ use domain::DomainResult;
use super::deps::ScheduleDeps; use super::deps::ScheduleDeps;
use super::queries::GetActiveScheduleQuery; use super::queries::GetActiveScheduleQuery;
/// Fetch the schedule currently active at `now`.
///
/// Returns `None` when no schedule covers the current time.
pub async fn execute( pub async fn execute(
deps: &ScheduleDeps, deps: &ScheduleDeps,
query: GetActiveScheduleQuery, query: GetActiveScheduleQuery,

View File

@@ -7,10 +7,6 @@ use domain::{DomainResult, ScheduleEngineService};
use super::deps::ScheduleDeps; use super::deps::ScheduleDeps;
use super::queries::GetCurrentBroadcastQuery; use super::queries::GetCurrentBroadcastQuery;
/// Determine what is currently broadcasting on a channel.
///
/// Returns `None` when no schedule is active or `now` falls in a gap
/// between blocks (no-signal / static screen).
pub async fn execute( pub async fn execute(
deps: &ScheduleDeps, deps: &ScheduleDeps,
query: GetCurrentBroadcastQuery, query: GetCurrentBroadcastQuery,

View File

@@ -7,10 +7,6 @@ use domain::{DomainResult, ScheduleEngineService};
use super::deps::ScheduleDeps; use super::deps::ScheduleDeps;
use super::queries::GetEpgQuery; use super::queries::GetEpgQuery;
/// Return EPG (electronic program guide) data for a channel.
///
/// Returns the slots that overlap the active schedule's validity window.
/// Returns an empty vec when no schedule is active.
pub async fn execute( pub async fn execute(
deps: &ScheduleDeps, deps: &ScheduleDeps,
query: GetEpgQuery, query: GetEpgQuery,

View File

@@ -5,9 +5,6 @@ use domain::DomainResult;
use super::deps::ScheduleDeps; use super::deps::ScheduleDeps;
use super::queries::GetStreamUrlQuery; use super::queries::GetStreamUrlQuery;
/// Resolve a playback URL for a media item.
///
/// Delegates to the schedule engine which routes via the provider registry.
pub async fn execute(deps: &ScheduleDeps, query: GetStreamUrlQuery) -> DomainResult<String> { pub async fn execute(deps: &ScheduleDeps, query: GetStreamUrlQuery) -> DomainResult<String> {
let item_id = MediaItemId::new(&query.item_id); let item_id = MediaItemId::new(&query.item_id);
deps.schedule_engine deps.schedule_engine

View File

@@ -5,7 +5,6 @@ use domain::DomainResult;
use super::deps::ScheduleDeps; use super::deps::ScheduleDeps;
use super::queries::ListHistoryQuery; use super::queries::ListHistoryQuery;
/// List all generated schedule headers for a channel, newest first.
pub async fn execute( pub async fn execute(
deps: &ScheduleDeps, deps: &ScheduleDeps,
query: ListHistoryQuery, query: ListHistoryQuery,

View File

@@ -1,28 +1,22 @@
use uuid::Uuid; use uuid::Uuid;
/// Fetch the schedule whose validity window contains `now`.
pub struct GetActiveScheduleQuery { pub struct GetActiveScheduleQuery {
pub channel_id: Uuid, pub channel_id: Uuid,
} }
/// Determine what is currently broadcasting on a channel.
pub struct GetCurrentBroadcastQuery { pub struct GetCurrentBroadcastQuery {
pub channel_id: Uuid, pub channel_id: Uuid,
} }
/// Return EPG (electronic program guide) data for a channel.
pub struct GetEpgQuery { pub struct GetEpgQuery {
pub channel_id: Uuid, pub channel_id: Uuid,
} }
/// Get a playback URL for a specific media item on a channel.
pub struct GetStreamUrlQuery { pub struct GetStreamUrlQuery {
pub channel_id: Uuid, pub channel_id: Uuid,
/// MediaItemId as string (e.g. "jellyfin::abc123").
pub item_id: String, pub item_id: String,
} }
/// List all generated schedule headers for a channel.
pub struct ListHistoryQuery { pub struct ListHistoryQuery {
pub channel_id: Uuid, pub channel_id: Uuid,
} }