218 lines
6.4 KiB
Rust
218 lines
6.4 KiB
Rust
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<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub location: Option<String>,
|
|
|
|
pub sources: Vec<SourceID>
|
|
}
|
|
|
|
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<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub credits: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub release_date: Option<DateTime<Utc>>,
|
|
|
|
pub sources: Vec<SourceID>
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
|
pub struct Track {
|
|
pub title: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub label: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub year: Option<u32>,
|
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
#[serde(default)]
|
|
pub genres: Vec<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub album: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub artist: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub bpm: Option<f64>,
|
|
|
|
pub sources: Vec<SourceID>
|
|
}
|
|
|
|
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<Artifact> 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<Artifact> 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<sqlite::Error> for MixxxError {
|
|
fn from(value: sqlite::Error) -> Self {
|
|
Self::Sql(value)
|
|
}
|
|
}
|
|
|
|
pub struct MixxxDB(());
|
|
|
|
impl MixxxDB {
|
|
pub fn load(episode_number: u32) -> Result<Vec<Artifact>, 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::<i64, _>("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::<f64, _>("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)
|
|
}
|
|
} |