1use rusqlite::{Connection, OptionalExtension, params};
2
3use crate::db::connection::DbError;
4
5use super::ArtistRow;
6
7pub(super) fn escape_like(s: &str) -> String {
9 s.replace('\\', "\\\\")
10 .replace('%', "\\%")
11 .replace('_', "\\_")
12}
13
14pub fn get_or_create_artist(
20 conn: &Connection,
21 name: &str,
22 remote_id: Option<&str>,
23) -> Result<i64, DbError> {
24 let key = super::sources::fold(name);
25 let existing: Option<(i64, Option<String>)> = conn
26 .prepare_cached("SELECT id, remote_id FROM artists WHERE name_key = ?1")?
27 .query_row(params![key], |row| Ok((row.get(0)?, row.get(1)?)))
28 .optional()?;
29
30 if let Some((id, stored)) = existing {
31 if let Some(rid) = remote_id {
34 if stored.as_deref() != Some(rid) {
35 conn.prepare_cached("UPDATE artists SET remote_id = ?1 WHERE id = ?2")?
36 .execute(params![rid, id])?;
37 }
38 super::adopt_uid(conn, super::UidKind::Artist, id, rid)?;
39 }
40 return Ok(id);
41 }
42
43 let uid = super::free_uid(conn, super::UidKind::Artist, remote_id)?;
44 conn.prepare_cached(
45 "INSERT INTO artists (name, name_key, remote_id, uid) VALUES (?1, ?2, ?3, ?4)",
46 )?
47 .execute(params![name, key, remote_id, uid])?;
48 let id = conn.last_insert_rowid();
49 if let Some(rid) = remote_id {
50 super::adopt_uid(conn, super::UidKind::Artist, id, rid)?;
51 }
52 Ok(id)
53}
54
55#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
57pub enum ArtistOrder {
58 #[default]
60 Name,
61 AlbumCount,
63 RecentlyAdded,
65 Id,
67 LastPlayed,
70 Relevance,
74}
75
76impl ArtistOrder {
77 fn clause(self) -> &'static str {
78 match self {
79 Self::Name | Self::Relevance => "a.name COLLATE LIBRARY",
80 Self::AlbumCount => "COUNT(DISTINCT al.id) DESC, a.name COLLATE LIBRARY",
81 Self::RecentlyAdded => "COALESCE(MAX(al.added_at), '') DESC, a.name COLLATE LIBRARY",
82 Self::Id => "a.id",
83 Self::LastPlayed => "MAX(p.last) DESC, MAX(p.last_id) DESC",
84 }
85 }
86}
87
88#[derive(Debug, Clone, Copy, Default)]
91pub struct ArtistQuery<'a> {
92 pub ids: Option<&'a [i64]>,
94 pub search: Option<&'a str>,
97 pub favourites_of: Option<i64>,
99 pub played: Option<super::history::PlayedSince>,
101 pub filter: super::albums::AlbumFilter<'a>,
104 pub order: ArtistOrder,
105 pub without_track_counts: bool,
108 pub limit: Option<u32>,
110 pub offset: u32,
111}
112
113pub fn list_artists(conn: &Connection, q: &ArtistQuery) -> Result<Vec<ArtistRow>, DbError> {
116 if let Some(ids) = fuzzy_fallback(conn, q)? {
117 return list_artists(
118 conn,
119 &ArtistQuery {
120 search: None,
121 ids: Some(&ids),
122 ..*q
123 },
124 );
125 }
126 let columns = if q.without_track_counts {
127 "a.id, a.name, a.sort_name, a.remote_id, COUNT(al.id), 0"
128 } else {
129 "a.id, a.name, a.sort_name, a.remote_id, COUNT(DISTINCT al.id), COUNT(t.id)"
130 };
131 let (body, mut params) = artist_body(conn, q)?;
132 let order = match q.order {
133 ArtistOrder::LastPlayed if q.played.is_none() => ArtistOrder::Name,
134 order => order,
135 };
136 let order_by = match (order, q.search, q.ids) {
137 (ArtistOrder::Relevance, Some(query), _) => {
138 params.extend(
139 super::search::match_rank_binds(query)
140 .map(|b| Box::new(b) as Box<dyn rusqlite::ToSql>),
141 );
142 format!(
143 "{}, a.name COLLATE LIBRARY",
144 super::search::match_rank("a.name")
145 )
146 }
147 (ArtistOrder::Relevance, None, Some(_)) => "MIN(r.key)".into(),
148 (order, ..) => order.clause().into(),
149 };
150 let mut sql = format!("SELECT {columns} {body} ORDER BY {order_by}");
151 if let Some(limit) = q.limit {
152 params.push(Box::new(limit as i64));
153 params.push(Box::new(q.offset as i64));
154 sql.push_str(" LIMIT ? OFFSET ?");
155 }
156
157 let mut stmt = conn.prepare(&sql)?;
158 let rows = stmt
159 .query_map(rusqlite::params_from_iter(params.iter()), artist_row)?
160 .collect::<Result<Vec<_>, _>>()?;
161 Ok(rows)
162}
163
164pub fn count_artists(conn: &Connection, q: &ArtistQuery) -> Result<u64, DbError> {
166 if let Some(ids) = fuzzy_fallback(conn, q)? {
167 return count_artists(
168 conn,
169 &ArtistQuery {
170 search: None,
171 ids: Some(&ids),
172 ..*q
173 },
174 );
175 }
176 let (body, params) = artist_body(conn, q)?;
177 let n: i64 = conn.query_row(
178 &format!("SELECT COUNT(*) FROM (SELECT a.id {body})"),
179 rusqlite::params_from_iter(params.iter()),
180 |r| r.get(0),
181 )?;
182 Ok(n as u64)
183}
184
185fn ranked_by_ids(q: &ArtistQuery) -> bool {
187 q.order == ArtistOrder::Relevance && q.search.is_none()
188}
189
190fn fuzzy_fallback(conn: &Connection, q: &ArtistQuery) -> Result<Option<Vec<i64>>, DbError> {
193 let Some(query) = q.search else {
194 return Ok(None);
195 };
196 let (body, params) = artist_body(conn, q)?;
197 let found: bool = conn.query_row(
198 &format!("SELECT EXISTS (SELECT 1 {body})"),
199 rusqlite::params_from_iter(params.iter()),
200 |r| r.get(0),
201 )?;
202 if found {
203 return Ok(None);
204 }
205 let mut ids = super::search::fuzzy_ids(conn, super::search::CorpusKind::Artist, query)?;
206 if let Some(only) = q.ids {
207 ids.retain(|id| only.contains(id));
208 }
209 Ok(Some(ids))
210}
211
212fn artist_body(
215 conn: &Connection,
216 q: &ArtistQuery,
217) -> Result<(String, Vec<Box<dyn rusqlite::ToSql>>), DbError> {
218 let mut sql = String::from(if q.without_track_counts {
219 "FROM artists a
220 INNER JOIN albums al ON al.artist_id = a.id"
221 } else {
222 "FROM artists a
223 INNER JOIN albums al ON al.artist_id = a.id
224 LEFT JOIN tracks t ON t.album_id = al.id"
225 });
226 let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
227 if let Some(user) = q.favourites_of {
228 params.push(Box::new(super::auth::resolve_user(conn, user)?));
229 sql.push_str(" JOIN favourite_artists f ON f.artist_id = a.id AND f.user_id = ?");
230 }
231 if let Some(played) = q.played {
232 params.push(Box::new(super::auth::resolve_user(conn, played.user)?));
233 params.push(Box::new(played.since));
234 sql.push_str(
235 " JOIN (SELECT pal.artist_id AS id, MAX(h.played_at) AS last, MAX(h.id) AS last_id
236 FROM play_history h
237 JOIN tracks pt ON pt.id = h.track_id
238 JOIN albums pal ON pal.id = pt.album_id
239 WHERE h.user_id = ? AND h.played_at >= ?
240 GROUP BY pal.artist_id) p ON p.id = a.id",
241 );
242 }
243 let mut wheres: Vec<String> = Vec::new();
244 match q.ids {
245 Some(ids) if ranked_by_ids(q) => {
248 params.push(Box::new(super::json_list(ids)));
249 sql.push_str(" JOIN json_each(?) r ON r.value = a.id");
250 }
251 Some(ids) => {
252 params.push(Box::new(super::json_list(ids)));
253 wheres.push("a.id IN (SELECT value FROM json_each(?))".into());
254 }
255 None => {}
256 }
257 if let Some(query) = q.search {
258 params.push(Box::new(format!("%{}%", escape_like(query))));
259 wheres.push("a.name LIKE ? COLLATE NOCASE ESCAPE '\\'".into());
260 }
261 q.filter.push(&mut wheres, &mut params);
262 if !wheres.is_empty() {
263 sql.push_str(" WHERE ");
264 sql.push_str(&wheres.join(" AND "));
265 }
266 sql.push_str(" GROUP BY a.id");
267 Ok((sql, params))
268}
269
270fn artist_row(row: &rusqlite::Row) -> rusqlite::Result<ArtistRow> {
271 Ok(ArtistRow {
272 id: row.get(0)?,
273 name: row.get(1)?,
274 sort_name: row.get(2)?,
275 remote_id: row.get(3)?,
276 album_count: row.get(4)?,
277 track_count: row.get(5)?,
278 })
279}
280
281pub fn get_artist(conn: &Connection, artist_id: i64) -> Result<Option<ArtistRow>, DbError> {
285 Ok(conn
286 .query_row(
287 "SELECT a.id, a.name, a.sort_name, a.remote_id,
288 (SELECT COUNT(*) FROM albums WHERE artist_id = a.id),
289 (SELECT COUNT(*) FROM tracks
290 WHERE artist_id = a.id
291 OR album_id IN (SELECT id FROM albums WHERE artist_id = a.id))
292 FROM artists a
293 WHERE a.id = ?1",
294 params![artist_id],
295 artist_row,
296 )
297 .ok())
298}
299
300pub fn find_artists(conn: &Connection, query: &str) -> Result<Vec<ArtistRow>, DbError> {
302 list_artists(
303 conn,
304 &ArtistQuery {
305 search: Some(query),
306 ..Default::default()
307 },
308 )
309}
310
311pub fn all_artists(conn: &Connection) -> Result<Vec<ArtistRow>, DbError> {
313 list_artists(conn, &ArtistQuery::default())
314}
315
316pub fn enrich_remote_artist(
322 conn: &Connection,
323 remote_id: &str,
324 mbid: Option<&str>,
325 sort_name: Option<&str>,
326) -> Result<(), DbError> {
327 conn.execute(
328 "UPDATE artists SET
329 mbid = COALESCE(mbid, ?2),
330 sort_name = COALESCE(sort_name, ?3)
331 WHERE remote_id = ?1",
332 params![remote_id, mbid, sort_name],
333 )?;
334 Ok(())
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340 use crate::db::connection::Database;
341
342 fn test_db() -> Database {
343 let conn = rusqlite::Connection::open_in_memory().unwrap();
344 conn.pragma_update(None, "foreign_keys", "on").unwrap();
345 crate::db::schema::create_tables(&conn).unwrap();
346 Database { conn }
347 }
348
349 fn stocked_db() -> Database {
352 use crate::db::queries::{sample_meta, upsert_track};
353 let db = test_db();
354 for (i, artist) in ["Autechre", "Boards of Canada", "Coil", "Dopplereffekt"]
355 .iter()
356 .enumerate()
357 {
358 let mut m = sample_meta("t", artist, "Album");
359 m.path = Some(format!("/music/{i}/t.flac"));
360 upsert_track(&db.conn, &m).unwrap();
361 }
362 db
363 }
364
365 #[test]
366 fn paging_walks_the_listing_without_repeating() {
367 let db = stocked_db();
368 let page = |offset| {
369 list_artists(
370 &db.conn,
371 &ArtistQuery {
372 limit: Some(2),
373 offset,
374 ..Default::default()
375 },
376 )
377 .unwrap()
378 .into_iter()
379 .map(|a| a.name)
380 .collect::<Vec<_>>()
381 };
382 assert_eq!(page(0), ["Autechre", "Boards of Canada"]);
383 assert_eq!(page(2), ["Coil", "Dopplereffekt"]);
384 assert!(page(4).is_empty());
385 }
386
387 #[test]
388 fn favourites_only_lists_what_was_hearted() {
389 use crate::db::queries::toggle_favourite_artist;
390 let db = stocked_db();
391 let coil: i64 = db
392 .conn
393 .query_row("SELECT id FROM artists WHERE name = 'Coil'", [], |r| {
394 r.get(0)
395 })
396 .unwrap();
397 toggle_favourite_artist(&db.conn, crate::db::queries::LOCAL_USER, coil).unwrap();
398 let rows = list_artists(
399 &db.conn,
400 &ArtistQuery {
401 favourites_of: Some(crate::db::queries::LOCAL_USER),
402 ..Default::default()
403 },
404 )
405 .unwrap();
406 assert_eq!(
407 rows.iter().map(|a| a.name.as_str()).collect::<Vec<_>>(),
408 ["Coil"]
409 );
410 }
411
412 #[test]
413 fn spellings_that_differ_only_in_case_are_one_artist() {
414 let db = stocked_db();
415 let a = get_or_create_artist(&db.conn, "The Squire of Gothos", None).unwrap();
416 let b = get_or_create_artist(&db.conn, "The Squire Of Gothos", None).unwrap();
417 assert_eq!(a, b);
418 }
419
420 #[test]
421 fn an_artist_with_tracks_but_no_albums_is_still_an_artist() {
422 let db = stocked_db();
423 let guest = get_or_create_artist(&db.conn, "A Guest", None).unwrap();
424 let album: i64 = db
425 .conn
426 .query_row("SELECT id FROM albums LIMIT 1", [], |r| r.get(0))
427 .unwrap();
428 db.conn
429 .execute(
430 "INSERT INTO tracks (title, album_id, artist_id, path) VALUES ('Feature', ?1, ?2, '/f.flac')",
431 params![album, guest],
432 )
433 .unwrap();
434 let artist = get_artist(&db.conn, guest)
435 .unwrap()
436 .expect("credited on a track");
437 assert_eq!((artist.album_count, artist.track_count), (0, 1));
438 }
439
440 #[test]
441 fn one_artist_carries_its_counts() {
442 let db = stocked_db();
443 let id = find_artists(&db.conn, "Coil").unwrap()[0].id;
444 let artist = get_artist(&db.conn, id)
445 .unwrap()
446 .expect("Coil owns an album");
447 assert_eq!(artist.name, "Coil");
448 assert_eq!(artist.album_count, 1);
449 assert_eq!(artist.track_count, 1);
450 assert!(get_artist(&db.conn, 9999).unwrap().is_none());
451 }
452
453 #[test]
454 fn test_artist_create_and_dedup() {
455 let db = test_db();
456 let id1 = get_or_create_artist(&db.conn, "Aphex Twin", None).unwrap();
457 let id2 = get_or_create_artist(&db.conn, "Aphex Twin", None).unwrap();
458 assert_eq!(id1, id2);
459
460 let id3 = get_or_create_artist(&db.conn, "Squarepusher", None).unwrap();
461 assert_ne!(id1, id3);
462 }
463}