use std::process::{Command, Stdio}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::artifacts::{Artifact, ArtifactBuilder, Contents, Merge, SourceID, Track, tools::{DataSource, ToolDescription}}; #[derive(Debug, Default, Serialize, Deserialize, Clone, JsonSchema)] pub struct BeatsQueryArgs { pub artist: Option, pub album: Option, pub genre: Option, pub title: Option, pub year: Option } #[derive(Debug, Default, Deserialize)] struct BeetsTrack { album: String, artist: String, genres: Option>, label: Option, title: String, year: u32, mb_trackid: Option } impl Into for BeetsTrack { fn into(self) -> Artifact { let track_data = 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, }; let builder = ArtifactBuilder::new(SourceID::Beets) .contents(track_data); if let Some(mbid) = self.mb_trackid { builder.mbid(Uuid::parse_str(&mbid).unwrap()).build() } else { builder.build() } } } pub struct BeetsDB; impl DataSource for BeetsDB { type Args = BeatsQueryArgs; type Error = (); async fn synchronize(&mut self, artifact: &mut Artifact) -> Result, Self::Error> { match artifact.contents { Contents::Track(ref mut target_track) => { let args = BeatsQueryArgs { title: Some(target_track.title.clone()), artist: target_track.artist.clone(), album: target_track.album.clone(), ..Default::default() }; let results = self.query(&args).await.unwrap(); if let Some(first) = results.first() { artifact.merge(first.clone()); } else { log::error!("Beets could not find {:?}", target_track); } }, _ => () } Ok(vec![]) } async fn query(&mut self, args: &Self::Args) -> Result, Self::Error> { let mut beets_cmd = Command::new("beet"); beets_cmd.args(["export", "-f", "json", "-i", "title,label,year,genres,album,artist,mb_trackid"]); let mut valid = false; if let Some(ref artist) = args.artist { beets_cmd.arg(format!("artist:{}", artist)); valid = true; } if let Some(ref genre) = args.genre { beets_cmd.arg(format!("genre:{}", genre)); valid = true; } if let Some(ref album) = args.album { beets_cmd.arg(format!("album:{}", album)); valid = true; } if let Some(ref title) = args.title { beets_cmd.arg(format!("title:{}", title)); valid = true; } if let Some(year) = args.year { beets_cmd.arg(format!("year:{}", year)); valid = true; } if !valid { log::warn!("Tried to execute an empty beets query"); return Err(()) } log::debug!("Executing beets: {:?}", beets_cmd); if let Ok(output) = beets_cmd.stdout(Stdio::piped()).stderr(Stdio::null()).spawn().unwrap().wait_with_output() { match serde_json::from_str::>(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(()) } } } impl ToolDescription for BeetsDB { fn description(&self) -> &str { "Queries the ship's musical artifact archives for tracks matching the given search parameters" } fn name(&self) -> &str { "query_beets" } }