A decentralized music tracking and discovery platform built on AT Protocol 🎵 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
0

Configure Feed

Select the types of activity you want to include in your feed.

rocksky / crates / navidrome / src / repo / album.rs
8.7 kB 258 lines
1use anyhow::Error; 2use sqlx::{Pool, Postgres}; 3 4use crate::xata::album::AlbumWithStats; 5 6pub async fn get_albums_by_artist( 7 pool: &Pool<Postgres>, 8 artist_id: &str, 9 user_id: &str, 10) -> Result<Vec<AlbumWithStats>, Error> { 11 let rows: Vec<AlbumWithStats> = sqlx::query_as( 12 r#" 13 SELECT 14 albums.xata_id, 15 albums.title, 16 albums.artist, 17 albums.year, 18 albums.album_art, 19 COUNT(DISTINCT album_tracks.track_id) AS song_count, 20 SUM(tracks.duration)::bigint AS total_duration, 21 MIN(user_uploads.uploaded_at)::timestamptz AS created_at, 22 $2::text AS artist_id 23 FROM albums 24 JOIN artist_albums ON albums.xata_id = artist_albums.album_id 25 JOIN album_tracks ON albums.xata_id = album_tracks.album_id 26 JOIN tracks ON album_tracks.track_id = tracks.xata_id 27 JOIN user_uploads ON tracks.xata_id = user_uploads.track_id 28 WHERE artist_albums.artist_id = $2 29 AND user_uploads.user_id = $1 30 GROUP BY albums.xata_id, albums.title, albums.artist, albums.year, albums.album_art 31 ORDER BY albums.year DESC NULLS LAST 32 "#, 33 ) 34 .bind(user_id) 35 .bind(artist_id) 36 .fetch_all(pool) 37 .await?; 38 39 Ok(rows) 40} 41 42pub async fn get_album_by_id( 43 pool: &Pool<Postgres>, 44 album_id: &str, 45 user_id: &str, 46) -> Result<Option<AlbumWithStats>, Error> { 47 let row: Option<AlbumWithStats> = sqlx::query_as( 48 r#" 49 SELECT 50 albums.xata_id, 51 albums.title, 52 albums.artist, 53 albums.year, 54 albums.album_art, 55 COUNT(DISTINCT album_tracks.track_id) AS song_count, 56 SUM(tracks.duration)::bigint AS total_duration, 57 MIN(user_uploads.uploaded_at)::timestamptz AS created_at, 58 (SELECT aa.artist_id FROM artist_albums aa WHERE aa.album_id = albums.xata_id LIMIT 1) AS artist_id 59 FROM albums 60 JOIN album_tracks ON albums.xata_id = album_tracks.album_id 61 JOIN tracks ON album_tracks.track_id = tracks.xata_id 62 JOIN user_uploads ON tracks.xata_id = user_uploads.track_id 63 WHERE user_uploads.user_id = $1 64 AND albums.xata_id = $2 65 GROUP BY albums.xata_id, albums.title, albums.artist, albums.year, albums.album_art 66 "#, 67 ) 68 .bind(user_id) 69 .bind(album_id) 70 .fetch_optional(pool) 71 .await?; 72 73 Ok(row) 74} 75 76pub async fn get_album_list( 77 pool: &Pool<Postgres>, 78 user_id: &str, 79 list_type: &str, 80 count: i64, 81 offset: i64, 82 from_year: Option<i32>, 83 to_year: Option<i32>, 84 genre: Option<&str>, 85) -> Result<Vec<AlbumWithStats>, Error> { 86 let order_clause = match list_type { 87 "newest" => "ORDER BY created_at DESC NULLS LAST", 88 "alphabeticalByName" => "ORDER BY albums.title ASC", 89 "alphabeticalByArtist" => "ORDER BY albums.artist ASC", 90 "random" => "ORDER BY RANDOM()", 91 "recent" => "ORDER BY created_at DESC NULLS LAST", 92 "byYear" => { 93 if from_year.unwrap_or(0) > to_year.unwrap_or(9999) { 94 "ORDER BY albums.year DESC NULLS LAST" 95 } else { 96 "ORDER BY albums.year ASC NULLS LAST" 97 } 98 } 99 _ => "ORDER BY created_at DESC NULLS LAST", 100 }; 101 102 let year_filter = if list_type == "byYear" { 103 let from = from_year.unwrap_or(0); 104 let to = to_year.unwrap_or(9999); 105 format!( 106 " AND albums.year BETWEEN {} AND {}", 107 from.min(to), 108 from.max(to) 109 ) 110 } else { 111 String::new() 112 }; 113 114 // byGenre needs an extra join through artist_albums → artists to check genres array 115 let (genre_join, genre_filter) = if list_type == "byGenre" { 116 if let Some(_g) = genre { 117 ( 118 "JOIN artist_albums ag ON albums.xata_id = ag.album_id JOIN artists ON ag.artist_id = artists.xata_id".to_string(), 119 format!(" AND $4::text = ANY(artists.genres)"), 120 ) 121 } else { 122 (String::new(), String::new()) 123 } 124 } else { 125 (String::new(), String::new()) 126 }; 127 128 let sql = format!( 129 r#" 130 SELECT 131 albums.xata_id, 132 albums.title, 133 albums.artist, 134 albums.year, 135 albums.album_art, 136 COUNT(DISTINCT album_tracks.track_id) AS song_count, 137 SUM(tracks.duration)::bigint AS total_duration, 138 MIN(user_uploads.uploaded_at)::timestamptz AS created_at, 139 (SELECT aa.artist_id FROM artist_albums aa WHERE aa.album_id = albums.xata_id LIMIT 1) AS artist_id 140 FROM albums 141 JOIN album_tracks ON albums.xata_id = album_tracks.album_id 142 JOIN tracks ON album_tracks.track_id = tracks.xata_id 143 JOIN user_uploads ON tracks.xata_id = user_uploads.track_id 144 {} 145 WHERE user_uploads.user_id = $1 146 {}{} 147 GROUP BY albums.xata_id, albums.title, albums.artist, albums.year, albums.album_art 148 {} 149 LIMIT $2 OFFSET $3 150 "#, 151 genre_join, year_filter, genre_filter, order_clause 152 ); 153 154 let mut q = sqlx::query_as::<_, AlbumWithStats>(&sql) 155 .bind(user_id) // $1 156 .bind(count) // $2 157 .bind(offset); // $3 158 if list_type == "byGenre" { 159 q = q.bind(genre.unwrap_or("")); // $4 160 } 161 let rows: Vec<AlbumWithStats> = q.fetch_all(pool).await?; 162 163 Ok(rows) 164} 165 166pub async fn search_albums( 167 pool: &Pool<Postgres>, 168 user_id: &str, 169 query: &str, 170 count: i64, 171 offset: i64, 172) -> Result<Vec<AlbumWithStats>, Error> { 173 let pattern = format!("%{}%", query); 174 let rows: Vec<AlbumWithStats> = sqlx::query_as( 175 r#" 176 SELECT 177 albums.xata_id, 178 albums.title, 179 albums.artist, 180 albums.year, 181 albums.album_art, 182 COUNT(DISTINCT album_tracks.track_id) AS song_count, 183 SUM(tracks.duration)::bigint AS total_duration, 184 MIN(user_uploads.uploaded_at)::timestamptz AS created_at, 185 (SELECT aa.artist_id FROM artist_albums aa WHERE aa.album_id = albums.xata_id LIMIT 1) AS artist_id 186 FROM albums 187 JOIN album_tracks ON albums.xata_id = album_tracks.album_id 188 JOIN tracks ON album_tracks.track_id = tracks.xata_id 189 JOIN user_uploads ON tracks.xata_id = user_uploads.track_id 190 WHERE user_uploads.user_id = $1 191 AND LOWER(albums.title) LIKE LOWER($2) 192 GROUP BY albums.xata_id, albums.title, albums.artist, albums.year, albums.album_art 193 ORDER BY albums.title ASC 194 LIMIT $3 OFFSET $4 195 "#, 196 ) 197 .bind(user_id) 198 .bind(&pattern) 199 .bind(count) 200 .bind(offset) 201 .fetch_all(pool) 202 .await?; 203 204 Ok(rows) 205} 206 207/// Fetch albums matching a list of (title, artist) pairs returned by Typesense. 208pub async fn get_albums_by_names( 209 pool: &Pool<Postgres>, 210 user_id: &str, 211 pairs: &[(String, String)], 212) -> Result<Vec<AlbumWithStats>, Error> { 213 if pairs.is_empty() { 214 return Ok(vec![]); 215 } 216 let titles: Vec<&str> = pairs.iter().map(|(t, _)| t.as_str()).collect(); 217 let artists: Vec<&str> = pairs.iter().map(|(_, a)| a.as_str()).collect(); 218 let rows: Vec<AlbumWithStats> = sqlx::query_as( 219 r#" 220 SELECT 221 albums.xata_id, 222 albums.title, 223 albums.artist, 224 albums.year, 225 albums.album_art, 226 COUNT(DISTINCT album_tracks.track_id) AS song_count, 227 SUM(tracks.duration)::bigint AS total_duration, 228 MIN(user_uploads.uploaded_at)::timestamptz AS created_at, 229 (SELECT aa.artist_id FROM artist_albums aa WHERE aa.album_id = albums.xata_id LIMIT 1) AS artist_id 230 FROM albums 231 JOIN album_tracks ON albums.xata_id = album_tracks.album_id 232 JOIN tracks ON album_tracks.track_id = tracks.xata_id 233 JOIN user_uploads ON tracks.xata_id = user_uploads.track_id 234 WHERE user_uploads.user_id = $1 235 AND albums.title = ANY($2) 236 AND albums.artist = ANY($3) 237 GROUP BY albums.xata_id, albums.title, albums.artist, albums.year, albums.album_art 238 ORDER BY albums.title ASC 239 "#, 240 ) 241 .bind(user_id) 242 .bind(&titles[..]) 243 .bind(&artists[..]) 244 .fetch_all(pool) 245 .await?; 246 247 Ok(rows) 248} 249 250pub async fn get_album_art(pool: &Pool<Postgres>, album_id: &str) -> Result<Option<String>, Error> { 251 let row: Option<(Option<String>,)> = 252 sqlx::query_as(r#"SELECT album_art FROM albums WHERE xata_id = $1"#) 253 .bind(album_id) 254 .fetch_optional(pool) 255 .await?; 256 257 Ok(row.and_then(|(art,)| art)) 258}