Your music, beautifully tracked. All yours. (coming soon)
teal.fm
teal-fm
atproto
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}