rename RecyclePolicy→RotationPolicy, collapse AccessMode, clean OIDC refs
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use domain::{DomainError, RecyclePolicy, ScheduleConfig, ScheduleConfigCompat};
|
||||
use domain::{DomainError, RotationPolicy, ScheduleConfig, ScheduleConfigCompat};
|
||||
use serde::de::DeserializeOwned;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -32,8 +32,8 @@ pub fn parse_schedule_config(json: &str) -> Result<ScheduleConfig, DomainError>
|
||||
Ok(ScheduleConfig::from(compat))
|
||||
}
|
||||
|
||||
pub fn parse_recycle_policy(json: &str) -> Result<RecyclePolicy, DomainError> {
|
||||
parse_json(json, "recycle_policy")
|
||||
pub fn parse_rotation_policy(json: &str) -> Result<RotationPolicy, DomainError> {
|
||||
parse_json(json, "rotation_policy")
|
||||
}
|
||||
|
||||
pub fn parse_enum_or_default<T: DeserializeOwned + Default>(value: String) -> T {
|
||||
@@ -151,9 +151,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_recycle_policy_valid() {
|
||||
fn parse_rotation_policy_valid() {
|
||||
let json = r#"{"cooldown_days":7,"cooldown_generations":3,"min_available_ratio":0.3}"#;
|
||||
let policy = parse_recycle_policy(json).unwrap();
|
||||
let policy = parse_rotation_policy(json).unwrap();
|
||||
assert_eq!(policy.cooldown_days, Some(7));
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use sqlx::{Row, SqlitePool};
|
||||
use uuid::Uuid;
|
||||
|
||||
use adapter_common::{
|
||||
map_sqlx_error, parse_dt, parse_enum_or_default, parse_recycle_policy, parse_schedule_config,
|
||||
map_sqlx_error, parse_dt, parse_enum_or_default, parse_rotation_policy, parse_schedule_config,
|
||||
parse_uuid, serialize_enum_as_string,
|
||||
};
|
||||
use domain::{
|
||||
@@ -23,7 +23,7 @@ impl SqliteChannelRepository {
|
||||
}
|
||||
}
|
||||
|
||||
const SELECT_COLS: &str = "id, owner_id, name, description, timezone, schedule_config, recycle_policy, auto_schedule, access_mode, access_password_hash, logo, logo_position, logo_opacity, webhook_url, webhook_poll_interval_secs, webhook_body_template, webhook_headers, created_at, updated_at";
|
||||
const SELECT_COLS: &str = "id, owner_id, name, description, timezone, schedule_config, recycle_policy AS rotation_policy, auto_schedule, access_mode, logo, logo_position, logo_opacity, webhook_url, webhook_poll_interval_secs, webhook_body_template, webhook_headers, created_at, updated_at";
|
||||
|
||||
#[derive(Debug, sqlx::FromRow)]
|
||||
struct ChannelRow {
|
||||
@@ -33,10 +33,9 @@ struct ChannelRow {
|
||||
description: Option<String>,
|
||||
timezone: String,
|
||||
schedule_config: String,
|
||||
recycle_policy: String,
|
||||
rotation_policy: String,
|
||||
auto_schedule: i64,
|
||||
access_mode: String,
|
||||
access_password_hash: Option<String>,
|
||||
logo: Option<String>,
|
||||
logo_position: String,
|
||||
logo_opacity: f32,
|
||||
@@ -53,7 +52,7 @@ impl ChannelRow {
|
||||
let id = ChannelId::from_uuid(parse_uuid(&self.id, "channel id")?);
|
||||
let owner_id = UserId::from_uuid(parse_uuid(&self.owner_id, "owner id")?);
|
||||
let schedule_config = parse_schedule_config(&self.schedule_config)?;
|
||||
let recycle_policy = parse_recycle_policy(&self.recycle_policy)?;
|
||||
let rotation_policy = parse_rotation_policy(&self.rotation_policy)?;
|
||||
let access_mode: AccessMode = parse_enum_or_default(self.access_mode);
|
||||
let logo_position: LogoPosition = parse_enum_or_default(self.logo_position);
|
||||
|
||||
@@ -64,10 +63,9 @@ impl ChannelRow {
|
||||
description: self.description,
|
||||
timezone: self.timezone,
|
||||
schedule_config,
|
||||
recycle_policy,
|
||||
rotation_policy,
|
||||
auto_schedule: self.auto_schedule != 0,
|
||||
access_mode,
|
||||
access_password_hash: self.access_password_hash,
|
||||
logo: self.logo,
|
||||
logo_position,
|
||||
logo_opacity: self.logo_opacity,
|
||||
@@ -109,8 +107,8 @@ impl ChannelCommand for SqliteChannelRepository {
|
||||
async fn save(&self, channel: &Channel) -> DomainResult<()> {
|
||||
let schedule_config = serde_json::to_string(channel.schedule_config())
|
||||
.map_err(|e| DomainError::RepositoryError(format!("serialize schedule_config: {e}")))?;
|
||||
let recycle_policy = serde_json::to_string(channel.recycle_policy())
|
||||
.map_err(|e| DomainError::RepositoryError(format!("serialize recycle_policy: {e}")))?;
|
||||
let rotation_policy = serde_json::to_string(channel.rotation_policy())
|
||||
.map_err(|e| DomainError::RepositoryError(format!("serialize rotation_policy: {e}")))?;
|
||||
let access_mode = serialize_enum_as_string(channel.access_mode(), "public");
|
||||
let logo_position = serialize_enum_as_string(channel.logo_position(), "top_right");
|
||||
|
||||
@@ -118,10 +116,10 @@ impl ChannelCommand for SqliteChannelRepository {
|
||||
r#"
|
||||
INSERT INTO channels
|
||||
(id, owner_id, name, description, timezone, schedule_config, recycle_policy,
|
||||
auto_schedule, access_mode, access_password_hash, logo, logo_position,
|
||||
auto_schedule, access_mode, logo, logo_position,
|
||||
logo_opacity, webhook_url, webhook_poll_interval_secs, webhook_body_template,
|
||||
webhook_headers, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
description = excluded.description,
|
||||
@@ -130,7 +128,6 @@ impl ChannelCommand for SqliteChannelRepository {
|
||||
recycle_policy = excluded.recycle_policy,
|
||||
auto_schedule = excluded.auto_schedule,
|
||||
access_mode = excluded.access_mode,
|
||||
access_password_hash = excluded.access_password_hash,
|
||||
logo = excluded.logo,
|
||||
logo_position = excluded.logo_position,
|
||||
logo_opacity = excluded.logo_opacity,
|
||||
@@ -147,10 +144,9 @@ impl ChannelCommand for SqliteChannelRepository {
|
||||
.bind(channel.description())
|
||||
.bind(channel.timezone())
|
||||
.bind(&schedule_config)
|
||||
.bind(&recycle_policy)
|
||||
.bind(&rotation_policy)
|
||||
.bind(channel.auto_schedule() as i64)
|
||||
.bind(&access_mode)
|
||||
.bind(channel.access_password_hash())
|
||||
.bind(channel.logo())
|
||||
.bind(&logo_position)
|
||||
.bind(channel.logo_opacity())
|
||||
|
||||
@@ -11,7 +11,6 @@ pub struct CreateChannelRequest {
|
||||
pub description: Option<String>,
|
||||
pub timezone: String,
|
||||
pub access_mode: Option<String>,
|
||||
pub access_password: Option<String>,
|
||||
pub webhook_url: Option<String>,
|
||||
pub webhook_poll_interval_secs: Option<u32>,
|
||||
pub webhook_body_template: Option<String>,
|
||||
@@ -26,10 +25,9 @@ pub struct UpdateChannelRequest {
|
||||
#[schema(value_type = Option<Object>)]
|
||||
pub schedule_config: Option<domain::models::ScheduleConfigCompat>,
|
||||
#[schema(value_type = Option<Object>)]
|
||||
pub recycle_policy: Option<domain::RecyclePolicy>,
|
||||
pub rotation_policy: Option<domain::RotationPolicy>,
|
||||
pub auto_schedule: Option<bool>,
|
||||
pub access_mode: Option<String>,
|
||||
pub access_password: Option<String>,
|
||||
pub logo: Option<Option<String>>,
|
||||
pub logo_position: Option<String>,
|
||||
pub logo_opacity: Option<f32>,
|
||||
@@ -47,7 +45,7 @@ pub struct ChannelResponse {
|
||||
pub description: Option<String>,
|
||||
pub timezone: String,
|
||||
pub schedule_config: serde_json::Value,
|
||||
pub recycle_policy: serde_json::Value,
|
||||
pub rotation_policy: serde_json::Value,
|
||||
pub auto_schedule: bool,
|
||||
pub access_mode: String,
|
||||
pub logo: Option<String>,
|
||||
@@ -70,7 +68,7 @@ impl From<domain::Channel> for ChannelResponse {
|
||||
description: c.description().map(|s| s.to_string()),
|
||||
timezone: c.timezone().to_string(),
|
||||
schedule_config: serde_json::to_value(c.schedule_config()).unwrap_or_default(),
|
||||
recycle_policy: serde_json::to_value(c.recycle_policy()).unwrap_or_default(),
|
||||
rotation_policy: serde_json::to_value(c.rotation_policy()).unwrap_or_default(),
|
||||
auto_schedule: c.auto_schedule(),
|
||||
access_mode: enum_to_string(c.access_mode()),
|
||||
logo: c.logo().map(|s| s.to_string()),
|
||||
|
||||
@@ -5,8 +5,6 @@ use uuid::Uuid;
|
||||
|
||||
use crate::common::enum_to_string;
|
||||
|
||||
const DEFAULT_ACCESS_MODE: &str = "public";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MediaItemResponse {
|
||||
pub id: String,
|
||||
@@ -47,8 +45,6 @@ pub struct SlotResponse {
|
||||
pub end_at: DateTime<Utc>,
|
||||
pub item: MediaItemResponse,
|
||||
pub source_block_id: Uuid,
|
||||
#[serde(default)]
|
||||
pub block_access_mode: String,
|
||||
}
|
||||
|
||||
impl From<domain::ScheduledSlot> for SlotResponse {
|
||||
@@ -59,26 +55,6 @@ impl From<domain::ScheduledSlot> for SlotResponse {
|
||||
end_at: s.end_at(),
|
||||
item: s.item().clone().into(),
|
||||
source_block_id: s.source_block_id().value(),
|
||||
block_access_mode: String::from(DEFAULT_ACCESS_MODE),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SlotResponse {
|
||||
pub fn with_block_access(slot: domain::ScheduledSlot, channel: &domain::Channel) -> Self {
|
||||
let block_access_mode = channel
|
||||
.schedule_config()
|
||||
.all_blocks()
|
||||
.find(|b| b.id() == slot.source_block_id())
|
||||
.map(|b| enum_to_string(b.access_mode()))
|
||||
.unwrap_or_else(|| String::from(DEFAULT_ACCESS_MODE));
|
||||
Self {
|
||||
id: slot.id().value(),
|
||||
start_at: slot.start_at(),
|
||||
end_at: slot.end_at(),
|
||||
item: slot.item().clone().into(),
|
||||
source_block_id: slot.source_block_id().value(),
|
||||
block_access_mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +63,6 @@ impl SlotResponse {
|
||||
pub struct CurrentBroadcastResponse {
|
||||
pub slot: SlotResponse,
|
||||
pub offset_secs: u32,
|
||||
pub block_access_mode: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
|
||||
@@ -121,10 +121,10 @@ async fn login_fails_for_unknown_email() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_fails_for_oidc_only_user() {
|
||||
async fn login_fails_for_user_without_password() {
|
||||
let repo = Arc::new(InMemoryUserRepository::new());
|
||||
let email = Email::new("oidc@example.com").unwrap();
|
||||
let user = domain::models::User::new("oidc|subject", email);
|
||||
let email = Email::new("external@example.com").unwrap();
|
||||
let user = domain::models::User::new("external|subject", email);
|
||||
repo.store.lock().unwrap().insert(user.id(), user);
|
||||
|
||||
let deps = AuthDeps {
|
||||
@@ -138,7 +138,7 @@ async fn login_fails_for_oidc_only_user() {
|
||||
let result = login::execute(
|
||||
&deps,
|
||||
LoginCommand {
|
||||
email: "oidc@example.com".into(),
|
||||
email: "external@example.com".into(),
|
||||
password: "password123".into(),
|
||||
remember_me: false,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use domain::models::ScheduleConfig;
|
||||
use domain::value_objects::{ChannelId, RecyclePolicy, UserId};
|
||||
use domain::value_objects::{ChannelId, RotationPolicy, UserId};
|
||||
|
||||
pub struct CreateChannelCommand {
|
||||
pub owner_id: UserId,
|
||||
@@ -14,7 +14,7 @@ pub struct UpdateChannelCommand {
|
||||
pub description: Option<Option<String>>,
|
||||
pub timezone: Option<String>,
|
||||
pub schedule_config: Option<ScheduleConfig>,
|
||||
pub recycle_policy: Option<RecyclePolicy>,
|
||||
pub rotation_policy: Option<RotationPolicy>,
|
||||
pub auto_schedule: Option<bool>,
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ async fn updates_channel_name() {
|
||||
description: None,
|
||||
timezone: None,
|
||||
schedule_config: None,
|
||||
recycle_policy: None,
|
||||
rotation_policy: None,
|
||||
auto_schedule: None,
|
||||
},
|
||||
)
|
||||
@@ -80,7 +80,7 @@ async fn update_fails_if_not_owner() {
|
||||
description: None,
|
||||
timezone: None,
|
||||
schedule_config: None,
|
||||
recycle_policy: None,
|
||||
rotation_policy: None,
|
||||
auto_schedule: None,
|
||||
},
|
||||
)
|
||||
@@ -106,7 +106,7 @@ async fn update_nonexistent_channel_returns_not_found() {
|
||||
description: None,
|
||||
timezone: None,
|
||||
schedule_config: None,
|
||||
recycle_policy: None,
|
||||
rotation_policy: None,
|
||||
auto_schedule: None,
|
||||
},
|
||||
)
|
||||
@@ -146,7 +146,7 @@ async fn update_config_creates_snapshot() {
|
||||
description: None,
|
||||
timezone: None,
|
||||
schedule_config: Some(new_config),
|
||||
recycle_policy: None,
|
||||
rotation_policy: None,
|
||||
auto_schedule: None,
|
||||
},
|
||||
)
|
||||
@@ -185,7 +185,7 @@ async fn update_without_config_skips_snapshot() {
|
||||
description: None,
|
||||
timezone: None,
|
||||
schedule_config: None,
|
||||
recycle_policy: None,
|
||||
rotation_policy: None,
|
||||
auto_schedule: None,
|
||||
},
|
||||
)
|
||||
@@ -223,7 +223,7 @@ async fn update_description_clear() {
|
||||
description: Some(Some("A description".into())),
|
||||
timezone: None,
|
||||
schedule_config: None,
|
||||
recycle_policy: None,
|
||||
rotation_policy: None,
|
||||
auto_schedule: None,
|
||||
},
|
||||
)
|
||||
@@ -241,7 +241,7 @@ async fn update_description_clear() {
|
||||
description: Some(None),
|
||||
timezone: None,
|
||||
schedule_config: None,
|
||||
recycle_policy: None,
|
||||
rotation_policy: None,
|
||||
auto_schedule: None,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -29,8 +29,8 @@ pub async fn execute(deps: &ChannelCommandDeps, cmd: UpdateChannelCommand) -> Do
|
||||
if let Some(config) = cmd.schedule_config {
|
||||
channel.set_schedule_config(config);
|
||||
}
|
||||
if let Some(policy) = cmd.recycle_policy {
|
||||
channel.set_recycle_policy(policy);
|
||||
if let Some(policy) = cmd.rotation_policy {
|
||||
channel.set_rotation_policy(policy);
|
||||
}
|
||||
if let Some(auto) = cmd.auto_schedule {
|
||||
channel.set_auto_schedule(auto);
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::value_objects::{
|
||||
AccessMode, BlockId, ChannelId, FillStrategy, LogoPosition, MediaFilter, MediaItemId,
|
||||
RecyclePolicy, UserId, Weekday,
|
||||
RotationPolicy, UserId, Weekday,
|
||||
};
|
||||
|
||||
const SECONDS_IN_DAY: u32 = 86_400;
|
||||
@@ -19,10 +19,9 @@ pub struct Channel {
|
||||
description: Option<String>,
|
||||
timezone: String,
|
||||
schedule_config: ScheduleConfig,
|
||||
recycle_policy: RecyclePolicy,
|
||||
rotation_policy: RotationPolicy,
|
||||
auto_schedule: bool,
|
||||
access_mode: AccessMode,
|
||||
access_password_hash: Option<String>,
|
||||
logo: Option<String>,
|
||||
logo_position: LogoPosition,
|
||||
logo_opacity: f32,
|
||||
@@ -41,10 +40,9 @@ pub struct ChannelRow {
|
||||
pub description: Option<String>,
|
||||
pub timezone: String,
|
||||
pub schedule_config: ScheduleConfig,
|
||||
pub recycle_policy: RecyclePolicy,
|
||||
pub rotation_policy: RotationPolicy,
|
||||
pub auto_schedule: bool,
|
||||
pub access_mode: AccessMode,
|
||||
pub access_password_hash: Option<String>,
|
||||
pub logo: Option<String>,
|
||||
pub logo_position: LogoPosition,
|
||||
pub logo_opacity: f32,
|
||||
@@ -70,10 +68,9 @@ impl Channel {
|
||||
description: None,
|
||||
timezone: timezone.into(),
|
||||
schedule_config: ScheduleConfig::default(),
|
||||
recycle_policy: RecyclePolicy::default(),
|
||||
rotation_policy: RotationPolicy::default(),
|
||||
auto_schedule: false,
|
||||
access_mode: AccessMode::default(),
|
||||
access_password_hash: None,
|
||||
logo: None,
|
||||
logo_position: LogoPosition::default(),
|
||||
logo_opacity: DEFAULT_LOGO_OPACITY,
|
||||
@@ -94,10 +91,9 @@ impl Channel {
|
||||
description: row.description,
|
||||
timezone: row.timezone,
|
||||
schedule_config: row.schedule_config,
|
||||
recycle_policy: row.recycle_policy,
|
||||
rotation_policy: row.rotation_policy,
|
||||
auto_schedule: row.auto_schedule,
|
||||
access_mode: row.access_mode,
|
||||
access_password_hash: row.access_password_hash,
|
||||
logo: row.logo,
|
||||
logo_position: row.logo_position,
|
||||
logo_opacity: row.logo_opacity,
|
||||
@@ -134,8 +130,8 @@ impl Channel {
|
||||
&self.schedule_config
|
||||
}
|
||||
|
||||
pub fn recycle_policy(&self) -> &RecyclePolicy {
|
||||
&self.recycle_policy
|
||||
pub fn rotation_policy(&self) -> &RotationPolicy {
|
||||
&self.rotation_policy
|
||||
}
|
||||
|
||||
pub fn auto_schedule(&self) -> bool {
|
||||
@@ -146,10 +142,6 @@ impl Channel {
|
||||
&self.access_mode
|
||||
}
|
||||
|
||||
pub fn access_password_hash(&self) -> Option<&str> {
|
||||
self.access_password_hash.as_deref()
|
||||
}
|
||||
|
||||
pub fn logo(&self) -> Option<&str> {
|
||||
self.logo.as_deref()
|
||||
}
|
||||
@@ -206,8 +198,8 @@ impl Channel {
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
pub fn set_recycle_policy(&mut self, policy: RecyclePolicy) {
|
||||
self.recycle_policy = policy;
|
||||
pub fn set_rotation_policy(&mut self, policy: RotationPolicy) {
|
||||
self.rotation_policy = policy;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
@@ -331,13 +323,7 @@ pub struct ProgrammingBlock {
|
||||
loop_on_finish: bool,
|
||||
|
||||
#[serde(default)]
|
||||
ignore_recycle_policy: bool,
|
||||
|
||||
#[serde(default)]
|
||||
access_mode: AccessMode,
|
||||
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
access_password_hash: Option<String>,
|
||||
ignore_rotation_policy: bool,
|
||||
}
|
||||
|
||||
impl ProgrammingBlock {
|
||||
@@ -359,9 +345,7 @@ impl ProgrammingBlock {
|
||||
provider_id: String::new(),
|
||||
},
|
||||
loop_on_finish: true,
|
||||
ignore_recycle_policy: false,
|
||||
access_mode: AccessMode::default(),
|
||||
access_password_hash: None,
|
||||
ignore_rotation_policy: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,9 +365,7 @@ impl ProgrammingBlock {
|
||||
provider_id: String::new(),
|
||||
},
|
||||
loop_on_finish: true,
|
||||
ignore_recycle_policy: false,
|
||||
access_mode: AccessMode::default(),
|
||||
access_password_hash: None,
|
||||
ignore_rotation_policy: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,16 +393,8 @@ impl ProgrammingBlock {
|
||||
self.loop_on_finish
|
||||
}
|
||||
|
||||
pub fn ignore_recycle_policy(&self) -> bool {
|
||||
self.ignore_recycle_policy
|
||||
}
|
||||
|
||||
pub fn access_mode(&self) -> &AccessMode {
|
||||
&self.access_mode
|
||||
}
|
||||
|
||||
pub fn access_password_hash(&self) -> Option<&str> {
|
||||
self.access_password_hash.as_deref()
|
||||
pub fn ignore_rotation_policy(&self) -> bool {
|
||||
self.ignore_rotation_policy
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ fn programming_block_getters() {
|
||||
assert_eq!(block.start_time(), t(8, 0));
|
||||
assert_eq!(block.duration_mins(), 120);
|
||||
assert!(block.loop_on_finish());
|
||||
assert!(!block.ignore_recycle_policy());
|
||||
assert!(!block.ignore_rotation_policy());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -3,10 +3,10 @@ use super::*;
|
||||
#[test]
|
||||
fn new_generates_id_and_timestamp() {
|
||||
let email = Email::new("test@example.com").unwrap();
|
||||
let user = User::new("oidc|123", email);
|
||||
let user = User::new("external|123", email);
|
||||
assert!(!user.is_admin());
|
||||
assert!(user.password_hash().is_none());
|
||||
assert_eq!(user.subject(), "oidc|123");
|
||||
assert_eq!(user.subject(), "external|123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -9,10 +9,10 @@ use crate::models::{
|
||||
ScheduledSlot,
|
||||
};
|
||||
use crate::ports::{ChannelQuery, IProviderRegistry, ScheduleCommand, ScheduleQuery, StreamQuality};
|
||||
use crate::value_objects::{BlockId, ChannelId, FillStrategy, MediaFilter, MediaItemId, RecyclePolicy, Weekday};
|
||||
use crate::value_objects::{BlockId, ChannelId, FillStrategy, MediaFilter, MediaItemId, RotationPolicy, Weekday};
|
||||
|
||||
mod fill;
|
||||
mod recycle;
|
||||
mod rotation;
|
||||
|
||||
const SCHEDULE_DURATION_DAYS: i64 = 7;
|
||||
|
||||
@@ -27,12 +27,12 @@ struct AlgorithmicParams<'a> {
|
||||
strategy: &'a FillStrategy,
|
||||
block_id: BlockId,
|
||||
loop_on_finish: bool,
|
||||
ignore_recycle_policy: bool,
|
||||
ignore_rotation_policy: bool,
|
||||
}
|
||||
|
||||
struct RecycleContext<'a> {
|
||||
struct RotationContext<'a> {
|
||||
history: &'a [PlaybackRecord],
|
||||
policy: &'a RecyclePolicy,
|
||||
policy: &'a RotationPolicy,
|
||||
generation: u32,
|
||||
last_item_id: Option<&'a MediaItemId>,
|
||||
}
|
||||
@@ -131,9 +131,9 @@ impl ScheduleEngineService {
|
||||
start: slot_start,
|
||||
end: slot_end,
|
||||
},
|
||||
RecycleContext {
|
||||
RotationContext {
|
||||
history: &history,
|
||||
policy: channel.recycle_policy(),
|
||||
policy: channel.rotation_policy(),
|
||||
generation,
|
||||
last_item_id,
|
||||
},
|
||||
@@ -255,7 +255,7 @@ impl ScheduleEngineService {
|
||||
&self,
|
||||
block: &ProgrammingBlock,
|
||||
window: BlockTimeWindow,
|
||||
recycle: RecycleContext<'_>,
|
||||
rotation: RotationContext<'_>,
|
||||
) -> DomainResult<Vec<ScheduledSlot>> {
|
||||
match block.content() {
|
||||
BlockContent::Manual { items, .. } => {
|
||||
@@ -274,10 +274,10 @@ impl ScheduleEngineService {
|
||||
strategy,
|
||||
block_id: block.id(),
|
||||
loop_on_finish: block.loop_on_finish(),
|
||||
ignore_recycle_policy: block.ignore_recycle_policy(),
|
||||
ignore_rotation_policy: block.ignore_rotation_policy(),
|
||||
},
|
||||
window,
|
||||
recycle,
|
||||
rotation,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -313,7 +313,7 @@ impl ScheduleEngineService {
|
||||
&self,
|
||||
params: AlgorithmicParams<'_>,
|
||||
window: BlockTimeWindow,
|
||||
recycle: RecycleContext<'_>,
|
||||
rotation: RotationContext<'_>,
|
||||
) -> DomainResult<Vec<ScheduledSlot>> {
|
||||
let candidates = self
|
||||
.provider_registry
|
||||
@@ -324,10 +324,10 @@ impl ScheduleEngineService {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let pool = if params.ignore_recycle_policy {
|
||||
let pool = if params.ignore_rotation_policy {
|
||||
candidates.clone()
|
||||
} else {
|
||||
recycle::apply_recycle_policy(&candidates, recycle.history, recycle.policy, recycle.generation)
|
||||
rotation::apply_rotation_policy(&candidates, rotation.history, rotation.policy, rotation.generation)
|
||||
};
|
||||
let target_secs = (window.end - window.start).num_seconds() as u32;
|
||||
let selected = fill::fill_block(
|
||||
@@ -335,7 +335,7 @@ impl ScheduleEngineService {
|
||||
&pool,
|
||||
target_secs,
|
||||
params.strategy,
|
||||
recycle.last_item_id,
|
||||
rotation.last_item_id,
|
||||
params.loop_on_finish,
|
||||
);
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ use std::collections::HashSet;
|
||||
use chrono::Utc;
|
||||
|
||||
use crate::models::{MediaItem, PlaybackRecord};
|
||||
use crate::value_objects::{MediaItemId, RecyclePolicy};
|
||||
use crate::value_objects::{MediaItemId, RotationPolicy};
|
||||
|
||||
pub(super) fn apply_recycle_policy(
|
||||
pub(super) fn apply_rotation_policy(
|
||||
candidates: &[MediaItem],
|
||||
history: &[PlaybackRecord],
|
||||
policy: &RecyclePolicy,
|
||||
policy: &RotationPolicy,
|
||||
current_generation: u32,
|
||||
) -> Vec<MediaItem> {
|
||||
let now = Utc::now();
|
||||
@@ -41,7 +41,7 @@ pub(super) fn apply_recycle_policy(
|
||||
(candidates.len() as f32 * policy.min_available_ratio).ceil() as usize;
|
||||
|
||||
if available.len() < min_count {
|
||||
// Pool too small after cooldowns — recycle everything
|
||||
// Pool too small after cooldowns — rotate everything back in
|
||||
candidates.to_vec()
|
||||
} else {
|
||||
available
|
||||
@@ -49,5 +49,5 @@ pub(super) fn apply_recycle_policy(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/recycle.rs"]
|
||||
#[path = "tests/rotation.rs"]
|
||||
mod tests;
|
||||
@@ -13,12 +13,12 @@ fn record(item_id: &str, generation: u32) -> PlaybackRecord {
|
||||
#[test]
|
||||
fn no_history_returns_all() {
|
||||
let pool = vec![item("a"), item("b"), item("c")];
|
||||
let policy = RecyclePolicy {
|
||||
let policy = RotationPolicy {
|
||||
cooldown_days: Some(7),
|
||||
cooldown_generations: None,
|
||||
min_available_ratio: 0.2,
|
||||
};
|
||||
let result = apply_recycle_policy(&pool, &[], &policy, 1);
|
||||
let result = apply_rotation_policy(&pool, &[], &policy, 1);
|
||||
assert_eq!(result.len(), 3);
|
||||
}
|
||||
|
||||
@@ -26,12 +26,12 @@ fn no_history_returns_all() {
|
||||
fn generation_cooldown_excludes() {
|
||||
let pool = vec![item("a"), item("b"), item("c")];
|
||||
let history = vec![record("a", 1)];
|
||||
let policy = RecyclePolicy {
|
||||
let policy = RotationPolicy {
|
||||
cooldown_days: None,
|
||||
cooldown_generations: Some(2),
|
||||
min_available_ratio: 0.0,
|
||||
};
|
||||
let result = apply_recycle_policy(&pool, &history, &policy, 2);
|
||||
let result = apply_rotation_policy(&pool, &history, &policy, 2);
|
||||
assert_eq!(result.len(), 2);
|
||||
assert!(result.iter().all(|i| i.id().value() != "a"));
|
||||
}
|
||||
@@ -40,11 +40,11 @@ fn generation_cooldown_excludes() {
|
||||
fn min_available_ratio_waives_cooldown() {
|
||||
let pool = vec![item("a"), item("b")];
|
||||
let history = vec![record("a", 1), record("b", 1)];
|
||||
let policy = RecyclePolicy {
|
||||
let policy = RotationPolicy {
|
||||
cooldown_days: None,
|
||||
cooldown_generations: Some(5),
|
||||
min_available_ratio: 0.5,
|
||||
};
|
||||
let result = apply_recycle_policy(&pool, &history, &policy, 2);
|
||||
let result = apply_rotation_policy(&pool, &history, &policy, 2);
|
||||
assert_eq!(result.len(), 2);
|
||||
}
|
||||
@@ -5,9 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||
pub enum AccessMode {
|
||||
#[default]
|
||||
Public,
|
||||
PasswordProtected,
|
||||
AccountRequired,
|
||||
OwnerOnly,
|
||||
Private,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
|
||||
@@ -34,13 +34,13 @@ const DEFAULT_COOLDOWN_DAYS: u32 = 30;
|
||||
const DEFAULT_MIN_AVAILABLE_RATIO: f32 = 0.2;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RecyclePolicy {
|
||||
pub struct RotationPolicy {
|
||||
pub cooldown_days: Option<u32>,
|
||||
pub cooldown_generations: Option<u32>,
|
||||
pub min_available_ratio: f32,
|
||||
}
|
||||
|
||||
impl Default for RecyclePolicy {
|
||||
impl Default for RotationPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cooldown_days: Some(DEFAULT_COOLDOWN_DAYS),
|
||||
|
||||
@@ -65,7 +65,7 @@ pub async fn update_channel(
|
||||
description: description.map(Some),
|
||||
timezone,
|
||||
schedule_config,
|
||||
recycle_policy: None,
|
||||
rotation_policy: None,
|
||||
auto_schedule: None,
|
||||
};
|
||||
match application::channels::update::execute(cmd_deps, cmd).await {
|
||||
|
||||
@@ -81,7 +81,7 @@ pub async fn update_channel(
|
||||
description: req.description.map(Some),
|
||||
timezone: req.timezone,
|
||||
schedule_config: req.schedule_config.map(Into::into),
|
||||
recycle_policy: req.recycle_policy,
|
||||
rotation_policy: req.rotation_policy,
|
||||
auto_schedule: req.auto_schedule,
|
||||
};
|
||||
let channel = application::channels::update::execute(&state.channel_command_deps, cmd).await?;
|
||||
|
||||
@@ -45,15 +45,10 @@ pub async fn get_current_broadcast(
|
||||
match application::schedule::get_current_broadcast::execute(&state.schedule_deps, query).await?
|
||||
{
|
||||
Some(result) => {
|
||||
let slot_response = match &result.channel {
|
||||
Some(ch) => SlotResponse::with_block_access(result.broadcast.slot().clone(), ch),
|
||||
None => SlotResponse::from(result.broadcast.slot().clone()),
|
||||
};
|
||||
let block_access_mode = slot_response.block_access_mode.clone();
|
||||
let slot_response = SlotResponse::from(result.broadcast.slot().clone());
|
||||
Ok(Json(CurrentBroadcastResponse {
|
||||
slot: slot_response,
|
||||
offset_secs: result.broadcast.offset_secs(),
|
||||
block_access_mode,
|
||||
})
|
||||
.into_response())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user