clean(presentation): strip comments, kill dead code, extract constants
- remove all doc/inline comments - delete unused ApiError variants (AuthRequired, NotImplemented) and helpers (internal, not_implemented) - prefix lifetime-only AppState fields with _ to suppress dead_code warning - extract magic numbers: TICK_INTERVAL_SECS, EXPIRY_THRESHOLD_HOURS, POLL_INTERVAL_SECS, EVENT_BUS_CAPACITY, DEFAULT_ACTIVITY_LIMIT, DEFAULT_SEARCH_LIMIT, etc - replace inline serde_json::json! in sync_status with typed SyncStatusEntry - fix mid-file import in schedule handler - fix extractors: strip_prefix instead of magic offset 6 - fix unreachable pattern in wire_repositories with cfg guard - use eq_ignore_ascii_case for content-type header check
This commit is contained in:
@@ -1,26 +1,21 @@
|
|||||||
//! Background auto-scheduler task.
|
|
||||||
//!
|
|
||||||
//! Runs every hour, finds channels with `auto_schedule = true`, and regenerates
|
|
||||||
//! their schedule if it is within 24 hours of expiry.
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
|
||||||
use application::schedule::ScheduleDeps;
|
use application::schedule::{GenerateScheduleCommand, ScheduleDeps};
|
||||||
use application::schedule::GenerateScheduleCommand;
|
|
||||||
|
const TICK_INTERVAL_SECS: u64 = 3600;
|
||||||
|
const EXPIRY_THRESHOLD_HOURS: i64 = 24;
|
||||||
|
|
||||||
/// Run the auto-scheduler loop.
|
|
||||||
pub async fn run(deps: Arc<ScheduleDeps>) {
|
pub async fn run(deps: Arc<ScheduleDeps>) {
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(Duration::from_secs(3600)).await;
|
tokio::time::sleep(Duration::from_secs(TICK_INTERVAL_SECS)).await;
|
||||||
tick(&deps).await;
|
tick(&deps).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn tick(deps: &ScheduleDeps) {
|
async fn tick(deps: &ScheduleDeps) {
|
||||||
// List all channels, find those with auto_schedule
|
|
||||||
let channels = match deps.channel_query.find_all().await {
|
let channels = match deps.channel_query.find_all().await {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -36,7 +31,6 @@ async fn tick(deps: &ScheduleDeps) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check latest schedule
|
|
||||||
let latest = match deps.schedule_query.find_latest(channel.id()).await {
|
let latest = match deps.schedule_query.find_latest(channel.id()).await {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -52,7 +46,7 @@ async fn tick(deps: &ScheduleDeps) {
|
|||||||
let should_generate = match &latest {
|
let should_generate = match &latest {
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
let remaining = s.valid_until() - now;
|
let remaining = s.valid_until() - now;
|
||||||
remaining < chrono::Duration::hours(24)
|
remaining < chrono::Duration::hours(EXPIRY_THRESHOLD_HOURS)
|
||||||
}
|
}
|
||||||
None => true,
|
None => true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
//! BroadcastPoller background task.
|
|
||||||
//!
|
|
||||||
//! Polls channels with webhook_url configured and emits domain events
|
|
||||||
//! when the current slot changes.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -16,18 +11,18 @@ use domain::value_objects::{ChannelId, SlotId};
|
|||||||
|
|
||||||
use application::schedule::ScheduleDeps;
|
use application::schedule::ScheduleDeps;
|
||||||
|
|
||||||
/// Per-channel poll state.
|
const POLL_INTERVAL_SECS: u64 = 1;
|
||||||
|
|
||||||
struct ChannelPollState {
|
struct ChannelPollState {
|
||||||
last_slot_id: Option<Uuid>,
|
last_slot_id: Option<Uuid>,
|
||||||
last_checked: Instant,
|
last_checked: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Polls channels and emits broadcast transition events.
|
|
||||||
pub async fn run(deps: Arc<ScheduleDeps>, event_publisher: Arc<dyn EventPublisher>) {
|
pub async fn run(deps: Arc<ScheduleDeps>, event_publisher: Arc<dyn EventPublisher>) {
|
||||||
let mut state: HashMap<Uuid, ChannelPollState> = HashMap::new();
|
let mut state: HashMap<Uuid, ChannelPollState> = HashMap::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
tokio::time::sleep(Duration::from_secs(POLL_INTERVAL_SECS)).await;
|
||||||
tick(&deps, &event_publisher, &mut state).await;
|
tick(&deps, &event_publisher, &mut state).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
//! Background library sync task.
|
|
||||||
//!
|
|
||||||
//! Fires 10 seconds after startup, then every N hours (read from app_settings).
|
|
||||||
//! Can be triggered on-demand via the sync_trigger watch channel.
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -11,6 +6,7 @@ use tokio::sync::watch;
|
|||||||
|
|
||||||
const STARTUP_DELAY_SECS: u64 = 10;
|
const STARTUP_DELAY_SECS: u64 = 10;
|
||||||
const DEFAULT_INTERVAL_HOURS: u64 = 6;
|
const DEFAULT_INTERVAL_HOURS: u64 = 6;
|
||||||
|
const SYNC_INTERVAL_SETTING_KEY: &str = "library_sync_interval_hours";
|
||||||
|
|
||||||
pub async fn run(
|
pub async fn run(
|
||||||
sync_adapter: Arc<dyn LibrarySyncAdapter>,
|
sync_adapter: Arc<dyn LibrarySyncAdapter>,
|
||||||
@@ -36,7 +32,7 @@ pub async fn run(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn load_interval_hours(repo: &Arc<dyn AppSettingsRepository>) -> u64 {
|
async fn load_interval_hours(repo: &Arc<dyn AppSettingsRepository>) -> u64 {
|
||||||
repo.get("library_sync_interval_hours")
|
repo.get(SYNC_INTERVAL_SETTING_KEY)
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
@@ -51,9 +47,6 @@ async fn do_sync(
|
|||||||
let provider_ids = registry.provider_ids();
|
let provider_ids = registry.provider_ids();
|
||||||
|
|
||||||
for provider_id in provider_ids {
|
for provider_id in provider_ids {
|
||||||
// We need a &dyn IMediaProvider, but IProviderRegistry doesn't expose one.
|
|
||||||
// The sync adapter will use the registry's fetch_items internally via its
|
|
||||||
// own stored reference to the provider. For now, we create a thin adapter.
|
|
||||||
tracing::info!("library-sync: syncing provider '{}'", provider_id);
|
tracing::info!("library-sync: syncing provider '{}'", provider_id);
|
||||||
|
|
||||||
let wrapper = RegistryProviderAdapter {
|
let wrapper = RegistryProviderAdapter {
|
||||||
@@ -76,8 +69,6 @@ async fn do_sync(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adapter that wraps IProviderRegistry calls for a specific provider_id,
|
|
||||||
/// implementing IMediaProvider so it can be passed to LibrarySyncAdapter.
|
|
||||||
struct RegistryProviderAdapter {
|
struct RegistryProviderAdapter {
|
||||||
registry: Arc<dyn IProviderRegistry>,
|
registry: Arc<dyn IProviderRegistry>,
|
||||||
provider_id: String,
|
provider_id: String,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Background tasks spawned at server startup.
|
|
||||||
|
|
||||||
pub mod auto_scheduler;
|
pub mod auto_scheduler;
|
||||||
pub mod broadcast_poller;
|
pub mod broadcast_poller;
|
||||||
pub mod library_sync;
|
pub mod library_sync;
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
//! WebhookConsumer background task.
|
|
||||||
//!
|
|
||||||
//! Subscribes to domain events and delivers them to per-channel webhook URLs.
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
@@ -13,7 +9,8 @@ use uuid::Uuid;
|
|||||||
use domain::events::DomainEvent;
|
use domain::events::DomainEvent;
|
||||||
use domain::ports::ChannelQuery;
|
use domain::ports::ChannelQuery;
|
||||||
|
|
||||||
/// Consumes domain events and delivers them to per-channel webhook URLs.
|
const DEFAULT_CONTENT_TYPE: &str = "application/json";
|
||||||
|
|
||||||
pub async fn run(
|
pub async fn run(
|
||||||
mut rx: broadcast::Receiver<DomainEvent>,
|
mut rx: broadcast::Receiver<DomainEvent>,
|
||||||
channel_query: Arc<dyn ChannelQuery>,
|
channel_query: Arc<dyn ChannelQuery>,
|
||||||
@@ -177,7 +174,7 @@ async fn post_webhook(
|
|||||||
if let Some(h) = headers_json {
|
if let Some(h) = headers_json {
|
||||||
if let Ok(map) = serde_json::from_str::<serde_json::Map<String, Value>>(h) {
|
if let Ok(map) = serde_json::from_str::<serde_json::Map<String, Value>>(h) {
|
||||||
for (k, v) in &map {
|
for (k, v) in &map {
|
||||||
if k.to_lowercase() == "content-type" {
|
if k.eq_ignore_ascii_case("content-type") {
|
||||||
has_content_type = true;
|
has_content_type = true;
|
||||||
}
|
}
|
||||||
if let Some(v_str) = v.as_str() {
|
if let Some(v_str) = v.as_str() {
|
||||||
@@ -188,7 +185,7 @@ async fn post_webhook(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !has_content_type {
|
if !has_content_type {
|
||||||
req = req.header("Content-Type", "application/json");
|
req = req.header("Content-Type", DEFAULT_CONTENT_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
match req.send().await {
|
match req.send().await {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! API error handling — maps domain errors to HTTP responses.
|
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Json,
|
Json,
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
@@ -10,7 +8,6 @@ use thiserror::Error;
|
|||||||
|
|
||||||
use domain::DomainError;
|
use domain::DomainError;
|
||||||
|
|
||||||
/// API-level errors.
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ApiError {
|
pub enum ApiError {
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
@@ -28,20 +25,13 @@ pub enum ApiError {
|
|||||||
#[error("Unauthorized: {0}")]
|
#[error("Unauthorized: {0}")]
|
||||||
Unauthorized(String),
|
Unauthorized(String),
|
||||||
|
|
||||||
#[error("auth_required")]
|
|
||||||
AuthRequired,
|
|
||||||
|
|
||||||
#[error("Not found: {0}")]
|
#[error("Not found: {0}")]
|
||||||
NotFound(String),
|
NotFound(String),
|
||||||
|
|
||||||
#[error("Not implemented: {0}")]
|
|
||||||
NotImplemented(String),
|
|
||||||
|
|
||||||
#[error("Conflict: {0}")]
|
#[error("Conflict: {0}")]
|
||||||
Conflict(String),
|
Conflict(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error response body.
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct ErrorResponse {
|
pub struct ErrorResponse {
|
||||||
pub error: String,
|
pub error: String,
|
||||||
@@ -118,14 +108,6 @@ impl IntoResponse for ApiError {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
ApiError::AuthRequired => (
|
|
||||||
StatusCode::UNAUTHORIZED,
|
|
||||||
ErrorResponse {
|
|
||||||
error: "auth_required".to_string(),
|
|
||||||
details: None,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
ApiError::NotFound(msg) => (
|
ApiError::NotFound(msg) => (
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
ErrorResponse {
|
ErrorResponse {
|
||||||
@@ -134,14 +116,6 @@ impl IntoResponse for ApiError {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
ApiError::NotImplemented(msg) => (
|
|
||||||
StatusCode::NOT_IMPLEMENTED,
|
|
||||||
ErrorResponse {
|
|
||||||
error: "Not implemented".to_string(),
|
|
||||||
details: Some(msg.clone()),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
ApiError::Conflict(msg) => (
|
ApiError::Conflict(msg) => (
|
||||||
StatusCode::CONFLICT,
|
StatusCode::CONFLICT,
|
||||||
ErrorResponse {
|
ErrorResponse {
|
||||||
@@ -160,10 +134,6 @@ impl ApiError {
|
|||||||
Self::Validation(msg.into())
|
Self::Validation(msg.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn internal(msg: impl Into<String>) -> Self {
|
|
||||||
Self::Internal(msg.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn not_found(msg: impl Into<String>) -> Self {
|
pub fn not_found(msg: impl Into<String>) -> Self {
|
||||||
Self::NotFound(msg.into())
|
Self::NotFound(msg.into())
|
||||||
}
|
}
|
||||||
@@ -171,8 +141,4 @@ impl ApiError {
|
|||||||
pub fn conflict(msg: impl Into<String>) -> Self {
|
pub fn conflict(msg: impl Into<String>) -> Self {
|
||||||
Self::Conflict(msg.into())
|
Self::Conflict(msg.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn not_implemented(msg: impl Into<String>) -> Self {
|
|
||||||
Self::NotImplemented(msg.into())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
//! Auth extractors for API handlers.
|
|
||||||
//!
|
|
||||||
//! Provides `CurrentUser`, `OptionalCurrentUser`, and `AdminUser` extractors.
|
|
||||||
|
|
||||||
use axum::extract::FromRequestParts;
|
use axum::extract::FromRequestParts;
|
||||||
use axum::http::request::Parts;
|
use axum::http::request::Parts;
|
||||||
use domain::User;
|
use domain::User;
|
||||||
@@ -9,7 +5,6 @@ use domain::User;
|
|||||||
use crate::errors::ApiError;
|
use crate::errors::ApiError;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
/// Extracted current user from JWT Bearer token.
|
|
||||||
pub struct CurrentUser(pub User);
|
pub struct CurrentUser(pub User);
|
||||||
|
|
||||||
impl FromRequestParts<AppState> for CurrentUser {
|
impl FromRequestParts<AppState> for CurrentUser {
|
||||||
@@ -37,9 +32,6 @@ impl FromRequestParts<AppState> for CurrentUser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Optional current user — returns None instead of error when auth missing.
|
|
||||||
///
|
|
||||||
/// Checks `Authorization: Bearer <token>` first; falls back to `?token=<jwt>`.
|
|
||||||
pub struct OptionalCurrentUser(pub Option<User>);
|
pub struct OptionalCurrentUser(pub Option<User>);
|
||||||
|
|
||||||
impl FromRequestParts<AppState> for OptionalCurrentUser {
|
impl FromRequestParts<AppState> for OptionalCurrentUser {
|
||||||
@@ -56,8 +48,8 @@ impl FromRequestParts<AppState> for OptionalCurrentUser {
|
|||||||
}
|
}
|
||||||
let query_token = parts.uri.query().and_then(|q| {
|
let query_token = parts.uri.query().and_then(|q| {
|
||||||
q.split('&')
|
q.split('&')
|
||||||
.find(|seg| seg.starts_with("token="))
|
.find_map(|seg| seg.strip_prefix("token="))
|
||||||
.map(|seg| seg[6..].to_owned())
|
.map(|v| v.to_owned())
|
||||||
});
|
});
|
||||||
if let Some(token) = query_token {
|
if let Some(token) = query_token {
|
||||||
let user = validate_jwt_token(&token, state).await.ok();
|
let user = validate_jwt_token(&token, state).await.ok();
|
||||||
@@ -74,7 +66,6 @@ impl FromRequestParts<AppState> for OptionalCurrentUser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracted admin user — returns 403 if user is not an admin.
|
|
||||||
pub struct AdminUser(pub User);
|
pub struct AdminUser(pub User);
|
||||||
|
|
||||||
impl FromRequestParts<AppState> for AdminUser {
|
impl FromRequestParts<AppState> for AdminUser {
|
||||||
@@ -92,7 +83,6 @@ impl FromRequestParts<AppState> for AdminUser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Authenticate via JWT Bearer token from `Authorization` header.
|
|
||||||
#[cfg(feature = "auth-jwt")]
|
#[cfg(feature = "auth-jwt")]
|
||||||
async fn try_jwt_auth(parts: &mut Parts, state: &AppState) -> Result<User, ApiError> {
|
async fn try_jwt_auth(parts: &mut Parts, state: &AppState) -> Result<User, ApiError> {
|
||||||
use axum::http::header::AUTHORIZATION;
|
use axum::http::header::AUTHORIZATION;
|
||||||
@@ -113,7 +103,6 @@ async fn try_jwt_auth(parts: &mut Parts, state: &AppState) -> Result<User, ApiEr
|
|||||||
validate_jwt_token(token, state).await
|
validate_jwt_token(token, state).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate a raw JWT string and return the corresponding `User`.
|
|
||||||
#[cfg(feature = "auth-jwt")]
|
#[cfg(feature = "auth-jwt")]
|
||||||
pub(crate) async fn validate_jwt_token(token: &str, state: &AppState) -> Result<User, ApiError> {
|
pub(crate) async fn validate_jwt_token(token: &str, state: &AppState) -> Result<User, ApiError> {
|
||||||
let validator = state
|
let validator = state
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
//! Factory — builds AppState from Config + DbPool.
|
|
||||||
//!
|
|
||||||
//! Connects to the database, runs migrations, creates all adapter instances,
|
|
||||||
//! constructs Deps structs, and returns a fully-wired AppState.
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use application::{
|
use application::{
|
||||||
@@ -21,31 +16,26 @@ use infra_wiring::{Config, ConfigSource, DbPool};
|
|||||||
|
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
/// Build a fully-wired AppState ready for the HTTP server.
|
const EVENT_BUS_CAPACITY: usize = 64;
|
||||||
|
const DEV_JWT_SECRET: &str = "k-template-dev-secret-not-for-production-use-only";
|
||||||
|
|
||||||
pub async fn build_app_state(config: Config) -> anyhow::Result<AppState> {
|
pub async fn build_app_state(config: Config) -> anyhow::Result<AppState> {
|
||||||
// Connect to database
|
|
||||||
let pool = DbPool::connect(&config.database_url).await?;
|
let pool = DbPool::connect(&config.database_url).await?;
|
||||||
pool.run_migrations().await?;
|
pool.run_migrations().await?;
|
||||||
|
|
||||||
// Wire up all repositories from the database pool
|
|
||||||
let wire_output = wire_repositories(&pool)?;
|
let wire_output = wire_repositories(&pool)?;
|
||||||
|
|
||||||
// Auth service
|
|
||||||
let auth_service: Arc<dyn domain::ports::AuthService> =
|
let auth_service: Arc<dyn domain::ports::AuthService> =
|
||||||
Arc::new(adapter_auth::PasswordAuthService);
|
Arc::new(adapter_auth::PasswordAuthService);
|
||||||
|
|
||||||
// Event bus
|
let event_bus = Arc::new(adapter_event_publisher::ChannelEventBus::new(EVENT_BUS_CAPACITY));
|
||||||
let event_bus = Arc::new(adapter_event_publisher::ChannelEventBus::new(64));
|
|
||||||
let event_publisher: Arc<dyn domain::ports::EventPublisher> = event_bus.clone();
|
let event_publisher: Arc<dyn domain::ports::EventPublisher> = event_bus.clone();
|
||||||
|
|
||||||
// Provider registry
|
|
||||||
let provider_registry = build_provider_registry(&config).await;
|
let provider_registry = build_provider_registry(&config).await;
|
||||||
|
|
||||||
// Library sync adapter — uses the LibraryCommand port internally
|
|
||||||
let library_sync: Arc<dyn domain::ports::LibrarySyncAdapter> =
|
let library_sync: Arc<dyn domain::ports::LibrarySyncAdapter> =
|
||||||
build_library_sync(wire_output.library_command.clone());
|
build_library_sync(wire_output.library_command.clone());
|
||||||
|
|
||||||
// Schedule engine
|
|
||||||
let schedule_engine = Arc::new(ScheduleEngineService::new(
|
let schedule_engine = Arc::new(ScheduleEngineService::new(
|
||||||
provider_registry.clone(),
|
provider_registry.clone(),
|
||||||
wire_output.channel_query.clone(),
|
wire_output.channel_query.clone(),
|
||||||
@@ -53,14 +43,11 @@ pub async fn build_app_state(config: Config) -> anyhow::Result<AppState> {
|
|||||||
wire_output.schedule_command.clone(),
|
wire_output.schedule_command.clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
// JWT validator
|
|
||||||
#[cfg(feature = "auth-jwt")]
|
#[cfg(feature = "auth-jwt")]
|
||||||
let jwt_validator = build_jwt_validator(&config)?;
|
let jwt_validator = build_jwt_validator(&config)?;
|
||||||
|
|
||||||
// Sync trigger channel
|
|
||||||
let (sync_tx, sync_rx) = tokio::sync::watch::channel(());
|
let (sync_tx, sync_rx) = tokio::sync::watch::channel(());
|
||||||
|
|
||||||
// Build all deps structs
|
|
||||||
let auth_deps = Arc::new(AuthDeps {
|
let auth_deps = Arc::new(AuthDeps {
|
||||||
user_command: wire_output.user_command.clone(),
|
user_command: wire_output.user_command.clone(),
|
||||||
user_query: wire_output.user_query.clone(),
|
user_query: wire_output.user_query.clone(),
|
||||||
@@ -120,7 +107,6 @@ pub async fn build_app_state(config: Config) -> anyhow::Result<AppState> {
|
|||||||
|
|
||||||
let config_arc = Arc::new(config);
|
let config_arc = Arc::new(config);
|
||||||
|
|
||||||
// Spawn background tasks
|
|
||||||
let bg_schedule_deps = schedule_deps.clone();
|
let bg_schedule_deps = schedule_deps.clone();
|
||||||
tokio::spawn(crate::background::auto_scheduler::run(bg_schedule_deps));
|
tokio::spawn(crate::background::auto_scheduler::run(bg_schedule_deps));
|
||||||
|
|
||||||
@@ -163,19 +149,14 @@ pub async fn build_app_state(config: Config) -> anyhow::Result<AppState> {
|
|||||||
#[cfg(feature = "auth-jwt")]
|
#[cfg(feature = "auth-jwt")]
|
||||||
jwt_validator,
|
jwt_validator,
|
||||||
provider_registry,
|
provider_registry,
|
||||||
library_sync,
|
_library_sync: library_sync,
|
||||||
settings_repo: wire_output.settings,
|
_settings_repo: wire_output.settings,
|
||||||
event_bus,
|
_event_bus: event_bus,
|
||||||
config: config_arc,
|
config: config_arc,
|
||||||
sync_trigger: sync_tx,
|
sync_trigger: sync_tx,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Helpers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Repository wiring output — trait objects ready for dependency injection.
|
|
||||||
struct WireOutput {
|
struct WireOutput {
|
||||||
user_command: Arc<dyn domain::ports::UserCommand>,
|
user_command: Arc<dyn domain::ports::UserCommand>,
|
||||||
user_query: Arc<dyn domain::ports::UserQuery>,
|
user_query: Arc<dyn domain::ports::UserQuery>,
|
||||||
@@ -229,12 +210,12 @@ fn wire_repositories(pool: &DbPool) -> anyhow::Result<WireOutput> {
|
|||||||
provider_config_query: w.provider_config_query,
|
provider_config_query: w.provider_config_query,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
#[cfg(not(any(feature = "sqlite", feature = "postgres")))]
|
||||||
_ => anyhow::bail!("database backend not compiled into this binary"),
|
_ => anyhow::bail!("database backend not compiled into this binary"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn build_provider_registry(config: &Config) -> Arc<dyn IProviderRegistry> {
|
async fn build_provider_registry(config: &Config) -> Arc<dyn IProviderRegistry> {
|
||||||
// Build a concrete registry that routes to configured providers.
|
|
||||||
let mut providers: Vec<(String, Arc<dyn IMediaProvider>)> = Vec::new();
|
let mut providers: Vec<(String, Arc<dyn IMediaProvider>)> = Vec::new();
|
||||||
|
|
||||||
match config.config_source {
|
match config.config_source {
|
||||||
@@ -259,8 +240,6 @@ async fn build_provider_registry(config: &Config) -> Arc<dyn IProviderRegistry>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ConfigSource::Db => {
|
ConfigSource::Db => {
|
||||||
// DB-based provider configs loaded elsewhere at runtime.
|
|
||||||
// For now, fall through to noop if nothing configured via env.
|
|
||||||
tracing::info!("CONFIG_SOURCE=db: provider configs loaded from database at runtime");
|
tracing::info!("CONFIG_SOURCE=db: provider configs loaded from database at runtime");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,7 +267,7 @@ fn build_jwt_validator(config: &Config) -> anyhow::Result<Option<Arc<adapter_aut
|
|||||||
anyhow::bail!("JWT_SECRET is required in production");
|
anyhow::bail!("JWT_SECRET is required in production");
|
||||||
}
|
}
|
||||||
tracing::warn!("JWT_SECRET not set — using insecure development secret");
|
tracing::warn!("JWT_SECRET not set — using insecure development secret");
|
||||||
"k-template-dev-secret-not-for-production-use-only".to_string()
|
DEV_JWT_SECRET.to_string()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -305,10 +284,6 @@ fn build_jwt_validator(config: &Config) -> anyhow::Result<Option<Arc<adapter_aut
|
|||||||
Ok(Some(Arc::new(adapter_auth::JwtValidator::new(jwt_config))))
|
Ok(Some(Arc::new(adapter_auth::JwtValidator::new(jwt_config))))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// NoopMediaProvider — fallback when nothing is configured
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
struct NoopMediaProvider;
|
struct NoopMediaProvider;
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
@@ -356,10 +331,6 @@ impl IMediaProvider for NoopMediaProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// SimpleProviderRegistry — implements IProviderRegistry for N providers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
struct SimpleProviderRegistry {
|
struct SimpleProviderRegistry {
|
||||||
providers: Vec<(String, Arc<dyn IMediaProvider>)>,
|
providers: Vec<(String, Arc<dyn IMediaProvider>)>,
|
||||||
}
|
}
|
||||||
@@ -377,7 +348,6 @@ impl SimpleProviderRegistry {
|
|||||||
self.providers.first().map(|(_, v)| v)
|
self.providers.first().map(|(_, v)| v)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract provider_id from a prefixed item ID (e.g. "jellyfin::abc123" → "jellyfin").
|
|
||||||
fn extract_provider_id(item_id: &str) -> Option<&str> {
|
fn extract_provider_id(item_id: &str) -> Option<&str> {
|
||||||
item_id.find("::").map(|pos| &item_id[..pos])
|
item_id.find("::").map(|pos| &item_id[..pos])
|
||||||
}
|
}
|
||||||
@@ -411,7 +381,6 @@ impl IProviderRegistry for SimpleProviderRegistry {
|
|||||||
return provider.fetch_by_id(item_id).await;
|
return provider.fetch_by_id(item_id).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fall back to primary
|
|
||||||
if let Some(provider) = self.primary() {
|
if let Some(provider) = self.primary() {
|
||||||
provider.fetch_by_id(item_id).await
|
provider.fetch_by_id(item_id).await
|
||||||
} else {
|
} else {
|
||||||
@@ -502,11 +471,6 @@ impl IProviderRegistry for SimpleProviderRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// SimpleSyncAdapter — wraps LibraryCommand for sync operations
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Convert a MediaItem from a provider into a LibraryItem for persistence.
|
|
||||||
fn media_item_to_library_item(item: domain::MediaItem, provider_id: &str) -> domain::LibraryItem {
|
fn media_item_to_library_item(item: domain::MediaItem, provider_id: &str) -> domain::LibraryItem {
|
||||||
let external_id = item.id().value().to_string();
|
let external_id = item.id().value().to_string();
|
||||||
let id = format!("{}::{}", provider_id, external_id);
|
let id = format!("{}::{}", provider_id, external_id);
|
||||||
@@ -526,8 +490,8 @@ fn media_item_to_library_item(item: domain::MediaItem, provider_id: &str) -> dom
|
|||||||
item.genres().to_vec(),
|
item.genres().to_vec(),
|
||||||
item.tags().to_vec(),
|
item.tags().to_vec(),
|
||||||
item.collection_id().map(|s| s.to_string()),
|
item.collection_id().map(|s| s.to_string()),
|
||||||
None, // collection_name not in MediaItem
|
None,
|
||||||
None, // collection_type not in MediaItem
|
None,
|
||||||
item.thumbnail_url().map(|s| s.to_string()),
|
item.thumbnail_url().map(|s| s.to_string()),
|
||||||
now,
|
now,
|
||||||
)
|
)
|
||||||
@@ -564,7 +528,6 @@ impl domain::ports::LibrarySyncAdapter for SimpleSyncAdapter {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch all items from provider
|
|
||||||
let filter = domain::MediaFilter::default();
|
let filter = domain::MediaFilter::default();
|
||||||
let items = match provider.fetch_items(&filter).await {
|
let items = match provider.fetch_items(&filter).await {
|
||||||
Ok(items) => items,
|
Ok(items) => items,
|
||||||
@@ -581,8 +544,6 @@ impl domain::ports::LibrarySyncAdapter for SimpleSyncAdapter {
|
|||||||
|
|
||||||
let items_found = items.len() as u32;
|
let items_found = items.len() as u32;
|
||||||
|
|
||||||
// Clear + insert (items are MediaItem; LibrarySyncAdapter implementations
|
|
||||||
// typically handle the conversion. Here we delegate to library_command directly.)
|
|
||||||
if let Err(e) = self.library_command.clear_provider(provider_id).await {
|
if let Err(e) = self.library_command.clear_provider(provider_id).await {
|
||||||
let result = domain::LibrarySyncResult::with_error(
|
let result = domain::LibrarySyncResult::with_error(
|
||||||
provider_id,
|
provider_id,
|
||||||
@@ -593,7 +554,6 @@ impl domain::ports::LibrarySyncAdapter for SimpleSyncAdapter {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert MediaItems to LibraryItems for storage
|
|
||||||
let library_items: Vec<domain::LibraryItem> = items
|
let library_items: Vec<domain::LibraryItem> = items
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|item| media_item_to_library_item(item, provider_id))
|
.map(|item| media_item_to_library_item(item, provider_id))
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Admin handlers.
|
|
||||||
|
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum::extract::{Query, State};
|
use axum::extract::{Query, State};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -12,7 +10,8 @@ use crate::errors::ApiError;
|
|||||||
use crate::extractors::AdminUser;
|
use crate::extractors::AdminUser;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
/// GET /admin/settings
|
const DEFAULT_ACTIVITY_LIMIT: u32 = 50;
|
||||||
|
|
||||||
pub async fn get_settings(
|
pub async fn get_settings(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AdminUser(_user): AdminUser,
|
AdminUser(_user): AdminUser,
|
||||||
@@ -23,7 +22,6 @@ pub async fn get_settings(
|
|||||||
Ok(Json(SettingsResponse { settings }))
|
Ok(Json(SettingsResponse { settings }))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PUT /admin/settings
|
|
||||||
pub async fn update_settings(
|
pub async fn update_settings(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AdminUser(_user): AdminUser,
|
AdminUser(_user): AdminUser,
|
||||||
@@ -35,7 +33,6 @@ pub async fn update_settings(
|
|||||||
};
|
};
|
||||||
application::admin::update_settings::execute(&state.admin_deps, cmd).await?;
|
application::admin::update_settings::execute(&state.admin_deps, cmd).await?;
|
||||||
|
|
||||||
// Re-read after update
|
|
||||||
let pairs =
|
let pairs =
|
||||||
application::admin::get_settings::execute(&state.admin_deps, GetSettingsQuery).await?;
|
application::admin::get_settings::execute(&state.admin_deps, GetSettingsQuery).await?;
|
||||||
let settings: HashMap<String, String> = pairs.into_iter().collect();
|
let settings: HashMap<String, String> = pairs.into_iter().collect();
|
||||||
@@ -47,14 +44,13 @@ pub struct ActivityLogParams {
|
|||||||
pub limit: Option<u32>,
|
pub limit: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /admin/activity
|
|
||||||
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,
|
||||||
Query(params): Query<ActivityLogParams>,
|
Query(params): Query<ActivityLogParams>,
|
||||||
) -> Result<Json<Vec<ActivityEventResponse>>, ApiError> {
|
) -> Result<Json<Vec<ActivityEventResponse>>, ApiError> {
|
||||||
let query = GetActivityLogQuery {
|
let query = GetActivityLogQuery {
|
||||||
limit: params.limit.unwrap_or(50),
|
limit: params.limit.unwrap_or(DEFAULT_ACTIVITY_LIMIT),
|
||||||
};
|
};
|
||||||
let events = application::admin::activity_log::execute(&state.admin_deps, query).await?;
|
let events = application::admin::activity_log::execute(&state.admin_deps, query).await?;
|
||||||
Ok(Json(
|
Ok(Json(
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Authentication handlers.
|
|
||||||
|
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
|
|
||||||
@@ -10,7 +8,9 @@ use crate::errors::ApiError;
|
|||||||
use crate::extractors::CurrentUser;
|
use crate::extractors::CurrentUser;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
/// POST /auth/register
|
const TOKEN_TYPE_BEARER: &str = "Bearer";
|
||||||
|
const SECS_PER_HOUR: u64 = 3600;
|
||||||
|
|
||||||
pub async fn register(
|
pub async fn register(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(req): Json<RegisterRequest>,
|
Json(req): Json<RegisterRequest>,
|
||||||
@@ -23,7 +23,6 @@ pub async fn register(
|
|||||||
Ok(Json(UserResponse::from(user)))
|
Ok(Json(UserResponse::from(user)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// POST /auth/login
|
|
||||||
pub async fn login(
|
pub async fn login(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(req): Json<LoginRequest>,
|
Json(req): Json<LoginRequest>,
|
||||||
@@ -36,25 +35,22 @@ pub async fn login(
|
|||||||
let (access_token, refresh_token) = create_tokens(&user, &state, req.remember_me)?;
|
let (access_token, refresh_token) = create_tokens(&user, &state, req.remember_me)?;
|
||||||
Ok(Json(TokenResponse {
|
Ok(Json(TokenResponse {
|
||||||
access_token,
|
access_token,
|
||||||
token_type: "Bearer".to_string(),
|
token_type: TOKEN_TYPE_BEARER.to_string(),
|
||||||
expires_in: state.config.jwt_expiry_hours * 3600,
|
expires_in: state.config.jwt_expiry_hours * SECS_PER_HOUR,
|
||||||
refresh_token,
|
refresh_token,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// POST /auth/logout — no-op for JWT (stateless)
|
|
||||||
pub async fn logout() -> Result<Json<serde_json::Value>, ApiError> {
|
pub async fn logout() -> Result<Json<serde_json::Value>, ApiError> {
|
||||||
Ok(Json(serde_json::json!({"message": "logged out"})))
|
Ok(Json(serde_json::json!({"message": "logged out"})))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /auth/me
|
|
||||||
pub async fn me(
|
pub async fn me(
|
||||||
CurrentUser(user): CurrentUser,
|
CurrentUser(user): CurrentUser,
|
||||||
) -> Result<Json<UserResponse>, ApiError> {
|
) -> Result<Json<UserResponse>, ApiError> {
|
||||||
Ok(Json(UserResponse::from(user)))
|
Ok(Json(UserResponse::from(user)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// POST /auth/token — exchange credentials for tokens
|
|
||||||
#[cfg(feature = "auth-jwt")]
|
#[cfg(feature = "auth-jwt")]
|
||||||
pub async fn get_token(
|
pub async fn get_token(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
@@ -68,13 +64,12 @@ pub async fn get_token(
|
|||||||
let (access_token, refresh_token) = create_tokens(&user, &state, req.remember_me)?;
|
let (access_token, refresh_token) = create_tokens(&user, &state, req.remember_me)?;
|
||||||
Ok(Json(TokenResponse {
|
Ok(Json(TokenResponse {
|
||||||
access_token,
|
access_token,
|
||||||
token_type: "Bearer".to_string(),
|
token_type: TOKEN_TYPE_BEARER.to_string(),
|
||||||
expires_in: state.config.jwt_expiry_hours * 3600,
|
expires_in: state.config.jwt_expiry_hours * SECS_PER_HOUR,
|
||||||
refresh_token,
|
refresh_token,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// POST /auth/refresh — refresh an access token
|
|
||||||
#[cfg(feature = "auth-jwt")]
|
#[cfg(feature = "auth-jwt")]
|
||||||
pub async fn refresh_token(
|
pub async fn refresh_token(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
@@ -106,8 +101,8 @@ pub async fn refresh_token(
|
|||||||
let (access_token, refresh_token) = create_tokens(&user, &state, true)?;
|
let (access_token, refresh_token) = create_tokens(&user, &state, true)?;
|
||||||
Ok(Json(TokenResponse {
|
Ok(Json(TokenResponse {
|
||||||
access_token,
|
access_token,
|
||||||
token_type: "Bearer".to_string(),
|
token_type: TOKEN_TYPE_BEARER.to_string(),
|
||||||
expires_in: state.config.jwt_expiry_hours * 3600,
|
expires_in: state.config.jwt_expiry_hours * SECS_PER_HOUR,
|
||||||
refresh_token,
|
refresh_token,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Channel CRUD handlers.
|
|
||||||
|
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
|
|
||||||
@@ -20,7 +18,6 @@ use crate::errors::ApiError;
|
|||||||
use crate::extractors::CurrentUser;
|
use crate::extractors::CurrentUser;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
/// GET /channels
|
|
||||||
pub async fn list_channels(
|
pub async fn list_channels(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -30,7 +27,6 @@ pub async fn list_channels(
|
|||||||
Ok(Json(channels.into_iter().map(ChannelResponse::from).collect()))
|
Ok(Json(channels.into_iter().map(ChannelResponse::from).collect()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /channels/mine
|
|
||||||
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,
|
||||||
@@ -43,7 +39,6 @@ pub async fn list_my_channels(
|
|||||||
Ok(Json(channels.into_iter().map(ChannelResponse::from).collect()))
|
Ok(Json(channels.into_iter().map(ChannelResponse::from).collect()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// POST /channels
|
|
||||||
pub async fn create_channel(
|
pub async fn create_channel(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(user): CurrentUser,
|
CurrentUser(user): CurrentUser,
|
||||||
@@ -58,7 +53,6 @@ pub async fn create_channel(
|
|||||||
Ok(Json(ChannelResponse::from(channel)))
|
Ok(Json(ChannelResponse::from(channel)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /channels/:id
|
|
||||||
pub async fn get_channel(
|
pub async fn get_channel(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -71,7 +65,6 @@ pub async fn get_channel(
|
|||||||
Ok(Json(ChannelResponse::from(channel)))
|
Ok(Json(ChannelResponse::from(channel)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PUT /channels/:id
|
|
||||||
pub async fn update_channel(
|
pub async fn update_channel(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(user): CurrentUser,
|
CurrentUser(user): CurrentUser,
|
||||||
@@ -108,7 +101,6 @@ pub async fn update_channel(
|
|||||||
Ok(Json(ChannelResponse::from(channel)))
|
Ok(Json(ChannelResponse::from(channel)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// DELETE /channels/:id
|
|
||||||
pub async fn delete_channel(
|
pub async fn delete_channel(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(user): CurrentUser,
|
CurrentUser(user): CurrentUser,
|
||||||
@@ -122,9 +114,6 @@ pub async fn delete_channel(
|
|||||||
Ok(axum::http::StatusCode::NO_CONTENT)
|
Ok(axum::http::StatusCode::NO_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Config snapshots ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// POST /channels/:id/snapshots
|
|
||||||
pub async fn save_snapshot(
|
pub async fn save_snapshot(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -138,7 +127,6 @@ pub async fn save_snapshot(
|
|||||||
Ok(Json(ConfigSnapshotResponse::from(snap)))
|
Ok(Json(ConfigSnapshotResponse::from(snap)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /channels/:id/snapshots
|
|
||||||
pub async fn list_snapshots(
|
pub async fn list_snapshots(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -149,7 +137,6 @@ pub async fn list_snapshots(
|
|||||||
Ok(Json(snaps.into_iter().map(ConfigSnapshotResponse::from).collect()))
|
Ok(Json(snaps.into_iter().map(ConfigSnapshotResponse::from).collect()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /channels/:id/snapshots/:snapshot_id
|
|
||||||
pub async fn get_snapshot(
|
pub async fn get_snapshot(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -165,7 +152,6 @@ pub async fn get_snapshot(
|
|||||||
Ok(Json(ConfigSnapshotResponse::from(snap)))
|
Ok(Json(ConfigSnapshotResponse::from(snap)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PATCH /channels/:id/snapshots/:snapshot_id
|
|
||||||
pub async fn patch_snapshot(
|
pub async fn patch_snapshot(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -184,7 +170,6 @@ pub async fn patch_snapshot(
|
|||||||
Ok(Json(ConfigSnapshotResponse::from(snap)))
|
Ok(Json(ConfigSnapshotResponse::from(snap)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// POST /channels/:id/snapshots/:snapshot_id/restore
|
|
||||||
pub async fn restore_snapshot(
|
pub async fn restore_snapshot(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! System configuration handler.
|
|
||||||
|
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
|
|
||||||
@@ -8,7 +6,8 @@ use api_types::{ConfigResponse, ProviderCapabilitiesResponse, ProviderInfo};
|
|||||||
use crate::errors::ApiError;
|
use crate::errors::ApiError;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
/// GET /config — public system configuration
|
const FALLBACK_STREAMING_PROTOCOL: &str = "direct_file";
|
||||||
|
|
||||||
pub async fn get_config(
|
pub async fn get_config(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> Result<Json<ConfigResponse>, ApiError> {
|
) -> Result<Json<ConfigResponse>, ApiError> {
|
||||||
@@ -36,7 +35,7 @@ pub async fn get_config(
|
|||||||
tags: false,
|
tags: false,
|
||||||
decade: false,
|
decade: false,
|
||||||
search: false,
|
search: false,
|
||||||
streaming_protocol: "direct_file".to_string(),
|
streaming_protocol: FALLBACK_STREAMING_PROTOCOL.to_string(),
|
||||||
rescan: false,
|
rescan: false,
|
||||||
transcode: false,
|
transcode: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
//! Local file streaming handlers (feature-gated).
|
|
||||||
//!
|
|
||||||
//! Placeholder — the actual streaming logic requires the local-files adapter
|
|
||||||
//! which provides file index and transcoding. This will be fleshed out once
|
|
||||||
//! the local-files adapter integration is complete.
|
|
||||||
|
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
@@ -11,22 +5,19 @@ use axum::response::IntoResponse;
|
|||||||
use crate::errors::ApiError;
|
use crate::errors::ApiError;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
/// GET /files/stream/:id — stream a local file
|
|
||||||
pub async fn stream_file(
|
pub async fn stream_file(
|
||||||
State(_state): State<AppState>,
|
State(_state): State<AppState>,
|
||||||
Path(_id): Path<String>,
|
Path(_id): Path<String>,
|
||||||
) -> Result<impl IntoResponse, ApiError> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
// TODO: integrate with adapter-local-files for actual streaming
|
Err::<StatusCode, _>(ApiError::NotFound(
|
||||||
Err::<StatusCode, _>(ApiError::not_implemented(
|
"Local file streaming not yet wired in presentation crate".to_string(),
|
||||||
"Local file streaming not yet wired in presentation crate",
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// POST /files/rescan — rescan local files
|
|
||||||
pub async fn rescan(
|
pub async fn rescan(
|
||||||
State(_state): State<AppState>,
|
State(_state): State<AppState>,
|
||||||
) -> Result<impl IntoResponse, ApiError> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
Err::<StatusCode, _>(ApiError::not_implemented(
|
Err::<StatusCode, _>(ApiError::NotFound(
|
||||||
"Local file rescan not yet wired in presentation crate",
|
"Local file rescan not yet wired in presentation crate".to_string(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! IPTV export handlers (M3U, XMLTV).
|
|
||||||
|
|
||||||
use axum::extract::{Query, State};
|
use axum::extract::{Query, State};
|
||||||
use axum::http::header;
|
use axum::http::header;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
@@ -11,12 +9,14 @@ use crate::errors::ApiError;
|
|||||||
use crate::extractors::OptionalCurrentUser;
|
use crate::extractors::OptionalCurrentUser;
|
||||||
use crate::state::AppState;
|
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";
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct IptvParams {
|
pub struct IptvParams {
|
||||||
pub token: Option<String>,
|
pub token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /iptv/playlist.m3u — M3U playlist
|
|
||||||
pub async fn m3u_playlist(
|
pub async fn m3u_playlist(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
OptionalCurrentUser(_user): OptionalCurrentUser,
|
OptionalCurrentUser(_user): OptionalCurrentUser,
|
||||||
@@ -27,20 +27,13 @@ pub async fn m3u_playlist(
|
|||||||
token: params.token,
|
token: params.token,
|
||||||
};
|
};
|
||||||
let content = application::iptv::m3u::execute(&state.iptv_deps, query).await?;
|
let content = application::iptv::m3u::execute(&state.iptv_deps, query).await?;
|
||||||
Ok((
|
Ok(([(header::CONTENT_TYPE, M3U_CONTENT_TYPE)], content))
|
||||||
[(header::CONTENT_TYPE, "audio/x-mpegurl; charset=utf-8")],
|
|
||||||
content,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /iptv/epg.xml — XMLTV electronic program guide
|
|
||||||
pub async fn xmltv_epg(
|
pub async fn xmltv_epg(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
OptionalCurrentUser(_user): OptionalCurrentUser,
|
OptionalCurrentUser(_user): OptionalCurrentUser,
|
||||||
) -> Result<impl IntoResponse, ApiError> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
let content = application::iptv::xmltv::execute(&state.iptv_deps, GetXmltvQuery).await?;
|
let content = application::iptv::xmltv::execute(&state.iptv_deps, GetXmltvQuery).await?;
|
||||||
Ok((
|
Ok(([(header::CONTENT_TYPE, XML_CONTENT_TYPE)], content))
|
||||||
[(header::CONTENT_TYPE, "application/xml; charset=utf-8")],
|
|
||||||
content,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
//! Library browsing handlers.
|
|
||||||
|
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum::extract::{Path, Query, State};
|
use axum::extract::{Path, Query, State};
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use api_types::{CollectionResponse, LibraryItemResponse, PaginatedResponse, SeasonResponse, ShowResponse};
|
use api_types::{CollectionResponse, LibraryItemResponse, PaginatedResponse, SeasonResponse, ShowResponse};
|
||||||
use application::library::{
|
use application::library::{
|
||||||
@@ -14,6 +12,8 @@ use crate::errors::ApiError;
|
|||||||
use crate::extractors::{AdminUser, CurrentUser};
|
use crate::extractors::{AdminUser, CurrentUser};
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
const DEFAULT_SEARCH_LIMIT: u32 = 50;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct LibrarySearchParams {
|
pub struct LibrarySearchParams {
|
||||||
pub provider: Option<String>,
|
pub provider: Option<String>,
|
||||||
@@ -30,7 +30,6 @@ pub struct LibrarySearchParams {
|
|||||||
pub limit: Option<u32>,
|
pub limit: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /library/items
|
|
||||||
pub async fn search_items(
|
pub async fn search_items(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -46,7 +45,7 @@ pub async fn search_items(
|
|||||||
season_number: params.season_number,
|
season_number: params.season_number,
|
||||||
decade: params.decade,
|
decade: params.decade,
|
||||||
offset: params.offset.unwrap_or(0),
|
offset: params.offset.unwrap_or(0),
|
||||||
limit: params.limit.unwrap_or(50),
|
limit: params.limit.unwrap_or(DEFAULT_SEARCH_LIMIT),
|
||||||
};
|
};
|
||||||
let (items, total) = application::library::search::execute(&state.library_query_deps, query).await?;
|
let (items, total) = application::library::search::execute(&state.library_query_deps, query).await?;
|
||||||
Ok(Json(PaginatedResponse::new(
|
Ok(Json(PaginatedResponse::new(
|
||||||
@@ -55,7 +54,6 @@ pub async fn search_items(
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /library/items/:id
|
|
||||||
pub async fn get_item(
|
pub async fn get_item(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -68,7 +66,6 @@ pub async fn get_item(
|
|||||||
Ok(Json(LibraryItemResponse::from(item)))
|
Ok(Json(LibraryItemResponse::from(item)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /library/collections
|
|
||||||
pub async fn list_collections(
|
pub async fn list_collections(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -100,7 +97,6 @@ pub struct ShowsParams {
|
|||||||
pub genres: Vec<String>,
|
pub genres: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /library/shows
|
|
||||||
pub async fn list_shows(
|
pub async fn list_shows(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -121,7 +117,6 @@ pub struct SeasonsParams {
|
|||||||
pub provider: Option<String>,
|
pub provider: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /library/seasons
|
|
||||||
pub async fn list_seasons(
|
pub async fn list_seasons(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -144,7 +139,6 @@ pub struct GenresParams {
|
|||||||
pub provider: Option<String>,
|
pub provider: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /library/genres
|
|
||||||
pub async fn list_genres(
|
pub async fn list_genres(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
@@ -159,51 +153,52 @@ pub async fn list_genres(
|
|||||||
Ok(Json(genres))
|
Ok(Json(genres))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /library/sync/status
|
#[derive(Debug, Serialize)]
|
||||||
|
pub(crate) struct SyncStatusEntry {
|
||||||
|
provider_id: String,
|
||||||
|
started_at: String,
|
||||||
|
finished_at: String,
|
||||||
|
items_found: u32,
|
||||||
|
status: String,
|
||||||
|
error_msg: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn sync_status(
|
pub async fn sync_status(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(_user): CurrentUser,
|
CurrentUser(_user): CurrentUser,
|
||||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
) -> Result<Json<Vec<SyncStatusEntry>>, ApiError> {
|
||||||
let entries =
|
let entries =
|
||||||
application::library::get_sync_status::execute(&state.library_query_deps, GetSyncStatusQuery)
|
application::library::get_sync_status::execute(&state.library_query_deps, GetSyncStatusQuery)
|
||||||
.await?;
|
.await?;
|
||||||
let result: Vec<serde_json::Value> = entries
|
let result: Vec<SyncStatusEntry> = entries
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|e| {
|
.map(|e| SyncStatusEntry {
|
||||||
serde_json::json!({
|
provider_id: e.provider_id().to_string(),
|
||||||
"provider_id": e.provider_id(),
|
started_at: e.started_at().to_string(),
|
||||||
"started_at": e.started_at(),
|
finished_at: e.finished_at().unwrap_or("").to_string(),
|
||||||
"finished_at": e.finished_at().unwrap_or(""),
|
items_found: e.items_found(),
|
||||||
"items_found": e.items_found(),
|
status: e.status().to_string(),
|
||||||
"status": e.status(),
|
error_msg: e.error_msg().unwrap_or("").to_string(),
|
||||||
"error_msg": e.error_msg().unwrap_or(""),
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Ok(Json(serde_json::Value::Array(result)))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// POST /library/sync — trigger sync (admin only)
|
|
||||||
///
|
|
||||||
/// Validates that no sync is already running, then sends a signal to the
|
|
||||||
/// background sync task to start a sync cycle immediately.
|
|
||||||
pub async fn trigger_sync(
|
pub async fn trigger_sync(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AdminUser(_user): AdminUser,
|
AdminUser(_user): AdminUser,
|
||||||
) -> Result<axum::http::StatusCode, ApiError> {
|
) -> Result<axum::http::StatusCode, ApiError> {
|
||||||
let cmd = TriggerSyncCommand { provider_id: None };
|
let cmd = TriggerSyncCommand { provider_id: None };
|
||||||
let _provider_ids =
|
application::library::sync::execute(&state.library_command_deps, cmd)
|
||||||
application::library::sync::execute(&state.library_command_deps, cmd)
|
.await
|
||||||
.await
|
.map_err(|e| {
|
||||||
.map_err(|e| {
|
if e.to_string().contains("already running") {
|
||||||
if e.to_string().contains("already running") {
|
ApiError::conflict(e.to_string())
|
||||||
ApiError::conflict(e.to_string())
|
} else {
|
||||||
} else {
|
ApiError::from(e)
|
||||||
ApiError::from(e)
|
}
|
||||||
}
|
})?;
|
||||||
})?;
|
|
||||||
|
|
||||||
// Signal the background sync task to run immediately
|
|
||||||
let _ = state.sync_trigger.send(());
|
let _ = state.sync_trigger.send(());
|
||||||
|
|
||||||
Ok(axum::http::StatusCode::ACCEPTED)
|
Ok(axum::http::StatusCode::ACCEPTED)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Provider configuration CRUD handlers.
|
|
||||||
|
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
|
|
||||||
@@ -12,7 +10,6 @@ use crate::errors::ApiError;
|
|||||||
use crate::extractors::AdminUser;
|
use crate::extractors::AdminUser;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
/// GET /admin/providers
|
|
||||||
pub async fn list_providers(
|
pub async fn list_providers(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AdminUser(_user): AdminUser,
|
AdminUser(_user): AdminUser,
|
||||||
@@ -27,7 +24,6 @@ pub async fn list_providers(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /admin/providers/:id
|
|
||||||
pub async fn get_provider(
|
pub async fn get_provider(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AdminUser(_user): AdminUser,
|
AdminUser(_user): AdminUser,
|
||||||
@@ -40,7 +36,6 @@ pub async fn get_provider(
|
|||||||
Ok(Json(ProviderConfigResponse::from(provider)))
|
Ok(Json(ProviderConfigResponse::from(provider)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PUT /admin/providers/:id
|
|
||||||
pub async fn upsert_provider(
|
pub async fn upsert_provider(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
AdminUser(_user): AdminUser,
|
AdminUser(_user): AdminUser,
|
||||||
@@ -60,7 +55,6 @@ pub async fn upsert_provider(
|
|||||||
Ok(Json(serde_json::json!({"status": "ok"})))
|
Ok(Json(serde_json::json!({"status": "ok"})))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// DELETE /admin/providers/:id
|
|
||||||
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,8 +1,7 @@
|
|||||||
//! Schedule, broadcast, and stream handlers.
|
|
||||||
|
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
|
||||||
use api_types::{
|
use api_types::{
|
||||||
CurrentBroadcastResponse, ScheduleHistoryEntry, ScheduleResponse, SlotResponse,
|
CurrentBroadcastResponse, ScheduleHistoryEntry, ScheduleResponse, SlotResponse,
|
||||||
@@ -16,7 +15,6 @@ use crate::errors::ApiError;
|
|||||||
use crate::extractors::CurrentUser;
|
use crate::extractors::CurrentUser;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
/// POST /channels/:id/schedule — generate a new schedule
|
|
||||||
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,7 +25,6 @@ pub async fn generate_schedule(
|
|||||||
Ok(Json(ScheduleResponse::from(schedule)))
|
Ok(Json(ScheduleResponse::from(schedule)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /channels/:id/schedule — get the active schedule
|
|
||||||
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,
|
||||||
@@ -40,9 +37,6 @@ pub async fn get_active_schedule(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use axum::response::IntoResponse;
|
|
||||||
|
|
||||||
/// GET /channels/:id/now — what's currently playing
|
|
||||||
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>,
|
||||||
@@ -51,7 +45,6 @@ pub async fn get_current_broadcast(
|
|||||||
match application::schedule::get_current_broadcast::execute(&state.schedule_deps, query).await?
|
match application::schedule::get_current_broadcast::execute(&state.schedule_deps, query).await?
|
||||||
{
|
{
|
||||||
Some(broadcast) => {
|
Some(broadcast) => {
|
||||||
// Look up the channel to resolve block access mode
|
|
||||||
let channel_query = application::channels::GetChannelQuery { channel_id: id };
|
let channel_query = application::channels::GetChannelQuery { channel_id: id };
|
||||||
let channel = application::channels::get::execute(&state.channel_query_deps, channel_query).await?;
|
let channel = application::channels::get::execute(&state.channel_query_deps, channel_query).await?;
|
||||||
|
|
||||||
@@ -73,7 +66,6 @@ pub async fn get_current_broadcast(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /channels/:id/epg — electronic program guide
|
|
||||||
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>,
|
||||||
@@ -83,12 +75,10 @@ pub async fn get_epg(
|
|||||||
Ok(Json(slots.into_iter().map(SlotResponse::from).collect()))
|
Ok(Json(slots.into_iter().map(SlotResponse::from).collect()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /channels/:id/stream — redirect to stream URL (307)
|
|
||||||
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>,
|
||||||
) -> Result<axum::response::Response, ApiError> {
|
) -> Result<axum::response::Response, ApiError> {
|
||||||
// Find the current broadcast first to get the item ID
|
|
||||||
let broadcast_query = GetCurrentBroadcastQuery { channel_id: id };
|
let broadcast_query = GetCurrentBroadcastQuery { channel_id: id };
|
||||||
let broadcast =
|
let broadcast =
|
||||||
application::schedule::get_current_broadcast::execute(&state.schedule_deps, broadcast_query)
|
application::schedule::get_current_broadcast::execute(&state.schedule_deps, broadcast_query)
|
||||||
@@ -113,7 +103,6 @@ pub async fn get_stream(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /channels/:id/schedule/history — list schedule generations
|
|
||||||
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,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! k-tv server entry point.
|
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
use tower_http::cors::{Any, CorsLayer};
|
use tower_http::cors::{Any, CorsLayer};
|
||||||
@@ -17,10 +15,8 @@ mod state;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
// Load .env file if present
|
|
||||||
let _ = dotenvy::dotenv();
|
let _ = dotenvy::dotenv();
|
||||||
|
|
||||||
// Initialize tracing
|
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_env_filter(
|
.with_env_filter(
|
||||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||||
@@ -28,7 +24,6 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
)
|
)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
// Load config
|
|
||||||
let config = infra_wiring::Config::from_env()
|
let config = infra_wiring::Config::from_env()
|
||||||
.map_err(|e| anyhow::anyhow!("Config error: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Config error: {}", e))?;
|
||||||
|
|
||||||
@@ -38,10 +33,8 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
info!("Starting k-tv server on {}:{}", host, port);
|
info!("Starting k-tv server on {}:{}", host, port);
|
||||||
|
|
||||||
// Build the application state (connects DB, creates adapters, spawns background tasks)
|
|
||||||
let app_state = factory::build_app_state(config).await?;
|
let app_state = factory::build_app_state(config).await?;
|
||||||
|
|
||||||
// Build CORS layer
|
|
||||||
let cors = if cors_origins.iter().any(|o| o == "*") {
|
let cors = if cors_origins.iter().any(|o| o == "*") {
|
||||||
CorsLayer::new()
|
CorsLayer::new()
|
||||||
.allow_origin(Any)
|
.allow_origin(Any)
|
||||||
@@ -58,14 +51,12 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.allow_headers(Any)
|
.allow_headers(Any)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build the router
|
|
||||||
let app = axum::Router::new()
|
let app = axum::Router::new()
|
||||||
.nest("/api/v1", routes::api_v1_router())
|
.nest("/api/v1", routes::api_v1_router())
|
||||||
.layer(cors)
|
.layer(cors)
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.with_state(app_state);
|
.with_state(app_state);
|
||||||
|
|
||||||
// Start serving
|
|
||||||
let addr: SocketAddr = format!("{}:{}", host, port).parse()?;
|
let addr: SocketAddr = format!("{}:{}", host, port).parse()?;
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||||
info!("Listening on {}", addr);
|
info!("Listening on {}", addr);
|
||||||
|
|||||||
@@ -1,6 +1 @@
|
|||||||
//! Domain → DTO mappings.
|
|
||||||
//!
|
|
||||||
//! Most conversions are already handled by `From` impls in the `api-types` crate.
|
|
||||||
//! This module is reserved for any presentation-layer-specific mappings that
|
|
||||||
//! don't belong in `api-types` (e.g., combining multiple domain objects into a
|
|
||||||
//! single response).
|
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
//! Router construction.
|
|
||||||
|
|
||||||
use axum::{Router, routing::{delete, get, post, put}};
|
use axum::{Router, routing::{delete, get, post, put}};
|
||||||
|
|
||||||
use crate::handlers;
|
use crate::handlers;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
/// Construct the API v1 router.
|
|
||||||
pub fn api_v1_router() -> Router<AppState> {
|
pub fn api_v1_router() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.nest("/auth", auth_router())
|
.nest("/auth", auth_router())
|
||||||
@@ -41,15 +38,12 @@ fn channel_router() -> Router<AppState> {
|
|||||||
.route("/{id}", get(handlers::channels::get_channel))
|
.route("/{id}", get(handlers::channels::get_channel))
|
||||||
.route("/{id}", put(handlers::channels::update_channel))
|
.route("/{id}", put(handlers::channels::update_channel))
|
||||||
.route("/{id}", delete(handlers::channels::delete_channel))
|
.route("/{id}", delete(handlers::channels::delete_channel))
|
||||||
// Schedule
|
|
||||||
.route("/{id}/schedule", post(handlers::schedule::generate_schedule))
|
.route("/{id}/schedule", post(handlers::schedule::generate_schedule))
|
||||||
.route("/{id}/schedule", get(handlers::schedule::get_active_schedule))
|
.route("/{id}/schedule", get(handlers::schedule::get_active_schedule))
|
||||||
.route("/{id}/schedule/history", get(handlers::schedule::list_schedule_history))
|
.route("/{id}/schedule/history", get(handlers::schedule::list_schedule_history))
|
||||||
// Broadcast
|
|
||||||
.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))
|
||||||
// Config snapshots
|
|
||||||
.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))
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//! Application state — holds pre-built Deps structs from the application layer.
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use application::{
|
use application::{
|
||||||
@@ -13,7 +11,6 @@ use application::{
|
|||||||
schedule::ScheduleDeps,
|
schedule::ScheduleDeps,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Shared application state, passed to all handlers via `State<AppState>`.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub auth_deps: Arc<AuthDeps>,
|
pub auth_deps: Arc<AuthDeps>,
|
||||||
@@ -27,25 +24,16 @@ pub struct AppState {
|
|||||||
pub iptv_deps: Arc<IptvDeps>,
|
pub iptv_deps: Arc<IptvDeps>,
|
||||||
pub provider_deps: Arc<ProviderDeps>,
|
pub provider_deps: Arc<ProviderDeps>,
|
||||||
|
|
||||||
/// JWT validator for token creation/validation in auth handlers.
|
|
||||||
#[cfg(feature = "auth-jwt")]
|
#[cfg(feature = "auth-jwt")]
|
||||||
pub jwt_validator: Option<Arc<adapter_auth::JwtValidator>>,
|
pub jwt_validator: Option<Arc<adapter_auth::JwtValidator>>,
|
||||||
|
|
||||||
/// Provider registry for config/capabilities endpoints.
|
|
||||||
pub provider_registry: Arc<dyn domain::ports::IProviderRegistry>,
|
pub provider_registry: Arc<dyn domain::ports::IProviderRegistry>,
|
||||||
|
|
||||||
/// Library sync adapter — needed for spawning background sync tasks.
|
// kept alive for background tasks — not read from handlers
|
||||||
pub library_sync: Arc<dyn domain::ports::LibrarySyncAdapter>,
|
pub _library_sync: Arc<dyn domain::ports::LibrarySyncAdapter>,
|
||||||
|
pub _settings_repo: Arc<dyn domain::ports::AppSettingsRepository>,
|
||||||
|
pub _event_bus: Arc<adapter_event_publisher::ChannelEventBus>,
|
||||||
|
|
||||||
/// App settings — read by library sync background task.
|
|
||||||
pub settings_repo: Arc<dyn domain::ports::AppSettingsRepository>,
|
|
||||||
|
|
||||||
/// Event bus for domain events.
|
|
||||||
pub event_bus: Arc<adapter_event_publisher::ChannelEventBus>,
|
|
||||||
|
|
||||||
/// Application config.
|
|
||||||
pub config: Arc<infra_wiring::Config>,
|
pub config: Arc<infra_wiring::Config>,
|
||||||
|
|
||||||
/// Trigger for on-demand library sync (sends () to wake the background task).
|
|
||||||
pub sync_trigger: tokio::sync::watch::Sender<()>,
|
pub sync_trigger: tokio::sync::watch::Sender<()>,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user