Files
k-tv/crates/domain/src/value_objects/scheduling.rs
Gabriel Kaszewski 60524c56f7 add domain concepts: FillStrategy variants, Chapter, InterstitialRule, MidRollRule, gap_filler
- FillStrategy: +Alternating (round-robin series), +Weighted (fresh-first), +Marathon (always ep1, loops)
- Chapter value object on MediaItem (JSON column in library_items)
- InterstitialRule + MidRollRule on ProgrammingBlock (data model only)
- gap_filler: Option<MediaFilter> on Channel (persisted as JSON column)
- migration: 20260712000002_add_chapters_and_gap_filler.sql
2026-07-12 07:41:57 +02:00

177 lines
4.1 KiB
Rust

use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContentType {
Movie,
Episode,
Short,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MediaFilter {
pub content_type: Option<ContentType>,
pub genres: Vec<String>,
pub decade: Option<u16>,
pub tags: Vec<String>,
pub min_duration_secs: Option<u32>,
pub max_duration_secs: Option<u32>,
pub collections: Vec<String>,
#[serde(default)]
pub series_names: Vec<String>,
pub search_term: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FillStrategy {
BestFit,
Sequential,
Random,
Alternating,
Weighted,
Marathon,
}
const DEFAULT_COOLDOWN_DAYS: u32 = 30;
const DEFAULT_MIN_AVAILABLE_RATIO: f32 = 0.2;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RotationPolicy {
pub cooldown_days: Option<u32>,
pub cooldown_generations: Option<u32>,
pub min_available_ratio: f32,
}
impl Default for RotationPolicy {
fn default() -> Self {
Self {
cooldown_days: Some(DEFAULT_COOLDOWN_DAYS),
cooldown_generations: None,
min_available_ratio: DEFAULT_MIN_AVAILABLE_RATIO,
}
}
}
#[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 first. Schedule engine depends on this ordering.
[
Weekday::Monday,
Weekday::Tuesday,
Weekday::Wednesday,
Weekday::Thursday,
Weekday::Friday,
Weekday::Saturday,
Weekday::Sunday,
]
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum MediaRole {
#[default]
Program,
Interstitial,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InterstitialRule {
pool_filter: MediaFilter,
strategy: FillStrategy,
min_gap_secs: u32,
}
impl InterstitialRule {
pub fn new(pool_filter: MediaFilter, strategy: FillStrategy, min_gap_secs: u32) -> Self {
Self {
pool_filter,
strategy,
min_gap_secs,
}
}
pub fn pool_filter(&self) -> &MediaFilter {
&self.pool_filter
}
pub fn strategy(&self) -> &FillStrategy {
&self.strategy
}
pub fn min_gap_secs(&self) -> u32 {
self.min_gap_secs
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MidRollRule {
prefer_chapters: bool,
fallback_interval_mins: u32,
break_duration_secs: u32,
pool_filter: MediaFilter,
}
impl MidRollRule {
pub fn new(
prefer_chapters: bool,
fallback_interval_mins: u32,
break_duration_secs: u32,
pool_filter: MediaFilter,
) -> Self {
Self {
prefer_chapters,
fallback_interval_mins,
break_duration_secs,
pool_filter,
}
}
pub fn prefer_chapters(&self) -> bool {
self.prefer_chapters
}
pub fn fallback_interval_mins(&self) -> u32 {
self.fallback_interval_mins
}
pub fn break_duration_secs(&self) -> u32 {
self.break_duration_secs
}
pub fn pool_filter(&self) -> &MediaFilter {
&self.pool_filter
}
}
#[cfg(test)]
#[path = "tests/scheduling.rs"]
mod tests;