refactor(application): strip comments, DRY ownership check + parse_content_type
This commit is contained in:
@@ -4,7 +4,6 @@ use domain::DomainResult;
|
||||
use super::deps::AdminDeps;
|
||||
use super::queries::GetActivityLogQuery;
|
||||
|
||||
/// Get recent activity log entries.
|
||||
pub async fn execute(
|
||||
deps: &AdminDeps,
|
||||
query: GetActivityLogQuery,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/// Update one or more admin settings (key-value pairs).
|
||||
pub struct UpdateSettingsCommand {
|
||||
pub settings: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::sync::Arc;
|
||||
|
||||
use domain::ports::{ActivityLogQuery, AppSettingsRepository};
|
||||
|
||||
/// Dependencies for admin use cases.
|
||||
pub struct AdminDeps {
|
||||
pub settings_repo: Arc<dyn AppSettingsRepository>,
|
||||
pub activity_query: Arc<dyn ActivityLogQuery>,
|
||||
|
||||
@@ -3,7 +3,6 @@ use domain::DomainResult;
|
||||
use super::deps::AdminDeps;
|
||||
use super::queries::GetSettingsQuery;
|
||||
|
||||
/// Get all admin settings as key-value pairs.
|
||||
pub async fn execute(
|
||||
deps: &AdminDeps,
|
||||
_query: GetSettingsQuery,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/// Get all admin settings.
|
||||
pub struct GetSettingsQuery;
|
||||
|
||||
/// Get recent activity log entries.
|
||||
pub struct GetActivityLogQuery {
|
||||
pub limit: u32,
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@ use domain::DomainResult;
|
||||
use super::commands::UpdateSettingsCommand;
|
||||
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<()> {
|
||||
for (key, value) in &cmd.settings {
|
||||
deps.settings_repo.set(key, value).await?;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
/// Register a new local user.
|
||||
pub struct RegisterCommand {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// Log in with email + password.
|
||||
pub struct LoginCommand {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
|
||||
@@ -2,10 +2,6 @@ use std::sync::Arc;
|
||||
|
||||
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 user_command: Arc<dyn UserCommand>,
|
||||
pub user_query: Arc<dyn UserQuery>,
|
||||
|
||||
@@ -4,30 +4,24 @@ use domain::{DomainError, DomainResult, Email};
|
||||
use super::commands::LoginCommand;
|
||||
use super::deps::AuthDeps;
|
||||
|
||||
/// Log in with email + password.
|
||||
///
|
||||
/// Flow: validate email -> find user -> verify password -> return User.
|
||||
/// JWT generation belongs in the presentation layer, not here.
|
||||
const INVALID_CREDENTIALS: &str = "Invalid credentials";
|
||||
|
||||
pub async fn execute(deps: &AuthDeps, cmd: LoginCommand) -> DomainResult<User> {
|
||||
// Validate email format
|
||||
let email = Email::new(&cmd.email)?;
|
||||
|
||||
// Find user
|
||||
let user = deps
|
||||
.user_query
|
||||
.find_by_email(email.as_ref())
|
||||
.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
|
||||
.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)?;
|
||||
if !valid {
|
||||
return Err(DomainError::unauthenticated("Invalid credentials"));
|
||||
return Err(DomainError::unauthenticated(INVALID_CREDENTIALS));
|
||||
}
|
||||
|
||||
Ok(user)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
// Auth queries (reserved for future use, e.g. GetCurrentUserQuery).
|
||||
|
||||
@@ -5,33 +5,23 @@ use domain::{DomainResult, Email, Password};
|
||||
use super::commands::RegisterCommand;
|
||||
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> {
|
||||
// Validate inputs via domain value objects
|
||||
let email = Email::new(&cmd.email)?;
|
||||
let password = Password::new(&cmd.password)?;
|
||||
|
||||
// Check for duplicate
|
||||
if deps.user_query.find_by_email(email.as_ref()).await?.is_some() {
|
||||
return Err(domain::DomainError::UserAlreadyExists(cmd.email));
|
||||
}
|
||||
|
||||
// Hash password
|
||||
let hash = deps.auth_service.hash_password(password.as_ref())?;
|
||||
|
||||
// Create user; first user gets admin
|
||||
let mut user = User::new_local(email, hash);
|
||||
if deps.user_query.count_users().await? == 0 {
|
||||
user.promote_to_admin();
|
||||
}
|
||||
|
||||
// Persist
|
||||
deps.user_command.save(&user).await?;
|
||||
|
||||
// Publish event
|
||||
deps.event_publisher
|
||||
.publish(DomainEvent::UserRegistered {
|
||||
user_id: user.id(),
|
||||
|
||||
@@ -3,20 +3,16 @@ use uuid::Uuid;
|
||||
use domain::models::ScheduleConfig;
|
||||
use domain::value_objects::RecyclePolicy;
|
||||
|
||||
/// Create a new channel.
|
||||
pub struct CreateChannelCommand {
|
||||
pub owner_id: Uuid,
|
||||
pub name: String,
|
||||
pub timezone: String,
|
||||
}
|
||||
|
||||
/// Update an existing channel (partial — only `Some` fields are applied).
|
||||
pub struct UpdateChannelCommand {
|
||||
pub channel_id: Uuid,
|
||||
/// Used for ownership check.
|
||||
pub owner_id: Uuid,
|
||||
pub name: Option<String>,
|
||||
/// `Some(None)` clears the description; `None` leaves it unchanged.
|
||||
pub description: Option<Option<String>>,
|
||||
pub timezone: Option<String>,
|
||||
pub schedule_config: Option<ScheduleConfig>,
|
||||
@@ -24,9 +20,7 @@ pub struct UpdateChannelCommand {
|
||||
pub auto_schedule: Option<bool>,
|
||||
}
|
||||
|
||||
/// Delete a channel.
|
||||
pub struct DeleteChannelCommand {
|
||||
pub channel_id: Uuid,
|
||||
/// Used for ownership check.
|
||||
pub owner_id: Uuid,
|
||||
}
|
||||
|
||||
@@ -6,9 +6,6 @@ use domain::DomainResult;
|
||||
use super::commands::CreateChannelCommand;
|
||||
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> {
|
||||
let owner_id = UserId::from(cmd.owner_id);
|
||||
let channel = Channel::new(owner_id, cmd.name, cmd.timezone);
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
use domain::events::DomainEvent;
|
||||
use domain::value_objects::{ChannelId, UserId};
|
||||
use domain::{DomainError, DomainResult};
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::commands::DeleteChannelCommand;
|
||||
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<()> {
|
||||
let channel_id = ChannelId::from(cmd.channel_id);
|
||||
let owner_id = UserId::from(cmd.owner_id);
|
||||
|
||||
let channel = deps
|
||||
.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"));
|
||||
}
|
||||
find_owned_channel(deps.channel_query.as_ref(), channel_id, owner_id, cmd.channel_id).await?;
|
||||
|
||||
deps.channel_command.delete(channel_id).await?;
|
||||
|
||||
|
||||
@@ -2,14 +2,12 @@ use std::sync::Arc;
|
||||
|
||||
use domain::ports::{ChannelCommand, ChannelQuery, EventPublisher};
|
||||
|
||||
/// Dependencies for channel write use cases (create, update, delete).
|
||||
pub struct ChannelCommandDeps {
|
||||
pub channel_command: Arc<dyn ChannelCommand>,
|
||||
pub channel_query: Arc<dyn ChannelQuery>,
|
||||
pub event_publisher: Arc<dyn EventPublisher>,
|
||||
}
|
||||
|
||||
/// Dependencies for channel read use cases (get, list, list_by_owner).
|
||||
pub struct ChannelQueryDeps {
|
||||
pub channel_query: Arc<dyn ChannelQuery>,
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ use domain::DomainResult;
|
||||
use super::deps::ChannelQueryDeps;
|
||||
use super::queries::GetChannelQuery;
|
||||
|
||||
/// Get a single channel by ID.
|
||||
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(channel_id).await
|
||||
|
||||
@@ -4,7 +4,6 @@ use domain::DomainResult;
|
||||
use super::deps::ChannelQueryDeps;
|
||||
use super::queries::ListChannelsQuery;
|
||||
|
||||
/// List all channels.
|
||||
pub async fn execute(deps: &ChannelQueryDeps, _query: ListChannelsQuery) -> DomainResult<Vec<Channel>> {
|
||||
deps.channel_query.find_all().await
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ use domain::DomainResult;
|
||||
use super::deps::ChannelQueryDeps;
|
||||
use super::queries::ListByOwnerQuery;
|
||||
|
||||
/// List channels belonging to a specific owner.
|
||||
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(owner_id).await
|
||||
|
||||
@@ -11,3 +11,27 @@ pub mod update;
|
||||
pub use commands::{CreateChannelCommand, DeleteChannelCommand, UpdateChannelCommand};
|
||||
pub use deps::{ChannelCommandDeps, ChannelQueryDeps};
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Fetch a single channel by ID.
|
||||
pub struct GetChannelQuery {
|
||||
pub channel_id: Uuid,
|
||||
}
|
||||
|
||||
/// List all channels.
|
||||
pub struct ListChannelsQuery;
|
||||
|
||||
/// List channels belonging to a specific owner.
|
||||
pub struct ListByOwnerQuery {
|
||||
pub owner_id: Uuid,
|
||||
}
|
||||
|
||||
@@ -1,38 +1,26 @@
|
||||
use domain::events::DomainEvent;
|
||||
use domain::models::Channel;
|
||||
use domain::value_objects::{ChannelId, UserId};
|
||||
use domain::{DomainError, DomainResult};
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::commands::UpdateChannelCommand;
|
||||
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> {
|
||||
let channel_id = ChannelId::from(cmd.channel_id);
|
||||
let owner_id = UserId::from(cmd.owner_id);
|
||||
|
||||
let mut channel = deps
|
||||
.channel_query
|
||||
.find_by_id(channel_id)
|
||||
.await?
|
||||
.ok_or(DomainError::ChannelNotFound(cmd.channel_id))?;
|
||||
let mut channel =
|
||||
find_owned_channel(deps.channel_query.as_ref(), channel_id, owner_id, cmd.channel_id)
|
||||
.await?;
|
||||
|
||||
// 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() {
|
||||
deps.channel_command
|
||||
.save_config_snapshot(channel_id, channel.schedule_config(), None)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Apply partial updates
|
||||
if let Some(name) = cmd.name {
|
||||
channel.set_name(name);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Save a snapshot of the channel's current config.
|
||||
pub struct SaveSnapshotCommand {
|
||||
pub channel_id: Uuid,
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
/// Update the label on an existing snapshot.
|
||||
pub struct PatchLabelCommand {
|
||||
pub channel_id: Uuid,
|
||||
pub snapshot_id: Uuid,
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
/// Restore a channel's config from a snapshot.
|
||||
pub struct RestoreSnapshotCommand {
|
||||
pub channel_id: Uuid,
|
||||
pub snapshot_id: Uuid,
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::sync::Arc;
|
||||
|
||||
use domain::ports::{ChannelCommand, ChannelQuery};
|
||||
|
||||
/// Dependencies for config snapshot use cases.
|
||||
pub struct ConfigSnapshotDeps {
|
||||
pub channel_command: Arc<dyn ChannelCommand>,
|
||||
pub channel_query: Arc<dyn ChannelQuery>,
|
||||
|
||||
@@ -5,7 +5,6 @@ use domain::DomainResult;
|
||||
use super::deps::ConfigSnapshotDeps;
|
||||
use super::queries::GetSnapshotQuery;
|
||||
|
||||
/// Get a specific config snapshot by channel and snapshot ID.
|
||||
pub async fn execute(
|
||||
deps: &ConfigSnapshotDeps,
|
||||
query: GetSnapshotQuery,
|
||||
|
||||
@@ -5,7 +5,6 @@ use domain::DomainResult;
|
||||
use super::deps::ConfigSnapshotDeps;
|
||||
use super::queries::ListSnapshotsQuery;
|
||||
|
||||
/// List all config snapshots for a channel, newest first.
|
||||
pub async fn execute(
|
||||
deps: &ConfigSnapshotDeps,
|
||||
query: ListSnapshotsQuery,
|
||||
|
||||
@@ -5,7 +5,6 @@ use domain::DomainResult;
|
||||
use super::commands::PatchLabelCommand;
|
||||
use super::deps::ConfigSnapshotDeps;
|
||||
|
||||
/// Update the label on an existing config snapshot.
|
||||
pub async fn execute(
|
||||
deps: &ConfigSnapshotDeps,
|
||||
cmd: PatchLabelCommand,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
/// List all config snapshots for a channel (newest first).
|
||||
pub struct ListSnapshotsQuery {
|
||||
pub channel_id: Uuid,
|
||||
}
|
||||
|
||||
/// Get a specific config snapshot.
|
||||
pub struct GetSnapshotQuery {
|
||||
pub channel_id: Uuid,
|
||||
pub snapshot_id: Uuid,
|
||||
|
||||
@@ -5,10 +5,6 @@ use domain::{DomainError, DomainResult};
|
||||
use super::commands::RestoreSnapshotCommand;
|
||||
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(
|
||||
deps: &ConfigSnapshotDeps,
|
||||
cmd: RestoreSnapshotCommand,
|
||||
@@ -30,12 +26,10 @@ pub async fn execute(
|
||||
.await?
|
||||
.ok_or(DomainError::ChannelNotFound(cmd.channel_id))?;
|
||||
|
||||
// Auto-snapshot the current config before overwriting
|
||||
deps.channel_command
|
||||
.save_config_snapshot(channel_id, channel.schedule_config(), None)
|
||||
.await?;
|
||||
|
||||
// Apply the snapshot's config
|
||||
channel.set_schedule_config(snapshot.config().clone());
|
||||
deps.channel_command.save(&channel).await?;
|
||||
|
||||
|
||||
@@ -5,9 +5,6 @@ use domain::{DomainError, DomainResult};
|
||||
use super::commands::SaveSnapshotCommand;
|
||||
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(
|
||||
deps: &ConfigSnapshotDeps,
|
||||
cmd: SaveSnapshotCommand,
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::sync::Arc;
|
||||
|
||||
use domain::ports::{ChannelQuery, ScheduleQuery};
|
||||
|
||||
/// Dependencies for IPTV export use cases.
|
||||
pub struct IptvDeps {
|
||||
pub channel_query: Arc<dyn ChannelQuery>,
|
||||
pub schedule_query: Arc<dyn ScheduleQuery>,
|
||||
|
||||
@@ -4,9 +4,6 @@ use domain::DomainResult;
|
||||
use super::deps::IptvDeps;
|
||||
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> {
|
||||
let channels = deps.channel_query.find_all().await?;
|
||||
let token = query.token.as_deref().unwrap_or("");
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/// Generate an M3U playlist for all channels.
|
||||
pub struct GetM3uQuery {
|
||||
pub base_url: String,
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
/// Generate an XMLTV EPG document for all channels.
|
||||
pub struct GetXmltvQuery;
|
||||
|
||||
@@ -8,10 +8,6 @@ use domain::DomainResult;
|
||||
use super::deps::IptvDeps;
|
||||
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> {
|
||||
let channels = deps.channel_query.find_all().await?;
|
||||
let now = Utc::now();
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/// Trigger a library sync for one or all providers.
|
||||
pub struct TriggerSyncCommand {
|
||||
/// Provider to sync. `None` means sync all registered providers.
|
||||
pub provider_id: Option<String>,
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::sync::Arc;
|
||||
|
||||
use domain::ports::{EventPublisher, IProviderRegistry, LibraryCommand, LibraryQuery, LibrarySyncAdapter};
|
||||
|
||||
/// Dependencies for library write use cases (trigger sync).
|
||||
pub struct LibraryCommandDeps {
|
||||
pub library_command: Arc<dyn LibraryCommand>,
|
||||
pub library_query: Arc<dyn LibraryQuery>,
|
||||
@@ -11,7 +10,6 @@ pub struct LibraryCommandDeps {
|
||||
pub event_publisher: Arc<dyn EventPublisher>,
|
||||
}
|
||||
|
||||
/// Dependencies for library read use cases (search, list, get).
|
||||
pub struct LibraryQueryDeps {
|
||||
pub library_query: Arc<dyn LibraryQuery>,
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ use domain::DomainResult;
|
||||
use super::deps::LibraryQueryDeps;
|
||||
use super::queries::GetItemQuery;
|
||||
|
||||
/// Get a single library item by its composite ID.
|
||||
pub async fn execute(
|
||||
deps: &LibraryQueryDeps,
|
||||
query: GetItemQuery,
|
||||
|
||||
@@ -4,7 +4,6 @@ use domain::DomainResult;
|
||||
use super::deps::LibraryQueryDeps;
|
||||
use super::queries::GetSyncStatusQuery;
|
||||
|
||||
/// Get the latest sync status per provider.
|
||||
pub async fn execute(
|
||||
deps: &LibraryQueryDeps,
|
||||
_query: GetSyncStatusQuery,
|
||||
|
||||
@@ -4,7 +4,6 @@ use domain::DomainResult;
|
||||
use super::deps::LibraryQueryDeps;
|
||||
use super::queries::ListCollectionsQuery;
|
||||
|
||||
/// List library collections, optionally filtered by provider.
|
||||
pub async fn execute(
|
||||
deps: &LibraryQueryDeps,
|
||||
query: ListCollectionsQuery,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use domain::errors::{DomainError, DomainResult};
|
||||
use domain::value_objects::ContentType;
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::deps::LibraryQueryDeps;
|
||||
use super::parse_content_type;
|
||||
use super::queries::ListGenresQuery;
|
||||
|
||||
/// List genres available in the library, optionally filtered.
|
||||
pub async fn execute(deps: &LibraryQueryDeps, query: ListGenresQuery) -> DomainResult<Vec<String>> {
|
||||
let content_type = query
|
||||
.content_type
|
||||
@@ -17,17 +16,6 @@ pub async fn execute(deps: &LibraryQueryDeps, query: ListGenresQuery) -> DomainR
|
||||
.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)]
|
||||
#[path = "tests/list_genres.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -4,7 +4,6 @@ use domain::DomainResult;
|
||||
use super::deps::LibraryQueryDeps;
|
||||
use super::queries::ListSeasonsQuery;
|
||||
|
||||
/// List season summaries for a specific series.
|
||||
pub async fn execute(
|
||||
deps: &LibraryQueryDeps,
|
||||
query: ListSeasonsQuery,
|
||||
|
||||
@@ -4,7 +4,6 @@ use domain::DomainResult;
|
||||
use super::deps::LibraryQueryDeps;
|
||||
use super::queries::ListShowsQuery;
|
||||
|
||||
/// List TV show summaries, optionally filtered.
|
||||
pub async fn execute(
|
||||
deps: &LibraryQueryDeps,
|
||||
query: ListShowsQuery,
|
||||
|
||||
@@ -16,3 +16,17 @@ pub use queries::{
|
||||
GetItemQuery, GetSyncStatusQuery, ListCollectionsQuery, ListGenresQuery, ListSeasonsQuery,
|
||||
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."
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/// Search library items with filters.
|
||||
pub struct SearchItemsQuery {
|
||||
pub provider_id: Option<String>,
|
||||
pub content_type: Option<String>,
|
||||
@@ -12,34 +11,28 @@ pub struct SearchItemsQuery {
|
||||
pub limit: u32,
|
||||
}
|
||||
|
||||
/// List library collections.
|
||||
pub struct ListCollectionsQuery {
|
||||
pub provider_id: Option<String>,
|
||||
}
|
||||
|
||||
/// List TV show summaries.
|
||||
pub struct ListShowsQuery {
|
||||
pub provider_id: Option<String>,
|
||||
pub search_term: Option<String>,
|
||||
pub genres: Vec<String>,
|
||||
}
|
||||
|
||||
/// List seasons for a specific series.
|
||||
pub struct ListSeasonsQuery {
|
||||
pub series_name: String,
|
||||
pub provider_id: Option<String>,
|
||||
}
|
||||
|
||||
/// List genres available in the library.
|
||||
pub struct ListGenresQuery {
|
||||
pub content_type: Option<String>,
|
||||
pub provider_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Get a single library item by its composite ID.
|
||||
pub struct GetItemQuery {
|
||||
pub item_id: String,
|
||||
}
|
||||
|
||||
/// Get the latest sync status per provider.
|
||||
pub struct GetSyncStatusQuery;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use domain::errors::{DomainError, DomainResult};
|
||||
use domain::DomainResult;
|
||||
use domain::models::LibraryItem;
|
||||
use domain::value_objects::{ContentType, LibrarySearchFilter};
|
||||
use domain::value_objects::LibrarySearchFilter;
|
||||
|
||||
use super::deps::LibraryQueryDeps;
|
||||
use super::parse_content_type;
|
||||
use super::queries::SearchItemsQuery;
|
||||
|
||||
/// Search library items with filters. Returns `(items, total_count)`.
|
||||
pub async fn execute(
|
||||
deps: &LibraryQueryDeps,
|
||||
query: SearchItemsQuery,
|
||||
@@ -48,17 +48,6 @@ pub async fn execute(
|
||||
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)]
|
||||
#[path = "tests/search.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -3,15 +3,6 @@ use domain::errors::{DomainError, DomainResult};
|
||||
use super::commands::TriggerSyncCommand;
|
||||
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(
|
||||
deps: &LibraryCommandDeps,
|
||||
cmd: TriggerSyncCommand,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/// Insert or update a provider configuration.
|
||||
pub struct UpsertProviderCommand {
|
||||
pub id: String,
|
||||
pub provider_type: String,
|
||||
@@ -6,7 +5,6 @@ pub struct UpsertProviderCommand {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// Delete a provider configuration.
|
||||
pub struct DeleteProviderCommand {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ use domain::DomainResult;
|
||||
use super::commands::DeleteProviderCommand;
|
||||
use super::deps::ProviderDeps;
|
||||
|
||||
/// Delete a provider configuration by ID.
|
||||
pub async fn execute(deps: &ProviderDeps, cmd: DeleteProviderCommand) -> DomainResult<()> {
|
||||
deps.provider_config_command.delete(&cmd.id).await
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::sync::Arc;
|
||||
|
||||
use domain::ports::{ProviderConfigCommand, ProviderConfigQuery};
|
||||
|
||||
/// Dependencies for provider config use cases.
|
||||
pub struct ProviderDeps {
|
||||
pub provider_config_command: Arc<dyn ProviderConfigCommand>,
|
||||
pub provider_config_query: Arc<dyn ProviderConfigQuery>,
|
||||
|
||||
@@ -4,7 +4,6 @@ use domain::DomainResult;
|
||||
use super::deps::ProviderDeps;
|
||||
use super::queries::GetProviderQuery;
|
||||
|
||||
/// Get a provider configuration by ID.
|
||||
pub async fn execute(
|
||||
deps: &ProviderDeps,
|
||||
query: GetProviderQuery,
|
||||
|
||||
@@ -4,7 +4,6 @@ use domain::DomainResult;
|
||||
use super::deps::ProviderDeps;
|
||||
use super::queries::ListProvidersQuery;
|
||||
|
||||
/// List all provider configurations.
|
||||
pub async fn execute(
|
||||
deps: &ProviderDeps,
|
||||
_query: ListProvidersQuery,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/// List all provider configurations.
|
||||
pub struct ListProvidersQuery;
|
||||
|
||||
/// Get a provider configuration by ID.
|
||||
pub struct GetProviderQuery {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ use domain::DomainResult;
|
||||
use super::commands::UpsertProviderCommand;
|
||||
use super::deps::ProviderDeps;
|
||||
|
||||
/// Insert or update a provider configuration.
|
||||
pub async fn execute(deps: &ProviderDeps, cmd: UpsertProviderCommand) -> DomainResult<()> {
|
||||
let row = ProviderConfigRow::from_persistence(
|
||||
cmd.id,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Generate a new schedule for a channel.
|
||||
pub struct GenerateScheduleCommand {
|
||||
pub channel_id: Uuid,
|
||||
}
|
||||
|
||||
/// Delete all schedules with generation > target_generation for a channel.
|
||||
pub struct DeleteSchedulesAfterCommand {
|
||||
pub channel_id: Uuid,
|
||||
pub target_generation: u32,
|
||||
|
||||
@@ -4,7 +4,6 @@ use domain::DomainResult;
|
||||
use super::commands::DeleteSchedulesAfterCommand;
|
||||
use super::deps::ScheduleDeps;
|
||||
|
||||
/// Delete all schedules with generation > target_generation for a channel.
|
||||
pub async fn execute(
|
||||
deps: &ScheduleDeps,
|
||||
cmd: DeleteSchedulesAfterCommand,
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::sync::Arc;
|
||||
use domain::ports::{ChannelQuery, EventPublisher, ScheduleCommand, ScheduleQuery};
|
||||
use domain::ScheduleEngineService;
|
||||
|
||||
/// Dependencies for schedule use cases.
|
||||
pub struct ScheduleDeps {
|
||||
pub schedule_engine: Arc<ScheduleEngineService>,
|
||||
pub channel_query: Arc<dyn ChannelQuery>,
|
||||
|
||||
@@ -8,10 +8,6 @@ use domain::DomainResult;
|
||||
use super::commands::GenerateScheduleCommand;
|
||||
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(
|
||||
deps: &ScheduleDeps,
|
||||
cmd: GenerateScheduleCommand,
|
||||
|
||||
@@ -7,9 +7,6 @@ use domain::DomainResult;
|
||||
use super::deps::ScheduleDeps;
|
||||
use super::queries::GetActiveScheduleQuery;
|
||||
|
||||
/// Fetch the schedule currently active at `now`.
|
||||
///
|
||||
/// Returns `None` when no schedule covers the current time.
|
||||
pub async fn execute(
|
||||
deps: &ScheduleDeps,
|
||||
query: GetActiveScheduleQuery,
|
||||
|
||||
@@ -7,10 +7,6 @@ use domain::{DomainResult, ScheduleEngineService};
|
||||
use super::deps::ScheduleDeps;
|
||||
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(
|
||||
deps: &ScheduleDeps,
|
||||
query: GetCurrentBroadcastQuery,
|
||||
|
||||
@@ -7,10 +7,6 @@ use domain::{DomainResult, ScheduleEngineService};
|
||||
use super::deps::ScheduleDeps;
|
||||
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(
|
||||
deps: &ScheduleDeps,
|
||||
query: GetEpgQuery,
|
||||
|
||||
@@ -5,9 +5,6 @@ use domain::DomainResult;
|
||||
use super::deps::ScheduleDeps;
|
||||
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> {
|
||||
let item_id = MediaItemId::new(&query.item_id);
|
||||
deps.schedule_engine
|
||||
|
||||
@@ -5,7 +5,6 @@ use domain::DomainResult;
|
||||
use super::deps::ScheduleDeps;
|
||||
use super::queries::ListHistoryQuery;
|
||||
|
||||
/// List all generated schedule headers for a channel, newest first.
|
||||
pub async fn execute(
|
||||
deps: &ScheduleDeps,
|
||||
query: ListHistoryQuery,
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Fetch the schedule whose validity window contains `now`.
|
||||
pub struct GetActiveScheduleQuery {
|
||||
pub channel_id: Uuid,
|
||||
}
|
||||
|
||||
/// Determine what is currently broadcasting on a channel.
|
||||
pub struct GetCurrentBroadcastQuery {
|
||||
pub channel_id: Uuid,
|
||||
}
|
||||
|
||||
/// Return EPG (electronic program guide) data for a channel.
|
||||
pub struct GetEpgQuery {
|
||||
pub channel_id: Uuid,
|
||||
}
|
||||
|
||||
/// Get a playback URL for a specific media item on a channel.
|
||||
pub struct GetStreamUrlQuery {
|
||||
pub channel_id: Uuid,
|
||||
/// MediaItemId as string (e.g. "jellyfin::abc123").
|
||||
pub item_id: String,
|
||||
}
|
||||
|
||||
/// List all generated schedule headers for a channel.
|
||||
pub struct ListHistoryQuery {
|
||||
pub channel_id: Uuid,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user