presentation: HTTP server crate w/ handlers, routes, background tasks
Axum binary that wires all clean-arch crates together: - AppState holds pre-built Deps structs (auth, channels, schedule, library, etc.) - JWT extractors (CurrentUser, AdminUser, OptionalCurrentUser) - Handlers delegate to application use cases, map to api-types DTOs - Routes: auth, channels, schedule, library, admin, providers, config, iptv - Background: auto-scheduler, broadcast poller, webhook consumer, library sync - Factory builds everything from Config + DbPool - SimpleProviderRegistry impl of IProviderRegistry trait - NoopMediaProvider fallback
This commit is contained in:
131
crates/presentation/src/handlers/schedule.rs
Normal file
131
crates/presentation/src/handlers/schedule.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
//! Schedule, broadcast, and stream handlers.
|
||||
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
|
||||
use api_types::{
|
||||
CurrentBroadcastResponse, ScheduleHistoryEntry, ScheduleResponse, SlotResponse,
|
||||
};
|
||||
use application::schedule::{
|
||||
GenerateScheduleCommand, GetActiveScheduleQuery, GetCurrentBroadcastQuery, GetEpgQuery,
|
||||
GetStreamUrlQuery, ListHistoryQuery,
|
||||
};
|
||||
|
||||
use crate::errors::ApiError;
|
||||
use crate::extractors::CurrentUser;
|
||||
use crate::state::AppState;
|
||||
|
||||
/// POST /channels/:id/schedule — generate a new schedule
|
||||
pub async fn generate_schedule(
|
||||
State(state): State<AppState>,
|
||||
CurrentUser(_user): CurrentUser,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> Result<Json<ScheduleResponse>, ApiError> {
|
||||
let cmd = GenerateScheduleCommand { channel_id: id };
|
||||
let schedule = application::schedule::generate::execute(&state.schedule_deps, cmd).await?;
|
||||
Ok(Json(ScheduleResponse::from(schedule)))
|
||||
}
|
||||
|
||||
/// GET /channels/:id/schedule — get the active schedule
|
||||
pub async fn get_active_schedule(
|
||||
State(state): State<AppState>,
|
||||
CurrentUser(_user): CurrentUser,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> Result<axum::response::Response, ApiError> {
|
||||
let query = GetActiveScheduleQuery { channel_id: id };
|
||||
match application::schedule::get_active::execute(&state.schedule_deps, query).await? {
|
||||
Some(schedule) => Ok(Json(ScheduleResponse::from(schedule)).into_response()),
|
||||
None => Ok(StatusCode::NO_CONTENT.into_response()),
|
||||
}
|
||||
}
|
||||
|
||||
use axum::response::IntoResponse;
|
||||
|
||||
/// GET /channels/:id/now — what's currently playing
|
||||
pub async fn get_current_broadcast(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> Result<axum::response::Response, ApiError> {
|
||||
let query = GetCurrentBroadcastQuery { channel_id: id };
|
||||
match application::schedule::get_current_broadcast::execute(&state.schedule_deps, query).await?
|
||||
{
|
||||
Some(broadcast) => {
|
||||
// Look up the channel to resolve block access mode
|
||||
let channel_query = application::channels::GetChannelQuery { channel_id: id };
|
||||
let channel = application::channels::get::execute(&state.channel_query_deps, channel_query).await?;
|
||||
|
||||
let slot_response = match &channel {
|
||||
Some(ch) => SlotResponse::with_block_access(broadcast.slot().clone(), ch),
|
||||
None => SlotResponse::from(broadcast.slot().clone()),
|
||||
};
|
||||
|
||||
let block_access_mode = slot_response.block_access_mode.clone();
|
||||
|
||||
Ok(Json(CurrentBroadcastResponse {
|
||||
slot: slot_response,
|
||||
offset_secs: broadcast.offset_secs(),
|
||||
block_access_mode,
|
||||
})
|
||||
.into_response())
|
||||
}
|
||||
None => Ok(StatusCode::NO_CONTENT.into_response()),
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /channels/:id/epg — electronic program guide
|
||||
pub async fn get_epg(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> Result<Json<Vec<SlotResponse>>, ApiError> {
|
||||
let query = GetEpgQuery { channel_id: id };
|
||||
let slots = application::schedule::get_epg::execute(&state.schedule_deps, query).await?;
|
||||
Ok(Json(slots.into_iter().map(SlotResponse::from).collect()))
|
||||
}
|
||||
|
||||
/// GET /channels/:id/stream — redirect to stream URL (307)
|
||||
pub async fn get_stream(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> Result<axum::response::Response, ApiError> {
|
||||
// Find the current broadcast first to get the item ID
|
||||
let broadcast_query = GetCurrentBroadcastQuery { channel_id: id };
|
||||
let broadcast =
|
||||
application::schedule::get_current_broadcast::execute(&state.schedule_deps, broadcast_query)
|
||||
.await?;
|
||||
|
||||
match broadcast {
|
||||
Some(b) => {
|
||||
let stream_query = GetStreamUrlQuery {
|
||||
channel_id: id,
|
||||
item_id: b.slot().item().id().value().to_string(),
|
||||
};
|
||||
let url =
|
||||
application::schedule::get_stream_url::execute(&state.schedule_deps, stream_query)
|
||||
.await?;
|
||||
Ok((
|
||||
StatusCode::TEMPORARY_REDIRECT,
|
||||
[("Location", url.as_str())],
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
None => Ok(StatusCode::NO_CONTENT.into_response()),
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /channels/:id/schedule/history — list schedule generations
|
||||
pub async fn list_schedule_history(
|
||||
State(state): State<AppState>,
|
||||
CurrentUser(_user): CurrentUser,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> Result<Json<Vec<ScheduleHistoryEntry>>, ApiError> {
|
||||
let query = ListHistoryQuery { channel_id: id };
|
||||
let history =
|
||||
application::schedule::list_history::execute(&state.schedule_deps, query).await?;
|
||||
Ok(Json(
|
||||
history
|
||||
.into_iter()
|
||||
.map(ScheduleHistoryEntry::from)
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
Reference in New Issue
Block a user