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
This commit is contained in:
2026-07-12 07:41:57 +02:00
parent 39f5f99bfd
commit 60524c56f7
16 changed files with 547 additions and 10 deletions

View File

@@ -3,8 +3,8 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::value_objects::{
AccessMode, BlockId, ChannelId, FillStrategy, LogoPosition, MediaFilter, MediaItemId,
RotationPolicy, UserId, Weekday,
AccessMode, BlockId, ChannelId, FillStrategy, InterstitialRule, LogoPosition, MediaFilter,
MediaItemId, MidRollRule, RotationPolicy, UserId, Weekday,
};
const SECONDS_IN_DAY: u32 = 86_400;
@@ -29,6 +29,8 @@ pub struct Channel {
webhook_poll_interval_secs: u32,
webhook_body_template: Option<String>,
webhook_headers: Option<String>,
#[serde(default)]
gap_filler: Option<MediaFilter>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
@@ -50,6 +52,7 @@ pub struct ChannelRow {
pub webhook_poll_interval_secs: u32,
pub webhook_body_template: Option<String>,
pub webhook_headers: Option<String>,
pub gap_filler: Option<MediaFilter>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -78,6 +81,7 @@ impl Channel {
webhook_poll_interval_secs: DEFAULT_WEBHOOK_POLL_INTERVAL_SECS,
webhook_body_template: None,
webhook_headers: None,
gap_filler: None,
created_at: now,
updated_at: now,
}
@@ -101,6 +105,7 @@ impl Channel {
webhook_poll_interval_secs: row.webhook_poll_interval_secs,
webhook_body_template: row.webhook_body_template,
webhook_headers: row.webhook_headers,
gap_filler: row.gap_filler,
created_at: row.created_at,
updated_at: row.updated_at,
}
@@ -207,6 +212,15 @@ impl Channel {
self.auto_schedule = enabled;
self.updated_at = Utc::now();
}
pub fn gap_filler(&self) -> Option<&MediaFilter> {
self.gap_filler.as_ref()
}
pub fn set_gap_filler(&mut self, gap_filler: Option<MediaFilter>) {
self.gap_filler = gap_filler;
self.updated_at = Utc::now();
}
}
// deny_unknown_fields required so #[serde(untagged)] compat enum correctly rejects V1 payloads
@@ -324,6 +338,12 @@ pub struct ProgrammingBlock {
#[serde(default)]
ignore_rotation_policy: bool,
#[serde(default)]
interstitial_rule: Option<InterstitialRule>,
#[serde(default)]
mid_roll_rule: Option<MidRollRule>,
}
impl ProgrammingBlock {
@@ -345,6 +365,8 @@ impl ProgrammingBlock {
},
loop_on_finish: true,
ignore_rotation_policy: false,
interstitial_rule: None,
mid_roll_rule: None,
}
}
@@ -364,6 +386,8 @@ impl ProgrammingBlock {
},
loop_on_finish: true,
ignore_rotation_policy: false,
interstitial_rule: None,
mid_roll_rule: None,
}
}
@@ -394,6 +418,14 @@ impl ProgrammingBlock {
pub fn ignore_rotation_policy(&self) -> bool {
self.ignore_rotation_policy
}
pub fn interstitial_rule(&self) -> Option<&InterstitialRule> {
self.interstitial_rule.as_ref()
}
pub fn mid_roll_rule(&self) -> Option<&MidRollRule> {
self.mid_roll_rule.as_ref()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]