use serde::{Deserialize, Serialize}; use std::fmt; /// Position of the channel logo watermark overlay. #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename_all = "snake_case")] pub enum LogoPosition { TopLeft, #[default] TopRight, BottomLeft, BottomRight, } /// Controls who can view a channel's broadcast and stream. #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum AccessMode { #[default] Public, PasswordProtected, AccountRequired, OwnerOnly, } /// Opaque media item identifier — format is provider-specific internally. /// The domain never inspects the string; it just passes it back to the provider. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct MediaItemId(String); impl MediaItemId { pub fn new(value: impl Into) -> Self { Self(value.into()) } pub fn into_inner(self) -> String { self.0 } } impl AsRef for MediaItemId { fn as_ref(&self) -> &str { &self.0 } } impl fmt::Display for MediaItemId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } impl From for MediaItemId { fn from(s: String) -> Self { Self(s) } } impl From<&str> for MediaItemId { fn from(s: &str) -> Self { Self(s.to_string()) } } /// 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, pub genres: Vec, /// Starting year of a decade: 1990 means 1990–1999. pub decade: Option, pub tags: Vec, pub min_duration_secs: Option, pub max_duration_secs: Option, /// Abstract groupings interpreted by each provider (Jellyfin library, Plex section, /// filesystem path, etc.). An empty list means "all available content". pub collections: Vec, /// 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, /// Free-text search term. Intended for library browsing; typically omitted /// during schedule generation. pub search_term: Option, } /// 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, /// Do not replay an item within this many schedule generations. pub cooldown_generations: Option, /// 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, } } }