//! Request and Response DTOs //! //! Data Transfer Objects for the API. //! Uses domain newtypes for validation instead of the validator crate. use chrono::{DateTime, Utc}; use domain::{Email, Password}; use serde::{Deserialize, Serialize}; use uuid::Uuid; /// Login request with validated email and password newtypes #[derive(Debug, Deserialize)] pub struct LoginRequest { /// Email is validated on deserialization pub email: Email, /// Password is validated on deserialization (min 8 chars) pub password: Password, } /// Register request with validated email and password newtypes #[derive(Debug, Deserialize)] pub struct RegisterRequest { /// Email is validated on deserialization pub email: Email, /// Password is validated on deserialization (min 8 chars) pub password: Password, } /// User response DTO #[derive(Debug, Serialize)] pub struct UserResponse { pub id: Uuid, pub email: String, pub created_at: DateTime, } /// JWT token response #[derive(Debug, Serialize)] pub struct TokenResponse { pub access_token: String, pub token_type: String, pub expires_in: u64, } /// System configuration response #[derive(Debug, Serialize)] pub struct ConfigResponse { pub allow_registration: bool, } // ============================================================================ // Channel DTOs // ============================================================================ #[derive(Debug, Deserialize)] pub struct CreateChannelRequest { pub name: String, pub description: Option, /// IANA timezone, e.g. "UTC" or "America/New_York" pub timezone: String, } /// All fields are optional — only provided fields are updated. #[derive(Debug, Deserialize)] pub struct UpdateChannelRequest { pub name: Option, pub description: Option, pub timezone: Option, /// Replace the entire schedule config (template import/edit) pub schedule_config: Option, pub recycle_policy: Option, } #[derive(Debug, Serialize)] pub struct ChannelResponse { pub id: Uuid, pub owner_id: Uuid, pub name: String, pub description: Option, pub timezone: String, pub schedule_config: domain::ScheduleConfig, pub recycle_policy: domain::RecyclePolicy, pub created_at: DateTime, pub updated_at: DateTime, } impl From for ChannelResponse { fn from(c: domain::Channel) -> Self { Self { id: c.id, owner_id: c.owner_id, name: c.name, description: c.description, timezone: c.timezone, schedule_config: c.schedule_config, recycle_policy: c.recycle_policy, created_at: c.created_at, updated_at: c.updated_at, } } } // ============================================================================ // EPG / playback DTOs // ============================================================================ #[derive(Debug, Serialize)] pub struct MediaItemResponse { pub id: String, pub title: String, pub content_type: domain::ContentType, pub duration_secs: u32, pub description: Option, pub genres: Vec, pub year: Option, pub tags: Vec, pub series_name: Option, pub season_number: Option, pub episode_number: Option, } impl From for MediaItemResponse { fn from(i: domain::MediaItem) -> Self { Self { id: i.id.into_inner(), title: i.title, content_type: i.content_type, duration_secs: i.duration_secs, description: i.description, genres: i.genres, year: i.year, tags: i.tags, series_name: i.series_name, season_number: i.season_number, episode_number: i.episode_number, } } } #[derive(Debug, Serialize)] pub struct ScheduledSlotResponse { pub id: Uuid, pub start_at: DateTime, pub end_at: DateTime, pub item: MediaItemResponse, pub source_block_id: Uuid, } impl From for ScheduledSlotResponse { fn from(s: domain::ScheduledSlot) -> Self { Self { id: s.id, start_at: s.start_at, end_at: s.end_at, item: s.item.into(), source_block_id: s.source_block_id, } } } /// What is currently playing on a channel. /// A 204 No Content response is returned instead when there is no active slot (no-signal). #[derive(Debug, Serialize)] pub struct CurrentBroadcastResponse { pub slot: ScheduledSlotResponse, /// Seconds elapsed since the start of the current item — use this as the /// initial seek position for the player. pub offset_secs: u32, } #[derive(Debug, Serialize)] pub struct ScheduleResponse { pub id: Uuid, pub channel_id: Uuid, pub valid_from: DateTime, pub valid_until: DateTime, pub generation: u32, pub slots: Vec, } impl From for ScheduleResponse { fn from(s: domain::GeneratedSchedule) -> Self { Self { id: s.id, channel_id: s.channel_id, valid_from: s.valid_from, valid_until: s.valid_until, generation: s.generation, slots: s.slots.into_iter().map(Into::into).collect(), } } }