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#[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
58pub 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
83pub 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
105pub 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
116pub 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
125pub async fn get_artists(db: &LibraryDatabase) -> Result<Vec<LibraryArtist>, DatabaseFetchError> {
129 Ok(db.select("artists").execute(&**db).await?.to_value_type()?)
130}
131
132pub 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
162pub 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
192pub 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
209pub 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
226pub 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
243pub 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
285pub 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
326pub 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
368pub 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
378pub 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
459pub 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
481pub 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
494pub 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
538pub 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
548pub 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
567pub 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
580pub 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
599pub 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
609pub 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
619pub 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
641pub 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
671pub 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
700pub 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
710pub 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
720pub 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
752pub 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
789pub 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
874pub 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
916pub 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}