Files
music-agregator/src/api/mod.rs
T
Alexander f24543f401 feat: add PostgreSQL persistence and unified sync endpoint
- 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
2026-04-29 10:06:01 +02:00

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(),
})
}