termusiclib/new_database/
artist_ops.rs1use anyhow::{Result, bail};
2use indoc::{formatdoc, indoc};
3use rusqlite::{Connection, OptionalExtension, Row, named_params};
4
5use crate::new_database::Integer;
6
7pub fn count_all_artists(conn: &Connection) -> Result<Integer> {
9 let count = conn.query_row("SELECT COUNT(id) FROM artists;", [], |v| v.get(0))?;
10
11 Ok(count)
12}
13
14#[derive(Debug, Clone, PartialEq)]
16pub struct ArtistRead {
17 pub id: Integer,
18
19 pub name: String,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum RowOrdering {
24 IdAsc,
25 IdDesc,
26 AddedAsc,
27 AddedDesc,
28}
29
30impl RowOrdering {
31 fn as_sql(self) -> &'static str {
33 match self {
34 RowOrdering::IdAsc => "artists.id ASC",
35 RowOrdering::IdDesc => "artists.id DESC",
36 RowOrdering::AddedAsc => "artists.added_at ASC",
37 RowOrdering::AddedDesc => "artists.added_at DESC",
38 }
39 }
40}
41
42pub fn get_all_artists(conn: &Connection, order: RowOrdering) -> Result<Vec<ArtistRead>> {
48 let stmt = formatdoc! {"
49 SELECT artists.id AS artist_id, artists.artist
50 FROM artists
51 ORDER BY {};
52 ",
53 order.as_sql()
54 };
55 let mut stmt = conn.prepare(&stmt)?;
56
57 let result: Vec<ArtistRead> = stmt
58 .query_map(named_params! {}, |row| {
59 let artist_read = common_row_to_artistread(row);
60
61 Ok(artist_read)
62 })?
63 .collect::<Result<Vec<_>, rusqlite::Error>>()?;
64
65 Ok(result)
66}
67
68pub fn get_all_artists_like(
74 conn: &Connection,
75 like: &str,
76 order: RowOrdering,
77) -> Result<Vec<ArtistRead>> {
78 let stmt = formatdoc! {"
79 SELECT artists.id AS artist_id, artists.artist
80 FROM artists
81 WHERE artists.artist LIKE :like
82 ORDER BY {};
83 ",
84 order.as_sql()
85 };
86 let mut stmt = conn.prepare(&stmt)?;
87
88 let result: Vec<ArtistRead> = stmt
89 .query_map(named_params! {":like": like}, |row| {
90 let artist_read = common_row_to_artistread(row);
91
92 Ok(artist_read)
93 })?
94 .collect::<Result<Vec<_>, rusqlite::Error>>()?;
95
96 Ok(result)
97}
98
99pub fn get_artist(conn: &Connection, artist: &str) -> Result<ArtistRead> {
105 if artist.trim().is_empty() {
106 bail!("Given artist is empty!");
107 }
108
109 let mut stmt = conn.prepare(indoc! {"
110 SELECT artists.id AS artist_id, artists.artist
111 FROM artists
112 WHERE artists.artist=:artist_name;
113 "})?;
114
115 let result: ArtistRead = stmt.query_row(named_params! {":artist_name": artist}, |row| {
116 let artist_read = common_row_to_artistread(row);
117
118 Ok(artist_read)
119 })?;
120
121 Ok(result)
122}
123
124pub fn artist_exists(conn: &Connection, artist: &str) -> Result<bool> {
130 if artist.trim().is_empty() {
131 bail!("Given artist is empty!");
132 }
133
134 let mut stmt = conn.prepare(indoc! {"
135 SELECT artists.id
136 FROM artists
137 WHERE artists.artist=:artist_name;
138 "})?;
139
140 let exists = stmt.exists(named_params! {":artist_name": artist})?;
141
142 Ok(exists)
143}
144
145pub(super) fn common_row_to_artistread(row: &Row<'_>) -> ArtistRead {
149 let id = row.get("artist_id").unwrap();
150 let artist_title = row.get("artist").unwrap();
151
152 ArtistRead {
153 id,
154 name: artist_title,
155 }
156}
157
158pub fn delete_all_unreferenced_artists(conn: &Connection) -> Result<usize> {
166 let mut stmt = conn.prepare_cached(indoc! {"
168 DELETE FROM artists
169 WHERE artists.id NOT IN (
170 SELECT tracks_artists.artist FROM tracks_artists
171 UNION
172 SELECT albums_artists.artist FROM albums_artists
173 );
174 "})?;
175
176 let affected = stmt
177 .execute(named_params! {})
178 .optional()?
179 .unwrap_or_default();
180
181 Ok(affected)
182}
183
184#[cfg(test)]
185mod tests {
186 use std::{path::Path, time::Duration};
187
188 use pretty_assertions::assert_eq;
189
190 use crate::{
191 new_database::{
192 album_ops::delete_all_unreferenced_albums,
193 artist_insert::ArtistInsertable,
194 artist_ops::{
195 RowOrdering, artist_exists, count_all_artists, delete_all_unreferenced_artists,
196 get_all_artists, get_all_artists_like, get_artist,
197 },
198 test_utils::{gen_database, test_path},
199 track_insert::TrackInsertable,
200 },
201 track::TrackMetadata,
202 };
203
204 #[test]
205 fn all_artists() {
206 let db = gen_database();
207
208 let artist = ArtistInsertable { artist: "ArtistA" };
209 let _artist_id = artist.try_insert_or_update(&db.get_connection()).unwrap();
210
211 let artist = ArtistInsertable { artist: "ArtistB" };
212 let _artist_id = artist.try_insert_or_update(&db.get_connection()).unwrap();
213
214 let artists: Vec<String> = get_all_artists(&db.get_connection(), RowOrdering::IdAsc)
215 .unwrap()
216 .into_iter()
217 .map(|v| v.name)
218 .collect();
219
220 assert_eq!(artists, &["ArtistA", "ArtistB"]);
221 }
222
223 #[test]
224 fn all_artists_like() {
225 let db = gen_database();
226
227 let artist = ArtistInsertable { artist: "ArtistA" };
228 let _artist_id = artist.try_insert_or_update(&db.get_connection()).unwrap();
229
230 let artist = ArtistInsertable { artist: "ArtistB" };
231 let _artist_id = artist.try_insert_or_update(&db.get_connection()).unwrap();
232
233 let artists: Vec<String> =
234 get_all_artists_like(&db.get_connection(), "%artista%", RowOrdering::IdAsc)
235 .unwrap()
236 .into_iter()
237 .map(|v| v.name)
238 .collect();
239
240 assert_eq!(artists, &["ArtistA"]);
241 }
242
243 #[test]
244 fn single_artist() {
245 let db = gen_database();
246
247 let artist = ArtistInsertable { artist: "ArtistA" };
248 let _artist_id = artist.try_insert_or_update(&db.get_connection()).unwrap();
249
250 let artist = ArtistInsertable { artist: "ArtistB" };
251 let _artist_id = artist.try_insert_or_update(&db.get_connection()).unwrap();
252
253 let artist_a = get_artist(&db.get_connection(), "ArtistA").unwrap();
254 assert_eq!(artist_a.name, "ArtistA");
255
256 let artist_b = get_artist(&db.get_connection(), "ArtistB").unwrap();
257 assert_eq!(artist_b.name, "ArtistB");
258
259 let err = get_artist(&db.get_connection(), "ArtistC").unwrap_err();
260 let err = err.downcast::<rusqlite::Error>().unwrap();
261 assert_eq!(err, rusqlite::Error::QueryReturnedNoRows);
262 }
263
264 #[test]
265 fn exists() {
266 let db = gen_database();
267
268 let artist = ArtistInsertable { artist: "ArtistA" };
269 let _artist_id = artist.try_insert_or_update(&db.get_connection()).unwrap();
270
271 let res = artist_exists(&db.get_connection(), "ArtistA").unwrap();
272
273 assert!(res);
274
275 let res = artist_exists(&db.get_connection(), "ArtistB").unwrap();
276
277 assert!(!res);
278 }
279
280 #[test]
281 fn delete_unreferenced_albums() {
282 let db = gen_database();
283
284 let metadata = TrackMetadata {
285 album: Some("AlbumA".to_string()),
286 album_artist: Some("ArtistA".to_string()),
287 album_artists: Some(vec!["ArtistA".to_string()]),
288 artist: Some("ArtistA".to_string()),
289 artists: Some(vec!["ArtistA".to_string()]),
290 title: Some("FileA1".to_string()),
291 duration: Some(Duration::from_secs(10)),
292 ..Default::default()
293 };
294 let path = &test_path(Path::new("/somewhere/fileA1.ext"));
295 let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
296 let _ = insertable
297 .try_insert_or_update(&db.get_connection())
298 .unwrap();
299
300 let metadata = TrackMetadata {
301 album: Some("AlbumB".to_string()),
302 album_artist: Some("ArtistB".to_string()),
303 album_artists: Some(vec!["ArtistB".to_string()]),
304 artist: Some("ArtistB".to_string()),
305 artists: Some(vec!["ArtistB".to_string()]),
306 title: Some("FileB1".to_string()),
307 duration: Some(Duration::from_secs(10)),
308 ..Default::default()
309 };
310 let path = &test_path(Path::new("/somewhere/fileB1.ext"));
311 let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
312 let _ = insertable
313 .try_insert_or_update(&db.get_connection())
314 .unwrap();
315
316 let artists = count_all_artists(&db.get_connection()).unwrap();
317
318 assert_eq!(artists, 2);
319
320 let affected = delete_all_unreferenced_artists(&db.get_connection()).unwrap();
321
322 assert_eq!(affected, 0);
323
324 let metadata = TrackMetadata {
325 album: Some("AlbumA".to_string()),
326 album_artist: Some("ArtistA".to_string()),
327 album_artists: Some(vec!["ArtistA".to_string()]),
328 artist: Some("ArtistA".to_string()),
329 artists: Some(vec!["ArtistA".to_string()]),
330 title: Some("FileB1".to_string()),
331 duration: Some(Duration::from_secs(10)),
332 ..Default::default()
333 };
334 let path = &test_path(Path::new("/somewhere/fileB1.ext"));
335 let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
336 let _ = insertable
337 .try_insert_or_update(&db.get_connection())
338 .unwrap();
339
340 let artists = count_all_artists(&db.get_connection()).unwrap();
341
342 assert_eq!(artists, 2);
343
344 let affected = delete_all_unreferenced_artists(&db.get_connection()).unwrap();
345
346 assert_eq!(affected, 0);
347
348 let _ = delete_all_unreferenced_albums(&db.get_connection()).unwrap();
351
352 let affected = delete_all_unreferenced_artists(&db.get_connection()).unwrap();
353
354 assert_eq!(affected, 1);
355
356 let artists = count_all_artists(&db.get_connection()).unwrap();
357
358 assert_eq!(artists, 1);
359 }
360}