A Wrapped / Replay like for teal.fm and rocksky.app (currently on hiatus)
1use super::GlobalState;
2use crate::{analysis, SqliteConnection};
3use crate::lex::queries::{
4 GetAnnualSummaryOutput, GetAnnualSummaryRequest, GetPeriodSummaryOutput,
5 GetPeriodSummaryRequest,
6};
7use crate::lex::{Album, Artist, Summary, TopAlbumEntry, TopArtistEntry, TopTrackEntry, Track};
8use crate::utils::{db_call, get_public_profile, resolve_atid};
9use axum::Json;
10use axum::extract::State;
11use axum::http::StatusCode;
12use chrono::prelude::*;
13use jacquard_axum::ExtractXrpc;
14
15pub async fn get_annual_summary(
16 State(state): State<GlobalState>,
17 ExtractXrpc(req): ExtractXrpc<GetAnnualSummaryRequest>,
18) -> Result<Json<GetAnnualSummaryOutput<'static>>, StatusCode> {
19 let did = resolve_atid(req.actor).await.map_err(|err| {
20 tracing::error!("Failed to resolve handle: {err}");
21 StatusCode::INTERNAL_SERVER_ERROR
22 })?;
23 let profile = get_public_profile(&did).await.map_err(|err| {
24 tracing::error!("Failed to load profile: {err}");
25 StatusCode::INTERNAL_SERVER_ERROR
26 })?;
27
28 // this sucks hard, but idk if there's a nicer way of doing it
29 let start = Utc
30 .with_ymd_and_hms(req.year as i32, 1, 1, 0, 0, 0)
31 .unwrap();
32 let end = Utc
33 .with_ymd_and_hms(req.year as i32, 12, 31, 23, 59, 59)
34 .unwrap();
35
36 let (albums, artists, tracks) = db_call(&state.db, move |conn| {
37 let (albums, artists, tracks) = get_core_summary(&conn, &did, start, end)?;
38
39 Ok((albums, artists, tracks))
40 })
41 .await
42 .map_err(|_err| StatusCode::INTERNAL_SERVER_ERROR)?
43 .map_err(|err| {
44 tracing::error!("Failed to load summary: {err}");
45 StatusCode::INTERNAL_SERVER_ERROR
46 })?;
47
48 Ok(Json(GetAnnualSummaryOutput {
49 year: req.year,
50 profile,
51 summary: Summary {
52 top_artists: artists,
53 top_albums: albums,
54 top_tracks: tracks,
55 },
56 extra_data: None,
57 }))
58}
59
60pub async fn get_period_summary(
61 State(state): State<GlobalState>,
62 ExtractXrpc(req): ExtractXrpc<GetPeriodSummaryRequest>,
63) -> Result<Json<GetPeriodSummaryOutput<'static>>, StatusCode> {
64 let did = resolve_atid(req.actor).await.map_err(|err| {
65 tracing::error!("Failed to resolve handle: {err}");
66 StatusCode::INTERNAL_SERVER_ERROR
67 })?;
68
69 let start = req.start.as_ref().to_utc();
70 let end = req.end.as_ref().to_utc();
71
72 let profile = get_public_profile(&did).await.map_err(|err| {
73 tracing::error!("Failed to load profile: {err}");
74 StatusCode::INTERNAL_SERVER_ERROR
75 })?;
76
77 let (albums, artists, tracks) = db_call(&state.db, move |conn| {
78 let (albums, artists, tracks) = get_core_summary(&conn, &did, start, end)?;
79
80 Ok((albums, artists, tracks))
81 })
82 .await
83 .map_err(|_err| StatusCode::INTERNAL_SERVER_ERROR)?
84 .map_err(|err| {
85 tracing::error!("Failed to load summary: {err}");
86 StatusCode::INTERNAL_SERVER_ERROR
87 })?;
88
89 Ok(Json(GetPeriodSummaryOutput {
90 start: req.start,
91 end: req.end,
92 profile,
93 summary: Summary {
94 top_artists: artists,
95 top_albums: albums,
96 top_tracks: tracks,
97 },
98 }))
99}
100
101fn get_core_summary(
102 conn: &SqliteConnection,
103 did: &str,
104 start: DateTime<Utc>,
105 end: DateTime<Utc>,
106) -> rusqlite::Result<(
107 Vec<TopAlbumEntry<'static>>,
108 Vec<TopArtistEntry<'static>>,
109 Vec<TopTrackEntry<'static>>,
110)> {
111 let albums = analysis::get_top_albums(conn, did, start, end, 10)?
112 .into_iter()
113 .map(|album| TopAlbumEntry {
114 album: Album {
115 album_name: album.name.into(),
116 album_mbid: Some(album.mbid.into()),
117 album_art_uri: None,
118 },
119 count: album.count,
120 })
121 .collect();
122
123 let artists = analysis::get_top_artists(conn, did, start, end, 10)?
124 .into_iter()
125 .map(|artist| TopArtistEntry {
126 artist: Artist {
127 artist_name: artist.name.into(),
128 artist_mbid: Some(artist.mbid.into()),
129 artist_art_uri: None,
130 },
131 count: artist.count,
132 })
133 .collect();
134
135 let tracks = analysis::get_top_tracks(conn, did, start, end, 10)?
136 .into_iter()
137 .map(|track| TopTrackEntry {
138 track: Track {
139 track_name: track.name.into(),
140 track_mbid: Some(track.mbid.into()),
141 },
142 count: track.count,
143 })
144 .collect();
145
146 Ok((albums, artists, tracks))
147}