rename RecyclePolicy→RotationPolicy, collapse AccessMode, clean OIDC refs

This commit is contained in:
2026-07-12 06:50:25 +02:00
parent efd15c4f53
commit 773e228e21
19 changed files with 81 additions and 145 deletions

View File

@@ -1,5 +1,5 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use domain::{DomainError, RecyclePolicy, ScheduleConfig, ScheduleConfigCompat}; use domain::{DomainError, RotationPolicy, ScheduleConfig, ScheduleConfigCompat};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use uuid::Uuid; use uuid::Uuid;
@@ -32,8 +32,8 @@ pub fn parse_schedule_config(json: &str) -> Result<ScheduleConfig, DomainError>
Ok(ScheduleConfig::from(compat)) Ok(ScheduleConfig::from(compat))
} }
pub fn parse_recycle_policy(json: &str) -> Result<RecyclePolicy, DomainError> { pub fn parse_rotation_policy(json: &str) -> Result<RotationPolicy, DomainError> {
parse_json(json, "recycle_policy") parse_json(json, "rotation_policy")
} }
pub fn parse_enum_or_default<T: DeserializeOwned + Default>(value: String) -> T { pub fn parse_enum_or_default<T: DeserializeOwned + Default>(value: String) -> T {
@@ -151,9 +151,9 @@ mod tests {
} }
#[test] #[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 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)); assert_eq!(policy.cooldown_days, Some(7));
} }

View File

@@ -4,7 +4,7 @@ use sqlx::{Row, SqlitePool};
use uuid::Uuid; use uuid::Uuid;
use adapter_common::{ 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, parse_uuid, serialize_enum_as_string,
}; };
use domain::{ 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)] #[derive(Debug, sqlx::FromRow)]
struct ChannelRow { struct ChannelRow {
@@ -33,10 +33,9 @@ struct ChannelRow {
description: Option<String>, description: Option<String>,
timezone: String, timezone: String,
schedule_config: String, schedule_config: String,
recycle_policy: String, rotation_policy: String,
auto_schedule: i64, auto_schedule: i64,
access_mode: String, access_mode: String,
access_password_hash: Option<String>,
logo: Option<String>, logo: Option<String>,
logo_position: String, logo_position: String,
logo_opacity: f32, logo_opacity: f32,
@@ -53,7 +52,7 @@ impl ChannelRow {
let id = ChannelId::from_uuid(parse_uuid(&self.id, "channel id")?); 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 owner_id = UserId::from_uuid(parse_uuid(&self.owner_id, "owner id")?);
let schedule_config = parse_schedule_config(&self.schedule_config)?; 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 access_mode: AccessMode = parse_enum_or_default(self.access_mode);
let logo_position: LogoPosition = parse_enum_or_default(self.logo_position); let logo_position: LogoPosition = parse_enum_or_default(self.logo_position);
@@ -64,10 +63,9 @@ impl ChannelRow {
description: self.description, description: self.description,
timezone: self.timezone, timezone: self.timezone,
schedule_config, schedule_config,
recycle_policy, rotation_policy,
auto_schedule: self.auto_schedule != 0, auto_schedule: self.auto_schedule != 0,
access_mode, access_mode,
access_password_hash: self.access_password_hash,
logo: self.logo, logo: self.logo,
logo_position, logo_position,
logo_opacity: self.logo_opacity, logo_opacity: self.logo_opacity,
@@ -109,8 +107,8 @@ impl ChannelCommand for SqliteChannelRepository {
async fn save(&self, channel: &Channel) -> DomainResult<()> { async fn save(&self, channel: &Channel) -> DomainResult<()> {
let schedule_config = serde_json::to_string(channel.schedule_config()) let schedule_config = serde_json::to_string(channel.schedule_config())
.map_err(|e| DomainError::RepositoryError(format!("serialize schedule_config: {e}")))?; .map_err(|e| DomainError::RepositoryError(format!("serialize schedule_config: {e}")))?;
let recycle_policy = serde_json::to_string(channel.recycle_policy()) let rotation_policy = serde_json::to_string(channel.rotation_policy())
.map_err(|e| DomainError::RepositoryError(format!("serialize recycle_policy: {e}")))?; .map_err(|e| DomainError::RepositoryError(format!("serialize rotation_policy: {e}")))?;
let access_mode = serialize_enum_as_string(channel.access_mode(), "public"); let access_mode = serialize_enum_as_string(channel.access_mode(), "public");
let logo_position = serialize_enum_as_string(channel.logo_position(), "top_right"); let logo_position = serialize_enum_as_string(channel.logo_position(), "top_right");
@@ -118,10 +116,10 @@ impl ChannelCommand for SqliteChannelRepository {
r#" r#"
INSERT INTO channels INSERT INTO channels
(id, owner_id, name, description, timezone, schedule_config, recycle_policy, (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, logo_opacity, webhook_url, webhook_poll_interval_secs, webhook_body_template,
webhook_headers, created_at, updated_at) webhook_headers, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET ON CONFLICT(id) DO UPDATE SET
name = excluded.name, name = excluded.name,
description = excluded.description, description = excluded.description,
@@ -130,7 +128,6 @@ impl ChannelCommand for SqliteChannelRepository {
recycle_policy = excluded.recycle_policy, recycle_policy = excluded.recycle_policy,
auto_schedule = excluded.auto_schedule, auto_schedule = excluded.auto_schedule,
access_mode = excluded.access_mode, access_mode = excluded.access_mode,
access_password_hash = excluded.access_password_hash,
logo = excluded.logo, logo = excluded.logo,
logo_position = excluded.logo_position, logo_position = excluded.logo_position,
logo_opacity = excluded.logo_opacity, logo_opacity = excluded.logo_opacity,
@@ -147,10 +144,9 @@ impl ChannelCommand for SqliteChannelRepository {
.bind(channel.description()) .bind(channel.description())
.bind(channel.timezone()) .bind(channel.timezone())
.bind(&schedule_config) .bind(&schedule_config)
.bind(&recycle_policy) .bind(&rotation_policy)
.bind(channel.auto_schedule() as i64) .bind(channel.auto_schedule() as i64)
.bind(&access_mode) .bind(&access_mode)
.bind(channel.access_password_hash())
.bind(channel.logo()) .bind(channel.logo())
.bind(&logo_position) .bind(&logo_position)
.bind(channel.logo_opacity()) .bind(channel.logo_opacity())

View File

@@ -11,7 +11,6 @@ pub struct CreateChannelRequest {
pub description: Option<String>, pub description: Option<String>,
pub timezone: String, pub timezone: String,
pub access_mode: Option<String>, pub access_mode: Option<String>,
pub access_password: Option<String>,
pub webhook_url: Option<String>, pub webhook_url: Option<String>,
pub webhook_poll_interval_secs: Option<u32>, pub webhook_poll_interval_secs: Option<u32>,
pub webhook_body_template: Option<String>, pub webhook_body_template: Option<String>,
@@ -26,10 +25,9 @@ pub struct UpdateChannelRequest {
#[schema(value_type = Option<Object>)] #[schema(value_type = Option<Object>)]
pub schedule_config: Option<domain::models::ScheduleConfigCompat>, pub schedule_config: Option<domain::models::ScheduleConfigCompat>,
#[schema(value_type = Option<Object>)] #[schema(value_type = Option<Object>)]
pub recycle_policy: Option<domain::RecyclePolicy>, pub rotation_policy: Option<domain::RotationPolicy>,
pub auto_schedule: Option<bool>, pub auto_schedule: Option<bool>,
pub access_mode: Option<String>, pub access_mode: Option<String>,
pub access_password: Option<String>,
pub logo: Option<Option<String>>, pub logo: Option<Option<String>>,
pub logo_position: Option<String>, pub logo_position: Option<String>,
pub logo_opacity: Option<f32>, pub logo_opacity: Option<f32>,
@@ -47,7 +45,7 @@ pub struct ChannelResponse {
pub description: Option<String>, pub description: Option<String>,
pub timezone: String, pub timezone: String,
pub schedule_config: serde_json::Value, pub schedule_config: serde_json::Value,
pub recycle_policy: serde_json::Value, pub rotation_policy: serde_json::Value,
pub auto_schedule: bool, pub auto_schedule: bool,
pub access_mode: String, pub access_mode: String,
pub logo: Option<String>, pub logo: Option<String>,
@@ -70,7 +68,7 @@ impl From<domain::Channel> for ChannelResponse {
description: c.description().map(|s| s.to_string()), description: c.description().map(|s| s.to_string()),
timezone: c.timezone().to_string(), timezone: c.timezone().to_string(),
schedule_config: serde_json::to_value(c.schedule_config()).unwrap_or_default(), 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(), auto_schedule: c.auto_schedule(),
access_mode: enum_to_string(c.access_mode()), access_mode: enum_to_string(c.access_mode()),
logo: c.logo().map(|s| s.to_string()), logo: c.logo().map(|s| s.to_string()),

View File

@@ -5,8 +5,6 @@ use uuid::Uuid;
use crate::common::enum_to_string; use crate::common::enum_to_string;
const DEFAULT_ACCESS_MODE: &str = "public";
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct MediaItemResponse { pub struct MediaItemResponse {
pub id: String, pub id: String,
@@ -47,8 +45,6 @@ pub struct SlotResponse {
pub end_at: DateTime<Utc>, pub end_at: DateTime<Utc>,
pub item: MediaItemResponse, pub item: MediaItemResponse,
pub source_block_id: Uuid, pub source_block_id: Uuid,
#[serde(default)]
pub block_access_mode: String,
} }
impl From<domain::ScheduledSlot> for SlotResponse { impl From<domain::ScheduledSlot> for SlotResponse {
@@ -59,26 +55,6 @@ impl From<domain::ScheduledSlot> for SlotResponse {
end_at: s.end_at(), end_at: s.end_at(),
item: s.item().clone().into(), item: s.item().clone().into(),
source_block_id: s.source_block_id().value(), 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 struct CurrentBroadcastResponse {
pub slot: SlotResponse, pub slot: SlotResponse,
pub offset_secs: u32, pub offset_secs: u32,
pub block_access_mode: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]

View File

@@ -121,10 +121,10 @@ async fn login_fails_for_unknown_email() {
} }
#[tokio::test] #[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 repo = Arc::new(InMemoryUserRepository::new());
let email = Email::new("oidc@example.com").unwrap(); let email = Email::new("external@example.com").unwrap();
let user = domain::models::User::new("oidc|subject", email); let user = domain::models::User::new("external|subject", email);
repo.store.lock().unwrap().insert(user.id(), user); repo.store.lock().unwrap().insert(user.id(), user);
let deps = AuthDeps { let deps = AuthDeps {
@@ -138,7 +138,7 @@ async fn login_fails_for_oidc_only_user() {
let result = login::execute( let result = login::execute(
&deps, &deps,
LoginCommand { LoginCommand {
email: "oidc@example.com".into(), email: "external@example.com".into(),
password: "password123".into(), password: "password123".into(),
remember_me: false, remember_me: false,
}, },

View File

@@ -1,5 +1,5 @@
use domain::models::ScheduleConfig; use domain::models::ScheduleConfig;
use domain::value_objects::{ChannelId, RecyclePolicy, UserId}; use domain::value_objects::{ChannelId, RotationPolicy, UserId};
pub struct CreateChannelCommand { pub struct CreateChannelCommand {
pub owner_id: UserId, pub owner_id: UserId,
@@ -14,7 +14,7 @@ pub struct UpdateChannelCommand {
pub description: Option<Option<String>>, pub description: Option<Option<String>>,
pub timezone: Option<String>, pub timezone: Option<String>,
pub schedule_config: Option<ScheduleConfig>, pub schedule_config: Option<ScheduleConfig>,
pub recycle_policy: Option<RecyclePolicy>, pub rotation_policy: Option<RotationPolicy>,
pub auto_schedule: Option<bool>, pub auto_schedule: Option<bool>,
} }

View File

@@ -43,7 +43,7 @@ async fn updates_channel_name() {
description: None, description: None,
timezone: None, timezone: None,
schedule_config: None, schedule_config: None,
recycle_policy: None, rotation_policy: None,
auto_schedule: None, auto_schedule: None,
}, },
) )
@@ -80,7 +80,7 @@ async fn update_fails_if_not_owner() {
description: None, description: None,
timezone: None, timezone: None,
schedule_config: None, schedule_config: None,
recycle_policy: None, rotation_policy: None,
auto_schedule: None, auto_schedule: None,
}, },
) )
@@ -106,7 +106,7 @@ async fn update_nonexistent_channel_returns_not_found() {
description: None, description: None,
timezone: None, timezone: None,
schedule_config: None, schedule_config: None,
recycle_policy: None, rotation_policy: None,
auto_schedule: None, auto_schedule: None,
}, },
) )
@@ -146,7 +146,7 @@ async fn update_config_creates_snapshot() {
description: None, description: None,
timezone: None, timezone: None,
schedule_config: Some(new_config), schedule_config: Some(new_config),
recycle_policy: None, rotation_policy: None,
auto_schedule: None, auto_schedule: None,
}, },
) )
@@ -185,7 +185,7 @@ async fn update_without_config_skips_snapshot() {
description: None, description: None,
timezone: None, timezone: None,
schedule_config: None, schedule_config: None,
recycle_policy: None, rotation_policy: None,
auto_schedule: None, auto_schedule: None,
}, },
) )
@@ -223,7 +223,7 @@ async fn update_description_clear() {
description: Some(Some("A description".into())), description: Some(Some("A description".into())),
timezone: None, timezone: None,
schedule_config: None, schedule_config: None,
recycle_policy: None, rotation_policy: None,
auto_schedule: None, auto_schedule: None,
}, },
) )
@@ -241,7 +241,7 @@ async fn update_description_clear() {
description: Some(None), description: Some(None),
timezone: None, timezone: None,
schedule_config: None, schedule_config: None,
recycle_policy: None, rotation_policy: None,
auto_schedule: None, auto_schedule: None,
}, },
) )

View File

@@ -29,8 +29,8 @@ pub async fn execute(deps: &ChannelCommandDeps, cmd: UpdateChannelCommand) -> Do
if let Some(config) = cmd.schedule_config { if let Some(config) = cmd.schedule_config {
channel.set_schedule_config(config); channel.set_schedule_config(config);
} }
if let Some(policy) = cmd.recycle_policy { if let Some(policy) = cmd.rotation_policy {
channel.set_recycle_policy(policy); channel.set_rotation_policy(policy);
} }
if let Some(auto) = cmd.auto_schedule { if let Some(auto) = cmd.auto_schedule {
channel.set_auto_schedule(auto); channel.set_auto_schedule(auto);

View File

@@ -4,7 +4,7 @@ use std::collections::HashMap;
use crate::value_objects::{ use crate::value_objects::{
AccessMode, BlockId, ChannelId, FillStrategy, LogoPosition, MediaFilter, MediaItemId, AccessMode, BlockId, ChannelId, FillStrategy, LogoPosition, MediaFilter, MediaItemId,
RecyclePolicy, UserId, Weekday, RotationPolicy, UserId, Weekday,
}; };
const SECONDS_IN_DAY: u32 = 86_400; const SECONDS_IN_DAY: u32 = 86_400;
@@ -19,10 +19,9 @@ pub struct Channel {
description: Option<String>, description: Option<String>,
timezone: String, timezone: String,
schedule_config: ScheduleConfig, schedule_config: ScheduleConfig,
recycle_policy: RecyclePolicy, rotation_policy: RotationPolicy,
auto_schedule: bool, auto_schedule: bool,
access_mode: AccessMode, access_mode: AccessMode,
access_password_hash: Option<String>,
logo: Option<String>, logo: Option<String>,
logo_position: LogoPosition, logo_position: LogoPosition,
logo_opacity: f32, logo_opacity: f32,
@@ -41,10 +40,9 @@ pub struct ChannelRow {
pub description: Option<String>, pub description: Option<String>,
pub timezone: String, pub timezone: String,
pub schedule_config: ScheduleConfig, pub schedule_config: ScheduleConfig,
pub recycle_policy: RecyclePolicy, pub rotation_policy: RotationPolicy,
pub auto_schedule: bool, pub auto_schedule: bool,
pub access_mode: AccessMode, pub access_mode: AccessMode,
pub access_password_hash: Option<String>,
pub logo: Option<String>, pub logo: Option<String>,
pub logo_position: LogoPosition, pub logo_position: LogoPosition,
pub logo_opacity: f32, pub logo_opacity: f32,
@@ -70,10 +68,9 @@ impl Channel {
description: None, description: None,
timezone: timezone.into(), timezone: timezone.into(),
schedule_config: ScheduleConfig::default(), schedule_config: ScheduleConfig::default(),
recycle_policy: RecyclePolicy::default(), rotation_policy: RotationPolicy::default(),
auto_schedule: false, auto_schedule: false,
access_mode: AccessMode::default(), access_mode: AccessMode::default(),
access_password_hash: None,
logo: None, logo: None,
logo_position: LogoPosition::default(), logo_position: LogoPosition::default(),
logo_opacity: DEFAULT_LOGO_OPACITY, logo_opacity: DEFAULT_LOGO_OPACITY,
@@ -94,10 +91,9 @@ impl Channel {
description: row.description, description: row.description,
timezone: row.timezone, timezone: row.timezone,
schedule_config: row.schedule_config, schedule_config: row.schedule_config,
recycle_policy: row.recycle_policy, rotation_policy: row.rotation_policy,
auto_schedule: row.auto_schedule, auto_schedule: row.auto_schedule,
access_mode: row.access_mode, access_mode: row.access_mode,
access_password_hash: row.access_password_hash,
logo: row.logo, logo: row.logo,
logo_position: row.logo_position, logo_position: row.logo_position,
logo_opacity: row.logo_opacity, logo_opacity: row.logo_opacity,
@@ -134,8 +130,8 @@ impl Channel {
&self.schedule_config &self.schedule_config
} }
pub fn recycle_policy(&self) -> &RecyclePolicy { pub fn rotation_policy(&self) -> &RotationPolicy {
&self.recycle_policy &self.rotation_policy
} }
pub fn auto_schedule(&self) -> bool { pub fn auto_schedule(&self) -> bool {
@@ -146,10 +142,6 @@ impl Channel {
&self.access_mode &self.access_mode
} }
pub fn access_password_hash(&self) -> Option<&str> {
self.access_password_hash.as_deref()
}
pub fn logo(&self) -> Option<&str> { pub fn logo(&self) -> Option<&str> {
self.logo.as_deref() self.logo.as_deref()
} }
@@ -206,8 +198,8 @@ impl Channel {
self.updated_at = Utc::now(); self.updated_at = Utc::now();
} }
pub fn set_recycle_policy(&mut self, policy: RecyclePolicy) { pub fn set_rotation_policy(&mut self, policy: RotationPolicy) {
self.recycle_policy = policy; self.rotation_policy = policy;
self.updated_at = Utc::now(); self.updated_at = Utc::now();
} }
@@ -331,13 +323,7 @@ pub struct ProgrammingBlock {
loop_on_finish: bool, loop_on_finish: bool,
#[serde(default)] #[serde(default)]
ignore_recycle_policy: bool, ignore_rotation_policy: bool,
#[serde(default)]
access_mode: AccessMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
access_password_hash: Option<String>,
} }
impl ProgrammingBlock { impl ProgrammingBlock {
@@ -359,9 +345,7 @@ impl ProgrammingBlock {
provider_id: String::new(), provider_id: String::new(),
}, },
loop_on_finish: true, loop_on_finish: true,
ignore_recycle_policy: false, ignore_rotation_policy: false,
access_mode: AccessMode::default(),
access_password_hash: None,
} }
} }
@@ -381,9 +365,7 @@ impl ProgrammingBlock {
provider_id: String::new(), provider_id: String::new(),
}, },
loop_on_finish: true, loop_on_finish: true,
ignore_recycle_policy: false, ignore_rotation_policy: false,
access_mode: AccessMode::default(),
access_password_hash: None,
} }
} }
@@ -411,16 +393,8 @@ impl ProgrammingBlock {
self.loop_on_finish self.loop_on_finish
} }
pub fn ignore_recycle_policy(&self) -> bool { pub fn ignore_rotation_policy(&self) -> bool {
self.ignore_recycle_policy self.ignore_rotation_policy
}
pub fn access_mode(&self) -> &AccessMode {
&self.access_mode
}
pub fn access_password_hash(&self) -> Option<&str> {
self.access_password_hash.as_deref()
} }
} }

View File

@@ -94,7 +94,7 @@ fn programming_block_getters() {
assert_eq!(block.start_time(), t(8, 0)); assert_eq!(block.start_time(), t(8, 0));
assert_eq!(block.duration_mins(), 120); assert_eq!(block.duration_mins(), 120);
assert!(block.loop_on_finish()); assert!(block.loop_on_finish());
assert!(!block.ignore_recycle_policy()); assert!(!block.ignore_rotation_policy());
} }
#[test] #[test]

View File

@@ -3,10 +3,10 @@ use super::*;
#[test] #[test]
fn new_generates_id_and_timestamp() { fn new_generates_id_and_timestamp() {
let email = Email::new("test@example.com").unwrap(); 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.is_admin());
assert!(user.password_hash().is_none()); assert!(user.password_hash().is_none());
assert_eq!(user.subject(), "oidc|123"); assert_eq!(user.subject(), "external|123");
} }
#[test] #[test]

View File

@@ -9,10 +9,10 @@ use crate::models::{
ScheduledSlot, ScheduledSlot,
}; };
use crate::ports::{ChannelQuery, IProviderRegistry, ScheduleCommand, ScheduleQuery, StreamQuality}; 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 fill;
mod recycle; mod rotation;
const SCHEDULE_DURATION_DAYS: i64 = 7; const SCHEDULE_DURATION_DAYS: i64 = 7;
@@ -27,12 +27,12 @@ struct AlgorithmicParams<'a> {
strategy: &'a FillStrategy, strategy: &'a FillStrategy,
block_id: BlockId, block_id: BlockId,
loop_on_finish: bool, loop_on_finish: bool,
ignore_recycle_policy: bool, ignore_rotation_policy: bool,
} }
struct RecycleContext<'a> { struct RotationContext<'a> {
history: &'a [PlaybackRecord], history: &'a [PlaybackRecord],
policy: &'a RecyclePolicy, policy: &'a RotationPolicy,
generation: u32, generation: u32,
last_item_id: Option<&'a MediaItemId>, last_item_id: Option<&'a MediaItemId>,
} }
@@ -131,9 +131,9 @@ impl ScheduleEngineService {
start: slot_start, start: slot_start,
end: slot_end, end: slot_end,
}, },
RecycleContext { RotationContext {
history: &history, history: &history,
policy: channel.recycle_policy(), policy: channel.rotation_policy(),
generation, generation,
last_item_id, last_item_id,
}, },
@@ -255,7 +255,7 @@ impl ScheduleEngineService {
&self, &self,
block: &ProgrammingBlock, block: &ProgrammingBlock,
window: BlockTimeWindow, window: BlockTimeWindow,
recycle: RecycleContext<'_>, rotation: RotationContext<'_>,
) -> DomainResult<Vec<ScheduledSlot>> { ) -> DomainResult<Vec<ScheduledSlot>> {
match block.content() { match block.content() {
BlockContent::Manual { items, .. } => { BlockContent::Manual { items, .. } => {
@@ -274,10 +274,10 @@ impl ScheduleEngineService {
strategy, strategy,
block_id: block.id(), block_id: block.id(),
loop_on_finish: block.loop_on_finish(), loop_on_finish: block.loop_on_finish(),
ignore_recycle_policy: block.ignore_recycle_policy(), ignore_rotation_policy: block.ignore_rotation_policy(),
}, },
window, window,
recycle, rotation,
) )
.await .await
} }
@@ -313,7 +313,7 @@ impl ScheduleEngineService {
&self, &self,
params: AlgorithmicParams<'_>, params: AlgorithmicParams<'_>,
window: BlockTimeWindow, window: BlockTimeWindow,
recycle: RecycleContext<'_>, rotation: RotationContext<'_>,
) -> DomainResult<Vec<ScheduledSlot>> { ) -> DomainResult<Vec<ScheduledSlot>> {
let candidates = self let candidates = self
.provider_registry .provider_registry
@@ -324,10 +324,10 @@ impl ScheduleEngineService {
return Ok(vec![]); return Ok(vec![]);
} }
let pool = if params.ignore_recycle_policy { let pool = if params.ignore_rotation_policy {
candidates.clone() candidates.clone()
} else { } 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 target_secs = (window.end - window.start).num_seconds() as u32;
let selected = fill::fill_block( let selected = fill::fill_block(
@@ -335,7 +335,7 @@ impl ScheduleEngineService {
&pool, &pool,
target_secs, target_secs,
params.strategy, params.strategy,
recycle.last_item_id, rotation.last_item_id,
params.loop_on_finish, params.loop_on_finish,
); );

View File

@@ -3,12 +3,12 @@ use std::collections::HashSet;
use chrono::Utc; use chrono::Utc;
use crate::models::{MediaItem, PlaybackRecord}; 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], candidates: &[MediaItem],
history: &[PlaybackRecord], history: &[PlaybackRecord],
policy: &RecyclePolicy, policy: &RotationPolicy,
current_generation: u32, current_generation: u32,
) -> Vec<MediaItem> { ) -> Vec<MediaItem> {
let now = Utc::now(); 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; (candidates.len() as f32 * policy.min_available_ratio).ceil() as usize;
if available.len() < min_count { if available.len() < min_count {
// Pool too small after cooldowns — recycle everything // Pool too small after cooldowns — rotate everything back in
candidates.to_vec() candidates.to_vec()
} else { } else {
available available
@@ -49,5 +49,5 @@ pub(super) fn apply_recycle_policy(
} }
#[cfg(test)] #[cfg(test)]
#[path = "tests/recycle.rs"] #[path = "tests/rotation.rs"]
mod tests; mod tests;

View File

@@ -13,12 +13,12 @@ fn record(item_id: &str, generation: u32) -> PlaybackRecord {
#[test] #[test]
fn no_history_returns_all() { fn no_history_returns_all() {
let pool = vec![item("a"), item("b"), item("c")]; let pool = vec![item("a"), item("b"), item("c")];
let policy = RecyclePolicy { let policy = RotationPolicy {
cooldown_days: Some(7), cooldown_days: Some(7),
cooldown_generations: None, cooldown_generations: None,
min_available_ratio: 0.2, 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); assert_eq!(result.len(), 3);
} }
@@ -26,12 +26,12 @@ fn no_history_returns_all() {
fn generation_cooldown_excludes() { fn generation_cooldown_excludes() {
let pool = vec![item("a"), item("b"), item("c")]; let pool = vec![item("a"), item("b"), item("c")];
let history = vec![record("a", 1)]; let history = vec![record("a", 1)];
let policy = RecyclePolicy { let policy = RotationPolicy {
cooldown_days: None, cooldown_days: None,
cooldown_generations: Some(2), cooldown_generations: Some(2),
min_available_ratio: 0.0, 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_eq!(result.len(), 2);
assert!(result.iter().all(|i| i.id().value() != "a")); assert!(result.iter().all(|i| i.id().value() != "a"));
} }
@@ -40,11 +40,11 @@ fn generation_cooldown_excludes() {
fn min_available_ratio_waives_cooldown() { fn min_available_ratio_waives_cooldown() {
let pool = vec![item("a"), item("b")]; let pool = vec![item("a"), item("b")];
let history = vec![record("a", 1), record("b", 1)]; let history = vec![record("a", 1), record("b", 1)];
let policy = RecyclePolicy { let policy = RotationPolicy {
cooldown_days: None, cooldown_days: None,
cooldown_generations: Some(5), cooldown_generations: Some(5),
min_available_ratio: 0.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); assert_eq!(result.len(), 2);
} }

View File

@@ -5,9 +5,7 @@ use serde::{Deserialize, Serialize};
pub enum AccessMode { pub enum AccessMode {
#[default] #[default]
Public, Public,
PasswordProtected, Private,
AccountRequired,
OwnerOnly,
} }
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]

View File

@@ -34,13 +34,13 @@ const DEFAULT_COOLDOWN_DAYS: u32 = 30;
const DEFAULT_MIN_AVAILABLE_RATIO: f32 = 0.2; const DEFAULT_MIN_AVAILABLE_RATIO: f32 = 0.2;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecyclePolicy { pub struct RotationPolicy {
pub cooldown_days: Option<u32>, pub cooldown_days: Option<u32>,
pub cooldown_generations: Option<u32>, pub cooldown_generations: Option<u32>,
pub min_available_ratio: f32, pub min_available_ratio: f32,
} }
impl Default for RecyclePolicy { impl Default for RotationPolicy {
fn default() -> Self { fn default() -> Self {
Self { Self {
cooldown_days: Some(DEFAULT_COOLDOWN_DAYS), cooldown_days: Some(DEFAULT_COOLDOWN_DAYS),

View File

@@ -65,7 +65,7 @@ pub async fn update_channel(
description: description.map(Some), description: description.map(Some),
timezone, timezone,
schedule_config, schedule_config,
recycle_policy: None, rotation_policy: None,
auto_schedule: None, auto_schedule: None,
}; };
match application::channels::update::execute(cmd_deps, cmd).await { match application::channels::update::execute(cmd_deps, cmd).await {

View File

@@ -81,7 +81,7 @@ pub async fn update_channel(
description: req.description.map(Some), description: req.description.map(Some),
timezone: req.timezone, timezone: req.timezone,
schedule_config: req.schedule_config.map(Into::into), schedule_config: req.schedule_config.map(Into::into),
recycle_policy: req.recycle_policy, rotation_policy: req.rotation_policy,
auto_schedule: req.auto_schedule, auto_schedule: req.auto_schedule,
}; };
let channel = application::channels::update::execute(&state.channel_command_deps, cmd).await?; let channel = application::channels::update::execute(&state.channel_command_deps, cmd).await?;

View File

@@ -45,15 +45,10 @@ pub async fn get_current_broadcast(
match application::schedule::get_current_broadcast::execute(&state.schedule_deps, query).await? match application::schedule::get_current_broadcast::execute(&state.schedule_deps, query).await?
{ {
Some(result) => { Some(result) => {
let slot_response = match &result.channel { let slot_response = SlotResponse::from(result.broadcast.slot().clone());
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();
Ok(Json(CurrentBroadcastResponse { Ok(Json(CurrentBroadcastResponse {
slot: slot_response, slot: slot_response,
offset_secs: result.broadcast.offset_secs(), offset_secs: result.broadcast.offset_secs(),
block_access_mode,
}) })
.into_response()) .into_response())
} }