artifacts: split out each artifact source into a submodule, move archive.rs into artifacts/beets.rs

This commit is contained in:
2026-06-15 15:27:20 +02:00
parent 59a03eb72c
commit 5595b02211
7 changed files with 98 additions and 90 deletions
+28
View File
@@ -0,0 +1,28 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::artifacts::{Album, Artifact, Artist, SourceID};
#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)]
pub struct BandcampQueryArgs {
pub query: String
}
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)}
})
}
}
+77
View File
@@ -0,0 +1,77 @@
use std::process::{Command, Stdio};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::artifacts::{Artifact, SourceID, Track};
#[derive(Debug, Default, Serialize, Deserialize, Clone, JsonSchema)]
pub struct BeatsQueryArgs {
artist: Option<String>,
album: Option<String>,
genre: Option<String>,
title: Option<String>,
year: Option<u32>
}
#[derive(Debug, Default, Deserialize)]
struct BeetsTrack {
album: String,
artist: String,
genres: Option<Vec<String>>,
label: Option<String>,
title: String,
year: u32
}
impl Into<Artifact> for BeetsTrack {
fn into(self) -> Artifact {
Artifact::Track(Track {
title: self.title,
label: self.label,
year: Some(self.year),
genres: self.genres.unwrap_or_default(),
album: Some(self.album),
artist: Some(self.artist),
bpm: None,
sources: vec![SourceID::Beets]
})
}
}
impl BeatsQueryArgs {
pub fn execute(self) -> Result<Vec<Artifact>, ()> {
let mut beets_cmd = Command::new("beet");
beets_cmd.args(["export", "-f", "json", "-i", "title,label,year,genres,album,artist"]);
if let Some(artist) = self.artist {
beets_cmd.arg(format!("artist:{}", artist));
}
if let Some(genre) = self.genre {
beets_cmd.arg(format!("genre:{}", genre));
}
if let Some(album) = self.album {
beets_cmd.arg(format!("album:{}", album));
}
if let Some(title) = self.title {
beets_cmd.arg(format!("title:{}", title));
}
if let Some(year) = self.year {
beets_cmd.arg(format!("year:{}", year));
}
log::debug!("Executing beets: {:?}", beets_cmd);
if let Ok(output) = beets_cmd.stdout(Stdio::piped()).spawn().unwrap().wait_with_output() {
match serde_json::from_str::<Vec<BeetsTrack>>(str::from_utf8(&output.stdout).unwrap()) {
Ok(track) => Ok(track.into_iter().map(|t| { t.into()}).collect()),
Err(err) => {
log::error!("Failed to decode beets json: {:?}", err);
Err(())
}
}
} else {
log::error!("Unable to execute query!");
Err(())
}
}
}
+63
View File
@@ -0,0 +1,63 @@
use sqlite::OpenFlags;
use crate::artifacts::{Album, Artifact, Artist, SourceID, Track};
#[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)
}
}
+136
View File
@@ -0,0 +1,136 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub mod bandcamp;
pub mod mixxx;
pub mod beets;
#[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);
},
_ => ()
}
}
}