diff --git a/Cargo.lock b/Cargo.lock index 09dbf5c..8b269fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -677,6 +677,16 @@ dependencies = [ "darling_macro 0.20.11", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + [[package]] name = "darling" version = "0.23.0" @@ -701,6 +711,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "darling_core" version = "0.23.0" @@ -725,6 +748,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.117", +] + [[package]] name = "darling_macro" version = "0.23.0" @@ -958,6 +992,28 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "enumset" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839c4174b41e75c8f7306110b2c51996a293b8d1d850edd529011841d9fede7d" +dependencies = [ + "enumset_derive", + "serde", +] + +[[package]] +name = "enumset_derive" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -994,6 +1050,7 @@ dependencies = [ "color-eyre", "crossterm", "dotenv", + "enumset", "futures", "futures-timer", "hound", diff --git a/Cargo.toml b/Cargo.toml index 1f88870..ca154f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ chrono = { version = "0.4.44", features = ["serde"] } color-eyre = "0.6.5" crossterm = { version = "0.29.0", features = ["event-stream"] } dotenv = "0.15.0" +enumset = { version = "1.1.13", features = ["serde"] } futures = "0.3.32" futures-timer = "3.0.4" hound = "3.5.1" diff --git a/src/archive.rs b/src/archive.rs index edbf8f1..1585768 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -3,7 +3,7 @@ use std::process::{Command, Stdio}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::artifacts::Artifact; +use crate::artifacts::{Artifact, Track}; #[derive(Debug, Default, Serialize, Deserialize, Clone, JsonSchema)] @@ -15,6 +15,30 @@ pub struct BeatsQueryArgs { year: Option } +#[derive(Debug, Default, Deserialize)] +struct BeetsTrack { + album: String, + artist: String, + genres: Vec, + label: String, + title: String, + year: u32 +} + +impl Into for BeetsTrack { + fn into(self) -> Artifact { + Artifact::Track(Track { + title: self.title, + label: Some(self.label), + year: Some(self.year), + genres: self.genres, + album: Some(self.album), + artist: Some(self.artist), + bpm: None + }) + } +} + impl BeatsQueryArgs { pub fn execute(self) -> Result { let mut beets_cmd = Command::new("beet"); @@ -35,12 +59,14 @@ impl BeatsQueryArgs { 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() { - //messages.push(ConversationEntry::ShipComputer(format!("Executing archive query {:?}", beets_cmd))); - Ok(Artifact::BeetsTrack(serde_json::from_str(str::from_utf8(&output.stdout).unwrap()).unwrap())) + let track: BeetsTrack = serde_json::from_str(str::from_utf8(&output.stdout).unwrap()).unwrap(); + + Ok(track.into()) } else { + log::error!("Unable to execute query!"); Err(()) - //messages.push(ConversationEntry::ShipComputer("Unable to execute query!".into())); } } } \ No newline at end of file diff --git a/src/artifacts.rs b/src/artifacts.rs index b8c684f..39ebe55 100644 --- a/src/artifacts.rs +++ b/src/artifacts.rs @@ -3,46 +3,109 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use sqlite::OpenFlags; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub enum Artifact { - Bandcamp(BandcampResult), - BeetsTrack(serde_json::Value) +#[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 } -#[derive(Debug, Serialize, Deserialize, Clone)] -pub enum BandcampResult { - Artist { name: String, bio: Option, location: Option }, - Album { title: String, about: Option, credits: Option, release_date: DateTime, artist: String } -} - -impl Into for bandcamp::Artist { - fn into(self) -> BandcampResult { - BandcampResult::Artist { name: self.name, bio: self.bio, location: self.location } - } -} - -impl Into for bandcamp::Album { - fn into(self) -> BandcampResult { - BandcampResult::Album { - about: self.about, - title: self.title, - artist: self.band.name, - credits: self.credits, - release_date: self.release_date +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) } } -impl Into for bandcamp::Album { - fn into(self) -> Artifact { - Artifact::Bandcamp(self.into()) +#[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>, +} + +#[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 +} + +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) +} + +impl Artifact { + pub fn merge(&mut self, other: &Artifact) { + } } impl Into for bandcamp::Artist { fn into(self) -> Artifact { - Artifact::Bandcamp(self.into()) + Artifact::Artist(Artist { name: self.name, bio: self.bio, location: self.location }) + } +} + +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) + }) } } @@ -51,14 +114,6 @@ pub struct BandcampQueryArgs { pub query: String } -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct MixxxTrack { - pub artist: String, - pub album: String, - pub title: String, - pub bpm: f64 -} - #[derive(Debug)] pub enum MixxxError { Sql(sqlite::Error) @@ -73,9 +128,10 @@ impl From for MixxxError { pub struct MixxxDB(()); impl MixxxDB { - pub fn load(episode_number: u32) -> Result, MixxxError> { + 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)?; @@ -89,12 +145,24 @@ impl MixxxDB { 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(MixxxTrack { - artist: artist.into(), - album: album.into(), + ret.push(Artifact::Track(Track { + artist: Some(artist.into()), + album: Some(album.into()), title: title.into(), - bpm - }); + bpm: Some(bpm), + ..Default::default() + })); + + ret.push(Artifact::Album(Album { + artist: artist.into(), + title: album.into(), + ..Default::default() + })); + + ret.push(Artifact::Artist(Artist { + name: artist.into(), + ..Default::default() + })); } Ok(ret) diff --git a/src/main.rs b/src/main.rs index 3046b56..b58747c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -253,7 +253,7 @@ impl App { let status_line = Line::from_iter([ Span::from(format!("Episode {}", self.scene.direction.episode_number)).style(ratatui::style::Color::LightBlue), Span::from(" | ").style(ratatui::style::Color::DarkGray), - Span::from(format!("{} tracks", self.scene.direction.current_playlist.len())).style(ratatui::style::Color::LightBlue), + Span::from(format!("{} tracks", self.scene.scenery().current_playlist.len())).style(ratatui::style::Color::LightBlue), Span::from(" | ").style(ratatui::style::Color::DarkGray), Span::from(format!("Time Remaining: {}", formatted_time)).style(time_style), Span::from(" | ").style(ratatui::style::Color::DarkGray), @@ -539,12 +539,15 @@ async fn main() { let mut terminal: Terminal> = ratatui::init(); let saved_session = if let Ok(save_data) = std::fs::read_to_string("save.json") { - if let Ok(ret) = serde_json::from_str(&save_data) { - log::info!("Loaded session from save.json"); - ret - } else { - log::warn!("Could not load saved session!"); - SaveData::default() + match serde_json::from_str(&save_data) { + Ok(ret) => { + log::info!("Loaded session from save.json"); + ret + }, + Err(err) => { + log::error!("Could not load saved session! {:?}", err); + SaveData::default() + } } } else { log::info!("Creating new session in save.json"); diff --git a/src/prediction.rs b/src/prediction.rs index bbc7fb5..d99efcb 100644 --- a/src/prediction.rs +++ b/src/prediction.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{Serializer, ser::CompactFormatter}; use tokio::sync::{RwLock, mpsc, watch}; -use crate::{SaveData, archive::BeatsQueryArgs, artifacts::BandcampQueryArgs, scene::{PredictionAction, Scene, Scenery, StageDirection, conversation::ConversationEntry}}; +use crate::{SaveData, archive::BeatsQueryArgs, artifacts::{Artifact, Artist, BandcampQueryArgs, MixxxDB}, scene::{PredictionAction, Scene, Scenery, StageDirection, conversation::ConversationEntry}}; const SYSTEM_PROMPT: &str = include_str!("system-prompt.txt"); @@ -110,7 +110,13 @@ impl Session { let artifact_count = json_results.len(); messages.push(ConversationEntry::ShipComputer(format!("Relay scan for '{}' complete. {} artifacts added to the archive.", args.query, artifact_count).into())); - self.scenery.artifacts.append(&mut json_results); + for track in &json_results { + if let Some(merge_target) = self.scenery.artifacts.iter_mut().find(|a| { *a == track }) { + merge_target.merge(track); + } else { + self.scenery.artifacts.push(track.clone()); + } + } ToolResults { result: Some(format!("{} artifacts were added to the archive.", artifact_count)), @@ -379,10 +385,19 @@ pub async fn start_prediction(saved_session: SaveData, mut messages: tokio::sync }, PredictionAction::SetEpisodeNumber(num) => { session.direction.episode_number = num; - if let Err(err) = session.direction.reload_mixxx_playlist() { - session.log(format!("Failed to load mixxx playlist: {:?}.", err)); - } else { - session.log("Mixxx playlist reloaded."); + match MixxxDB::load(num) { + Err(err) => session.log(format!("Failed to load mixxx playlist: {:?}.", err)), + Ok(playlist) => { + for track in &playlist { + if let Some(merge_target) = session.scenery.artifacts.iter_mut().find(|a| { *a == track }) { + merge_target.merge(track); + } else { + session.scenery.artifacts.push(track.clone()); + } + } + session.scenery.current_playlist = playlist; + session.log("Mixxx playlist reloaded."); + } } false }, diff --git a/src/scene/conversation.rs b/src/scene/conversation.rs index f03633a..97f5cc6 100644 --- a/src/scene/conversation.rs +++ b/src/scene/conversation.rs @@ -7,6 +7,7 @@ pub enum ConversationEntry { Eva(String), ShipComputer(String), StageDirection(String), + #[serde(skip)] SystemMessage(String) } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 5e49bb2..7a9a4e0 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1,8 +1,7 @@ use chrono::{DateTime, Duration, Utc}; use serde::{Deserialize, Serialize}; -use sqlite::OpenFlags; -use crate::{artifacts::{Artifact, MixxxDB, MixxxError, MixxxTrack}, prediction::{GeneratedResponses, PossibleResponse}, scene::conversation::ConversationEntry}; +use crate::{artifacts::{Artifact, MixxxDB, MixxxError}, prediction::{GeneratedResponses, PossibleResponse}, scene::conversation::ConversationEntry}; pub mod conversation; @@ -12,7 +11,6 @@ pub struct StageDirection { #[serde(skip)] pub end_time: DateTime, pub narrative: String, - pub current_playlist: Vec } impl StageDirection { @@ -27,22 +25,14 @@ impl Default for StageDirection { episode_number: 0, end_time: Utc::now() + Duration::hours(2), narrative: Default::default(), - current_playlist: Default::default(), } } } #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct Scenery { - pub artifacts: Vec -} - -impl StageDirection { - pub fn reload_mixxx_playlist(&mut self) -> Result<(), MixxxError> { - self.current_playlist = MixxxDB::load(self.episode_number)?; - - Ok(()) - } + pub artifacts: Vec, + pub current_playlist: Vec } #[derive(Debug, Clone)]