From ff9ca7ecce91114a0ac6a873407141f7f149bc8d Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 28 Apr 2026 18:05:32 +0200 Subject: [PATCH] feat: add rest api for music tracks Ultraworked with [Sisyphus](https://github.com/code-yeongyu/claude-agent) Co-authored-by: Sisyphus --- src/api/mod.rs | 86 +++++++++++++++++++++++++++++++++++++++++++++ src/models/mod.rs | 31 ++++++++++++++++ src/services/mod.rs | 45 ++++++++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 src/api/mod.rs create mode 100644 src/models/mod.rs create mode 100644 src/services/mod.rs diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..33db271 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,86 @@ +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("/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)) + .with_state(state) +} + +async fn list_tracks(State(state): State) -> Json> { + let agg = state.read().await; + Json(agg.get_all().to_vec()) +} + +async fn create_track( + State(state): State, + Json(input): Json, +) -> (StatusCode, Json) { + let mut agg = state.write().await; + let track = agg.add_track(input.into()); + (StatusCode::CREATED, Json(track)) +} + +async fn get_track( + State(state): State, + Path(id): Path, +) -> Result, StatusCode> { + let agg = state.read().await; + agg.get_by_id(id) + .cloned() + .map(Json) + .ok_or(StatusCode::NOT_FOUND) +} + +async fn delete_track( + State(state): State, + Path(id): Path, +) -> StatusCode { + let mut agg = state.write().await; + if agg.delete(id) { + StatusCode::NO_CONTENT + } else { + StatusCode::NOT_FOUND + } +} + +#[derive(Deserialize)] +struct SearchQuery { + artist: String, +} + +async fn search_tracks( + State(state): State, + Query(query): Query, +) -> Json> { + let agg = state.read().await; + Json(agg.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) -> Json { + let agg = state.read().await; + Json(Stats { + track_count: agg.get_all().len(), + total_duration_secs: agg.total_duration(), + }) +} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..79875f5 --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,31 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Track { + pub id: Uuid, + pub title: String, + pub artist: String, + pub album: Option, + pub duration_secs: u32, +} + +#[derive(Debug, Deserialize)] +pub struct CreateTrack { + pub title: String, + pub artist: String, + pub album: Option, + pub duration_secs: u32, +} + +impl From for Track { + fn from(input: CreateTrack) -> Self { + Self { + id: Uuid::new_v4(), + title: input.title, + artist: input.artist, + album: input.album, + duration_secs: input.duration_secs, + } + } +} diff --git a/src/services/mod.rs b/src/services/mod.rs new file mode 100644 index 0000000..d60b40d --- /dev/null +++ b/src/services/mod.rs @@ -0,0 +1,45 @@ +use uuid::Uuid; + +use crate::models::Track; + +#[derive(Default)] +pub struct Aggregator { + tracks: Vec, +} + +impl Aggregator { + pub fn new() -> Self { + Self::default() + } + + pub fn add_track(&mut self, track: Track) -> Track { + self.tracks.push(track.clone()); + track + } + + pub fn get_all(&self) -> &[Track] { + &self.tracks + } + + pub fn get_by_id(&self, id: Uuid) -> Option<&Track> { + self.tracks.iter().find(|t| t.id == id) + } + + pub fn search_by_artist(&self, artist: &str) -> Vec<&Track> { + let artist_lower = artist.to_lowercase(); + self.tracks + .iter() + .filter(|t| t.artist.to_lowercase().contains(&artist_lower)) + .collect() + } + + pub fn delete(&mut self, id: Uuid) -> bool { + let len_before = self.tracks.len(); + self.tracks.retain(|t| t.id != id); + self.tracks.len() != len_before + } + + pub fn total_duration(&self) -> u32 { + self.tracks.iter().map(|t| t.duration_secs).sum() + } +}