domain value objects: auth, scheduling, channel, oidc, search

This commit is contained in:
2026-07-12 01:05:31 +02:00
parent 3ff146615f
commit b2e71403d8
7 changed files with 1004 additions and 0 deletions

View 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);
}
}