pub mod albums;
pub mod artists;
use albums::propagate_api_sources_from_library_album;
use gimbal_database::profiles::LibraryDatabase;
use moosicbox_date_utils::chrono;
use moosicbox_json_utils::database::DatabaseFetchError;
use moosicbox_library::{
cache::{CacheItemType, CacheRequest, get_or_set_to_cache},
db,
models::LibraryAlbum,
};
use moosicbox_music_api::{ArtistError, MusicApi};
use moosicbox_music_models::{Album, ApiSource, Artist, id::Id};
use std::{
sync::{Arc, PoisonError},
time::Duration,
};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum GetArtistError {
#[error(transparent)]
Artist(#[from] ArtistError),
#[error("Invalid request")]
InvalidRequest,
}
#[allow(clippy::too_many_arguments)]
pub async fn get_artist(
api: &dyn MusicApi,
artist_id: Option<&Id>,
album_id: Option<&Id>,
) -> Result<Option<Artist>, GetArtistError> {
if let Some(artist_id) = artist_id {
Ok(api.artist(artist_id).await?)
} else if let Some(album_id) = album_id {
Ok(api.album_artist(album_id).await?)
} else {
Err(GetArtistError::InvalidRequest)
}
}
#[derive(Debug, Error)]
pub enum GetAlbumError {
#[error("Too many albums found with ID {album_id:?}")]
TooManyAlbumsFound { album_id: i32 },
#[error("Unknown source: {album_source:?}")]
UnknownSource { album_source: String },
#[error("Poison error")]
PoisonError,
#[error(transparent)]
GetAlbums(#[from] GetAlbumsError),
#[error(transparent)]
DatabaseFetch(#[from] DatabaseFetchError),
#[error("Invalid request")]
InvalidRequest,
#[error(transparent)]
ChronoParse(#[from] chrono::ParseError),
}
impl<T> From<PoisonError<T>> for GetAlbumError {
fn from(_err: PoisonError<T>) -> Self {
Self::PoisonError
}
}
pub async fn get_album_from_source(
db: &LibraryDatabase,
album_id: &Id,
source: ApiSource,
) -> Result<Option<Album>, GetAlbumError> {
let mut album = match source {
ApiSource::Library => {
let albums = get_albums(db).await?;
albums
.iter()
.find(|album| &Into::<Id>::into(album.id) == album_id)
.cloned()
.map(TryInto::try_into)
}
#[cfg(feature = "tidal")]
ApiSource::Tidal => moosicbox_tidal::album(db, album_id, None, None, None, None)
.await
.ok()
.map(TryInto::try_into),
#[cfg(feature = "qobuz")]
ApiSource::Qobuz => moosicbox_qobuz::album(db, album_id, None, None)
.await
.ok()
.map(TryInto::try_into),
#[cfg(feature = "yt")]
ApiSource::Yt => moosicbox_yt::album(db, album_id, None, None, None, None)
.await
.ok()
.map(TryInto::try_into),
}
.transpose()?;
if let Some(album) = &mut album {
let library_albums = get_albums(db).await?;
propagate_api_sources_from_library_album(source, album, &library_albums);
}
Ok(album)
}
pub async fn get_library_album(
db: &LibraryDatabase,
album_id: &Id,
source: ApiSource,
) -> Result<Option<LibraryAlbum>, GetAlbumError> {
let albums = get_albums(db).await?;
Ok(match source {
ApiSource::Library => albums
.iter()
.find(|album| &Into::<Id>::into(album.id) == album_id)
.cloned(),
#[cfg(feature = "tidal")]
ApiSource::Tidal => albums
.iter()
.find(|album| {
album
.album_sources
.iter()
.any(|x| x.source == ApiSource::Tidal && &x.id == album_id)
})
.cloned(),
#[cfg(feature = "qobuz")]
ApiSource::Qobuz => albums
.iter()
.find(|album| {
album
.album_sources
.iter()
.any(|x| x.source == ApiSource::Qobuz && &x.id == album_id)
})
.cloned(),
#[cfg(feature = "yt")]
ApiSource::Yt => albums
.iter()
.find(|album| {
album
.album_sources
.iter()
.any(|x| x.source == ApiSource::Yt && &x.id == album_id)
})
.cloned(),
})
}
#[derive(Debug, Error)]
pub enum GetAlbumsError {
#[error("Poison error")]
Poison,
#[error(transparent)]
DatabaseFetch(#[from] DatabaseFetchError),
}
impl<T> From<PoisonError<T>> for GetAlbumsError {
fn from(_err: PoisonError<T>) -> Self {
Self::Poison
}
}
pub async fn get_albums(db: &LibraryDatabase) -> Result<Arc<Vec<LibraryAlbum>>, GetAlbumsError> {
let request = CacheRequest {
key: "sqlite|local_albums",
expiration: Duration::from_secs(5 * 60),
};
let start = gimbal_time::now();
let albums = get_or_set_to_cache(request, || async {
Ok::<CacheItemType, GetAlbumsError>(CacheItemType::Albums(Arc::new(
db::get_albums(db).await?,
)))
})
.await?
.into_albums()
.unwrap();
let elapsed = gimbal_time::now()
.duration_since(start)
.unwrap()
.as_millis();
log::debug!("Took {elapsed}ms to get albums");
Ok(albums)
}
#[derive(Debug, Error)]
pub enum GetArtistAlbumsError {
#[error("Poison error")]
Poison,
#[error(transparent)]
DatabaseFetch(#[from] DatabaseFetchError),
}
impl<T> From<PoisonError<T>> for GetArtistAlbumsError {
fn from(_err: PoisonError<T>) -> Self {
Self::Poison
}
}
pub async fn get_artist_albums(
artist_id: &Id,
db: &LibraryDatabase,
) -> Result<Arc<Vec<LibraryAlbum>>, GetArtistAlbumsError> {
let request = CacheRequest {
key: &format!("sqlite|local_artist_albums|{artist_id}"),
expiration: Duration::from_secs(5 * 60),
};
Ok(get_or_set_to_cache(request, || async {
Ok::<CacheItemType, GetArtistAlbumsError>(CacheItemType::ArtistAlbums(Arc::new(
db::get_artist_albums(db, artist_id).await?,
)))
})
.await?
.into_artist_albums()
.unwrap())
}