moosicbox_library/db/
mod.rs

1use moosicbox_json_utils::{
2    ParseError, ToValueType,
3    database::{AsModelResultMapped as _, DatabaseFetchError, ToValue as _},
4};
5use moosicbox_music_models::{
6    ApiSource, AudioFormat, PlaybackQuality, TrackApiSource, TrackSize, id::Id,
7};
8use switchy_database::{
9    DatabaseError, DatabaseValue, Row, boxed,
10    profiles::LibraryDatabase,
11    query::{
12        FilterableQuery, SortDirection, coalesce, identifier, literal, select, where_in,
13        where_not_eq,
14    },
15};
16use thiserror::Error;
17
18pub mod models;
19
20use crate::{
21    db::models::LibraryConfig,
22    models::{LibraryAlbum, LibraryArtist, LibraryTrack},
23};
24
25/// # Errors
26///
27/// * If there was a database error
28#[allow(clippy::too_many_arguments)]
29pub async fn create_library_config(
30    db: &LibraryDatabase,
31    client_id: &str,
32    access_token: &str,
33    refresh_token: &str,
34    client_name: &str,
35    expires_in: u32,
36    scope: &str,
37    token_type: &str,
38    user: &str,
39    user_id: u32,
40) -> Result<(), DatabaseError> {
41    db.upsert("library_config")
42        .value("client_id", client_id)
43        .value("access_token", access_token)
44        .value("refresh_token", refresh_token)
45        .value("client_name", client_name)
46        .value("expires_in", expires_in)
47        .value("scope", scope)
48        .value("token_type", token_type)
49        .value("user", user)
50        .value("user_id", user_id)
51        .where_eq("refresh_token", refresh_token)
52        .execute(&**db)
53        .await?;
54
55    Ok(())
56}
57
58/// # Errors
59///
60/// * If there was a database error
61pub async fn delete_library_config(
62    db: &LibraryDatabase,
63    refresh_token: &str,
64) -> Result<(), DatabaseError> {
65    db.delete("library_config")
66        .where_eq("refresh_token", refresh_token)
67        .execute(&**db)
68        .await?;
69
70    Ok(())
71}
72
73#[derive(Debug, Error)]
74pub enum LibraryConfigError {
75    #[error(transparent)]
76    Database(#[from] DatabaseError),
77    #[error(transparent)]
78    Parse(#[from] moosicbox_json_utils::ParseError),
79    #[error("No configs available")]
80    NoConfigsAvailable,
81}
82
83/// # Errors
84///
85/// * If there was a database error
86/// * If there were no configs available
87pub async fn get_library_config(
88    db: &LibraryDatabase,
89) -> Result<Option<LibraryConfig>, LibraryConfigError> {
90    let mut configs = db
91        .select("library_config")
92        .execute(&**db)
93        .await?
94        .to_value_type()?;
95
96    if configs.is_empty() {
97        return Err(LibraryConfigError::NoConfigsAvailable);
98    }
99
100    configs.sort_by(|a: &LibraryConfig, b: &LibraryConfig| a.issued_at.cmp(&b.issued_at));
101
102    Ok(configs.first().cloned())
103}
104
105/// # Errors
106///
107/// * If there was a database error
108pub async fn get_library_access_tokens(
109    db: &LibraryDatabase,
110) -> Result<Option<(String, String)>, LibraryConfigError> {
111    Ok(get_library_config(db)
112        .await?
113        .map(|c| (c.access_token.clone(), c.refresh_token)))
114}
115
116/// # Errors
117///
118/// * If there was a database error
119pub async fn get_library_access_token(
120    db: &LibraryDatabase,
121) -> Result<Option<String>, LibraryConfigError> {
122    Ok(get_library_access_tokens(db).await?.map(|c| c.0))
123}
124
125/// # Errors
126///
127/// * If there was a database error
128pub async fn get_artists(db: &LibraryDatabase) -> Result<Vec<LibraryArtist>, DatabaseFetchError> {
129    Ok(db.select("artists").execute(&**db).await?.to_value_type()?)
130}
131
132/// # Errors
133///
134/// * If there was a database error
135pub async fn get_albums(db: &LibraryDatabase) -> Result<Vec<LibraryAlbum>, DatabaseFetchError> {
136    db.select("albums")
137        .distinct()
138        .columns(&[
139            "albums.*",
140            "albums.id as album_id",
141            "track_sizes.bit_depth",
142            "track_sizes.sample_rate",
143            "track_sizes.channels",
144            "track_sizes.format",
145            "artists.title as artist",
146            "tracks.source",
147            "artists.api_sources as artist_api_sources",
148        ])
149        .left_join("tracks", "tracks.album_id=albums.id")
150        .left_join("track_sizes", "track_sizes.track_id=tracks.id")
151        .join("artists", "artists.id=albums.artist_id")
152        .sort("albums.id", SortDirection::Desc)
153        .where_or(boxed![
154            where_not_eq("track_sizes.format", AudioFormat::Source.as_ref()),
155            where_not_eq("tracks.source", TrackApiSource::Local.to_string())
156        ])
157        .execute(&**db)
158        .await?
159        .as_model_mapped()
160}
161
162/// # Errors
163///
164/// * If there was a database error
165pub async fn get_artist(
166    db: &LibraryDatabase,
167    api_source: &ApiSource,
168    id: &Id,
169) -> Result<Option<LibraryArtist>, DatabaseFetchError> {
170    Ok(if api_source.is_library() {
171        db.select("artists")
172            .where_eq("id", id)
173            .execute_first(&**db)
174            .await?
175            .as_ref()
176            .to_value_type()?
177    } else {
178        db.select("artists")
179            .join(
180                "api_sources",
181                "api_sources.entity_type='artists' AND api_sources.entity_id = artists.id",
182            )
183            .where_eq("api_sources.source", api_source.as_ref())
184            .where_eq("api_sources.source_id", id)
185            .execute_first(&**db)
186            .await?
187            .as_ref()
188            .to_value_type()?
189    })
190}
191
192/// # Errors
193///
194/// * If there was a database error
195pub async fn get_artist_by_album_id(
196    db: &LibraryDatabase,
197    id: u64,
198) -> Result<Option<LibraryArtist>, DatabaseFetchError> {
199    Ok(db
200        .select("artists")
201        .where_eq("albums.id", id)
202        .join("albums", "albums.artist_id = artists.id")
203        .execute_first(&**db)
204        .await?
205        .as_ref()
206        .to_value_type()?)
207}
208
209/// # Errors
210///
211/// * If there was a database error
212pub async fn get_artists_by_album_ids(
213    db: &LibraryDatabase,
214    album_ids: &[u64],
215) -> Result<Vec<LibraryArtist>, DatabaseFetchError> {
216    Ok(db
217        .select("artists")
218        .distinct()
219        .join("albums", "albums.artist_id = artists.id")
220        .where_in("album.id", album_ids.to_vec())
221        .execute(&**db)
222        .await?
223        .to_value_type()?)
224}
225
226/// # Errors
227///
228/// * If there was a database error
229pub async fn get_album_artist(
230    db: &LibraryDatabase,
231    album_id: u64,
232) -> Result<Option<LibraryArtist>, DatabaseFetchError> {
233    Ok(db
234        .select("artists")
235        .join("albums", "albums.artist_id=artists.id")
236        .where_eq("albums.id", album_id)
237        .execute_first(&**db)
238        .await?
239        .map(|x| x.to_value_type())
240        .transpose()?)
241}
242
243/// # Errors
244///
245/// * If there was a database error
246pub async fn get_album(
247    db: &LibraryDatabase,
248    api_source: &ApiSource,
249    id: &Id,
250) -> Result<Option<LibraryAlbum>, DatabaseFetchError> {
251    Ok(if api_source.is_library() {
252        db.select("albums")
253            .columns(&[
254                "albums.*",
255                "artists.title as artist",
256                "artists.api_sources as artist_api_sources",
257            ])
258            .join("artists", "artists.id = albums.artist_id")
259            .where_eq("albums.id", id)
260            .execute_first(&**db)
261            .await?
262            .as_ref()
263            .to_value_type()?
264    } else {
265        db.select("albums")
266            .columns(&[
267                "albums.*",
268                "artists.title as artist",
269                "artists.api_sources as artist_api_sources",
270            ])
271            .join("artists", "artists.id = albums.artist_id")
272            .join(
273                "api_sources",
274                "api_sources.entity_type='albums' AND api_sources.entity_id = albums.id",
275            )
276            .where_eq("api_sources.source", api_source.as_ref())
277            .where_eq("api_sources.source_id", id)
278            .execute_first(&**db)
279            .await?
280            .as_ref()
281            .to_value_type()?
282    })
283}
284
285/// # Errors
286///
287/// * If there was a database error
288pub async fn get_album_tracks(
289    db: &LibraryDatabase,
290    album_id: &Id,
291) -> Result<Vec<LibraryTrack>, DatabaseFetchError> {
292    Ok(db
293        .select("tracks")
294        .columns(&[
295            "tracks.*",
296            "albums.title as album",
297            "albums.blur as blur",
298            "albums.date_released as date_released",
299            "albums.date_added as date_added",
300            "artists.title as artist",
301            "artists.id as artist_id",
302            "albums.artwork",
303            "track_sizes.format",
304            "track_sizes.bytes",
305            "track_sizes.bit_depth",
306            "track_sizes.audio_bitrate",
307            "track_sizes.overall_bitrate",
308            "track_sizes.sample_rate",
309            "track_sizes.channels",
310            "albums.api_sources as album_api_sources",
311            "artists.api_sources as artist_api_sources",
312        ])
313        .where_eq("tracks.album_id", album_id)
314        .join("albums", "albums.id=tracks.album_id")
315        .join("artists", "artists.id=albums.artist_id")
316        .left_join(
317            "track_sizes",
318            "tracks.id=track_sizes.track_id AND track_sizes.format=tracks.format",
319        )
320        .sort("number", SortDirection::Asc)
321        .execute(&**db)
322        .await?
323        .to_value_type()?)
324}
325
326/// # Errors
327///
328/// * If there was a database error
329pub async fn get_artist_albums(
330    db: &LibraryDatabase,
331    artist_id: &Id,
332) -> Result<Vec<LibraryAlbum>, DatabaseFetchError> {
333    db.select("albums")
334        .distinct()
335        .columns(&[
336            "albums.*",
337            "albums.id as album_id",
338            "track_sizes.bit_depth",
339            "track_sizes.sample_rate",
340            "track_sizes.channels",
341            "artists.title as artist",
342            "tracks.format",
343            "tracks.source",
344            "artists.api_sources as artist_api_sources",
345        ])
346        .left_join("tracks", "tracks.album_id=albums.id")
347        .left_join("track_sizes", "track_sizes.track_id=tracks.id")
348        .join("artists", "artists.id=albums.artist_id")
349        .where_eq("albums.artist_id", artist_id)
350        .sort("albums.id", SortDirection::Desc)
351        .execute(&**db)
352        .await?
353        .as_model_mapped()
354}
355
356#[derive(Debug, Clone)]
357pub struct SetTrackSize {
358    pub track_id: u64,
359    pub quality: PlaybackQuality,
360    pub bytes: Option<Option<u64>>,
361    pub bit_depth: Option<Option<u8>>,
362    pub audio_bitrate: Option<Option<u32>>,
363    pub overall_bitrate: Option<Option<u32>>,
364    pub sample_rate: Option<Option<u32>>,
365    pub channels: Option<Option<u8>>,
366}
367
368/// # Errors
369///
370/// * If there was a database error
371pub async fn set_track_size(
372    db: &LibraryDatabase,
373    value: SetTrackSize,
374) -> Result<Option<TrackSize>, DatabaseFetchError> {
375    Ok(set_track_sizes(db, &[value]).await?.first().cloned())
376}
377
378/// # Errors
379///
380/// * If there was a database error
381pub async fn set_track_sizes(
382    db: &LibraryDatabase,
383    values: &[SetTrackSize],
384) -> Result<Vec<TrackSize>, DatabaseFetchError> {
385    let values = values
386        .iter()
387        .map(|v| {
388            let mut values = vec![
389                (
390                    "track_id",
391                    #[allow(clippy::cast_possible_wrap)]
392                    DatabaseValue::Number(v.track_id as i64),
393                ),
394                (
395                    "format",
396                    DatabaseValue::String(v.quality.format.as_ref().to_string()),
397                ),
398            ];
399
400            if let Some(bytes) = v.bytes {
401                values.push((
402                    "bytes",
403                    #[allow(clippy::cast_possible_wrap)]
404                    DatabaseValue::NumberOpt(bytes.map(|x| x as i64)),
405                ));
406            }
407            if let Some(bit_depth) = v.bit_depth {
408                values.push((
409                    "bit_depth",
410                    DatabaseValue::NumberOpt(bit_depth.map(i64::from)),
411                ));
412            }
413            if let Some(audio_bitrate) = v.audio_bitrate {
414                values.push((
415                    "audio_bitrate",
416                    DatabaseValue::NumberOpt(audio_bitrate.map(i64::from)),
417                ));
418            }
419            if let Some(overall_bitrate) = v.overall_bitrate {
420                values.push((
421                    "overall_bitrate",
422                    DatabaseValue::NumberOpt(overall_bitrate.map(i64::from)),
423                ));
424            }
425            if let Some(sample_rate) = v.sample_rate {
426                values.push((
427                    "sample_rate",
428                    DatabaseValue::NumberOpt(sample_rate.map(i64::from)),
429                ));
430            }
431            if let Some(channels) = v.channels {
432                values.push((
433                    "channels",
434                    DatabaseValue::NumberOpt(channels.map(i64::from)),
435                ));
436            }
437
438            values
439        })
440        .collect::<Vec<_>>();
441
442    Ok(db
443        .upsert_multi("track_sizes")
444        .unique(boxed![
445            identifier("track_id"),
446            coalesce(boxed![identifier("format"), literal("''")]),
447            coalesce(boxed![identifier("audio_bitrate"), literal("0")]),
448            coalesce(boxed![identifier("overall_bitrate"), literal("0")]),
449            coalesce(boxed![identifier("bit_depth"), literal("0")]),
450            coalesce(boxed![identifier("sample_rate"), literal("0")]),
451            coalesce(boxed![identifier("channels"), literal("0")]),
452        ])
453        .values(values.clone())
454        .execute(&**db)
455        .await?
456        .to_value_type()?)
457}
458
459/// # Errors
460///
461/// * If there was a database error
462pub async fn get_track_size(
463    db: &LibraryDatabase,
464    id: &Id,
465    quality: &PlaybackQuality,
466) -> Result<Option<u64>, DatabaseFetchError> {
467    Ok(db
468        .select("track_sizes")
469        .columns(&["bytes"])
470        .where_eq("track_id", id.to_string())
471        .where_eq("format", quality.format.as_ref())
472        .execute_first(&**db)
473        .await?
474        .and_then(|x| x.columns.first().cloned())
475        .map(|(_, value)| value)
476        .map(|col| col.to_value_type() as Result<Option<u64>, _>)
477        .transpose()?
478        .flatten())
479}
480
481/// # Errors
482///
483/// * If there was a database error
484pub async fn get_track(
485    db: &LibraryDatabase,
486    id: &Id,
487) -> Result<Option<LibraryTrack>, DatabaseFetchError> {
488    Ok(get_tracks(db, Some(&[id.to_owned()]))
489        .await?
490        .into_iter()
491        .next())
492}
493
494/// # Errors
495///
496/// * If there was a database error
497pub async fn get_tracks(
498    db: &LibraryDatabase,
499    ids: Option<&[Id]>,
500) -> Result<Vec<LibraryTrack>, DatabaseFetchError> {
501    if ids.is_some_and(<[Id]>::is_empty) {
502        return Ok(vec![]);
503    }
504
505    Ok(db
506        .select("tracks")
507        .columns(&[
508            "tracks.*",
509            "albums.title as album",
510            "albums.blur as blur",
511            "albums.date_released as date_released",
512            "albums.date_added as date_added",
513            "artists.title as artist",
514            "artists.id as artist_id",
515            "albums.artwork",
516            "track_sizes.format",
517            "track_sizes.bytes",
518            "track_sizes.bit_depth",
519            "track_sizes.audio_bitrate",
520            "track_sizes.overall_bitrate",
521            "track_sizes.sample_rate",
522            "track_sizes.channels",
523            "albums.api_sources as album_api_sources",
524            "artists.api_sources as artist_api_sources",
525        ])
526        .filter_if_some(ids.map(|ids| where_in("tracks.id", ids.to_vec())))
527        .join("albums", "albums.id=tracks.album_id")
528        .join("artists", "artists.id=albums.artist_id")
529        .left_join(
530            "track_sizes",
531            "tracks.id=track_sizes.track_id AND track_sizes.format=tracks.format",
532        )
533        .execute(&**db)
534        .await?
535        .to_value_type()?)
536}
537
538/// # Errors
539///
540/// * If there was a database error
541pub async fn delete_track(
542    db: &LibraryDatabase,
543    id: u64,
544) -> Result<Option<LibraryTrack>, DatabaseFetchError> {
545    Ok(delete_tracks(db, Some(&vec![id])).await?.into_iter().next())
546}
547
548/// # Errors
549///
550/// * If there was a database error
551pub async fn delete_tracks(
552    db: &LibraryDatabase,
553    ids: Option<&Vec<u64>>,
554) -> Result<Vec<LibraryTrack>, DatabaseFetchError> {
555    if ids.is_some_and(Vec::is_empty) {
556        return Ok(vec![]);
557    }
558
559    Ok(db
560        .delete("tracks")
561        .filter_if_some(ids.map(|ids| where_in("id", ids.clone())))
562        .execute(&**db)
563        .await?
564        .to_value_type()?)
565}
566
567/// # Errors
568///
569/// * If there was a database error
570pub async fn delete_track_size_by_track_id(
571    db: &LibraryDatabase,
572    id: u64,
573) -> Result<Option<TrackSize>, DatabaseFetchError> {
574    Ok(delete_track_sizes_by_track_id(db, Some(&vec![id]))
575        .await?
576        .into_iter()
577        .next())
578}
579
580/// # Errors
581///
582/// * If there was a database error
583pub async fn delete_track_sizes_by_track_id(
584    db: &LibraryDatabase,
585    ids: Option<&Vec<u64>>,
586) -> Result<Vec<TrackSize>, DatabaseFetchError> {
587    if ids.is_some_and(Vec::is_empty) {
588        return Ok(vec![]);
589    }
590
591    Ok(db
592        .delete("track_sizes")
593        .filter_if_some(ids.map(|ids| where_in("track_id", ids.clone())))
594        .execute(&**db)
595        .await?
596        .to_value_type()?)
597}
598
599/// # Errors
600///
601/// * If there was a database error
602pub async fn add_artist_and_get_artist(
603    db: &LibraryDatabase,
604    artist: LibraryArtist,
605) -> Result<LibraryArtist, DatabaseFetchError> {
606    Ok(add_artists_and_get_artists(db, vec![artist]).await?[0].clone())
607}
608
609/// # Errors
610///
611/// * If there was a database error
612pub async fn add_artist_map_and_get_artist(
613    db: &LibraryDatabase,
614    artist: Vec<(&str, DatabaseValue)>,
615) -> Result<LibraryArtist, DatabaseFetchError> {
616    Ok(add_artist_maps_and_get_artists(db, vec![artist]).await?[0].clone())
617}
618
619/// # Errors
620///
621/// * If there was a database error
622pub async fn add_artists_and_get_artists(
623    db: &LibraryDatabase,
624    artists: Vec<LibraryArtist>,
625) -> Result<Vec<LibraryArtist>, DatabaseFetchError> {
626    add_artist_maps_and_get_artists(
627        db,
628        artists
629            .into_iter()
630            .map(|artist| {
631                vec![
632                    ("title", DatabaseValue::String(artist.title)),
633                    ("cover", DatabaseValue::StringOpt(artist.cover)),
634                ]
635            })
636            .collect(),
637    )
638    .await
639}
640
641/// # Errors
642///
643/// * If there was a database error
644pub async fn add_artist_maps_and_get_artists(
645    db: &LibraryDatabase,
646    artists: Vec<Vec<(&str, DatabaseValue)>>,
647) -> Result<Vec<LibraryArtist>, DatabaseFetchError> {
648    let mut results = vec![];
649
650    for artist in artists {
651        let title = artist
652            .iter()
653            .find(|(key, _)| *key == "title")
654            .and_then(|(_, value)| value.as_str().map(ToString::to_string))
655            .ok_or(DatabaseFetchError::InvalidRequest)?;
656
657        let row: LibraryArtist = db
658            .upsert("artists")
659            .where_eq("title", title)
660            .values(artist.into_iter().collect::<Vec<_>>())
661            .execute_first(&**db)
662            .await?
663            .to_value_type()?;
664
665        results.push(row);
666    }
667
668    Ok(results)
669}
670
671/// # Errors
672///
673/// * If there was a database error
674pub async fn add_albums(
675    db: &LibraryDatabase,
676    albums: Vec<LibraryAlbum>,
677) -> Result<Vec<LibraryAlbum>, DatabaseFetchError> {
678    let mut data: Vec<LibraryAlbum> = Vec::new();
679
680    for album in albums {
681        data.push(
682            db.upsert("albums")
683                .where_eq("artist_id", album.artist_id)
684                .where_eq("title", album.title.clone())
685                .where_eq("directory", album.directory.clone())
686                .value("artist_id", album.artist_id)
687                .value("title", album.title)
688                .value("directory", album.directory)
689                .value("date_released", album.date_released)
690                .value("artwork", album.artwork)
691                .execute_first(&**db)
692                .await?
693                .to_value_type()?,
694        );
695    }
696
697    Ok(data)
698}
699
700/// # Errors
701///
702/// * If there was a database error
703pub async fn add_album_and_get_album(
704    db: &LibraryDatabase,
705    album: LibraryAlbum,
706) -> Result<LibraryAlbum, DatabaseFetchError> {
707    Ok(add_albums_and_get_albums(db, vec![album]).await?[0].clone())
708}
709
710/// # Errors
711///
712/// * If there was a database error
713pub async fn add_album_map_and_get_album(
714    db: &LibraryDatabase,
715    album: Vec<(&str, DatabaseValue)>,
716) -> Result<LibraryAlbum, DatabaseFetchError> {
717    Ok(add_album_maps_and_get_albums(db, vec![album]).await?[0].clone())
718}
719
720/// # Errors
721///
722/// * If there was a database error
723pub async fn add_albums_and_get_albums(
724    db: &LibraryDatabase,
725    albums: Vec<LibraryAlbum>,
726) -> Result<Vec<LibraryAlbum>, DatabaseFetchError> {
727    add_album_maps_and_get_albums(
728        db,
729        albums
730            .into_iter()
731            .map(|album| {
732                vec![
733                    (
734                        "artist_id",
735                        #[allow(clippy::cast_possible_wrap)]
736                        DatabaseValue::Number(album.artist_id as i64),
737                    ),
738                    ("title", DatabaseValue::String(album.title)),
739                    (
740                        "date_released",
741                        DatabaseValue::StringOpt(album.date_released),
742                    ),
743                    ("artwork", DatabaseValue::StringOpt(album.artwork)),
744                    ("directory", DatabaseValue::StringOpt(album.directory)),
745                ]
746            })
747            .collect(),
748    )
749    .await
750}
751
752/// # Errors
753///
754/// * If there was a database error
755pub async fn add_album_maps_and_get_albums(
756    db: &LibraryDatabase,
757    albums: Vec<Vec<(&str, DatabaseValue)>>,
758) -> Result<Vec<LibraryAlbum>, DatabaseFetchError> {
759    let mut values = vec![];
760
761    for album in albums {
762        if !album.iter().any(|(x, _)| *x == "artist_id")
763            || !album.iter().any(|(x, _)| *x == "title")
764        {
765            return Err(DatabaseFetchError::InvalidRequest);
766        }
767
768        let mut album_values = album.into_iter().collect::<Vec<_>>();
769        album_values.sort_by(|a, b| a.0.cmp(b.0));
770        values.push(album_values);
771    }
772
773    Ok(db
774        .upsert_multi("albums")
775        .unique(boxed![identifier("artist_id"), identifier("title")])
776        .values(values)
777        .execute(&**db)
778        .await?
779        .to_value_type()?)
780}
781
782#[derive(Debug, Clone, Default)]
783pub struct InsertTrack {
784    pub track: LibraryTrack,
785    pub album_id: u64,
786    pub file: Option<String>,
787}
788
789/// # Errors
790///
791/// * If there was a database error
792pub async fn add_tracks(
793    db: &LibraryDatabase,
794    tracks: Vec<InsertTrack>,
795) -> Result<Vec<LibraryTrack>, DatabaseFetchError> {
796    let values = tracks
797        .iter()
798        .map(|insert| {
799            let mut values = vec![
800                (
801                    "number",
802                    DatabaseValue::Number(i64::from(insert.track.number)),
803                ),
804                ("duration", DatabaseValue::Real(insert.track.duration)),
805                (
806                    "album_id",
807                    #[allow(clippy::cast_possible_wrap)]
808                    DatabaseValue::Number(insert.album_id as i64),
809                ),
810                ("title", DatabaseValue::String(insert.track.title.clone())),
811                (
812                    "format",
813                    DatabaseValue::String(
814                        insert.track.format.unwrap_or_default().as_ref().to_string(),
815                    ),
816                ),
817                (
818                    "source",
819                    DatabaseValue::String(insert.track.source.to_string()),
820                ),
821            ];
822
823            if let Some(file) = &insert.file {
824                values.push(("file", DatabaseValue::String(file.clone())));
825            }
826
827            values
828        })
829        .collect::<Vec<_>>();
830
831    Ok(db
832        .upsert_multi("tracks")
833        .unique(boxed![
834            coalesce(boxed![identifier("file"), literal("''")]),
835            identifier("album_id"),
836            identifier("title"),
837            identifier("duration"),
838            identifier("number"),
839            coalesce(boxed![identifier("format"), literal("''")]),
840            identifier("source"),
841        ])
842        .values(values)
843        .execute(&**db)
844        .await?
845        .to_value_type()?)
846}
847
848#[derive(Debug, Clone, Default)]
849pub struct InsertApiSource {
850    pub entity_type: String,
851    pub entity_id: u64,
852    pub source: String,
853    pub source_id: String,
854}
855
856pub struct ApiSourceMapping {
857    pub entity_type: String,
858    pub entity_id: u64,
859    pub source: String,
860    pub source_id: String,
861}
862
863impl ToValueType<ApiSourceMapping> for &switchy_database::Row {
864    fn to_value_type(self) -> Result<ApiSourceMapping, ParseError> {
865        Ok(ApiSourceMapping {
866            entity_type: self.to_value("entity_type")?,
867            entity_id: self.to_value("entity_id")?,
868            source: self.to_value("source")?,
869            source_id: self.to_value("source_id")?,
870        })
871    }
872}
873
874/// # Errors
875///
876/// * If there was a database error
877pub async fn add_api_sources(
878    db: &LibraryDatabase,
879    api_sources: Vec<InsertApiSource>,
880) -> Result<Vec<ApiSourceMapping>, DatabaseFetchError> {
881    let values = api_sources
882        .iter()
883        .map(|insert| {
884            vec![
885                (
886                    "entity_type",
887                    DatabaseValue::String(insert.entity_type.clone()),
888                ),
889                ("entity_id", DatabaseValue::UNumber(insert.entity_id)),
890                ("source", DatabaseValue::String(insert.source.clone())),
891                ("source_id", DatabaseValue::String(insert.source_id.clone())),
892            ]
893        })
894        .collect::<Vec<_>>();
895
896    Ok(db
897        .upsert_multi("api_sources")
898        .unique(boxed![
899            identifier("entity_type"),
900            identifier("entity_id"),
901            identifier("source"),
902        ])
903        .values(values)
904        .execute(&**db)
905        .await?
906        .to_value_type()?)
907}
908
909#[derive(Debug, Clone, Default)]
910pub struct UpdateApiSource {
911    pub entity_id: u64,
912    pub source: String,
913    pub source_id: String,
914}
915
916/// # Errors
917///
918/// * If there was a database error
919pub async fn update_api_sources(
920    db: &LibraryDatabase,
921    table: &str,
922) -> Result<Vec<Row>, DatabaseFetchError> {
923    Ok(db
924        .update(table)
925        .value(
926            "api_sources",
927            Box::new(
928                select("api_sources")
929                    .columns(&["\
930                    json_group_array(
931                        json_object(
932                           'id', api_sources.source_id,
933                           'source', api_sources.source
934                        )
935                    )\
936                    "])
937                    .where_eq("api_sources.entity_type", table)
938                    .where_eq("api_sources.entity_id", identifier(&format!("{table}.id"))),
939            ) as Box<dyn switchy_database::query::Expression>,
940        )
941        .execute(&**db)
942        .await?)
943}