feat: add indexer and torrent REST controllers with service layer

- Add config module for yaml config (database, indexers, torrent)
- Add indexer module with Torznab protocol support
- Add IndexerService and TorrentService for business logic
- Add REST controllers for indexer search and torrent management
- Add Docker Compose for PostgreSQL and Jackett
- Add ERD documentation for database schema
This commit is contained in:
Alexander
2026-04-28 18:53:50 +02:00
parent f77806ba46
commit 1aaaab4640
23 changed files with 1841 additions and 51 deletions
+28 -17
View File
@@ -1,3 +1,6 @@
mod indexer_controller;
mod torrent_controller;
use axum::{
extract::{Path, Query, State},
http::StatusCode,
@@ -18,20 +21,22 @@ pub fn routes(state: AppState) -> Router {
.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())
.with_state(state)
}
async fn list_tracks(State(state): State<AppState>) -> Json<Vec<Track>> {
let agg = state.read().await;
Json(agg.get_all().to_vec())
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 agg = state.write().await;
let track = agg.add_track(input.into());
let mut state = state.write().await;
let track = state.aggregator.add_track(input.into());
(StatusCode::CREATED, Json(track))
}
@@ -39,19 +44,18 @@ async fn get_track(
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<Json<Track>, StatusCode> {
let agg = state.read().await;
agg.get_by_id(id)
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 agg = state.write().await;
if agg.delete(id) {
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
@@ -67,8 +71,15 @@ async fn search_tracks(
State(state): State<AppState>,
Query(query): Query<SearchQuery>,
) -> Json<Vec<Track>> {
let agg = state.read().await;
Json(agg.search_by_artist(&query.artist).into_iter().cloned().collect())
let state = state.read().await;
Json(
state
.aggregator
.search_by_artist(&query.artist)
.into_iter()
.cloned()
.collect(),
)
}
#[derive(serde::Serialize)]
@@ -78,9 +89,9 @@ struct Stats {
}
async fn get_stats(State(state): State<AppState>) -> Json<Stats> {
let agg = state.read().await;
let state = state.read().await;
Json(Stats {
track_count: agg.get_all().len(),
total_duration_secs: agg.total_duration(),
track_count: state.aggregator.get_all().len(),
total_duration_secs: state.aggregator.total_duration(),
})
}