use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use sqlite::OpenFlags; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum SourceID { Bandcamp(u64), Mixxx, Beets } #[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct Artist { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub bio: Option, #[serde(skip_serializing_if = "Option::is_none")] pub location: Option, pub sources: Vec } impl PartialEq for Artist { fn eq(&self, other: &Self) -> bool { if self.name != other.name { return false; } true } fn ne(&self, other: &Self) -> bool { !self.eq(other) } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] pub struct Album { pub title: String, pub artist: String, #[serde(skip_serializing_if = "Option::is_none")] pub about: Option, #[serde(skip_serializing_if = "Option::is_none")] pub credits: Option, #[serde(skip_serializing_if = "Option::is_none")] pub release_date: Option>, pub sources: Vec } #[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct Track { pub title: String, #[serde(skip_serializing_if = "Option::is_none")] pub label: Option, #[serde(skip_serializing_if = "Option::is_none")] pub year: Option, #[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default)] pub genres: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub album: Option, #[serde(skip_serializing_if = "Option::is_none")] pub artist: Option, #[serde(skip_serializing_if = "Option::is_none")] pub bpm: Option, pub sources: Vec } impl PartialEq for Track { fn eq(&self, other: &Self) -> bool { if self.title != other.title { return false; } if self.artist.is_some() && self.artist != other.artist { return false; } if self.album.is_some() && self.album != other.album { return false; } true } fn ne(&self, other: &Self) -> bool { !self.eq(other) } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub enum Artifact { Artist(Artist), Album(Album), Track(Track) } macro_rules! merge_fields { ($this:expr, $that:expr, $field:tt) => { if $this.$field.is_none() { $this.$field = $that.$field; } }; ($this:tt, $that:tt, $($fields:tt),+) => { $( merge_fields!($this, $that, $fields); )+ } } impl Artifact { pub fn merge(&mut self, other: Artifact) { if *self != other { return; } match (self, other) { (Artifact::Track(this_track), Artifact::Track(that_track)) => { merge_fields!(this_track, that_track, album, label, year, artist, bpm); // FIXME: genres }, (Artifact::Album(this_album), Artifact::Album(that_album)) => { merge_fields!(this_album, that_album, about, credits, release_date); }, (Artifact::Artist(this_artist), Artifact::Artist(that_artist)) => { merge_fields!(this_artist, that_artist, bio, location); }, _ => () } } } impl Into for bandcamp::Artist { fn into(self) -> Artifact { Artifact::Artist(Artist { name: self.name, bio: self.bio, location: self.location, sources: vec![SourceID::Bandcamp(self.id)] }) } } impl Into for bandcamp::Album { fn into(self) -> Artifact { Artifact::Album(Album { about: self.about, title: self.title, artist: self.band.name, credits: self.credits, release_date: Some(self.release_date), sources: vec!{SourceID::Bandcamp(self.id)} }) } } #[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] pub struct BandcampQueryArgs { pub query: String } #[derive(Debug)] #[allow(unused)] pub enum MixxxError { Sql(sqlite::Error) } impl From for MixxxError { fn from(value: sqlite::Error) -> Self { Self::Sql(value) } } pub struct MixxxDB(()); impl MixxxDB { pub fn load(episode_number: u32) -> Result, MixxxError> { let mut ret = vec![]; let playlist_name = format!("BFF.fm - Episode {}", episode_number); log::info!("Loading Mixxx playlist {}", playlist_name); let connection = sqlite::Connection::open_thread_safe_with_flags("mixxxdb.sqlite", OpenFlags::new().with_read_only())?; let query = "SELECT id FROM Playlists WHERE name = ? ORDER BY id DESC LIMIT 1"; let mut statement = connection.prepare(query)?; statement.bind((1, playlist_name.as_str()))?; statement.next()?; let latest_id = statement.read::("id")?; let query = "SELECT title, artist, album, comment, url, bpm FROM library LEFT JOIN PlaylistTracks ON PlaylistTracks.track_id = library.id WHERE PlaylistTracks.playlist_id = ? ORDER BY position"; for track in connection.prepare(query)?.into_iter().bind((1, latest_id))? { let track = track?; let title = track.try_read::<&str, _>("title").unwrap_or("Untitled Track"); let artist = track.try_read::<&str, _>("artist").unwrap_or("Unknown Artist"); let album = track.try_read::<&str, _>("album").unwrap_or("Unknown Album"); let bpm = track.try_read::("bpm").unwrap_or(0.); ret.push(Artifact::Track(Track { artist: Some(artist.into()), album: Some(album.into()), title: title.into(), bpm: Some(bpm), sources: vec![SourceID::Mixxx], ..Default::default() })); ret.push(Artifact::Album(Album { artist: artist.into(), title: album.into(), sources: vec![SourceID::Mixxx], ..Default::default() })); ret.push(Artifact::Artist(Artist { name: artist.into(), sources: vec![SourceID::Mixxx], ..Default::default() })); } Ok(ret) } }