feat: add rest api for music tracks
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/claude-agent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -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<AppState>) -> Json<Vec<Track>> {
|
||||
let agg = state.read().await;
|
||||
Json(agg.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());
|
||||
(StatusCode::CREATED, Json(track))
|
||||
}
|
||||
|
||||
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)
|
||||
.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) {
|
||||
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 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<AppState>) -> Json<Stats> {
|
||||
let agg = state.read().await;
|
||||
Json(Stats {
|
||||
track_count: agg.get_all().len(),
|
||||
total_duration_secs: agg.total_duration(),
|
||||
})
|
||||
}
|
||||
@@ -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<String>,
|
||||
pub duration_secs: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateTrack {
|
||||
pub title: String,
|
||||
pub artist: String,
|
||||
pub album: Option<String>,
|
||||
pub duration_secs: u32,
|
||||
}
|
||||
|
||||
impl From<CreateTrack> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::Track;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Aggregator {
|
||||
tracks: Vec<Track>,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user