#[cfg(feature = "daemon")]
use lunar_lib::database::{DbIdExt, Entry, TransactionError, caching::IdCacheIterExt};
use lunar_lib::{id::Id, iterator_ext::IteratorExtensions};
use selene_core::library::{
album, artist,
track::{self, lyric_data::LyricData},
};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
#[cfg(feature = "daemon")]
use crate::{library_db, playlist};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DtoEntry<T> {
pub name: String,
#[serde(with = "id_as_string")]
pub id: Id<T>,
}
pub trait IntoDto {
type Dto: Serialize + DeserializeOwned;
type Err;
fn into_dto(self) -> Result<Self::Dto, Self::Err>;
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Track {
#[serde(with = "id_as_string")]
pub id: Id<track::Track>,
pub date: Option<i64>,
pub genre: Vec<String>,
pub lyric_data: Option<LyricData>,
pub other: Vec<(String, String)>,
pub title: Option<String>,
pub duration: f64,
pub album: Option<TrackAlbumInfo>,
pub artists: Vec<DtoEntry<artist::Artist>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TrackAlbumInfo {
#[serde(with = "id_as_string")]
id: Id<album::Album>,
title: String,
artists: Vec<DtoEntry<artist::Artist>>,
track_num: Option<u32>,
disc_num: Option<u32>,
}
#[cfg(feature = "daemon")]
impl IntoDto for Entry<track::Track> {
type Dto = Track;
type Err = TransactionError;
fn into_dto(self) -> Result<Self::Dto, Self::Err> {
let id = self.id();
let track = self.into_item();
let duration = track.container().stream().duration();
let meta = track.metadata;
let artists = meta
.artists()
.cache_get(library_db())?
.into_iter()
.map(|a| DtoEntry {
name: a.name().to_owned(),
id: a.id(),
})
.to_vec();
let album = meta
.album()
.map(|album_id| -> Result<TrackAlbumInfo, TransactionError> {
let album = album_id.cache_get(library_db())?.expect("Dangling ref");
let artists = album
.artists()
.cache_get(library_db())?
.into_iter()
.map(|a| DtoEntry {
name: a.name().to_owned(),
id: a.id(),
})
.to_vec();
let (track_num, disc_num) = album
.track_refs()
.iter()
.find_map(|t| (t.id == id).then_some((t.track_num, t.disc_num)))
.expect("Dangling ref");
Ok(TrackAlbumInfo {
id: album_id,
title: album.title.clone(),
artists,
track_num,
disc_num,
})
})
.transpose()?;
Ok(Track {
id,
date: meta.date.map(|d| d.timestamp_millis()),
genre: meta.genre,
lyric_data: meta.lyric_data,
other: meta.other,
title: meta.title,
album,
artists,
duration,
})
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Album {
#[serde(with = "id_as_string")]
pub id: Id<album::Album>,
pub name: String,
pub tracks: Vec<TrackReference>,
pub artists: Vec<DtoEntry<artist::Artist>>,
pub disc_total: Option<u32>,
pub genre: Vec<String>,
pub track_total: Option<u32>,
pub date: Option<i64>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TrackReference {
#[serde(with = "id_as_string")]
pub id: Id<track::Track>,
pub title: String,
pub track_num: Option<u32>,
pub disc_num: Option<u32>,
}
#[cfg(feature = "daemon")]
impl IntoDto for Entry<album::Album> {
type Dto = Album;
type Err = TransactionError;
fn into_dto(self) -> Result<Self::Dto, Self::Err> {
let id = self.id();
let album = self.into_item();
let tracks = album
.tracks()
.cache_get(library_db())?
.into_iter()
.map(|t| {
let t_id = t.id();
let (track_num, disc_num) = album
.track_refs()
.iter()
.find_map(|tr| (tr.id == t_id).then_some((tr.track_num, tr.disc_num)))
.expect("Dangling ref");
TrackReference {
id: t_id,
title: t.metadata.safe_title().to_owned(),
track_num,
disc_num,
}
})
.to_vec();
let artists = album
.artists()
.cache_get(library_db())?
.into_iter()
.map(|a| DtoEntry {
name: a.name().to_owned(),
id: a.id(),
})
.to_vec();
Ok(Album {
id,
name: album.title,
tracks,
artists,
disc_total: album.disc_total,
genre: album.genre,
track_total: album.track_total,
date: album.date.map(|d| d.timestamp_millis()),
})
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Artist {
#[serde(with = "id_as_string")]
pub id: Id<artist::Artist>,
pub name: String,
pub description: Option<String>,
pub(crate) tracks: Vec<DtoEntry<track::Track>>,
pub(crate) albums: Vec<DtoEntry<album::Album>>,
}
#[cfg(feature = "daemon")]
impl IntoDto for Entry<artist::Artist> {
type Dto = Artist;
type Err = TransactionError;
fn into_dto(self) -> Result<Self::Dto, Self::Err> {
let id = self.id();
let artist = self.into_item();
let tracks = artist
.tracks()
.cache_get(library_db())?
.into_iter()
.map(|t| DtoEntry {
name: t.metadata.safe_title().to_owned(),
id: t.id(),
})
.to_vec();
let albums = artist
.albums()
.cache_get(library_db())?
.into_iter()
.map(|a| DtoEntry {
name: a.name().to_owned(),
id: a.id(),
})
.to_vec();
Ok(Artist {
id,
name: artist.name().to_owned(),
description: artist.description,
tracks,
albums,
})
}
}
#[derive(Debug, Serialize, Deserialize)]
pub enum Playable {
Track(Track),
Album(Album),
Artist(Artist),
}
#[cfg(feature = "daemon")]
impl IntoDto for playlist::Playable {
type Dto = Playable;
type Err = TransactionError;
fn into_dto(self) -> Result<Self::Dto, Self::Err> {
let playable = match self {
Self::Track { track: t } => Playable::Track((*t).clone().into_dto()?),
Self::Album { album: v } => Playable::Album((*v.album).clone().into_dto()?),
Self::Artist { artist, .. } => Playable::Artist(
artist
.db_get(library_db())?
.expect("Exists or was deleted during playback")
.into_dto()?,
),
};
Ok(playable)
}
}
mod id_as_string {
use super::{Deserialize, Id};
use serde::{Deserializer, Serializer};
pub fn serialize<T, S: Serializer>(id: &Id<T>, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&id.to_string())
}
pub fn deserialize<'de, T, D: Deserializer<'de>>(d: D) -> Result<Id<T>, D::Error> {
let s = String::deserialize(d)?;
s.parse().map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "plugin-support")]
pub(crate) mod sdk_impls {
use std::sync::Arc;
use lunar_lib::database::{DatabaseEntry, DbIdExt, Entry};
use selene_core::{
library::{
album::Album,
artist::Artist,
track::{Track, lyric_data::LyricData},
},
lyrics::LyricFormat,
};
use selene_plugin_sdk::{
self as sdk, RawId, StableResult, album::StableAlbum, artist::StableArtist,
track::StableTrack,
};
use crate::{dto::IntoDto, image_db, library_db};
pub struct Stable<T>(Arc<<Entry<T> as IntoDto>::Dto>)
where
T: DatabaseEntry,
Entry<T>: IntoDto;
impl<T> std::ops::Deref for Stable<T>
where
T: DatabaseEntry,
Entry<T>: IntoDto,
{
type Target = <Entry<T> as IntoDto>::Dto;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> Stable<T>
where
Entry<T>: IntoDto,
T: DatabaseEntry,
{
pub fn from_dto(dto: Arc<<Entry<T> as IntoDto>::Dto>) -> Self {
Self(dto)
}
}
impl StableTrack for Stable<Track> {
extern "C" fn id(&self) -> RawId {
RawId::from(*self.id)
}
extern "C" fn load_art(&self, x: u32, y: u32) -> StableResult<sdk::Option<sdk::PathBuf>> {
let item = match self.id.cache_get(library_db()) {
Ok(item) => item.expect("Item was deleted before plugin requested art"),
Err(err) => return StableResult::Err(err.into()),
};
if let Some(art_id) = item.metadata.art {
let art = match art_id.db_get(image_db()) {
Ok(art) => art.expect("Dangling ref"),
Err(err) => return StableResult::Err(err.into()),
};
match art.cache_file(x, y) {
Ok(file) => StableResult::Ok(sdk::Option::Some(
sdk::PathBuf::try_from(file).expect("Path was invalid UTF-8"),
)),
Err(err) => StableResult::Err(err.into()),
}
} else {
StableResult::Ok(sdk::Option::None())
}
}
extern "C" fn duration(&self) -> f64 {
self.duration
}
extern "C" fn album(&self) -> sdk::Option<sdk::track::TrackAlbumInfo<'_>> {
self.album
.as_ref()
.map(|t| sdk::track::TrackAlbumInfo {
title: t.title.as_str().into(),
artists: t
.artists
.iter()
.map(|t| sdk::Str::from(t.name.as_str()))
.collect(),
track_num: t.track_num.into(),
disc_num: t.disc_num.into(),
})
.into()
}
extern "C" fn artists(&self) -> sdk::Vec<sdk::Str<'_>> {
self.artists
.iter()
.map(|artist| sdk::Str::from(artist.name.as_str()))
.collect()
}
extern "C" fn date(&self) -> sdk::Option<i64> {
self.date.into()
}
extern "C" fn genre(&self) -> sdk::Vec<sdk::Str<'_>> {
self.genre
.iter()
.map(std::ops::Deref::deref)
.map(sdk::Str::from)
.collect()
}
extern "C" fn lyric_data(&self) -> sdk::Option<sdk::LyricData> {
self.lyric_data
.clone()
.map(|ld| match ld {
LyricData::Instrumental => sdk::LyricData::Instrumental,
LyricData::Plain(plain_lyrics) => {
sdk::LyricData::Plain(plain_lyrics.to_string().into())
}
LyricData::Synced(synced_lyrics) => sdk::LyricData::Synced(
synced_lyrics
.to_lyrics(LyricFormat::Lrc { a2: false })
.into(),
),
})
.into()
}
extern "C" fn other(&self) -> sdk::Vec<sdk::Tuple2<sdk::Str<'_>, sdk::Str<'_>>> {
self.other
.iter()
.map(|(k, v)| sdk::Tuple2(sdk::Str::from(&**k), sdk::Str::from(&**v)))
.collect()
}
extern "C" fn title(&self) -> sdk::Option<sdk::Str<'_>> {
self.title.as_deref().map(sdk::Str::from).into()
}
}
impl StableAlbum for Stable<Album> {
extern "C" fn id(&self) -> RawId {
RawId::from(*self.id)
}
extern "C" fn load_art(&self, x: u32, y: u32) -> StableResult<sdk::Option<sdk::PathBuf>> {
let item = match self.id.cache_get(library_db()) {
Ok(item) => item.expect("Item was deleted before plugin requested art"),
Err(err) => return StableResult::Err(err.into()),
};
if let Some(art_id) = item.art {
let art = match art_id.db_get(image_db()) {
Ok(art) => art.expect("Dangling ref"),
Err(err) => return StableResult::Err(err.into()),
};
match art.cache_file(x, y) {
Ok(file) => StableResult::Ok(sdk::Option::Some(
sdk::PathBuf::try_from(file).expect("Path was invalid UTF-8"),
)),
Err(err) => StableResult::Err(err.into()),
}
} else {
StableResult::Ok(sdk::Option::None())
}
}
extern "C" fn name(&self) -> sdk::Str<'_> {
self.name.as_str().into()
}
extern "C" fn tracks(&self) -> sdk::Vec<sdk::TrackReference<'_>> {
self.tracks
.iter()
.map(|t| sdk::TrackReference {
name: t.title.as_str().into(),
track_num: t.track_num.into(),
disc_num: t.disc_num.into(),
})
.collect()
}
extern "C" fn artists(&self) -> sdk::Vec<sdk::Str<'_>> {
self.artists
.iter()
.map(|a| sdk::Str::from(a.name.as_str()))
.collect()
}
extern "C" fn genre(&self) -> sdk::Vec<sdk::Str<'_>> {
self.genre.iter().map(|s| sdk::Str::from(&**s)).collect()
}
extern "C" fn date(&self) -> sdk::Option<i64> {
self.date.into()
}
extern "C" fn disc_total(&self) -> sdk::Option<u32> {
self.disc_total.into()
}
extern "C" fn track_total(&self) -> sdk::Option<u32> {
self.track_total.into()
}
}
impl StableArtist for Stable<Artist> {
extern "C" fn id(&self) -> RawId {
RawId::from(*self.id)
}
extern "C" fn load_art(&self, x: u32, y: u32) -> StableResult<sdk::Option<sdk::PathBuf>> {
let item = match self.id.cache_get(library_db()) {
Ok(item) => item.expect("Item was deleted before plugin requested art"),
Err(err) => return StableResult::Err(err.into()),
};
if let Some(art_id) = item.art {
let art = match art_id.db_get(image_db()) {
Ok(art) => art.expect("Dangling ref"),
Err(err) => return StableResult::Err(err.into()),
};
match art.cache_file(x, y) {
Ok(file) => StableResult::Ok(sdk::Option::Some(
sdk::PathBuf::try_from(file).expect("Path was invalid UTF-8"),
)),
Err(err) => StableResult::Err(err.into()),
}
} else {
StableResult::Ok(sdk::Option::None())
}
}
extern "C" fn name(&self) -> sdk::Str<'_> {
self.name.as_str().into()
}
extern "C" fn description(&self) -> sdk::Option<sdk::Str<'_>> {
self.description.as_deref().map(sdk::Str::from).into()
}
extern "C" fn tracks(&self) -> sdk::Vec<sdk::Str<'_>> {
self.tracks
.iter()
.map(|id| sdk::Str::from(id.name.as_str()))
.collect()
}
extern "C" fn albums(&self) -> sdk::Vec<sdk::Str<'_>> {
self.albums
.iter()
.map(|id| sdk::Str::from(id.name.as_str()))
.collect()
}
}
}