termusiclib/new_database/
album_ops.rs1use anyhow::{Result, bail};
2use indoc::{formatdoc, indoc};
3use rusqlite::{Connection, OptionalExtension, Row, named_params};
4
5use crate::new_database::{
6 Integer,
7 artist_ops::{ArtistRead, common_row_to_artistread},
8};
9
10pub fn count_all_albums(conn: &Connection) -> Result<Integer> {
12 let count = conn.query_row("SELECT COUNT(id) FROM albums;", [], |v| v.get(0))?;
13
14 Ok(count)
15}
16
17#[cfg(test)]
19pub(super) fn count_all_albums_artist_mapping(conn: &Connection) -> Result<Integer> {
20 let count = conn.query_row("SELECT COUNT(album) FROM albums_artists;", [], |v| v.get(0))?;
21
22 Ok(count)
23}
24
25#[derive(Debug, Clone, PartialEq)]
26pub struct AlbumRead {
27 pub id: Integer,
28
29 pub title: String,
30 pub artist_display: String,
31
32 pub artists: Vec<ArtistRead>,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum RowOrdering {
37 IdAsc,
38 IdDesc,
39}
40
41impl RowOrdering {
42 fn as_sql(self) -> &'static str {
44 match self {
45 RowOrdering::IdAsc => "albums.id ASC",
46 RowOrdering::IdDesc => "albums.id DESC",
47 }
48 }
49}
50
51pub fn get_all_albums(conn: &Connection, order: RowOrdering) -> Result<Vec<AlbumRead>> {
57 let stmt = formatdoc! {"
58 SELECT albums.id as album_id, albums.title, albums.artist_display
59 FROM albums
60 ORDER BY {};
61 ",
62 order.as_sql()
63 };
64 let mut stmt = conn.prepare(&stmt)?;
65
66 let result: Vec<AlbumRead> = stmt
67 .query_map(named_params! {}, |row| {
68 let album_read = common_row_to_album(conn, row);
69
70 Ok(album_read)
71 })?
72 .collect::<Result<Vec<_>, rusqlite::Error>>()?;
73
74 Ok(result)
75}
76
77pub fn get_all_albums_like(
83 conn: &Connection,
84 like: &str,
85 order: RowOrdering,
86) -> Result<Vec<AlbumRead>> {
87 let stmt = formatdoc! {"
88 SELECT albums.id as album_id, albums.title, albums.artist_display
89 FROM albums
90 WHERE albums.title LIKE :like
91 ORDER BY {};
92 ",
93 order.as_sql()
94 };
95 let mut stmt = conn.prepare(&stmt)?;
96
97 let result: Vec<AlbumRead> = stmt
98 .query_map(named_params! {":like": like}, |row| {
99 let album_read = common_row_to_album(conn, row);
100
101 Ok(album_read)
102 })?
103 .collect::<Result<Vec<_>, rusqlite::Error>>()?;
104
105 Ok(result)
106}
107
108pub fn get_all_artists_for_album(conn: &Connection, album_id: Integer) -> Result<Vec<ArtistRead>> {
115 let mut stmt = conn.prepare(indoc! {"
116 SELECT artists.id AS artist_id, artists.artist FROM artists
117 INNER JOIN albums_artists ON albums_artists.album=(:album_id)
118 WHERE artists.id=albums_artists.artist;
119 "})?;
120
121 let result: Vec<ArtistRead> = stmt
122 .query_map(named_params! {":album_id": album_id}, |row| {
123 let artist_read = common_row_to_artistread(row);
124
125 Ok(artist_read)
126 })?
127 .collect::<Result<Vec<_>, rusqlite::Error>>()?;
128
129 Ok(result)
130}
131
132pub fn album_exists(conn: &Connection, album: &str) -> Result<bool> {
138 if album.trim().is_empty() {
139 bail!("Given album is empty!");
140 }
141
142 let mut stmt = conn.prepare(indoc! {"
143 SELECT albums.id
144 FROM albums
145 WHERE albums.title=:album_name;
146 "})?;
147
148 let exists = stmt.exists(named_params! {":album_name": album})?;
149
150 Ok(exists)
151}
152
153fn common_row_to_album(conn: &Connection, row: &Row<'_>) -> AlbumRead {
157 let id = row.get("album_id").unwrap();
158 let title = row.get("title").unwrap();
159 let artist_display = row.get("artist_display").unwrap();
160
161 let artists = match get_all_artists_for_album(conn, id) {
162 Ok(v) => v,
163 Err(err) => {
164 warn!("Error resolving artists for a album: {err:#?}");
165 Vec::new()
166 }
167 };
168
169 AlbumRead {
170 id,
171 title,
172 artist_display,
173 artists,
174 }
175}
176
177pub fn delete_albums_artist_mapping_for(conn: &Connection, album_id: Integer) -> Result<usize> {
185 let mut stmt = conn.prepare_cached(indoc! {"
186 DELETE FROM albums_artists
187 WHERE albums_artists.album=:album_id;
188 "})?;
189
190 let affected = stmt
191 .execute(named_params! {":album_id": album_id})
192 .optional()?
193 .unwrap_or_default();
194
195 Ok(affected)
196}
197
198pub fn delete_all_unreferenced_albums(conn: &Connection) -> Result<usize> {
206 let mut stmt = conn.prepare_cached(indoc! {"
208 DELETE FROM albums
209 WHERE albums.id NOT IN (
210 SELECT tracks.album FROM tracks
211 WHERE tracks.album IS NOT NULL
212 );
213 "})?;
214
215 let affected = stmt
216 .execute(named_params! {})
217 .optional()?
218 .unwrap_or_default();
219
220 Ok(affected)
221}
222
223#[cfg(test)]
224mod tests {
225 use std::{path::Path, time::Duration};
226
227 use either::Either;
228
229 use crate::{
230 new_database::{
231 album_insert::AlbumInsertable,
232 album_ops::{
233 AlbumRead, RowOrdering, album_exists, count_all_albums,
234 count_all_albums_artist_mapping, delete_albums_artist_mapping_for,
235 delete_all_unreferenced_albums, get_all_albums, get_all_albums_like,
236 get_all_artists_for_album,
237 },
238 artist_insert::ArtistInsertable,
239 artist_ops::ArtistRead,
240 test_utils::{gen_database, test_path},
241 track_insert::TrackInsertable,
242 },
243 track::TrackMetadata,
244 };
245
246 #[test]
247 fn artists_for_album() {
248 let db = gen_database();
249
250 let album = AlbumInsertable {
251 title: "AlbumA",
252 artist_display: "ArtistA",
253 artists: vec![
254 Either::Left(ArtistInsertable { artist: "ArtistA" }.into()),
255 Either::Left(ArtistInsertable { artist: "ArtistB" }.into()),
256 ],
257 };
258 let album_id = album.try_insert_or_update(&db.get_connection()).unwrap();
259
260 let mut all_artists: Vec<String> =
261 get_all_artists_for_album(&db.get_connection(), album_id)
262 .unwrap()
263 .into_iter()
264 .map(|v| v.name)
265 .collect();
266 all_artists.sort();
268
269 assert_eq!(all_artists, &["ArtistA", "ArtistB"]);
270 }
271
272 #[test]
273 fn all_albums() {
274 let db = gen_database();
275
276 let album = AlbumInsertable {
277 title: "AlbumA",
278 artist_display: "ArtistA",
279 artists: vec![Either::Left(ArtistInsertable { artist: "ArtistA" }.into())],
280 };
281 let _album_id = album.try_insert_or_update(&db.get_connection()).unwrap();
282
283 let album = AlbumInsertable {
284 title: "AlbumB",
285 artist_display: "ArtistB",
286 artists: vec![Either::Left(ArtistInsertable { artist: "ArtistB" }.into())],
287 };
288 let _album_id = album.try_insert_or_update(&db.get_connection()).unwrap();
289
290 let all_albums = get_all_albums(&db.get_connection(), RowOrdering::IdAsc).unwrap();
291
292 assert_eq!(
293 all_albums,
294 &[
295 AlbumRead {
296 id: 1,
297 title: "AlbumA".to_string(),
298 artist_display: "ArtistA".to_string(),
299 artists: vec![ArtistRead {
300 id: 1,
301 name: "ArtistA".to_string()
302 }]
303 },
304 AlbumRead {
305 id: 2,
306 title: "AlbumB".to_string(),
307 artist_display: "ArtistB".to_string(),
308 artists: vec![ArtistRead {
309 id: 2,
310 name: "ArtistB".to_string()
311 }]
312 },
313 ]
314 );
315 }
316
317 #[test]
318 fn all_albums_like() {
319 let db = gen_database();
320
321 let album = AlbumInsertable {
322 title: "AlbumA",
323 artist_display: "ArtistA",
324 artists: vec![Either::Left(ArtistInsertable { artist: "ArtistA" }.into())],
325 };
326 let _album_id = album.try_insert_or_update(&db.get_connection()).unwrap();
327
328 let album = AlbumInsertable {
329 title: "AlbumB",
330 artist_display: "ArtistB",
331 artists: vec![Either::Left(ArtistInsertable { artist: "ArtistB" }.into())],
332 };
333 let _album_id = album.try_insert_or_update(&db.get_connection()).unwrap();
334
335 let all_albums =
336 get_all_albums_like(&db.get_connection(), "%albuma%", RowOrdering::IdAsc).unwrap();
337
338 assert_eq!(
339 all_albums,
340 &[AlbumRead {
341 id: 1,
342 title: "AlbumA".to_string(),
343 artist_display: "ArtistA".to_string(),
344 artists: vec![ArtistRead {
345 id: 1,
346 name: "ArtistA".to_string()
347 }]
348 },]
349 );
350 }
351
352 #[test]
353 fn exists() {
354 let db = gen_database();
355
356 let album = AlbumInsertable {
357 title: "AlbumA",
358 artist_display: "ArtistA",
359 artists: vec![Either::Left(ArtistInsertable { artist: "ArtistA" }.into())],
360 };
361 let _album_id = album.try_insert_or_update(&db.get_connection()).unwrap();
362
363 let res = album_exists(&db.get_connection(), "AlbumA").unwrap();
364
365 assert!(res);
366
367 let res = album_exists(&db.get_connection(), "AlbumB").unwrap();
368
369 assert!(!res);
370 }
371
372 #[test]
373 fn delete_albums_artists_mapping() {
374 let db = gen_database();
375
376 let album = AlbumInsertable {
377 title: "AlbumA",
378 artist_display: "ArtistA feat. ArtistB",
379 artists: vec![
380 Either::Left(ArtistInsertable { artist: "ArtistA" }.into()),
381 Either::Left(ArtistInsertable { artist: "ArtistB" }.into()),
382 ],
383 };
384 let album_id_a = album.try_insert_or_update(&db.get_connection()).unwrap();
385
386 let album = AlbumInsertable {
387 title: "AlbumB",
388 artist_display: "ArtistA feat. ArtistB",
389 artists: vec![
390 Either::Left(ArtistInsertable { artist: "ArtistA" }.into()),
391 Either::Left(ArtistInsertable { artist: "ArtistB" }.into()),
392 ],
393 };
394 let _album_id = album.try_insert_or_update(&db.get_connection()).unwrap();
395
396 let mapping_counts = count_all_albums_artist_mapping(&db.get_connection()).unwrap();
397
398 assert_eq!(mapping_counts, 4);
399
400 let affected = delete_albums_artist_mapping_for(&db.get_connection(), album_id_a).unwrap();
401
402 assert_eq!(affected, 2);
403
404 let mapping_counts = count_all_albums_artist_mapping(&db.get_connection()).unwrap();
405
406 assert_eq!(mapping_counts, 2);
407 }
408
409 #[test]
410 fn delete_unreferenced_albums() {
411 let db = gen_database();
412
413 let metadata = TrackMetadata {
414 album: Some("AlbumA".to_string()),
415 album_artist: Some("ArtistA".to_string()),
416 album_artists: Some(vec!["ArtistA".to_string()]),
417 artist: Some("ArtistA".to_string()),
418 artists: Some(vec!["ArtistA".to_string()]),
419 title: Some("FileA1".to_string()),
420 duration: Some(Duration::from_secs(10)),
421 ..Default::default()
422 };
423 let path = &test_path(Path::new("/somewhere/fileA1.ext"));
424 let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
425 let _ = insertable
426 .try_insert_or_update(&db.get_connection())
427 .unwrap();
428
429 let metadata = TrackMetadata {
430 album: Some("AlbumB".to_string()),
431 album_artist: Some("ArtistA".to_string()),
432 album_artists: Some(vec!["ArtistA".to_string()]),
433 artist: Some("ArtistA".to_string()),
434 artists: Some(vec!["ArtistA".to_string()]),
435 title: Some("FileB1".to_string()),
436 duration: Some(Duration::from_secs(10)),
437 ..Default::default()
438 };
439 let path = &test_path(Path::new("/somewhere/fileB1.ext"));
440 let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
441 let _ = insertable
442 .try_insert_or_update(&db.get_connection())
443 .unwrap();
444
445 let albums = count_all_albums(&db.get_connection()).unwrap();
446
447 assert_eq!(albums, 2);
448
449 let affected = delete_all_unreferenced_albums(&db.get_connection()).unwrap();
450
451 assert_eq!(affected, 0);
452
453 let metadata = TrackMetadata {
454 album: Some("AlbumA".to_string()),
455 album_artist: Some("ArtistA".to_string()),
456 album_artists: Some(vec!["ArtistA".to_string()]),
457 artist: Some("ArtistA".to_string()),
458 artists: Some(vec!["ArtistA".to_string()]),
459 title: Some("FileB1".to_string()),
460 duration: Some(Duration::from_secs(10)),
461 ..Default::default()
462 };
463 let path = &test_path(Path::new("/somewhere/fileB1.ext"));
464 let insertable = TrackInsertable::try_from_track(path, &metadata).unwrap();
465 let _ = insertable
466 .try_insert_or_update(&db.get_connection())
467 .unwrap();
468
469 let albums = count_all_albums(&db.get_connection()).unwrap();
470
471 assert_eq!(albums, 2);
472
473 let affected = delete_all_unreferenced_albums(&db.get_connection()).unwrap();
474
475 assert_eq!(affected, 1);
476
477 let albums = count_all_albums(&db.get_connection()).unwrap();
478
479 assert_eq!(albums, 1);
480 }
481}