- 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
177 lines
4.1 KiB
Rust
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;
|