141 lines
4.4 KiB
Rust
141 lines
4.4 KiB
Rust
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<String>) -> Self {
|
||
Self(value.into())
|
||
}
|
||
|
||
pub fn into_inner(self) -> String {
|
||
self.0
|
||
}
|
||
}
|
||
|
||
impl AsRef<str> 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<String> 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<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,
|
||
}
|
||
}
|
||
}
|