wire utoipa OpenAPI: #[utoipa::path] on all handlers, Scalar UI (#14)

This commit is contained in:
2026-07-12 14:29:52 +02:00
parent b00272aceb
commit e27e2ab6d1
18 changed files with 651 additions and 48 deletions

View File

@@ -11,6 +11,17 @@ use crate::state::AppState;
const DEFAULT_ACTIVITY_LIMIT: u32 = 50;
#[utoipa::path(
get,
path = "/api/v1/admin/settings",
tag = "admin",
security(("bearer" = [])),
responses(
(status = 200, body = SettingsResponse),
(status = 401, body = api_types::ErrorResponse),
(status = 403, body = api_types::ErrorResponse),
)
)]
pub async fn get_settings(
State(state): State<AppState>,
AdminUser(_user): AdminUser,
@@ -20,6 +31,18 @@ pub async fn get_settings(
Ok(Json(SettingsResponse { settings }))
}
#[utoipa::path(
put,
path = "/api/v1/admin/settings",
tag = "admin",
security(("bearer" = [])),
request_body = HashMap<String, String>,
responses(
(status = 200, body = SettingsResponse),
(status = 401, body = api_types::ErrorResponse),
(status = 403, body = api_types::ErrorResponse),
)
)]
pub async fn update_settings(
State(state): State<AppState>,
AdminUser(_user): AdminUser,
@@ -34,6 +57,18 @@ pub async fn update_settings(
Ok(Json(SettingsResponse { settings }))
}
#[utoipa::path(
get,
path = "/api/v1/admin/activity",
tag = "admin",
security(("bearer" = [])),
params(ActivityLogParams),
responses(
(status = 200, body = Vec<ActivityEventResponse>),
(status = 401, body = api_types::ErrorResponse),
(status = 403, body = api_types::ErrorResponse),
)
)]
pub async fn get_activity_log(
State(state): State<AppState>,
AdminUser(_user): AdminUser,

View File

@@ -10,6 +10,16 @@ use crate::state::AppState;
const TOKEN_TYPE_BEARER: &str = "Bearer";
#[utoipa::path(
post,
path = "/api/v1/auth/register",
tag = "auth",
request_body = RegisterRequest,
responses(
(status = 200, body = UserResponse),
(status = 409, body = api_types::ErrorResponse),
)
)]
pub async fn register(
State(state): State<AppState>,
Json(req): Json<RegisterRequest>,
@@ -22,6 +32,16 @@ pub async fn register(
Ok(Json(UserResponse::from(user)))
}
#[utoipa::path(
post,
path = "/api/v1/auth/login",
tag = "auth",
request_body = LoginRequest,
responses(
(status = 200, body = TokenResponse),
(status = 401, body = api_types::ErrorResponse),
)
)]
pub async fn login(
State(state): State<AppState>,
Json(req): Json<LoginRequest>,
@@ -40,15 +60,43 @@ pub async fn login(
}))
}
#[utoipa::path(
post,
path = "/api/v1/auth/logout",
tag = "auth",
responses(
(status = 200, body = serde_json::Value),
)
)]
pub async fn logout() -> Result<Json<serde_json::Value>, AppError> {
Ok(Json(serde_json::json!({"message": "logged out"})))
}
#[utoipa::path(
get,
path = "/api/v1/auth/me",
tag = "auth",
security(("bearer" = [])),
responses(
(status = 200, body = UserResponse),
(status = 401, body = api_types::ErrorResponse),
)
)]
pub async fn me(CurrentUser(user): CurrentUser) -> Result<Json<UserResponse>, AppError> {
Ok(Json(UserResponse::from(user)))
}
#[cfg(feature = "auth-jwt")]
#[utoipa::path(
post,
path = "/api/v1/auth/refresh",
tag = "auth",
request_body = RefreshRequest,
responses(
(status = 200, body = TokenResponse),
(status = 401, body = api_types::ErrorResponse),
)
)]
pub async fn refresh_token(
State(state): State<AppState>,
Json(req): Json<RefreshRequest>,

View File

@@ -13,6 +13,16 @@ use crate::errors::AppError;
use crate::extractors::CurrentUser;
use crate::state::AppState;
#[utoipa::path(
get,
path = "/api/v1/channels",
tag = "channels",
security(("bearer" = [])),
responses(
(status = 200, body = Vec<ChannelResponse>),
(status = 401, body = api_types::ErrorResponse),
)
)]
pub async fn list_channels(
State(state): State<AppState>,
CurrentUser(_user): CurrentUser,
@@ -21,6 +31,16 @@ pub async fn list_channels(
Ok(Json(channels.into_iter().map(ChannelResponse::from).collect()))
}
#[utoipa::path(
get,
path = "/api/v1/channels/mine",
tag = "channels",
security(("bearer" = [])),
responses(
(status = 200, body = Vec<ChannelResponse>),
(status = 401, body = api_types::ErrorResponse),
)
)]
pub async fn list_my_channels(
State(state): State<AppState>,
CurrentUser(user): CurrentUser,
@@ -29,6 +49,18 @@ pub async fn list_my_channels(
Ok(Json(channels.into_iter().map(ChannelResponse::from).collect()))
}
#[utoipa::path(
post,
path = "/api/v1/channels",
tag = "channels",
security(("bearer" = [])),
request_body = CreateChannelRequest,
responses(
(status = 200, body = ChannelResponse),
(status = 400, body = api_types::ErrorResponse),
(status = 401, body = api_types::ErrorResponse),
)
)]
pub async fn create_channel(
State(state): State<AppState>,
CurrentUser(user): CurrentUser,
@@ -43,6 +75,20 @@ pub async fn create_channel(
Ok(Json(ChannelResponse::from(channel)))
}
#[utoipa::path(
get,
path = "/api/v1/channels/{id}",
tag = "channels",
security(("bearer" = [])),
params(
("id" = uuid::Uuid, Path, description = "Channel ID"),
),
responses(
(status = 200, body = ChannelResponse),
(status = 401, body = api_types::ErrorResponse),
(status = 404, body = api_types::ErrorResponse),
)
)]
pub async fn get_channel(
State(state): State<AppState>,
CurrentUser(_user): CurrentUser,
@@ -56,6 +102,22 @@ pub async fn get_channel(
Ok(Json(ChannelResponse::from(channel)))
}
#[utoipa::path(
put,
path = "/api/v1/channels/{id}",
tag = "channels",
security(("bearer" = [])),
params(
("id" = uuid::Uuid, Path, description = "Channel ID"),
),
request_body = UpdateChannelRequest,
responses(
(status = 200, body = ChannelResponse),
(status = 400, body = api_types::ErrorResponse),
(status = 401, body = api_types::ErrorResponse),
(status = 404, body = api_types::ErrorResponse),
)
)]
pub async fn update_channel(
State(state): State<AppState>,
CurrentUser(user): CurrentUser,
@@ -77,6 +139,21 @@ pub async fn update_channel(
Ok(Json(ChannelResponse::from(channel)))
}
#[utoipa::path(
delete,
path = "/api/v1/channels/{id}",
tag = "channels",
security(("bearer" = [])),
params(
("id" = uuid::Uuid, Path, description = "Channel ID"),
),
responses(
(status = 204),
(status = 401, body = api_types::ErrorResponse),
(status = 403, body = api_types::ErrorResponse),
(status = 404, body = api_types::ErrorResponse),
)
)]
pub async fn delete_channel(
State(state): State<AppState>,
CurrentUser(user): CurrentUser,
@@ -90,6 +167,20 @@ pub async fn delete_channel(
Ok(axum::http::StatusCode::NO_CONTENT)
}
#[utoipa::path(
post,
path = "/api/v1/channels/{id}/snapshots",
tag = "channels",
security(("bearer" = [])),
params(
("id" = uuid::Uuid, Path, description = "Channel ID"),
),
responses(
(status = 200, body = ConfigSnapshotResponse),
(status = 401, body = api_types::ErrorResponse),
(status = 404, body = api_types::ErrorResponse),
)
)]
pub async fn save_snapshot(
State(state): State<AppState>,
CurrentUser(_user): CurrentUser,
@@ -103,6 +194,20 @@ pub async fn save_snapshot(
Ok(Json(ConfigSnapshotResponse::from(snap)))
}
#[utoipa::path(
get,
path = "/api/v1/channels/{id}/snapshots",
tag = "channels",
security(("bearer" = [])),
params(
("id" = uuid::Uuid, Path, description = "Channel ID"),
),
responses(
(status = 200, body = Vec<ConfigSnapshotResponse>),
(status = 401, body = api_types::ErrorResponse),
(status = 404, body = api_types::ErrorResponse),
)
)]
pub async fn list_snapshots(
State(state): State<AppState>,
CurrentUser(_user): CurrentUser,
@@ -112,6 +217,21 @@ pub async fn list_snapshots(
Ok(Json(snaps.into_iter().map(ConfigSnapshotResponse::from).collect()))
}
#[utoipa::path(
get,
path = "/api/v1/channels/{id}/snapshots/{snapshot_id}",
tag = "channels",
security(("bearer" = [])),
params(
("id" = uuid::Uuid, Path, description = "Channel ID"),
("snapshot_id" = uuid::Uuid, Path, description = "Snapshot ID"),
),
responses(
(status = 200, body = ConfigSnapshotResponse),
(status = 401, body = api_types::ErrorResponse),
(status = 404, body = api_types::ErrorResponse),
)
)]
pub async fn get_snapshot(
State(state): State<AppState>,
CurrentUser(_user): CurrentUser,
@@ -125,6 +245,22 @@ pub async fn get_snapshot(
Ok(Json(ConfigSnapshotResponse::from(snap)))
}
#[utoipa::path(
patch,
path = "/api/v1/channels/{id}/snapshots/{snapshot_id}",
tag = "channels",
security(("bearer" = [])),
params(
("id" = uuid::Uuid, Path, description = "Channel ID"),
("snapshot_id" = uuid::Uuid, Path, description = "Snapshot ID"),
),
request_body = PatchSnapshotRequest,
responses(
(status = 200, body = ConfigSnapshotResponse),
(status = 401, body = api_types::ErrorResponse),
(status = 404, body = api_types::ErrorResponse),
)
)]
pub async fn patch_snapshot(
State(state): State<AppState>,
CurrentUser(_user): CurrentUser,
@@ -140,6 +276,21 @@ pub async fn patch_snapshot(
Ok(Json(ConfigSnapshotResponse::from(snap)))
}
#[utoipa::path(
post,
path = "/api/v1/channels/{id}/snapshots/{snapshot_id}/restore",
tag = "channels",
security(("bearer" = [])),
params(
("id" = uuid::Uuid, Path, description = "Channel ID"),
("snapshot_id" = uuid::Uuid, Path, description = "Snapshot ID"),
),
responses(
(status = 200, body = ChannelResponse),
(status = 401, body = api_types::ErrorResponse),
(status = 404, body = api_types::ErrorResponse),
)
)]
pub async fn restore_snapshot(
State(state): State<AppState>,
CurrentUser(_user): CurrentUser,

View File

@@ -7,6 +7,14 @@ use application::config::GetConfigQuery;
use crate::errors::AppError;
use crate::state::AppState;
#[utoipa::path(
get,
path = "/api/v1/config",
tag = "config",
responses(
(status = 200, body = ConfigResponse),
)
)]
pub async fn get_config(
State(state): State<AppState>,
) -> Result<Json<ConfigResponse>, AppError> {

View File

@@ -12,6 +12,15 @@ use crate::state::AppState;
const M3U_CONTENT_TYPE: &str = "audio/x-mpegurl; charset=utf-8";
const XML_CONTENT_TYPE: &str = "application/xml; charset=utf-8";
#[utoipa::path(
get,
path = "/api/v1/iptv/playlist.m3u",
tag = "iptv",
params(IptvParams),
responses(
(status = 200, content_type = "audio/x-mpegurl", body = String),
)
)]
pub async fn m3u_playlist(
State(state): State<AppState>,
OptionalCurrentUser(_user): OptionalCurrentUser,
@@ -25,6 +34,14 @@ pub async fn m3u_playlist(
Ok(([(header::CONTENT_TYPE, M3U_CONTENT_TYPE)], content))
}
#[utoipa::path(
get,
path = "/api/v1/iptv/epg.xml",
tag = "iptv",
responses(
(status = 200, content_type = "application/xml", body = String),
)
)]
pub async fn xmltv_epg(
State(state): State<AppState>,
OptionalCurrentUser(_user): OptionalCurrentUser,

View File

@@ -15,6 +15,17 @@ use crate::state::AppState;
const DEFAULT_SEARCH_LIMIT: u32 = 50;
#[utoipa::path(
get,
path = "/api/v1/library/items",
tag = "library",
security(("bearer" = [])),
params(LibrarySearchParams),
responses(
(status = 200, body = PaginatedResponse<LibraryItemResponse>),
(status = 401, body = api_types::ErrorResponse),
)
)]
pub async fn search_items(
State(state): State<AppState>,
CurrentUser(_user): CurrentUser,
@@ -39,6 +50,20 @@ pub async fn search_items(
)))
}
#[utoipa::path(
get,
path = "/api/v1/library/items/{id}",
tag = "library",
security(("bearer" = [])),
params(
("id" = String, Path, description = "Library item ID"),
),
responses(
(status = 200, body = LibraryItemResponse),
(status = 401, body = api_types::ErrorResponse),
(status = 404, body = api_types::ErrorResponse),
)
)]
pub async fn get_item(
State(state): State<AppState>,
CurrentUser(_user): CurrentUser,
@@ -52,6 +77,17 @@ pub async fn get_item(
Ok(Json(LibraryItemResponse::from(item)))
}
#[utoipa::path(
get,
path = "/api/v1/library/collections",
tag = "library",
security(("bearer" = [])),
params(ProviderParam),
responses(
(status = 200, body = Vec<CollectionResponse>),
(status = 401, body = api_types::ErrorResponse),
)
)]
pub async fn list_collections(
State(state): State<AppState>,
CurrentUser(_user): CurrentUser,
@@ -69,6 +105,17 @@ pub async fn list_collections(
))
}
#[utoipa::path(
get,
path = "/api/v1/library/shows",
tag = "library",
security(("bearer" = [])),
params(ShowsParams),
responses(
(status = 200, body = Vec<ShowResponse>),
(status = 401, body = api_types::ErrorResponse),
)
)]
pub async fn list_shows(
State(state): State<AppState>,
CurrentUser(_user): CurrentUser,
@@ -85,6 +132,17 @@ pub async fn list_shows(
Ok(Json(shows.into_iter().map(ShowResponse::from).collect()))
}
#[utoipa::path(
get,
path = "/api/v1/library/seasons",
tag = "library",
security(("bearer" = [])),
params(SeasonsParams),
responses(
(status = 200, body = Vec<SeasonResponse>),
(status = 401, body = api_types::ErrorResponse),
)
)]
pub async fn list_seasons(
State(state): State<AppState>,
CurrentUser(_user): CurrentUser,
@@ -99,6 +157,17 @@ pub async fn list_seasons(
))
}
#[utoipa::path(
get,
path = "/api/v1/library/genres",
tag = "library",
security(("bearer" = [])),
params(GenresParams),
responses(
(status = 200, body = Vec<String>),
(status = 401, body = api_types::ErrorResponse),
)
)]
pub async fn list_genres(
State(state): State<AppState>,
CurrentUser(_user): CurrentUser,
@@ -116,6 +185,16 @@ pub async fn list_genres(
Ok(Json(genres))
}
#[utoipa::path(
get,
path = "/api/v1/library/sync/status",
tag = "library",
security(("bearer" = [])),
responses(
(status = 200, body = Vec<SyncStatusEntry>),
(status = 401, body = api_types::ErrorResponse),
)
)]
pub async fn sync_status(
State(state): State<AppState>,
CurrentUser(_user): CurrentUser,
@@ -124,6 +203,17 @@ pub async fn sync_status(
Ok(Json(entries.into_iter().map(SyncStatusEntry::from).collect()))
}
#[utoipa::path(
post,
path = "/api/v1/library/sync",
tag = "library",
security(("bearer" = [])),
responses(
(status = 202),
(status = 401, body = api_types::ErrorResponse),
(status = 403, body = api_types::ErrorResponse),
)
)]
pub async fn trigger_sync(
State(state): State<AppState>,
AdminUser(_user): AdminUser,

View File

@@ -9,6 +9,17 @@ use crate::errors::AppError;
use crate::extractors::AdminUser;
use crate::state::AppState;
#[utoipa::path(
get,
path = "/api/v1/admin/providers",
tag = "providers",
security(("bearer" = [])),
responses(
(status = 200, body = Vec<ProviderConfigResponse>),
(status = 401, body = api_types::ErrorResponse),
(status = 403, body = api_types::ErrorResponse),
)
)]
pub async fn list_providers(
State(state): State<AppState>,
AdminUser(_user): AdminUser,
@@ -22,6 +33,21 @@ pub async fn list_providers(
))
}
#[utoipa::path(
get,
path = "/api/v1/admin/providers/{id}",
tag = "providers",
security(("bearer" = [])),
params(
("id" = String, Path, description = "Provider ID"),
),
responses(
(status = 200, body = ProviderConfigResponse),
(status = 401, body = api_types::ErrorResponse),
(status = 403, body = api_types::ErrorResponse),
(status = 404, body = api_types::ErrorResponse),
)
)]
pub async fn get_provider(
State(state): State<AppState>,
AdminUser(_user): AdminUser,
@@ -35,6 +61,21 @@ pub async fn get_provider(
Ok(Json(ProviderConfigResponse::from(provider)))
}
#[utoipa::path(
put,
path = "/api/v1/admin/providers/{id}",
tag = "providers",
security(("bearer" = [])),
params(
("id" = String, Path, description = "Provider ID"),
),
request_body = ProviderConfigRequest,
responses(
(status = 200, body = serde_json::Value),
(status = 401, body = api_types::ErrorResponse),
(status = 403, body = api_types::ErrorResponse),
)
)]
pub async fn upsert_provider(
State(state): State<AppState>,
AdminUser(_user): AdminUser,
@@ -51,6 +92,21 @@ pub async fn upsert_provider(
Ok(Json(serde_json::json!({"status": "ok"})))
}
#[utoipa::path(
delete,
path = "/api/v1/admin/providers/{id}",
tag = "providers",
security(("bearer" = [])),
params(
("id" = String, Path, description = "Provider ID"),
),
responses(
(status = 204),
(status = 401, body = api_types::ErrorResponse),
(status = 403, body = api_types::ErrorResponse),
(status = 404, body = api_types::ErrorResponse),
)
)]
pub async fn delete_provider(
State(state): State<AppState>,
AdminUser(_user): AdminUser,

View File

@@ -1,6 +1,6 @@
use axum::Json;
use axum::extract::{Path, State};
use axum::http::{StatusCode, header};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use chrono::Utc;
@@ -10,13 +10,26 @@ use api_types::{
use application::schedule::{
GenerateScheduleCommand, GetCurrentBroadcastQuery, GetEpgQuery, GetSourceQuery,
};
use domain::DomainError;
use domain::value_objects::ChannelId;
use crate::errors::AppError;
use crate::extractors::CurrentUser;
use crate::state::AppState;
#[utoipa::path(
post,
path = "/api/v1/channels/{id}/schedule",
tag = "schedule",
security(("bearer" = [])),
params(
("id" = uuid::Uuid, Path, description = "Channel ID"),
),
responses(
(status = 200, body = ScheduleResponse),
(status = 401, body = api_types::ErrorResponse),
(status = 404, body = api_types::ErrorResponse),
)
)]
pub async fn generate_schedule(
State(state): State<AppState>,
CurrentUser(_user): CurrentUser,
@@ -27,6 +40,20 @@ pub async fn generate_schedule(
Ok(Json(ScheduleResponse::from(schedule)))
}
#[utoipa::path(
get,
path = "/api/v1/channels/{id}/schedule",
tag = "schedule",
security(("bearer" = [])),
params(
("id" = uuid::Uuid, Path, description = "Channel ID"),
),
responses(
(status = 200, body = ScheduleResponse),
(status = 204),
(status = 401, body = api_types::ErrorResponse),
)
)]
pub async fn get_active_schedule(
State(state): State<AppState>,
CurrentUser(_user): CurrentUser,
@@ -39,6 +66,19 @@ pub async fn get_active_schedule(
}
}
#[utoipa::path(
get,
path = "/api/v1/channels/{id}/now",
tag = "schedule",
params(
("id" = uuid::Uuid, Path, description = "Channel ID"),
),
responses(
(status = 200, body = CurrentBroadcastResponse),
(status = 204),
(status = 404, body = api_types::ErrorResponse),
)
)]
pub async fn get_current_broadcast(
State(state): State<AppState>,
Path(id): Path<uuid::Uuid>,
@@ -58,6 +98,18 @@ pub async fn get_current_broadcast(
}
}
#[utoipa::path(
get,
path = "/api/v1/channels/{id}/epg",
tag = "schedule",
params(
("id" = uuid::Uuid, Path, description = "Channel ID"),
),
responses(
(status = 200, body = Vec<SlotResponse>),
(status = 404, body = api_types::ErrorResponse),
)
)]
pub async fn get_epg(
State(state): State<AppState>,
Path(id): Path<uuid::Uuid>,
@@ -67,6 +119,19 @@ pub async fn get_epg(
Ok(Json(slots.into_iter().map(SlotResponse::from).collect()))
}
#[utoipa::path(
get,
path = "/api/v1/channels/{id}/stream",
tag = "schedule",
params(
("id" = uuid::Uuid, Path, description = "Channel ID"),
),
responses(
(status = 200, body = String),
(status = 204),
(status = 404, body = api_types::ErrorResponse),
)
)]
pub async fn get_stream(
State(state): State<AppState>,
Path(id): Path<uuid::Uuid>,
@@ -78,6 +143,19 @@ pub async fn get_stream(
}
}
#[utoipa::path(
get,
path = "/api/v1/channels/{id}/schedule/history",
tag = "schedule",
security(("bearer" = [])),
params(
("id" = uuid::Uuid, Path, description = "Channel ID"),
),
responses(
(status = 200, body = Vec<ScheduleHistoryEntry>),
(status = 401, body = api_types::ErrorResponse),
)
)]
pub async fn list_schedule_history(
State(state): State<AppState>,
CurrentUser(_user): CurrentUser,
@@ -92,26 +170,3 @@ pub async fn list_schedule_history(
.collect(),
))
}
pub async fn export_ical(
State(state): State<AppState>,
Path(id): Path<uuid::Uuid>,
) -> Result<axum::response::Response, AppError> {
let channel = state
.channel_query
.find_by_id(id.into())
.await?
.ok_or_else(|| AppError(DomainError::NotFound(format!("Channel {id} not found"))))?;
let ical = domain::generate_ical(channel.name(), channel.timezone(), channel.schedule_config());
let disposition = format!("attachment; filename=\"{}.ics\"", channel.name());
Ok((
[
(header::CONTENT_TYPE, "text/calendar; charset=utf-8".to_string()),
(header::CONTENT_DISPOSITION, disposition),
],
ical,
)
.into_response())
}

View File

@@ -9,6 +9,7 @@ mod extractors;
mod factory;
mod handlers;
mod mappers;
mod openapi;
mod routes;
mod state;
@@ -52,6 +53,7 @@ async fn main() -> anyhow::Result<()> {
let app = axum::Router::new()
.nest("/api/v1", routes::api_v1_router())
.nest("/api", routes::docs_router())
.layer(cors)
.layer(TraceLayer::new_for_http())
.with_state(app_state);

View File

@@ -0,0 +1,124 @@
use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
use utoipa::{Modify, OpenApi};
struct BearerAuth;
impl Modify for BearerAuth {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
if let Some(components) = openapi.components.as_mut() {
components.add_security_scheme(
"bearer",
SecurityScheme::Http(
HttpBuilder::new()
.scheme(HttpAuthScheme::Bearer)
.bearer_format("JWT")
.build(),
),
);
}
}
}
#[derive(OpenApi)]
#[openapi(
info(
title = "K-TV API",
version = "1.0.0",
description = "Self-hosted linear TV channel orchestration",
),
modifiers(&BearerAuth),
paths(
crate::handlers::auth::register,
crate::handlers::auth::login,
crate::handlers::auth::logout,
crate::handlers::auth::me,
crate::handlers::auth::refresh_token,
crate::handlers::channels::list_channels,
crate::handlers::channels::list_my_channels,
crate::handlers::channels::create_channel,
crate::handlers::channels::get_channel,
crate::handlers::channels::update_channel,
crate::handlers::channels::delete_channel,
crate::handlers::channels::save_snapshot,
crate::handlers::channels::list_snapshots,
crate::handlers::channels::get_snapshot,
crate::handlers::channels::patch_snapshot,
crate::handlers::channels::restore_snapshot,
crate::handlers::schedule::generate_schedule,
crate::handlers::schedule::get_active_schedule,
crate::handlers::schedule::get_current_broadcast,
crate::handlers::schedule::get_epg,
crate::handlers::schedule::get_stream,
crate::handlers::schedule::list_schedule_history,
crate::handlers::admin::get_settings,
crate::handlers::admin::update_settings,
crate::handlers::admin::get_activity_log,
crate::handlers::providers::list_providers,
crate::handlers::providers::get_provider,
crate::handlers::providers::upsert_provider,
crate::handlers::providers::delete_provider,
crate::handlers::config::get_config,
crate::handlers::iptv::m3u_playlist,
crate::handlers::iptv::xmltv_epg,
crate::handlers::library::search_items,
crate::handlers::library::get_item,
crate::handlers::library::list_collections,
crate::handlers::library::list_shows,
crate::handlers::library::list_seasons,
crate::handlers::library::list_genres,
crate::handlers::library::sync_status,
crate::handlers::library::trigger_sync,
),
components(schemas(
api_types::LoginRequest,
api_types::RegisterRequest,
api_types::RefreshRequest,
api_types::TokenResponse,
api_types::UserResponse,
api_types::ChannelResponse,
api_types::CreateChannelRequest,
api_types::UpdateChannelRequest,
api_types::ConfigSnapshotResponse,
api_types::PatchSnapshotRequest,
api_types::ScheduleResponse,
api_types::SlotResponse,
api_types::MediaItemResponse,
api_types::CurrentBroadcastResponse,
api_types::ScheduleHistoryEntry,
api_types::SettingsResponse,
api_types::ActivityEventResponse,
api_types::ActivityLogParams,
api_types::ProviderConfigRequest,
api_types::ProviderConfigResponse,
api_types::ConfigResponse,
api_types::ProviderCapabilitiesResponse,
api_types::ProviderInfo,
api_types::IptvParams,
api_types::LibraryItemResponse,
api_types::CollectionResponse,
api_types::ShowResponse,
api_types::SeasonResponse,
api_types::SyncStatusEntry,
api_types::LibrarySearchParams,
api_types::ProviderParam,
api_types::ShowsParams,
api_types::SeasonsParams,
api_types::GenresParams,
api_types::PaginatedResponse<api_types::LibraryItemResponse>,
api_types::ErrorResponse,
)),
security(
("bearer" = []),
),
tags(
(name = "auth", description = "Authentication"),
(name = "channels", description = "Channel management"),
(name = "schedule", description = "Schedule generation and playback"),
(name = "admin", description = "Admin settings and activity"),
(name = "providers", description = "Media provider configuration"),
(name = "config", description = "Public system configuration"),
(name = "iptv", description = "IPTV playlist and EPG feeds"),
(name = "library", description = "Media library browsing and sync"),
),
)]
pub struct ApiDoc;

View File

@@ -1,6 +1,9 @@
use axum::{Router, routing::{delete, get, post, put}};
use axum::{Json, Router, routing::{delete, get, post, put}};
use utoipa::OpenApi;
use utoipa_scalar::{Scalar, Servable};
use crate::handlers;
use crate::openapi::ApiDoc;
use crate::state::AppState;
pub fn api_v1_router() -> Router<AppState> {
@@ -15,6 +18,12 @@ pub fn api_v1_router() -> Router<AppState> {
.merge(local_files_router())
}
pub fn docs_router() -> Router<AppState> {
Router::new()
.route("/docs", get(|| async { Json(ApiDoc::openapi()) }))
.merge(Scalar::with_url("/docs/ui", ApiDoc::openapi()))
}
fn auth_router() -> Router<AppState> {
let r = Router::new()
.route("/register", post(handlers::auth::register))
@@ -44,7 +53,6 @@ fn channel_router() -> Router<AppState> {
.route("/{id}/now", get(handlers::schedule::get_current_broadcast))
.route("/{id}/epg", get(handlers::schedule::get_epg))
.route("/{id}/stream", get(handlers::schedule::get_stream))
.route("/{id}/export.ics", get(handlers::schedule::export_ical))
.route("/{id}/snapshots", post(handlers::channels::save_snapshot))
.route("/{id}/snapshots", get(handlers::channels::list_snapshots))
.route("/{id}/snapshots/{snapshot_id}", get(handlers::channels::get_snapshot))