api-types: HTTP DTOs with utoipa OpenAPI derives
This commit is contained in:
60
Cargo.lock
generated
60
Cargo.lock
generated
@@ -11,6 +11,18 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "api-types"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"domain",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"utoipa",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "application"
|
name = "application"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -141,6 +153,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
@@ -212,6 +230,12 @@ dependencies = [
|
|||||||
"r-efi",
|
"r-efi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.17.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.65"
|
version = "0.1.65"
|
||||||
@@ -339,6 +363,18 @@ dependencies = [
|
|||||||
"icu_properties",
|
"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]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@@ -759,6 +795,30 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
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]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.23.4"
|
version = "1.23.4"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["crates/domain", "crates/application"]
|
members = ["crates/domain", "crates/application", "crates/api-types"]
|
||||||
exclude = ["k-tv-backend", "k-tv-frontend"]
|
exclude = ["k-tv-backend", "k-tv-frontend"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|||||||
12
crates/api-types/Cargo.toml
Normal file
12
crates/api-types/Cargo.toml
Normal file
@@ -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 }
|
||||||
34
crates/api-types/src/admin.rs
Normal file
34
crates/api-types/src/admin.rs
Normal file
@@ -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<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An activity log entry for the admin dashboard.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct ActivityEventResponse {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub timestamp: DateTime<Utc>,
|
||||||
|
pub event_type: String,
|
||||||
|
pub detail: String,
|
||||||
|
pub channel_id: Option<Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<domain::ActivityEvent> 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
crates/api-types/src/auth.rs
Normal file
59
crates/api-types/src/auth.rs
Normal file
@@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<domain::User> 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
132
crates/api-types/src/channels.rs
Normal file
132
crates/api-types/src/channels.rs
Normal file
@@ -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<String>,
|
||||||
|
/// 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<String>,
|
||||||
|
/// Plain-text password; hashed before storage.
|
||||||
|
pub access_password: Option<String>,
|
||||||
|
pub webhook_url: Option<String>,
|
||||||
|
pub webhook_poll_interval_secs: Option<u32>,
|
||||||
|
pub webhook_body_template: Option<String>,
|
||||||
|
pub webhook_headers: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update channel request. All fields are optional -- only provided fields are
|
||||||
|
/// updated.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
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<serde_json::Value>,
|
||||||
|
pub recycle_policy: Option<serde_json::Value>,
|
||||||
|
pub auto_schedule: Option<bool>,
|
||||||
|
/// One of: "public", "password_protected", "account_required", "owner_only".
|
||||||
|
pub access_mode: Option<String>,
|
||||||
|
/// Empty string clears the password; non-empty re-hashes.
|
||||||
|
pub access_password: Option<String>,
|
||||||
|
/// `null` = clear logo, string = set logo URL. Omit to leave unchanged.
|
||||||
|
pub logo: Option<Option<String>>,
|
||||||
|
/// One of: "top_left", "top_right", "bottom_left", "bottom_right".
|
||||||
|
pub logo_position: Option<String>,
|
||||||
|
pub logo_opacity: Option<f32>,
|
||||||
|
/// `null` = clear, string = set. Omit to leave unchanged.
|
||||||
|
pub webhook_url: Option<Option<String>>,
|
||||||
|
pub webhook_poll_interval_secs: Option<u32>,
|
||||||
|
/// `null` = clear, string = set. Omit to leave unchanged.
|
||||||
|
pub webhook_body_template: Option<Option<String>>,
|
||||||
|
/// `null` = clear, string = set. Omit to leave unchanged.
|
||||||
|
pub webhook_headers: Option<Option<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<String>,
|
||||||
|
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<String>,
|
||||||
|
/// E.g. "top_left", "top_right", "bottom_left", "bottom_right".
|
||||||
|
pub logo_position: String,
|
||||||
|
pub logo_opacity: f32,
|
||||||
|
pub webhook_url: Option<String>,
|
||||||
|
pub webhook_poll_interval_secs: u32,
|
||||||
|
pub webhook_body_template: Option<String>,
|
||||||
|
pub webhook_headers: Option<String>,
|
||||||
|
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().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<String>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<domain::ChannelConfigSnapshot> 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<String>,
|
||||||
|
}
|
||||||
55
crates/api-types/src/common.rs
Normal file
55
crates/api-types/src/common.rs
Normal file
@@ -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<T: ToSchema> {
|
||||||
|
pub items: Vec<T>,
|
||||||
|
pub total: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ToSchema> PaginatedResponse<T> {
|
||||||
|
pub fn new(items: Vec<T>, 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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorResponse {
|
||||||
|
pub fn new(error: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
error: error.into(),
|
||||||
|
details: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_details(error: impl Into<String>, details: impl Into<String>) -> 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<T: Serialize>(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()
|
||||||
|
}
|
||||||
58
crates/api-types/src/config.rs
Normal file
58
crates/api-types/src/config.rs
Normal file
@@ -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<domain::ports::ProviderCapabilities> 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<ProviderInfo>,
|
||||||
|
/// 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<String>,
|
||||||
|
}
|
||||||
35
crates/api-types/src/lib.rs
Normal file
35
crates/api-types/src/lib.rs
Normal file
@@ -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<DomainType>` 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};
|
||||||
111
crates/api-types/src/library.rs
Normal file
111
crates/api-types/src/library.rs
Normal file
@@ -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<String>,
|
||||||
|
pub season_number: Option<u32>,
|
||||||
|
pub episode_number: Option<u32>,
|
||||||
|
pub year: Option<u16>,
|
||||||
|
pub genres: Vec<String>,
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
pub collection_id: Option<String>,
|
||||||
|
pub collection_name: Option<String>,
|
||||||
|
pub collection_type: Option<String>,
|
||||||
|
pub thumbnail_url: Option<String>,
|
||||||
|
pub synced_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<domain::LibraryItem> 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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<domain::LibraryCollection> 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<String>,
|
||||||
|
pub genres: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<domain::ShowSummary> 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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<domain::SeasonSummary> 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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
crates/api-types/src/providers.rs
Normal file
44
crates/api-types/src/providers.rs
Normal file
@@ -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<domain::ProviderConfigRow> 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
156
crates/api-types/src/schedule.rs
Normal file
156
crates/api-types/src/schedule.rs
Normal file
@@ -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<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().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<Utc>,
|
||||||
|
pub end_at: DateTime<Utc>,
|
||||||
|
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<domain::ScheduledSlot> 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<Utc>,
|
||||||
|
pub valid_until: DateTime<Utc>,
|
||||||
|
pub generation: u32,
|
||||||
|
pub slots: Vec<SlotResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<domain::GeneratedSchedule> 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<Utc>,
|
||||||
|
pub valid_until: DateTime<Utc>,
|
||||||
|
pub slot_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<domain::GeneratedSchedule> 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
crates/api-types/src/transcode.rs
Normal file
23
crates/api-types/src/transcode.rs
Normal file
@@ -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,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user