f24543f401
- Add sqlx with PostgreSQL support for database operations - Create DbService with artist/album upsert and listing methods - Add database schema (14 tables) in containers/init.sql - Add library controller (GET /api/library/artists, /albums, /stats) - Merge sync_artist + ingest into single POST /api/sync endpoint - Support configurable sync: download (bool), store (bool), album filter - Connect to database at startup with graceful fallback
158 lines
3.9 KiB
Rust
158 lines
3.9 KiB
Rust
mod indexer_controller;
|
|
mod library_controller;
|
|
mod metadata_controller;
|
|
mod sync_controller;
|
|
mod torrent_controller;
|
|
|
|
use axum::{
|
|
extract::{Path, Query, State},
|
|
http::StatusCode,
|
|
routing::{delete, get, post},
|
|
Json, Router,
|
|
};
|
|
use serde::Deserialize;
|
|
use uuid::Uuid;
|
|
|
|
use crate::models::{CreateTrack, Track};
|
|
use crate::AppState;
|
|
|
|
pub fn routes(state: AppState) -> Router {
|
|
Router::new()
|
|
.route("/health", get(health))
|
|
.route("/reload", post(reload))
|
|
.route("/tracks", get(list_tracks))
|
|
.route("/tracks", post(create_track))
|
|
.route("/tracks/{id}", get(get_track))
|
|
.route("/tracks/{id}", delete(delete_track))
|
|
.route("/tracks/search", get(search_tracks))
|
|
.route("/stats", get(get_stats))
|
|
.nest("/indexers", indexer_controller::routes())
|
|
.nest("/torrents", torrent_controller::routes())
|
|
.nest("/metadata", metadata_controller::routes())
|
|
.nest("/sync", sync_controller::routes())
|
|
.nest("/library", library_controller::routes())
|
|
.with_state(state)
|
|
}
|
|
|
|
#[derive(serde::Serialize)]
|
|
struct Health {
|
|
status: &'static str,
|
|
services: ServiceStatus,
|
|
}
|
|
|
|
#[derive(serde::Serialize)]
|
|
struct ServiceStatus {
|
|
torrent: bool,
|
|
metadata: bool,
|
|
indexers: Vec<String>,
|
|
}
|
|
|
|
async fn health(State(state): State<AppState>) -> Json<Health> {
|
|
let state = state.read().await;
|
|
let indexers = state
|
|
.indexer_service
|
|
.list_indexers()
|
|
.into_iter()
|
|
.map(|i| i.name)
|
|
.collect();
|
|
Json(Health {
|
|
status: "ok",
|
|
services: ServiceStatus {
|
|
torrent: state.torrent_service.is_connected().await,
|
|
metadata: state.metadata_service.is_connected(),
|
|
indexers,
|
|
},
|
|
})
|
|
}
|
|
|
|
#[derive(serde::Serialize)]
|
|
struct ReloadResponse {
|
|
success: bool,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
error: Option<String>,
|
|
}
|
|
|
|
async fn reload(State(state): State<AppState>) -> Json<ReloadResponse> {
|
|
let mut state = state.write().await;
|
|
match state.reload().await {
|
|
Ok(()) => Json(ReloadResponse {
|
|
success: true,
|
|
error: None,
|
|
}),
|
|
Err(e) => Json(ReloadResponse {
|
|
success: false,
|
|
error: Some(e),
|
|
}),
|
|
}
|
|
}
|
|
|
|
async fn list_tracks(State(state): State<AppState>) -> Json<Vec<Track>> {
|
|
let state = state.read().await;
|
|
Json(state.aggregator.get_all().to_vec())
|
|
}
|
|
|
|
async fn create_track(
|
|
State(state): State<AppState>,
|
|
Json(input): Json<CreateTrack>,
|
|
) -> (StatusCode, Json<Track>) {
|
|
let mut state = state.write().await;
|
|
let track = state.aggregator.add_track(input.into());
|
|
(StatusCode::CREATED, Json(track))
|
|
}
|
|
|
|
async fn get_track(
|
|
State(state): State<AppState>,
|
|
Path(id): Path<Uuid>,
|
|
) -> Result<Json<Track>, StatusCode> {
|
|
let state = state.read().await;
|
|
state
|
|
.aggregator
|
|
.get_by_id(id)
|
|
.cloned()
|
|
.map(Json)
|
|
.ok_or(StatusCode::NOT_FOUND)
|
|
}
|
|
|
|
async fn delete_track(State(state): State<AppState>, Path(id): Path<Uuid>) -> StatusCode {
|
|
let mut state = state.write().await;
|
|
if state.aggregator.delete(id) {
|
|
StatusCode::NO_CONTENT
|
|
} else {
|
|
StatusCode::NOT_FOUND
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct SearchQuery {
|
|
artist: String,
|
|
}
|
|
|
|
async fn search_tracks(
|
|
State(state): State<AppState>,
|
|
Query(query): Query<SearchQuery>,
|
|
) -> Json<Vec<Track>> {
|
|
let state = state.read().await;
|
|
Json(
|
|
state
|
|
.aggregator
|
|
.search_by_artist(&query.artist)
|
|
.into_iter()
|
|
.cloned()
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
#[derive(serde::Serialize)]
|
|
struct Stats {
|
|
track_count: usize,
|
|
total_duration_secs: u32,
|
|
}
|
|
|
|
async fn get_stats(State(state): State<AppState>) -> Json<Stats> {
|
|
let state = state.read().await;
|
|
Json(Stats {
|
|
track_count: state.aggregator.get_all().len(),
|
|
total_duration_secs: state.aggregator.total_duration(),
|
|
})
|
|
}
|