Your music, beautifully tracked. All yours. (coming soon) teal.fm
teal-fm atproto
at fix-compose-path 150 lines 4.5 kB view raw
1use axum::{ 2 extract::{Query, State}, 3 http::StatusCode, 4 Json, 5}; 6use serde::{Deserialize, Serialize}; 7use sqlx::FromRow; 8use uuid::Uuid; 9 10use crate::AppState; 11 12#[derive(Serialize, Deserialize, Debug, FromRow)] 13pub struct GlobalPlayCount { 14 pub play_count: i64, 15} 16 17pub async fn get_global_play_count( 18 State(state): State<AppState>, 19) -> Result<Json<GlobalPlayCount>, (axum::http::StatusCode, String)> { 20 let result = sqlx::query_as::<_, GlobalPlayCount>( 21 "SELECT play_count FROM mv_global_play_count WHERE id = 1", 22 ) 23 .fetch_one(&state.db_pool) 24 .await; 25 26 match result { 27 Ok(count) => Ok(Json(count)), 28 Err(e) => Err(( 29 axum::http::StatusCode::INTERNAL_SERVER_ERROR, 30 format!("Database error: {}", e), 31 )), 32 } 33} 34 35const fn default_limit() -> i64 { 36 12 37} 38 39#[derive(Debug, Clone, Deserialize, Serialize)] 40pub struct LatestPlayQueryParams { 41 #[serde(default = "default_limit")] 42 pub limit: i64, 43} 44 45#[derive(FromRow, Debug)] 46pub struct Play { 47 pub did: String, 48 pub track_name: String, 49 pub recording_mbid: Option<Uuid>, 50 pub release_name: Option<String>, 51 pub release_mbid: Option<Uuid>, 52 pub duration: Option<i32>, 53 pub uri: Option<String>, 54 // MASSIVE HUGE HACK 55 pub artists: Option<String>, 56} 57 58#[derive(FromRow, Debug, Deserialize, Serialize)] 59pub struct PlayReturn { 60 pub did: String, 61 pub track_name: String, 62 pub recording_mbid: Option<Uuid>, 63 pub release_name: Option<String>, 64 pub release_mbid: Option<Uuid>, 65 pub duration: Option<i32>, 66 pub uri: Option<String>, 67 pub artists: Vec<Artist>, 68} 69 70#[derive(sqlx::Type, Debug, Deserialize, Serialize)] 71pub struct Artist { 72 pub artist_name: String, 73 pub artist_mbid: Option<Uuid>, 74} 75 76pub async fn get_latest_plays( 77 State(state): State<AppState>, 78 Query(params): Query<LatestPlayQueryParams>, 79) -> Result<Json<Vec<PlayReturn>>, (axum::http::StatusCode, String)> { 80 if params.limit < 1 || params.limit > 50 { 81 return Err((StatusCode::BAD_REQUEST, "Invalid limit".to_string())); 82 } 83 let result = sqlx::query_as!( 84 Play, 85 r#" 86 SELECT 87 p.did, 88 p.track_name, 89 -- TODO: replace with actual 90 STRING_AGG(pa.artist_name || '|' || TEXT(pa.artist_mbid), ',') AS artists, 91 p.release_name, 92 p.duration, 93 p.uri, 94 p.recording_mbid, 95 p.release_mbid 96 97 FROM plays AS p 98 LEFT JOIN play_to_artists AS pa ON pa.play_uri = p.uri 99 GROUP BY p.did, p.track_name, p.release_name, p.played_time, p.duration, p.uri, p.recording_mbid, p.release_mbid 100 ORDER BY p.played_time DESC 101 LIMIT $1 102 "#, 103 params.limit 104 ) 105 .fetch_all(&state.db_pool) 106 .await; 107 108 match result { 109 Ok(counts) => { 110 let fin: Vec<PlayReturn> = counts 111 .into_iter() 112 .map(|play| -> PlayReturn { 113 let artists = play 114 .artists 115 .expect("Artists found") 116 .split(',') 117 .map(|artist| { 118 let mut parts = artist.split('|'); 119 Artist { 120 artist_name: parts 121 .next() 122 .expect("Artist name is required") 123 .to_string(), 124 artist_mbid: parts 125 .next() 126 .and_then(|mbid| Uuid::parse_str(mbid).ok()), 127 } 128 }) 129 .collect(); 130 PlayReturn { 131 did: play.did.to_string(), 132 track_name: play.track_name, 133 recording_mbid: play.recording_mbid, 134 release_name: play.release_name, 135 release_mbid: play.release_mbid, 136 duration: play.duration, 137 uri: play.uri, 138 artists, 139 } 140 }) 141 .collect(); 142 143 Ok(Json(fin)) 144 } 145 Err(e) => Err(( 146 axum::http::StatusCode::INTERNAL_SERVER_ERROR, 147 format!("Database error: {}", e), 148 )), 149 } 150}