diff --git a/Cargo.lock b/Cargo.lock index 3139eef..b802c9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,18 @@ dependencies = [ "libc", ] +[[package]] +name = "api-types" +version = "0.1.0" +dependencies = [ + "chrono", + "domain", + "serde", + "serde_json", + "utoipa", + "uuid", +] + [[package]] name = "application" version = "0.1.0" @@ -141,6 +153,12 @@ dependencies = [ "serde", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -212,6 +230,12 @@ dependencies = [ "r-efi", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -339,6 +363,18 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "serde_core", +] + [[package]] name = "itoa" version = "1.0.18" @@ -759,6 +795,30 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utoipa" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bde15df68e80b16c7d16b9616e80770ad158988daa56a27dccd1e55558b0160" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba0b99ee52df3028635d93840c797102da61f8a7bb3cf751032455895b52ef8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "uuid", +] + [[package]] name = "uuid" version = "1.23.4" diff --git a/Cargo.toml b/Cargo.toml index a0722fd..088dbd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/domain", "crates/application"] +members = ["crates/domain", "crates/application", "crates/api-types"] exclude = ["k-tv-backend", "k-tv-frontend"] resolver = "2" diff --git a/crates/api-types/Cargo.toml b/crates/api-types/Cargo.toml new file mode 100644 index 0000000..f8a52ed --- /dev/null +++ b/crates/api-types/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "api-types" +version = "0.1.0" +edition = "2024" + +[dependencies] +domain = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +utoipa = { workspace = true } +chrono = { workspace = true } +uuid = { workspace = true } diff --git a/crates/api-types/src/admin.rs b/crates/api-types/src/admin.rs new file mode 100644 index 0000000..f974943 --- /dev/null +++ b/crates/api-types/src/admin.rs @@ -0,0 +1,34 @@ +//! Admin response DTOs. + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use uuid::Uuid; + +/// Admin settings response (key-value pairs from `app_settings` table). +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct SettingsResponse { + pub settings: std::collections::HashMap, +} + +/// An activity log entry for the admin dashboard. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct ActivityEventResponse { + pub id: Uuid, + pub timestamp: DateTime, + pub event_type: String, + pub detail: String, + pub channel_id: Option, +} + +impl From for ActivityEventResponse { + fn from(e: domain::ActivityEvent) -> Self { + Self { + id: e.id(), + timestamp: e.timestamp(), + event_type: e.event_type().to_string(), + detail: e.detail().to_string(), + channel_id: e.channel_id(), + } + } +} diff --git a/crates/api-types/src/auth.rs b/crates/api-types/src/auth.rs new file mode 100644 index 0000000..2fb35b4 --- /dev/null +++ b/crates/api-types/src/auth.rs @@ -0,0 +1,59 @@ +//! Authentication request and response DTOs. + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use uuid::Uuid; + +/// Login request. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct LoginRequest { + pub email: String, + pub password: String, + #[serde(default)] + pub remember_me: bool, +} + +/// Register request. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct RegisterRequest { + pub email: String, + pub password: String, +} + +/// Refresh token request. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct RefreshRequest { + pub refresh_token: String, +} + +/// JWT token response. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct TokenResponse { + pub access_token: String, + pub token_type: String, + pub expires_in: u64, + /// Only present when `remember_me` was true at login, or on token refresh. + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, +} + +/// User response DTO. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct UserResponse { + pub id: Uuid, + pub email: String, + pub is_admin: bool, + pub created_at: DateTime, +} + +impl From for UserResponse { + fn from(user: domain::User) -> Self { + Self { + id: user.id().value(), + email: user.email().to_string(), + is_admin: user.is_admin(), + created_at: user.created_at(), + } + } +} diff --git a/crates/api-types/src/channels.rs b/crates/api-types/src/channels.rs new file mode 100644 index 0000000..fcabb07 --- /dev/null +++ b/crates/api-types/src/channels.rs @@ -0,0 +1,132 @@ +//! Channel request and response DTOs. + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use uuid::Uuid; + +use crate::common::enum_to_string; + +/// Create channel request. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct CreateChannelRequest { + pub name: String, + pub description: Option, + /// IANA timezone, e.g. "UTC" or "America/New_York". + pub timezone: String, + /// One of: "public", "password_protected", "account_required", "owner_only". + pub access_mode: Option, + /// Plain-text password; hashed before storage. + pub access_password: Option, + pub webhook_url: Option, + pub webhook_poll_interval_secs: Option, + pub webhook_body_template: Option, + pub webhook_headers: Option, +} + +/// Update channel request. All fields are optional -- only provided fields are +/// updated. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +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, + pub auto_schedule: Option, + /// One of: "public", "password_protected", "account_required", "owner_only". + pub access_mode: Option, + /// Empty string clears the password; non-empty re-hashes. + pub access_password: Option, + /// `null` = clear logo, string = set logo URL. Omit to leave unchanged. + pub logo: Option>, + /// One of: "top_left", "top_right", "bottom_left", "bottom_right". + pub logo_position: Option, + pub logo_opacity: Option, + /// `null` = clear, string = set. Omit to leave unchanged. + pub webhook_url: Option>, + pub webhook_poll_interval_secs: Option, + /// `null` = clear, string = set. Omit to leave unchanged. + pub webhook_body_template: Option>, + /// `null` = clear, string = set. Omit to leave unchanged. + pub webhook_headers: Option>, +} + +/// Channel response DTO. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct ChannelResponse { + pub id: Uuid, + pub owner_id: Uuid, + pub name: String, + pub description: Option, + pub timezone: String, + /// The full schedule config as a JSON object, decoupled from domain internals. + pub schedule_config: serde_json::Value, + /// The recycle policy as a JSON object. + pub recycle_policy: serde_json::Value, + pub auto_schedule: bool, + /// E.g. "public", "password_protected", "account_required", "owner_only". + pub access_mode: String, + pub logo: Option, + /// E.g. "top_left", "top_right", "bottom_left", "bottom_right". + pub logo_position: String, + pub logo_opacity: f32, + pub webhook_url: Option, + pub webhook_poll_interval_secs: u32, + pub webhook_body_template: Option, + pub webhook_headers: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl From for ChannelResponse { + fn from(c: domain::Channel) -> Self { + Self { + id: c.id().value(), + owner_id: c.owner_id().value(), + name: c.name().to_string(), + 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(), + auto_schedule: c.auto_schedule(), + access_mode: enum_to_string(c.access_mode()), + logo: c.logo().map(|s| s.to_string()), + logo_position: enum_to_string(c.logo_position()), + logo_opacity: c.logo_opacity(), + webhook_url: c.webhook_url().map(|s| s.to_string()), + webhook_poll_interval_secs: c.webhook_poll_interval_secs(), + webhook_body_template: c.webhook_body_template().map(|s| s.to_string()), + webhook_headers: c.webhook_headers().map(|s| s.to_string()), + created_at: c.created_at(), + updated_at: c.updated_at(), + } + } +} + +/// Config history snapshot response. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct ConfigSnapshotResponse { + pub id: Uuid, + pub version_num: i64, + pub label: Option, + pub created_at: DateTime, +} + +impl From for ConfigSnapshotResponse { + fn from(s: domain::ChannelConfigSnapshot) -> Self { + Self { + id: s.id(), + version_num: s.version_num(), + label: s.label().map(|s| s.to_string()), + created_at: s.created_at(), + } + } +} + +/// Patch snapshot request (rename label). +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct PatchSnapshotRequest { + pub label: Option, +} diff --git a/crates/api-types/src/common.rs b/crates/api-types/src/common.rs new file mode 100644 index 0000000..203c9ed --- /dev/null +++ b/crates/api-types/src/common.rs @@ -0,0 +1,55 @@ +//! Common types shared across API modules. + +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +/// Paginated response wrapper. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct PaginatedResponse { + pub items: Vec, + pub total: u64, +} + +impl PaginatedResponse { + pub fn new(items: Vec, total: u64) -> Self { + Self { items, total } + } +} + +/// Standard error response body. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct ErrorResponse { + pub error: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub details: Option, +} + +impl ErrorResponse { + pub fn new(error: impl Into) -> Self { + Self { + error: error.into(), + details: None, + } + } + + pub fn with_details(error: impl Into, details: impl Into) -> Self { + Self { + error: error.into(), + details: Some(details.into()), + } + } +} + +/// Serialize a `serde::Serialize` enum to its snake_case string representation. +/// +/// Used internally by `From` impls to convert domain enums (AccessMode, +/// LogoPosition, ContentType, etc.) into plain strings for API responses. +pub(crate) fn enum_to_string(val: &T) -> String { + serde_json::to_value(val) + .ok() + .and_then(|v| match v { + serde_json::Value::String(s) => Some(s), + _ => None, + }) + .unwrap_or_default() +} diff --git a/crates/api-types/src/config.rs b/crates/api-types/src/config.rs new file mode 100644 index 0000000..db699ac --- /dev/null +++ b/crates/api-types/src/config.rs @@ -0,0 +1,58 @@ +//! System configuration response DTOs. + +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::common::enum_to_string; + +/// Provider capabilities response, mirroring the domain type for OpenAPI docs. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct ProviderCapabilitiesResponse { + pub collections: bool, + pub series: bool, + pub genres: bool, + pub tags: bool, + pub decade: bool, + pub search: bool, + /// E.g. "hls" or "direct_file". + pub streaming_protocol: String, + /// Whether `POST /files/rescan` is available. + pub rescan: bool, + /// Whether on-demand FFmpeg transcoding to HLS is available. + pub transcode: bool, +} + +impl From for ProviderCapabilitiesResponse { + fn from(c: domain::ports::ProviderCapabilities) -> Self { + Self { + collections: c.collections, + series: c.series, + genres: c.genres, + tags: c.tags, + decade: c.decade, + search: c.search, + streaming_protocol: enum_to_string(&c.streaming_protocol), + rescan: c.rescan, + transcode: c.transcode, + } + } +} + +/// Per-provider info returned in the system config response. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct ProviderInfo { + pub id: String, + pub capabilities: ProviderCapabilitiesResponse, +} + +/// System configuration response (`GET /config`). +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct ConfigResponse { + pub allow_registration: bool, + /// All registered providers with their capabilities. + pub providers: Vec, + /// Capabilities of the primary provider -- kept for backward compatibility. + pub provider_capabilities: ProviderCapabilitiesResponse, + /// Provider type strings supported by this build (feature-gated). + pub available_provider_types: Vec, +} diff --git a/crates/api-types/src/lib.rs b/crates/api-types/src/lib.rs new file mode 100644 index 0000000..7ca000c --- /dev/null +++ b/crates/api-types/src/lib.rs @@ -0,0 +1,35 @@ +//! HTTP request and response DTOs with OpenAPI schema generation. +//! +//! Pure data transfer objects for the API layer. All structs derive +//! `Serialize`, `Deserialize`, and `utoipa::ToSchema` for automatic +//! OpenAPI documentation. +//! +//! Response types provide `From` implementations to +//! convert domain models into API-facing DTOs. + +pub mod admin; +pub mod auth; +pub mod channels; +pub mod common; +pub mod config; +pub mod library; +pub mod providers; +pub mod schedule; +pub mod transcode; + +// Re-export all public types for convenience. +pub use admin::{ActivityEventResponse, SettingsResponse}; +pub use auth::{LoginRequest, RefreshRequest, RegisterRequest, TokenResponse, UserResponse}; +pub use channels::{ + ChannelResponse, ConfigSnapshotResponse, CreateChannelRequest, PatchSnapshotRequest, + UpdateChannelRequest, +}; +pub use common::{ErrorResponse, PaginatedResponse}; +pub use config::{ConfigResponse, ProviderCapabilitiesResponse, ProviderInfo}; +pub use library::{CollectionResponse, LibraryItemResponse, SeasonResponse, ShowResponse}; +pub use providers::{ProviderConfigRequest, ProviderConfigResponse}; +pub use schedule::{ + CurrentBroadcastResponse, MediaItemResponse, ScheduleHistoryEntry, ScheduleResponse, + SlotResponse, +}; +pub use transcode::{TranscodeSettingsResponse, TranscodeStatsResponse, UpdateTranscodeSettingsRequest}; diff --git a/crates/api-types/src/library.rs b/crates/api-types/src/library.rs new file mode 100644 index 0000000..22f9697 --- /dev/null +++ b/crates/api-types/src/library.rs @@ -0,0 +1,111 @@ +//! Library browsing response DTOs. + +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::common::enum_to_string; + +/// Library item response (synced from a media provider). +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct LibraryItemResponse { + pub id: String, + pub provider_id: String, + pub external_id: String, + pub title: String, + /// E.g. "movie", "episode", "short". + pub content_type: String, + pub duration_secs: u32, + pub series_name: Option, + pub season_number: Option, + pub episode_number: Option, + pub year: Option, + pub genres: Vec, + pub tags: Vec, + pub collection_id: Option, + pub collection_name: Option, + pub collection_type: Option, + pub thumbnail_url: Option, + pub synced_at: String, +} + +impl From for LibraryItemResponse { + fn from(i: domain::LibraryItem) -> Self { + Self { + id: i.id().to_string(), + provider_id: i.provider_id().to_string(), + external_id: i.external_id().to_string(), + title: i.title().to_string(), + content_type: enum_to_string(i.content_type()), + duration_secs: i.duration_secs(), + series_name: i.series_name().map(|s| s.to_string()), + season_number: i.season_number(), + episode_number: i.episode_number(), + year: i.year(), + genres: i.genres().to_vec(), + tags: i.tags().to_vec(), + collection_id: i.collection_id().map(|s| s.to_string()), + collection_name: i.collection_name().map(|s| s.to_string()), + collection_type: i.collection_type().map(|s| s.to_string()), + thumbnail_url: i.thumbnail_url().map(|s| s.to_string()), + synced_at: i.synced_at().to_string(), + } + } +} + +/// Library collection summary. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct CollectionResponse { + pub id: String, + pub name: String, + pub collection_type: Option, +} + +impl From for CollectionResponse { + fn from(c: domain::LibraryCollection) -> Self { + Self { + id: c.id().to_string(), + name: c.name().to_string(), + collection_type: c.collection_type().map(|s| s.to_string()), + } + } +} + +/// TV show summary aggregated from synced episodes. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct ShowResponse { + pub series_name: String, + pub episode_count: u32, + pub season_count: u32, + pub thumbnail_url: Option, + pub genres: Vec, +} + +impl From for ShowResponse { + fn from(s: domain::ShowSummary) -> Self { + Self { + series_name: s.series_name().to_string(), + episode_count: s.episode_count(), + season_count: s.season_count(), + thumbnail_url: s.thumbnail_url().map(|s| s.to_string()), + genres: s.genres().to_vec(), + } + } +} + +/// Season summary within a TV show. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct SeasonResponse { + pub season_number: u32, + pub episode_count: u32, + pub thumbnail_url: Option, +} + +impl From for SeasonResponse { + fn from(s: domain::SeasonSummary) -> Self { + Self { + season_number: s.season_number(), + episode_count: s.episode_count(), + thumbnail_url: s.thumbnail_url().map(|s| s.to_string()), + } + } +} diff --git a/crates/api-types/src/providers.rs b/crates/api-types/src/providers.rs new file mode 100644 index 0000000..ebe69e3 --- /dev/null +++ b/crates/api-types/src/providers.rs @@ -0,0 +1,44 @@ +//! Provider configuration request and response DTOs. + +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +/// Request to create or update a provider configuration. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct ProviderConfigRequest { + /// E.g. "jellyfin", "local_files". + pub provider_type: String, + /// Provider-specific configuration blob (URL, API key, path, etc.). + pub config: serde_json::Value, + #[serde(default = "default_true")] + pub enabled: bool, +} + +fn default_true() -> bool { + true +} + +/// Provider configuration response. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct ProviderConfigResponse { + pub id: String, + pub provider_type: String, + /// Provider-specific configuration blob (deserialized from stored JSON). + pub config: serde_json::Value, + pub enabled: bool, + pub updated_at: String, +} + +impl From for ProviderConfigResponse { + fn from(r: domain::ProviderConfigRow) -> Self { + let config = serde_json::from_str(r.config_json()) + .unwrap_or(serde_json::Value::Object(Default::default())); + Self { + id: r.id().to_string(), + provider_type: r.provider_type().to_string(), + config, + enabled: r.enabled(), + updated_at: r.updated_at().to_string(), + } + } +} diff --git a/crates/api-types/src/schedule.rs b/crates/api-types/src/schedule.rs new file mode 100644 index 0000000..472d09d --- /dev/null +++ b/crates/api-types/src/schedule.rs @@ -0,0 +1,156 @@ +//! Schedule and EPG response DTOs. + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use uuid::Uuid; + +use crate::common::enum_to_string; + +/// Media item snapshot within a scheduled slot. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct MediaItemResponse { + pub id: String, + pub title: String, + /// E.g. "movie", "episode", "short". + pub content_type: String, + 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().value().to_string(), + title: i.title().to_string(), + content_type: enum_to_string(i.content_type()), + duration_secs: i.duration_secs(), + description: i.description().map(|s| s.to_string()), + genres: i.genres().to_vec(), + year: i.year(), + tags: i.tags().to_vec(), + series_name: i.series_name().map(|s| s.to_string()), + season_number: i.season_number(), + episode_number: i.episode_number(), + } + } +} + +/// A single resolved broadcast slot within a schedule. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct SlotResponse { + pub id: Uuid, + pub start_at: DateTime, + pub end_at: DateTime, + pub item: MediaItemResponse, + pub source_block_id: Uuid, + /// Access mode of the programming block that produced this slot. + #[serde(default)] + pub block_access_mode: String, +} + +impl From for SlotResponse { + fn from(s: domain::ScheduledSlot) -> Self { + Self { + id: s.id().value(), + start_at: s.start_at(), + end_at: s.end_at(), + item: s.item().clone().into(), + source_block_id: s.source_block_id().value(), + block_access_mode: String::from("public"), + } + } +} + +impl SlotResponse { + /// Build a slot response with the block-level access mode resolved from the + /// channel's schedule config. + 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("public")); + 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, + } + } +} + +/// What is currently playing on a channel. +/// +/// A 204 No Content response is returned instead when there is no active slot +/// (no-signal / dead air). +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct CurrentBroadcastResponse { + pub slot: SlotResponse, + /// Seconds elapsed since the start of the current item -- use 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: String, +} + +/// Full schedule response with all resolved slots. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +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 { + let id = s.id().value(); + let channel_id = s.channel_id().value(); + let valid_from = s.valid_from(); + let valid_until = s.valid_until(); + let generation = s.generation(); + let slots = s.into_slots().into_iter().map(Into::into).collect(); + Self { + id, + channel_id, + valid_from, + valid_until, + generation, + slots, + } + } +} + +/// Compact schedule history entry (no slots, just metadata). +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct ScheduleHistoryEntry { + pub id: Uuid, + pub generation: u32, + pub valid_from: DateTime, + pub valid_until: DateTime, + pub slot_count: usize, +} + +impl From for ScheduleHistoryEntry { + fn from(s: domain::GeneratedSchedule) -> Self { + Self { + id: s.id().value(), + generation: s.generation(), + valid_from: s.valid_from(), + valid_until: s.valid_until(), + slot_count: s.slots().len(), + } + } +} diff --git a/crates/api-types/src/transcode.rs b/crates/api-types/src/transcode.rs new file mode 100644 index 0000000..4da14de --- /dev/null +++ b/crates/api-types/src/transcode.rs @@ -0,0 +1,23 @@ +//! Transcode settings and stats response DTOs. + +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +/// Transcode settings response. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct TranscodeSettingsResponse { + pub cleanup_ttl_hours: u32, +} + +/// Request to update transcode settings. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct UpdateTranscodeSettingsRequest { + pub cleanup_ttl_hours: u32, +} + +/// Transcode cache statistics. +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct TranscodeStatsResponse { + pub cache_size_bytes: u64, + pub item_count: usize, +}