wire utoipa OpenAPI: #[utoipa::path] on all handlers, Scalar UI (#14)
This commit is contained in:
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -1843,6 +1843,8 @@ dependencies = [
|
|||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"utoipa",
|
||||||
|
"utoipa-scalar",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3029,6 +3031,18 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utoipa-scalar"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59559e1509172f6b26c1cdbc7247c4ddd1ac6560fe94b584f81ee489b141f719"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"utoipa",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.23.4"
|
version = "1.23.4"
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ tracing = "0.1"
|
|||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||||
reqwest = { version = "0.12", features = ["json"] }
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
utoipa = { version = "5", features = ["chrono", "uuid"] }
|
utoipa = { version = "5", features = ["chrono", "uuid"] }
|
||||||
|
utoipa-scalar = { version = "0.3", features = ["axum"] }
|
||||||
jsonwebtoken = "9"
|
jsonwebtoken = "9"
|
||||||
|
|
||||||
# Internal crates
|
# Internal crates
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::ToSchema;
|
use utoipa::{IntoParams, ToSchema};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
@@ -17,7 +17,7 @@ pub struct ActivityEventResponse {
|
|||||||
pub channel_id: Option<Uuid>,
|
pub channel_id: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, ToSchema)]
|
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
||||||
pub struct ActivityLogParams {
|
pub struct ActivityLogParams {
|
||||||
pub limit: Option<u32>,
|
pub limit: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use utoipa::ToSchema;
|
use utoipa::{IntoParams, ToSchema};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, ToSchema)]
|
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
||||||
pub struct IptvParams {
|
pub struct IptvParams {
|
||||||
pub token: Option<String>,
|
pub token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::ToSchema;
|
use utoipa::{IntoParams, ToSchema};
|
||||||
|
|
||||||
use crate::common::enum_to_string;
|
use crate::common::enum_to_string;
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ impl From<domain::LibrarySyncLogEntry> for SyncStatusEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, ToSchema)]
|
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
||||||
pub struct LibrarySearchParams {
|
pub struct LibrarySearchParams {
|
||||||
pub provider: Option<String>,
|
pub provider: Option<String>,
|
||||||
pub content_type: Option<String>,
|
pub content_type: Option<String>,
|
||||||
@@ -144,12 +144,12 @@ pub struct LibrarySearchParams {
|
|||||||
pub limit: Option<u32>,
|
pub limit: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, ToSchema)]
|
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
||||||
pub struct ProviderParam {
|
pub struct ProviderParam {
|
||||||
pub provider: Option<String>,
|
pub provider: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, ToSchema)]
|
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
||||||
pub struct ShowsParams {
|
pub struct ShowsParams {
|
||||||
pub provider: Option<String>,
|
pub provider: Option<String>,
|
||||||
pub search_term: Option<String>,
|
pub search_term: Option<String>,
|
||||||
@@ -157,13 +157,13 @@ pub struct ShowsParams {
|
|||||||
pub genres: Vec<String>,
|
pub genres: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, ToSchema)]
|
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
||||||
pub struct SeasonsParams {
|
pub struct SeasonsParams {
|
||||||
pub series_name: String,
|
pub series_name: String,
|
||||||
pub provider: Option<String>,
|
pub provider: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, ToSchema)]
|
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
|
||||||
pub struct GenresParams {
|
pub struct GenresParams {
|
||||||
pub content_type: Option<String>,
|
pub content_type: Option<String>,
|
||||||
pub provider: Option<String>,
|
pub provider: Option<String>,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use schemars::JsonSchema;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::tools::{channels, ical, library, schedule};
|
use crate::tools::{channels, library, schedule};
|
||||||
|
|
||||||
const SERVER_NAME: &str = "k-tv-mcp";
|
const SERVER_NAME: &str = "k-tv-mcp";
|
||||||
|
|
||||||
@@ -201,16 +201,6 @@ impl KTvMcpServer {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tool(
|
|
||||||
description = "Export a channel's schedule as iCalendar (.ics). Returns RFC 5545 text."
|
|
||||||
)]
|
|
||||||
async fn export_schedule_ical(&self, #[tool(aggr)] p: ChannelIdParam) -> String {
|
|
||||||
match parse_uuid(&p.channel_id) {
|
|
||||||
Ok(id) => ical::export_schedule_ical(&self.channel_query, id).await,
|
|
||||||
Err(e) => e,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tool(tool_box)]
|
#[tool(tool_box)]
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ adapter-sqlite = { workspace = true, optional = true }
|
|||||||
adapter-jellyfin = { workspace = true, optional = true }
|
adapter-jellyfin = { workspace = true, optional = true }
|
||||||
adapter-local-files = { workspace = true, optional = true }
|
adapter-local-files = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
# OpenAPI
|
||||||
|
utoipa = { workspace = true }
|
||||||
|
utoipa-scalar = { workspace = true }
|
||||||
|
|
||||||
# Framework
|
# Framework
|
||||||
axum = { workspace = true }
|
axum = { workspace = true }
|
||||||
axum-extra = { workspace = true, features = ["typed-header"] }
|
axum-extra = { workspace = true, features = ["typed-header"] }
|
||||||
|
|||||||
@@ -11,6 +11,17 @@ use crate::state::AppState;
|
|||||||
|
|
||||||
const DEFAULT_ACTIVITY_LIMIT: u32 = 50;
|
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(
|
pub async fn get_settings(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AdminUser(_user): AdminUser,
|
AdminUser(_user): AdminUser,
|
||||||
@@ -20,6 +31,18 @@ pub async fn get_settings(
|
|||||||
Ok(Json(SettingsResponse { 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(
|
pub async fn update_settings(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AdminUser(_user): AdminUser,
|
AdminUser(_user): AdminUser,
|
||||||
@@ -34,6 +57,18 @@ pub async fn update_settings(
|
|||||||
Ok(Json(SettingsResponse { 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(
|
pub async fn get_activity_log(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AdminUser(_user): AdminUser,
|
AdminUser(_user): AdminUser,
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ use crate::state::AppState;
|
|||||||
|
|
||||||
const TOKEN_TYPE_BEARER: &str = "Bearer";
|
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(
|
pub async fn register(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(req): Json<RegisterRequest>,
|
Json(req): Json<RegisterRequest>,
|
||||||
@@ -22,6 +32,16 @@ pub async fn register(
|
|||||||
Ok(Json(UserResponse::from(user)))
|
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(
|
pub async fn login(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(req): Json<LoginRequest>,
|
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> {
|
pub async fn logout() -> Result<Json<serde_json::Value>, AppError> {
|
||||||
Ok(Json(serde_json::json!({"message": "logged out"})))
|
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> {
|
pub async fn me(CurrentUser(user): CurrentUser) -> Result<Json<UserResponse>, AppError> {
|
||||||
Ok(Json(UserResponse::from(user)))
|
Ok(Json(UserResponse::from(user)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "auth-jwt")]
|
#[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(
|
pub async fn refresh_token(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(req): Json<RefreshRequest>,
|
Json(req): Json<RefreshRequest>,
|
||||||
|
|||||||
@@ -13,6 +13,16 @@ use crate::errors::AppError;
|
|||||||
use crate::extractors::CurrentUser;
|
use crate::extractors::CurrentUser;
|
||||||
use crate::state::AppState;
|
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(
|
pub async fn list_channels(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -21,6 +31,16 @@ pub async fn list_channels(
|
|||||||
Ok(Json(channels.into_iter().map(ChannelResponse::from).collect()))
|
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(
|
pub async fn list_my_channels(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(user): CurrentUser,
|
CurrentUser(user): CurrentUser,
|
||||||
@@ -29,6 +49,18 @@ pub async fn list_my_channels(
|
|||||||
Ok(Json(channels.into_iter().map(ChannelResponse::from).collect()))
|
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(
|
pub async fn create_channel(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(user): CurrentUser,
|
CurrentUser(user): CurrentUser,
|
||||||
@@ -43,6 +75,20 @@ pub async fn create_channel(
|
|||||||
Ok(Json(ChannelResponse::from(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(
|
pub async fn get_channel(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -56,6 +102,22 @@ pub async fn get_channel(
|
|||||||
Ok(Json(ChannelResponse::from(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(
|
pub async fn update_channel(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(user): CurrentUser,
|
CurrentUser(user): CurrentUser,
|
||||||
@@ -77,6 +139,21 @@ pub async fn update_channel(
|
|||||||
Ok(Json(ChannelResponse::from(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(
|
pub async fn delete_channel(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(user): CurrentUser,
|
CurrentUser(user): CurrentUser,
|
||||||
@@ -90,6 +167,20 @@ pub async fn delete_channel(
|
|||||||
Ok(axum::http::StatusCode::NO_CONTENT)
|
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(
|
pub async fn save_snapshot(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -103,6 +194,20 @@ pub async fn save_snapshot(
|
|||||||
Ok(Json(ConfigSnapshotResponse::from(snap)))
|
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(
|
pub async fn list_snapshots(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -112,6 +217,21 @@ pub async fn list_snapshots(
|
|||||||
Ok(Json(snaps.into_iter().map(ConfigSnapshotResponse::from).collect()))
|
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(
|
pub async fn get_snapshot(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -125,6 +245,22 @@ pub async fn get_snapshot(
|
|||||||
Ok(Json(ConfigSnapshotResponse::from(snap)))
|
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(
|
pub async fn patch_snapshot(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -140,6 +276,21 @@ pub async fn patch_snapshot(
|
|||||||
Ok(Json(ConfigSnapshotResponse::from(snap)))
|
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(
|
pub async fn restore_snapshot(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ use application::config::GetConfigQuery;
|
|||||||
use crate::errors::AppError;
|
use crate::errors::AppError;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/config",
|
||||||
|
tag = "config",
|
||||||
|
responses(
|
||||||
|
(status = 200, body = ConfigResponse),
|
||||||
|
)
|
||||||
|
)]
|
||||||
pub async fn get_config(
|
pub async fn get_config(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> Result<Json<ConfigResponse>, AppError> {
|
) -> Result<Json<ConfigResponse>, AppError> {
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ use crate::state::AppState;
|
|||||||
const M3U_CONTENT_TYPE: &str = "audio/x-mpegurl; charset=utf-8";
|
const M3U_CONTENT_TYPE: &str = "audio/x-mpegurl; charset=utf-8";
|
||||||
const XML_CONTENT_TYPE: &str = "application/xml; 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(
|
pub async fn m3u_playlist(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
OptionalCurrentUser(_user): OptionalCurrentUser,
|
OptionalCurrentUser(_user): OptionalCurrentUser,
|
||||||
@@ -25,6 +34,14 @@ pub async fn m3u_playlist(
|
|||||||
Ok(([(header::CONTENT_TYPE, M3U_CONTENT_TYPE)], content))
|
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(
|
pub async fn xmltv_epg(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
OptionalCurrentUser(_user): OptionalCurrentUser,
|
OptionalCurrentUser(_user): OptionalCurrentUser,
|
||||||
|
|||||||
@@ -15,6 +15,17 @@ use crate::state::AppState;
|
|||||||
|
|
||||||
const DEFAULT_SEARCH_LIMIT: u32 = 50;
|
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(
|
pub async fn search_items(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
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(
|
pub async fn get_item(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -52,6 +77,17 @@ pub async fn get_item(
|
|||||||
Ok(Json(LibraryItemResponse::from(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(
|
pub async fn list_collections(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
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(
|
pub async fn list_shows(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -85,6 +132,17 @@ pub async fn list_shows(
|
|||||||
Ok(Json(shows.into_iter().map(ShowResponse::from).collect()))
|
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(
|
pub async fn list_seasons(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
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(
|
pub async fn list_genres(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -116,6 +185,16 @@ pub async fn list_genres(
|
|||||||
Ok(Json(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(
|
pub async fn sync_status(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -124,6 +203,17 @@ pub async fn sync_status(
|
|||||||
Ok(Json(entries.into_iter().map(SyncStatusEntry::from).collect()))
|
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(
|
pub async fn trigger_sync(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AdminUser(_user): AdminUser,
|
AdminUser(_user): AdminUser,
|
||||||
|
|||||||
@@ -9,6 +9,17 @@ use crate::errors::AppError;
|
|||||||
use crate::extractors::AdminUser;
|
use crate::extractors::AdminUser;
|
||||||
use crate::state::AppState;
|
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(
|
pub async fn list_providers(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AdminUser(_user): AdminUser,
|
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(
|
pub async fn get_provider(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AdminUser(_user): AdminUser,
|
AdminUser(_user): AdminUser,
|
||||||
@@ -35,6 +61,21 @@ pub async fn get_provider(
|
|||||||
Ok(Json(ProviderConfigResponse::from(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(
|
pub async fn upsert_provider(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AdminUser(_user): AdminUser,
|
AdminUser(_user): AdminUser,
|
||||||
@@ -51,6 +92,21 @@ pub async fn upsert_provider(
|
|||||||
Ok(Json(serde_json::json!({"status": "ok"})))
|
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(
|
pub async fn delete_provider(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AdminUser(_user): AdminUser,
|
AdminUser(_user): AdminUser,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
use axum::http::{StatusCode, header};
|
use axum::http::StatusCode;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
|
||||||
@@ -10,13 +10,26 @@ use api_types::{
|
|||||||
use application::schedule::{
|
use application::schedule::{
|
||||||
GenerateScheduleCommand, GetCurrentBroadcastQuery, GetEpgQuery, GetSourceQuery,
|
GenerateScheduleCommand, GetCurrentBroadcastQuery, GetEpgQuery, GetSourceQuery,
|
||||||
};
|
};
|
||||||
use domain::DomainError;
|
|
||||||
use domain::value_objects::ChannelId;
|
use domain::value_objects::ChannelId;
|
||||||
|
|
||||||
use crate::errors::AppError;
|
use crate::errors::AppError;
|
||||||
use crate::extractors::CurrentUser;
|
use crate::extractors::CurrentUser;
|
||||||
use crate::state::AppState;
|
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(
|
pub async fn generate_schedule(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -27,6 +40,20 @@ pub async fn generate_schedule(
|
|||||||
Ok(Json(ScheduleResponse::from(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(
|
pub async fn get_active_schedule(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
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(
|
pub async fn get_current_broadcast(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(id): Path<uuid::Uuid>,
|
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(
|
pub async fn get_epg(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(id): Path<uuid::Uuid>,
|
Path(id): Path<uuid::Uuid>,
|
||||||
@@ -67,6 +119,19 @@ pub async fn get_epg(
|
|||||||
Ok(Json(slots.into_iter().map(SlotResponse::from).collect()))
|
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(
|
pub async fn get_stream(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(id): Path<uuid::Uuid>,
|
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(
|
pub async fn list_schedule_history(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -92,26 +170,3 @@ pub async fn list_schedule_history(
|
|||||||
.collect(),
|
.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())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ mod extractors;
|
|||||||
mod factory;
|
mod factory;
|
||||||
mod handlers;
|
mod handlers;
|
||||||
mod mappers;
|
mod mappers;
|
||||||
|
mod openapi;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let app = axum::Router::new()
|
let app = axum::Router::new()
|
||||||
.nest("/api/v1", routes::api_v1_router())
|
.nest("/api/v1", routes::api_v1_router())
|
||||||
|
.nest("/api", routes::docs_router())
|
||||||
.layer(cors)
|
.layer(cors)
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.with_state(app_state);
|
.with_state(app_state);
|
||||||
|
|||||||
124
crates/presentation/src/openapi.rs
Normal file
124
crates/presentation/src/openapi.rs
Normal 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;
|
||||||
@@ -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::handlers;
|
||||||
|
use crate::openapi::ApiDoc;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
pub fn api_v1_router() -> Router<AppState> {
|
pub fn api_v1_router() -> Router<AppState> {
|
||||||
@@ -15,6 +18,12 @@ pub fn api_v1_router() -> Router<AppState> {
|
|||||||
.merge(local_files_router())
|
.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> {
|
fn auth_router() -> Router<AppState> {
|
||||||
let r = Router::new()
|
let r = Router::new()
|
||||||
.route("/register", post(handlers::auth::register))
|
.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}/now", get(handlers::schedule::get_current_broadcast))
|
||||||
.route("/{id}/epg", get(handlers::schedule::get_epg))
|
.route("/{id}/epg", get(handlers::schedule::get_epg))
|
||||||
.route("/{id}/stream", get(handlers::schedule::get_stream))
|
.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", post(handlers::channels::save_snapshot))
|
||||||
.route("/{id}/snapshots", get(handlers::channels::list_snapshots))
|
.route("/{id}/snapshots", get(handlers::channels::list_snapshots))
|
||||||
.route("/{id}/snapshots/{snapshot_id}", get(handlers::channels::get_snapshot))
|
.route("/{id}/snapshots/{snapshot_id}", get(handlers::channels::get_snapshot))
|
||||||
|
|||||||
Reference in New Issue
Block a user