279 lines
8.5 KiB
Rust
279 lines
8.5 KiB
Rust
//! 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<Utc>,
|
|
}
|
|
|
|
/// JWT token response
|
|
#[derive(Debug, Serialize)]
|
|
pub struct TokenResponse {
|
|
pub access_token: String,
|
|
pub token_type: String,
|
|
pub expires_in: u64,
|
|
}
|
|
|
|
/// Per-provider info returned by `GET /config`.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ProviderInfo {
|
|
pub id: String,
|
|
pub capabilities: domain::ProviderCapabilities,
|
|
}
|
|
|
|
/// System configuration response
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ConfigResponse {
|
|
pub allow_registration: bool,
|
|
/// All registered providers with their capabilities.
|
|
pub providers: Vec<ProviderInfo>,
|
|
/// Capabilities of the primary provider — kept for backward compatibility.
|
|
pub provider_capabilities: domain::ProviderCapabilities,
|
|
}
|
|
|
|
// ============================================================================
|
|
// Channel DTOs
|
|
// ============================================================================
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateChannelRequest {
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
/// IANA timezone, e.g. "UTC" or "America/New_York"
|
|
pub timezone: String,
|
|
pub access_mode: Option<domain::AccessMode>,
|
|
/// Plain-text password; hashed before storage.
|
|
pub access_password: Option<String>,
|
|
pub webhook_url: Option<String>,
|
|
pub webhook_poll_interval_secs: Option<u32>,
|
|
}
|
|
|
|
/// All fields are optional — only provided fields are updated.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct UpdateChannelRequest {
|
|
pub name: Option<String>,
|
|
pub description: Option<String>,
|
|
pub timezone: Option<String>,
|
|
/// Replace the entire schedule config (template import/edit)
|
|
pub schedule_config: Option<domain::ScheduleConfig>,
|
|
pub recycle_policy: Option<domain::RecyclePolicy>,
|
|
pub auto_schedule: Option<bool>,
|
|
pub access_mode: Option<domain::AccessMode>,
|
|
/// Empty string clears the password; non-empty re-hashes.
|
|
pub access_password: Option<String>,
|
|
/// `Some(None)` = clear logo, `Some(Some(url))` = set logo, `None` = unchanged.
|
|
pub logo: Option<Option<String>>,
|
|
pub logo_position: Option<domain::LogoPosition>,
|
|
pub logo_opacity: Option<f32>,
|
|
/// `Some(None)` = clear, `Some(Some(url))` = set, `None` = unchanged.
|
|
pub webhook_url: Option<Option<String>>,
|
|
pub webhook_poll_interval_secs: Option<u32>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ChannelResponse {
|
|
pub id: Uuid,
|
|
pub owner_id: Uuid,
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub timezone: String,
|
|
pub schedule_config: domain::ScheduleConfig,
|
|
pub recycle_policy: domain::RecyclePolicy,
|
|
pub auto_schedule: bool,
|
|
pub access_mode: domain::AccessMode,
|
|
pub logo: Option<String>,
|
|
pub logo_position: domain::LogoPosition,
|
|
pub logo_opacity: f32,
|
|
pub webhook_url: Option<String>,
|
|
pub webhook_poll_interval_secs: u32,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
impl From<domain::Channel> 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,
|
|
auto_schedule: c.auto_schedule,
|
|
access_mode: c.access_mode,
|
|
logo: c.logo,
|
|
logo_position: c.logo_position,
|
|
logo_opacity: c.logo_opacity,
|
|
webhook_url: c.webhook_url,
|
|
webhook_poll_interval_secs: c.webhook_poll_interval_secs,
|
|
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<String>,
|
|
pub genres: Vec<String>,
|
|
pub year: Option<u16>,
|
|
pub tags: Vec<String>,
|
|
pub series_name: Option<String>,
|
|
pub season_number: Option<u32>,
|
|
pub episode_number: Option<u32>,
|
|
}
|
|
|
|
impl From<domain::MediaItem> 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<Utc>,
|
|
pub end_at: DateTime<Utc>,
|
|
pub item: MediaItemResponse,
|
|
pub source_block_id: Uuid,
|
|
#[serde(default)]
|
|
pub block_access_mode: domain::AccessMode,
|
|
}
|
|
|
|
impl From<domain::ScheduledSlot> 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,
|
|
block_access_mode: domain::AccessMode::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ScheduledSlotResponse {
|
|
pub fn with_block_access(slot: domain::ScheduledSlot, channel: &domain::Channel) -> Self {
|
|
let block_access_mode = channel
|
|
.schedule_config
|
|
.blocks
|
|
.iter()
|
|
.find(|b| b.id == slot.source_block_id)
|
|
.map(|b| b.access_mode.clone())
|
|
.unwrap_or_default();
|
|
Self {
|
|
id: slot.id,
|
|
start_at: slot.start_at,
|
|
end_at: slot.end_at,
|
|
item: slot.item.into(),
|
|
source_block_id: slot.source_block_id,
|
|
block_access_mode,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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,
|
|
/// Access mode of the block currently playing. The stream is gated by this.
|
|
pub block_access_mode: domain::AccessMode,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ScheduleResponse {
|
|
pub id: Uuid,
|
|
pub channel_id: Uuid,
|
|
pub valid_from: DateTime<Utc>,
|
|
pub valid_until: DateTime<Utc>,
|
|
pub generation: u32,
|
|
pub slots: Vec<ScheduledSlotResponse>,
|
|
}
|
|
|
|
// ============================================================================
|
|
// Transcode DTOs
|
|
// ============================================================================
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct TranscodeSettingsResponse {
|
|
pub cleanup_ttl_hours: u32,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct UpdateTranscodeSettingsRequest {
|
|
pub cleanup_ttl_hours: u32,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct TranscodeStatsResponse {
|
|
pub cache_size_bytes: u64,
|
|
pub item_count: usize,
|
|
}
|
|
|
|
impl From<domain::GeneratedSchedule> 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(),
|
|
}
|
|
}
|
|
}
|