domain value objects: auth, scheduling, channel, oidc, search
This commit is contained in:
144
crates/domain/src/value_objects/scheduling.rs
Normal file
144
crates/domain/src/value_objects/scheduling.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The broad category of a media item.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ContentType {
|
||||
Movie,
|
||||
Episode,
|
||||
Short,
|
||||
}
|
||||
|
||||
/// Provider-agnostic filter for querying media items.
|
||||
///
|
||||
/// Each field is optional -- omitting it means "no constraint on this dimension".
|
||||
/// The `IMediaProvider` adapter interprets these fields in terms of its own API.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct MediaFilter {
|
||||
pub content_type: Option<ContentType>,
|
||||
pub genres: Vec<String>,
|
||||
/// Starting year of a decade: 1990 means 1990-1999.
|
||||
pub decade: Option<u16>,
|
||||
pub tags: Vec<String>,
|
||||
pub min_duration_secs: Option<u32>,
|
||||
pub max_duration_secs: Option<u32>,
|
||||
/// Abstract groupings interpreted by each provider (Jellyfin library, Plex section,
|
||||
/// filesystem path, etc.). An empty list means "all available content".
|
||||
pub collections: Vec<String>,
|
||||
/// Filter to one or more TV series by name. Use with `content_type: Episode`.
|
||||
/// With `Sequential` strategy each series plays in chronological order.
|
||||
/// Multiple series are OR-combined: any episode from any listed show is eligible.
|
||||
#[serde(default)]
|
||||
pub series_names: Vec<String>,
|
||||
/// Free-text search term. Intended for library browsing; typically omitted
|
||||
/// during schedule generation.
|
||||
pub search_term: Option<String>,
|
||||
}
|
||||
|
||||
/// How the scheduling engine fills a time block with selected media items.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FillStrategy {
|
||||
/// Greedy bin-packing: at each step pick the longest item that still fits,
|
||||
/// minimising dead air. Good for variety blocks.
|
||||
BestFit,
|
||||
/// Pick items in the order returned by the provider -- ideal for series
|
||||
/// where episode sequence matters.
|
||||
Sequential,
|
||||
/// Shuffle the pool randomly then fill sequentially. Good for "shuffle play" channels.
|
||||
Random,
|
||||
}
|
||||
|
||||
/// Controls when previously aired items become eligible to play again.
|
||||
///
|
||||
/// An item is *on cooldown* if *either* threshold is met.
|
||||
/// `min_available_ratio` is a safety valve: if honouring the cooldown would
|
||||
/// leave fewer items than this fraction of the total pool, the cooldown is
|
||||
/// ignored and all items become eligible. This prevents small libraries from
|
||||
/// running completely dry.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RecyclePolicy {
|
||||
/// Do not replay an item within this many calendar days.
|
||||
pub cooldown_days: Option<u32>,
|
||||
/// Do not replay an item within this many schedule generations.
|
||||
pub cooldown_generations: Option<u32>,
|
||||
/// Always keep at least this fraction (0.0-1.0) of the matching pool
|
||||
/// available for selection, even if their cooldown has not yet expired.
|
||||
pub min_available_ratio: f32,
|
||||
}
|
||||
|
||||
impl Default for RecyclePolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cooldown_days: Some(30),
|
||||
cooldown_generations: None,
|
||||
min_available_ratio: 0.2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Day of week, used as key in weekly schedule configs.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Weekday {
|
||||
Monday,
|
||||
Tuesday,
|
||||
Wednesday,
|
||||
Thursday,
|
||||
Friday,
|
||||
Saturday,
|
||||
Sunday,
|
||||
}
|
||||
|
||||
impl From<chrono::Weekday> for Weekday {
|
||||
fn from(w: chrono::Weekday) -> Self {
|
||||
match w {
|
||||
chrono::Weekday::Mon => Weekday::Monday,
|
||||
chrono::Weekday::Tue => Weekday::Tuesday,
|
||||
chrono::Weekday::Wed => Weekday::Wednesday,
|
||||
chrono::Weekday::Thu => Weekday::Thursday,
|
||||
chrono::Weekday::Fri => Weekday::Friday,
|
||||
chrono::Weekday::Sat => Weekday::Saturday,
|
||||
chrono::Weekday::Sun => Weekday::Sunday,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Weekday {
|
||||
pub fn all() -> [Weekday; 7] {
|
||||
// ISO week order: Monday = index 0, Sunday = index 6.
|
||||
// The schedule engine depends on this order when iterating days.
|
||||
[
|
||||
Weekday::Monday,
|
||||
Weekday::Tuesday,
|
||||
Weekday::Wednesday,
|
||||
Weekday::Thursday,
|
||||
Weekday::Friday,
|
||||
Weekday::Saturday,
|
||||
Weekday::Sunday,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_chrono_weekday_all_variants() {
|
||||
assert_eq!(Weekday::from(chrono::Weekday::Mon), Weekday::Monday);
|
||||
assert_eq!(Weekday::from(chrono::Weekday::Tue), Weekday::Tuesday);
|
||||
assert_eq!(Weekday::from(chrono::Weekday::Wed), Weekday::Wednesday);
|
||||
assert_eq!(Weekday::from(chrono::Weekday::Thu), Weekday::Thursday);
|
||||
assert_eq!(Weekday::from(chrono::Weekday::Fri), Weekday::Friday);
|
||||
assert_eq!(Weekday::from(chrono::Weekday::Sat), Weekday::Saturday);
|
||||
assert_eq!(Weekday::from(chrono::Weekday::Sun), Weekday::Sunday);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_returns_monday_first_sunday_last() {
|
||||
let days = Weekday::all();
|
||||
assert_eq!(days[0], Weekday::Monday);
|
||||
assert_eq!(days[6], Weekday::Sunday);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user