domain crate code quality cleanup
strip all comments, extract tests to tests/ dirs, remove #[allow(clippy::...)], extract magic numbers to constants, refactor schedule engine private methods to use param structs, add Default impls, clippy.toml for persistence constructors
This commit is contained in:
@@ -1,17 +1,11 @@
|
||||
//! Activity log port.
|
||||
//!
|
||||
//! Records user and system actions for the admin dashboard's activity feed.
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::errors::DomainResult;
|
||||
use crate::models::ActivityEvent;
|
||||
use crate::value_objects::ChannelId;
|
||||
|
||||
/// Port for activity log persistence.
|
||||
#[async_trait]
|
||||
pub trait ActivityLogCommand: Send + Sync {
|
||||
/// Log a new activity event.
|
||||
async fn log(
|
||||
&self,
|
||||
event_type: &str,
|
||||
@@ -20,9 +14,7 @@ pub trait ActivityLogCommand: Send + Sync {
|
||||
) -> DomainResult<()>;
|
||||
}
|
||||
|
||||
/// Port for reading activity log entries.
|
||||
#[async_trait]
|
||||
pub trait ActivityLogQuery: Send + Sync {
|
||||
/// Retrieve the most recent activity events.
|
||||
async fn recent(&self, limit: u32) -> DomainResult<Vec<ActivityEvent>>;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
//! Authentication port.
|
||||
//!
|
||||
//! Abstracts password hashing and verification so the domain layer
|
||||
//! never depends on a specific hashing algorithm (bcrypt, argon2, etc.).
|
||||
|
||||
use crate::errors::DomainResult;
|
||||
|
||||
/// Port for password hashing and verification.
|
||||
///
|
||||
/// Implementations live in the infra layer (e.g. `BcryptAuthService`).
|
||||
/// These methods are intentionally synchronous — hashing libraries are CPU-bound
|
||||
/// and should be spawned on a blocking thread pool by the caller if needed.
|
||||
// Intentionally sync: CPU-bound hashing should run on a blocking thread pool
|
||||
pub trait AuthService: Send + Sync {
|
||||
/// Hash a plaintext password and return the encoded hash string.
|
||||
fn hash_password(&self, password: &str) -> DomainResult<String>;
|
||||
|
||||
/// Verify a plaintext password against an encoded hash.
|
||||
fn verify_password(&self, password: &str, hash: &str) -> DomainResult<bool>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
//! Channel persistence ports (CQRS split).
|
||||
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -7,19 +5,12 @@ use crate::errors::DomainResult;
|
||||
use crate::models::{Channel, ChannelConfigSnapshot, ScheduleConfig};
|
||||
use crate::value_objects::{ChannelId, UserId};
|
||||
|
||||
/// Write-side port for channel persistence.
|
||||
#[async_trait]
|
||||
pub trait ChannelCommand: Send + Sync {
|
||||
/// Insert or update a channel.
|
||||
async fn save(&self, channel: &Channel) -> DomainResult<()>;
|
||||
|
||||
/// Delete a channel by ID.
|
||||
async fn delete(&self, id: ChannelId) -> DomainResult<()>;
|
||||
|
||||
/// Snapshot the current config before saving a new one.
|
||||
///
|
||||
/// `version_num` is computed by the infra layer as `MAX(version_num)+1`
|
||||
/// inside a transaction.
|
||||
async fn save_config_snapshot(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
@@ -27,7 +18,6 @@ pub trait ChannelCommand: Send + Sync {
|
||||
label: Option<String>,
|
||||
) -> DomainResult<ChannelConfigSnapshot>;
|
||||
|
||||
/// Update the label on an existing config snapshot.
|
||||
async fn patch_config_snapshot_label(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
@@ -36,28 +26,21 @@ pub trait ChannelCommand: Send + Sync {
|
||||
) -> DomainResult<Option<ChannelConfigSnapshot>>;
|
||||
}
|
||||
|
||||
/// Read-side port for channel persistence.
|
||||
#[async_trait]
|
||||
pub trait ChannelQuery: Send + Sync {
|
||||
/// Find a channel by its ID.
|
||||
async fn find_by_id(&self, id: ChannelId) -> DomainResult<Option<Channel>>;
|
||||
|
||||
/// Find all channels owned by a user.
|
||||
async fn find_by_owner(&self, owner_id: UserId) -> DomainResult<Vec<Channel>>;
|
||||
|
||||
/// List all channels.
|
||||
async fn find_all(&self) -> DomainResult<Vec<Channel>>;
|
||||
|
||||
/// Find channels with auto-schedule enabled.
|
||||
async fn find_auto_schedule_enabled(&self) -> DomainResult<Vec<Channel>>;
|
||||
|
||||
/// List all config snapshots for a channel, newest first.
|
||||
async fn list_config_snapshots(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
) -> DomainResult<Vec<ChannelConfigSnapshot>>;
|
||||
|
||||
/// Get a specific config snapshot by channel and snapshot ID.
|
||||
async fn get_config_snapshot(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
||||
@@ -1,38 +1,19 @@
|
||||
//! Domain event ports.
|
||||
//!
|
||||
//! Minimal event infrastructure for publishing domain events.
|
||||
//! The consumer/handler side is intentionally simple — no subscription
|
||||
//! mechanism yet; handlers are registered at startup and dispatched
|
||||
//! synchronously by the publisher.
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::errors::DomainResult;
|
||||
pub use crate::events::DomainEvent;
|
||||
|
||||
/// Port for publishing domain events.
|
||||
///
|
||||
/// Implementations may log, fan out to handlers, or push to a message bus.
|
||||
#[async_trait]
|
||||
pub trait EventPublisher: Send + Sync {
|
||||
/// Publish a single domain event.
|
||||
async fn publish(&self, event: DomainEvent) -> DomainResult<()>;
|
||||
}
|
||||
|
||||
/// Port for consuming domain events from a queue or channel.
|
||||
#[async_trait]
|
||||
pub trait EventConsumer: Send + Sync {
|
||||
/// Block until the next event is available and return it.
|
||||
async fn recv(&self) -> DomainResult<DomainEvent>;
|
||||
}
|
||||
|
||||
/// Port for handling domain events.
|
||||
///
|
||||
/// Each handler is responsible for one side-effect (e.g. logging, webhook
|
||||
/// dispatch, cache invalidation). Handlers are registered at startup.
|
||||
#[async_trait]
|
||||
pub trait EventHandler: Send + Sync {
|
||||
/// Handle a domain event. Errors are logged but do not abort the
|
||||
/// originating operation.
|
||||
async fn handle(&self, event: &DomainEvent) -> DomainResult<()>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
//! Library persistence ports (CQRS split) and sync adapter.
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::errors::DomainResult;
|
||||
@@ -11,58 +9,43 @@ use crate::value_objects::{ContentType, LibrarySearchFilter};
|
||||
|
||||
use super::media::IMediaProvider;
|
||||
|
||||
/// Write-side port for library persistence.
|
||||
#[async_trait]
|
||||
pub trait LibraryCommand: Send + Sync {
|
||||
/// Upsert a batch of library items for a given provider.
|
||||
async fn upsert_items(&self, provider_id: &str, items: Vec<LibraryItem>) -> DomainResult<()>;
|
||||
|
||||
/// Remove all items belonging to a provider (used before full re-sync).
|
||||
async fn clear_provider(&self, provider_id: &str) -> DomainResult<()>;
|
||||
|
||||
/// Create a sync log entry marking the start of a sync run.
|
||||
/// Returns the log entry ID for later completion.
|
||||
async fn log_sync_start(&self, provider_id: &str) -> DomainResult<i64>;
|
||||
|
||||
/// Mark a sync log entry as finished with the given result.
|
||||
async fn log_sync_finish(&self, log_id: i64, result: &LibrarySyncResult) -> DomainResult<()>;
|
||||
}
|
||||
|
||||
/// Read-side port for library persistence.
|
||||
#[async_trait]
|
||||
pub trait LibraryQuery: Send + Sync {
|
||||
/// Search the library with the given filter. Returns (items, total_count).
|
||||
async fn search(
|
||||
&self,
|
||||
filter: &LibrarySearchFilter,
|
||||
) -> DomainResult<(Vec<LibraryItem>, u32)>;
|
||||
|
||||
/// Get a single library item by its composite ID.
|
||||
async fn get_by_id(&self, id: &str) -> DomainResult<Option<LibraryItem>>;
|
||||
|
||||
/// List all collections, optionally filtered by provider.
|
||||
async fn list_collections(
|
||||
&self,
|
||||
provider_id: Option<&str>,
|
||||
) -> DomainResult<Vec<LibraryCollection>>;
|
||||
|
||||
/// List all unique series names, optionally filtered by provider.
|
||||
async fn list_series(&self, provider_id: Option<&str>) -> DomainResult<Vec<String>>;
|
||||
|
||||
/// List all genres, optionally filtered by content type and provider.
|
||||
async fn list_genres(
|
||||
&self,
|
||||
content_type: Option<&ContentType>,
|
||||
provider_id: Option<&str>,
|
||||
) -> DomainResult<Vec<String>>;
|
||||
|
||||
/// Get the latest sync log entries (one per provider).
|
||||
async fn latest_sync_status(&self) -> DomainResult<Vec<LibrarySyncLogEntry>>;
|
||||
|
||||
/// Check whether a sync is currently running for a provider.
|
||||
async fn is_sync_running(&self, provider_id: &str) -> DomainResult<bool>;
|
||||
|
||||
/// List TV show summaries, optionally filtered by provider, search term, and genres.
|
||||
async fn list_shows(
|
||||
&self,
|
||||
provider_id: Option<&str>,
|
||||
@@ -70,7 +53,6 @@ pub trait LibraryQuery: Send + Sync {
|
||||
genres: &[String],
|
||||
) -> DomainResult<Vec<ShowSummary>>;
|
||||
|
||||
/// List season summaries for a specific series.
|
||||
async fn list_seasons(
|
||||
&self,
|
||||
series_name: &str,
|
||||
@@ -78,9 +60,6 @@ pub trait LibraryQuery: Send + Sync {
|
||||
) -> DomainResult<Vec<SeasonSummary>>;
|
||||
}
|
||||
|
||||
/// Port: sync one provider's items into the library.
|
||||
///
|
||||
/// DB writes are handled entirely inside implementations — no pool in the trait.
|
||||
#[async_trait]
|
||||
pub trait LibrarySyncAdapter: Send + Sync {
|
||||
async fn sync_provider(
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
//! Media provider ports and associated types.
|
||||
//!
|
||||
//! Abstract interfaces for fetching media from any source.
|
||||
//! The domain never knows whether the backing provider is Jellyfin, Plex,
|
||||
//! a local filesystem, or anything else — adapters in the infra crate implement
|
||||
//! these traits for each concrete source.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -12,37 +5,19 @@ use crate::errors::{DomainError, DomainResult};
|
||||
use crate::models::MediaItem;
|
||||
use crate::value_objects::{ContentType, MediaFilter, MediaItemId};
|
||||
|
||||
// ============================================================================
|
||||
// Stream quality
|
||||
// ============================================================================
|
||||
|
||||
/// Requested stream quality for `get_stream_url`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum StreamQuality {
|
||||
/// Try direct stream via PlaybackInfo; fall back to HLS at 8 Mbps.
|
||||
Direct,
|
||||
/// Force HLS transcode at this bitrate (bits per second).
|
||||
Transcode(u32),
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Provider capabilities
|
||||
// ============================================================================
|
||||
|
||||
/// How a provider delivers video to the client.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum StreamingProtocol {
|
||||
/// HLS playlist (`.m3u8`). Requires hls.js on non-Safari browsers.
|
||||
Hls,
|
||||
/// Direct file URL with Range-header support. Native `<video>` element.
|
||||
DirectFile,
|
||||
}
|
||||
|
||||
/// Feature matrix for a media provider.
|
||||
///
|
||||
/// The API and frontend use this to gate calls and hide UI controls that
|
||||
/// the active provider does not support.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProviderCapabilities {
|
||||
pub collections: bool,
|
||||
@@ -52,96 +27,46 @@ pub struct ProviderCapabilities {
|
||||
pub decade: bool,
|
||||
pub search: bool,
|
||||
pub streaming_protocol: StreamingProtocol,
|
||||
/// Whether `POST /files/rescan` is available.
|
||||
pub rescan: bool,
|
||||
/// Whether on-demand FFmpeg transcoding to HLS is available.
|
||||
pub transcode: bool,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Library browsing types
|
||||
// ============================================================================
|
||||
|
||||
/// A top-level media collection / library exposed by a provider.
|
||||
///
|
||||
/// In Jellyfin this maps to a virtual library (Movies, TV Shows, ...).
|
||||
/// In Plex it maps to a section. The `id` is provider-specific and is used
|
||||
/// as the value for `MediaFilter::collections`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Collection {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
/// Provider-specific type hint, e.g. "movies", "tvshows". `None` when the
|
||||
/// provider does not expose this information.
|
||||
pub collection_type: Option<String>,
|
||||
}
|
||||
|
||||
/// Lightweight summary of a TV series available in the provider's library.
|
||||
/// Returned by `IMediaProvider::list_series` for the dashboard browser.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SeriesSummary {
|
||||
/// Provider-specific series ID (opaque -- used for ParentId filtering).
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
/// Total number of episodes across all seasons, if the provider exposes it.
|
||||
pub episode_count: u32,
|
||||
pub genres: Vec<String>,
|
||||
pub year: Option<u16>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// IMediaProvider
|
||||
// ============================================================================
|
||||
|
||||
/// Port for reading media content from an external provider.
|
||||
///
|
||||
/// Implementations live in the infra layer. One adapter per provider type
|
||||
/// (e.g. `JellyfinMediaProvider`, `PlexMediaProvider`, `LocalFileProvider`).
|
||||
///
|
||||
/// The three browsing methods (`list_collections`, `list_series`, `list_genres`)
|
||||
/// have default implementations that return an `InfrastructureError`. Adapters
|
||||
/// that support library browsing override them; those that don't (e.g. the
|
||||
/// `NoopMediaProvider`) inherit the default and return a clear error.
|
||||
#[async_trait]
|
||||
pub trait IMediaProvider: Send + Sync {
|
||||
/// Declare what features this provider supports.
|
||||
///
|
||||
/// Called at request time (not cached) so the response always reflects the
|
||||
/// active provider. Implementations return a plain struct -- no I/O needed.
|
||||
fn capabilities(&self) -> ProviderCapabilities;
|
||||
|
||||
/// Fetch metadata for all items matching `filter` from this provider.
|
||||
///
|
||||
/// The provider interprets each field of `MediaFilter` in terms of its own
|
||||
/// API (e.g. Jellyfin libraries, Plex sections, filesystem paths).
|
||||
/// Returns an empty vec -- not an error -- when nothing matches.
|
||||
async fn fetch_items(&self, filter: &MediaFilter) -> DomainResult<Vec<MediaItem>>;
|
||||
|
||||
/// Fetch metadata for a single item by its opaque ID.
|
||||
///
|
||||
/// Used by the scheduler when resolving `BlockContent::Manual` blocks, where
|
||||
/// the user has hand-picked specific items. Returns `None` if the item no
|
||||
/// longer exists in the provider (deleted, unavailable, etc.).
|
||||
async fn fetch_by_id(&self, item_id: &MediaItemId) -> DomainResult<Option<MediaItem>>;
|
||||
|
||||
/// Get a playback URL for an item, called on-demand at tune-in time.
|
||||
///
|
||||
/// URLs are intentionally *not* stored in the schedule because they may be
|
||||
/// short-lived (signed URLs, session tokens) or depend on client context.
|
||||
async fn get_stream_url(
|
||||
&self,
|
||||
item_id: &MediaItemId,
|
||||
quality: &StreamQuality,
|
||||
) -> DomainResult<String>;
|
||||
|
||||
/// List top-level collections (libraries/sections) available in this provider.
|
||||
async fn list_collections(&self) -> DomainResult<Vec<Collection>> {
|
||||
Err(DomainError::InfrastructureError(
|
||||
"list_collections is not supported by this provider".into(),
|
||||
))
|
||||
}
|
||||
|
||||
/// List TV series available in an optional collection.
|
||||
async fn list_series(&self, collection_id: Option<&str>) -> DomainResult<Vec<SeriesSummary>> {
|
||||
let _ = collection_id;
|
||||
Err(DomainError::InfrastructureError(
|
||||
@@ -149,7 +74,6 @@ pub trait IMediaProvider: Send + Sync {
|
||||
))
|
||||
}
|
||||
|
||||
/// List all genres available for a given content type.
|
||||
async fn list_genres(
|
||||
&self,
|
||||
content_type: Option<&ContentType>,
|
||||
@@ -161,56 +85,36 @@ pub trait IMediaProvider: Send + Sync {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// IProviderRegistry
|
||||
// ============================================================================
|
||||
|
||||
/// Port for routing media operations across multiple named providers.
|
||||
///
|
||||
/// The registry holds all configured providers (Jellyfin, local files, ...)
|
||||
/// and dispatches each call to the right one. Item IDs are prefixed with the
|
||||
/// provider key (e.g. `"jellyfin::abc123"`, `"local::base64path"`) so every
|
||||
/// fetch and stream call is self-routing.
|
||||
#[async_trait]
|
||||
pub trait IProviderRegistry: Send + Sync {
|
||||
/// Fetch items from a named provider (used by Algorithmic blocks).
|
||||
/// Empty `provider_id` uses the primary provider.
|
||||
async fn fetch_items(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
filter: &MediaFilter,
|
||||
) -> DomainResult<Vec<MediaItem>>;
|
||||
|
||||
/// Fetch a single item by its (possibly prefixed) ID.
|
||||
async fn fetch_by_id(&self, item_id: &MediaItemId) -> DomainResult<Option<MediaItem>>;
|
||||
|
||||
/// Get a playback URL. Routes via prefix in `item_id`.
|
||||
async fn get_stream_url(
|
||||
&self,
|
||||
item_id: &MediaItemId,
|
||||
quality: &StreamQuality,
|
||||
) -> DomainResult<String>;
|
||||
|
||||
/// List all registered provider keys in registration order.
|
||||
fn provider_ids(&self) -> Vec<String>;
|
||||
|
||||
/// Key of the primary (first-registered) provider.
|
||||
fn primary_id(&self) -> &str;
|
||||
|
||||
/// Capability matrix for a specific provider. Returns `None` if the key is unknown.
|
||||
fn capabilities(&self, provider_id: &str) -> Option<ProviderCapabilities>;
|
||||
|
||||
/// List collections for a provider. Empty `provider_id` = primary.
|
||||
async fn list_collections(&self, provider_id: &str) -> DomainResult<Vec<Collection>>;
|
||||
|
||||
/// List series for a provider. Empty `provider_id` = primary.
|
||||
async fn list_series(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
collection_id: Option<&str>,
|
||||
) -> DomainResult<Vec<SeriesSummary>>;
|
||||
|
||||
/// List genres for a provider. Empty `provider_id` = primary.
|
||||
async fn list_genres(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
//! Domain ports (trait definitions).
|
||||
//!
|
||||
//! These traits define the abstract interfaces that infrastructure adapters
|
||||
//! implement. The domain layer depends only on these traits, never on concrete
|
||||
//! implementations.
|
||||
//!
|
||||
//! Repository traits follow a CQRS split: separate Command (write) and Query
|
||||
//! (read) traits for each aggregate. Small or rarely-split repositories keep
|
||||
//! a single trait when the split adds no value.
|
||||
|
||||
pub mod activity;
|
||||
pub mod auth;
|
||||
pub mod channel;
|
||||
@@ -20,8 +10,6 @@ pub mod settings;
|
||||
pub mod transcode;
|
||||
pub mod user;
|
||||
|
||||
// -- Re-exports for convenience --
|
||||
|
||||
pub use activity::{ActivityLogCommand, ActivityLogQuery};
|
||||
pub use auth::AuthService;
|
||||
pub use channel::{ChannelCommand, ChannelQuery};
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
//! Provider configuration port (CQRS split).
|
||||
//!
|
||||
//! Stores the JSON configuration blob for registered media providers
|
||||
//! (e.g. Jellyfin URL + API key).
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::errors::DomainResult;
|
||||
use crate::models::ProviderConfigRow;
|
||||
|
||||
/// Write-side port for provider configuration persistence.
|
||||
#[async_trait]
|
||||
pub trait ProviderConfigCommand: Send + Sync {
|
||||
/// Insert or update a provider configuration.
|
||||
async fn upsert(&self, row: &ProviderConfigRow) -> DomainResult<()>;
|
||||
|
||||
/// Delete a provider configuration by ID.
|
||||
async fn delete(&self, id: &str) -> DomainResult<()>;
|
||||
}
|
||||
|
||||
/// Read-side port for provider configuration persistence.
|
||||
#[async_trait]
|
||||
pub trait ProviderConfigQuery: Send + Sync {
|
||||
/// Get all provider configurations.
|
||||
async fn get_all(&self) -> DomainResult<Vec<ProviderConfigRow>>;
|
||||
|
||||
/// Get a provider configuration by ID.
|
||||
async fn get_by_id(&self, id: &str) -> DomainResult<Option<ProviderConfigRow>>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
//! Schedule persistence ports (CQRS split).
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use async_trait::async_trait;
|
||||
@@ -8,19 +6,12 @@ use crate::errors::DomainResult;
|
||||
use crate::models::{GeneratedSchedule, PlaybackRecord};
|
||||
use crate::value_objects::{BlockId, ChannelId, MediaItemId, ScheduleId};
|
||||
|
||||
/// Write-side port for schedule and playback persistence.
|
||||
#[async_trait]
|
||||
pub trait ScheduleCommand: Send + Sync {
|
||||
/// Insert or replace a generated schedule.
|
||||
async fn save(&self, schedule: &GeneratedSchedule) -> DomainResult<()>;
|
||||
|
||||
/// Persist a playback record (item was aired on a channel).
|
||||
async fn save_playback_record(&self, record: &PlaybackRecord) -> DomainResult<()>;
|
||||
|
||||
/// Delete all schedules with generation > `target_generation` for this channel.
|
||||
///
|
||||
/// Also deletes matching playback_records (no DB cascade between those tables).
|
||||
/// `scheduled_slots` cascade via FK from `generated_schedules`.
|
||||
async fn delete_schedules_after(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
@@ -28,44 +19,34 @@ pub trait ScheduleCommand: Send + Sync {
|
||||
) -> DomainResult<()>;
|
||||
}
|
||||
|
||||
/// Read-side port for schedule and playback persistence.
|
||||
#[async_trait]
|
||||
pub trait ScheduleQuery: Send + Sync {
|
||||
/// Find the schedule whose `[valid_from, valid_until)` window contains `at`.
|
||||
async fn find_active(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
at: DateTime<Utc>,
|
||||
) -> DomainResult<Option<GeneratedSchedule>>;
|
||||
|
||||
/// Find the most recently generated schedule for a channel.
|
||||
/// Used to derive the next generation number.
|
||||
async fn find_latest(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
) -> DomainResult<Option<GeneratedSchedule>>;
|
||||
|
||||
/// All playback records for a channel, used by the recycle policy engine.
|
||||
async fn find_playback_history(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
) -> DomainResult<Vec<PlaybackRecord>>;
|
||||
|
||||
/// Return the most recent slot per block_id across ALL schedules for a channel.
|
||||
///
|
||||
/// Resilient to any single generation having empty slots for a block.
|
||||
async fn find_last_slot_per_block(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
) -> DomainResult<HashMap<BlockId, MediaItemId>>;
|
||||
|
||||
/// List all generated schedule headers for a channel, newest first.
|
||||
async fn list_schedule_history(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
) -> DomainResult<Vec<GeneratedSchedule>>;
|
||||
|
||||
/// Fetch a specific schedule with its slots, verifying channel ownership.
|
||||
async fn get_schedule_by_id(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
//! Application settings port.
|
||||
//!
|
||||
//! Key-value admin configuration (e.g. `library_sync_interval_hours`).
|
||||
//! Small enough to keep as a single trait rather than CQRS split.
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::errors::DomainResult;
|
||||
|
||||
/// Port for general admin settings persistence (`app_settings` table).
|
||||
#[async_trait]
|
||||
pub trait AppSettingsRepository: Send + Sync {
|
||||
/// Get a setting value by key. Returns `None` if not set.
|
||||
async fn get(&self, key: &str) -> DomainResult<Option<String>>;
|
||||
|
||||
/// Set a setting value (upsert).
|
||||
async fn set(&self, key: &str, value: &str) -> DomainResult<()>;
|
||||
|
||||
/// Get all settings as (key, value) pairs.
|
||||
async fn get_all(&self) -> DomainResult<Vec<(String, String)>>;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
//! Transcode settings port.
|
||||
//!
|
||||
//! Persists FFmpeg transcoding configuration (cleanup TTL, etc.).
|
||||
//! Small enough to keep as a single trait.
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::errors::DomainResult;
|
||||
|
||||
/// Port for transcode settings persistence.
|
||||
#[async_trait]
|
||||
pub trait TranscodeSettingsRepository: Send + Sync {
|
||||
/// Load the persisted cleanup TTL. Returns `None` if no row exists yet.
|
||||
async fn load_cleanup_ttl(&self) -> DomainResult<Option<u32>>;
|
||||
|
||||
/// Persist the cleanup TTL (upsert -- always row id=1).
|
||||
async fn save_cleanup_ttl(&self, hours: u32) -> DomainResult<()>;
|
||||
}
|
||||
|
||||
@@ -1,33 +1,23 @@
|
||||
//! User persistence ports (CQRS split).
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::errors::DomainResult;
|
||||
use crate::models::User;
|
||||
use crate::value_objects::UserId;
|
||||
|
||||
/// Write-side port for user persistence.
|
||||
#[async_trait]
|
||||
pub trait UserCommand: Send + Sync {
|
||||
/// Insert or update a user.
|
||||
async fn save(&self, user: &User) -> DomainResult<()>;
|
||||
|
||||
/// Delete a user by their internal ID.
|
||||
async fn delete(&self, id: UserId) -> DomainResult<()>;
|
||||
}
|
||||
|
||||
/// Read-side port for user persistence.
|
||||
#[async_trait]
|
||||
pub trait UserQuery: Send + Sync {
|
||||
/// Find a user by their internal ID.
|
||||
async fn find_by_id(&self, id: UserId) -> DomainResult<Option<User>>;
|
||||
|
||||
/// Find a user by their OIDC subject (used for authentication).
|
||||
async fn find_by_subject(&self, subject: &str) -> DomainResult<Option<User>>;
|
||||
|
||||
/// Find a user by their email address.
|
||||
async fn find_by_email(&self, email: &str) -> DomainResult<Option<User>>;
|
||||
|
||||
/// Count total number of users (used for first-user admin promotion).
|
||||
async fn count_users(&self) -> DomainResult<u64>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user