artifacts: rewrite the artifacts model to be source agnostic for now

This commit is contained in:
2026-06-09 22:00:37 +02:00
parent 44afe5a713
commit 2fe1cc3d5c
8 changed files with 234 additions and 73 deletions
Generated
+57
View File
@@ -677,6 +677,16 @@ dependencies = [
"darling_macro 0.20.11", "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]] [[package]]
name = "darling" name = "darling"
version = "0.23.0" version = "0.23.0"
@@ -701,6 +711,19 @@ dependencies = [
"syn 2.0.117", "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]] [[package]]
name = "darling_core" name = "darling_core"
version = "0.23.0" version = "0.23.0"
@@ -725,6 +748,17 @@ dependencies = [
"syn 2.0.117", "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]] [[package]]
name = "darling_macro" name = "darling_macro"
version = "0.23.0" version = "0.23.0"
@@ -958,6 +992,28 @@ dependencies = [
"syn 2.0.117", "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]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@@ -994,6 +1050,7 @@ dependencies = [
"color-eyre", "color-eyre",
"crossterm", "crossterm",
"dotenv", "dotenv",
"enumset",
"futures", "futures",
"futures-timer", "futures-timer",
"hound", "hound",
+1
View File
@@ -11,6 +11,7 @@ chrono = { version = "0.4.44", features = ["serde"] }
color-eyre = "0.6.5" color-eyre = "0.6.5"
crossterm = { version = "0.29.0", features = ["event-stream"] } crossterm = { version = "0.29.0", features = ["event-stream"] }
dotenv = "0.15.0" dotenv = "0.15.0"
enumset = { version = "1.1.13", features = ["serde"] }
futures = "0.3.32" futures = "0.3.32"
futures-timer = "3.0.4" futures-timer = "3.0.4"
hound = "3.5.1" hound = "3.5.1"
+30 -4
View File
@@ -3,7 +3,7 @@ use std::process::{Command, Stdio};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::artifacts::Artifact; use crate::artifacts::{Artifact, Track};
#[derive(Debug, Default, Serialize, Deserialize, Clone, JsonSchema)] #[derive(Debug, Default, Serialize, Deserialize, Clone, JsonSchema)]
@@ -15,6 +15,30 @@ pub struct BeatsQueryArgs {
year: Option<u32> year: Option<u32>
} }
#[derive(Debug, Default, Deserialize)]
struct BeetsTrack {
album: String,
artist: String,
genres: Vec<String>,
label: String,
title: String,
year: u32
}
impl Into<Artifact> 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 { impl BeatsQueryArgs {
pub fn execute(self) -> Result<Artifact, ()> { pub fn execute(self) -> Result<Artifact, ()> {
let mut beets_cmd = Command::new("beet"); let mut beets_cmd = Command::new("beet");
@@ -35,12 +59,14 @@ impl BeatsQueryArgs {
beets_cmd.arg(format!("year:{}", 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() { if let Ok(output) = beets_cmd.stdout(Stdio::piped()).spawn().unwrap().wait_with_output() {
//messages.push(ConversationEntry::ShipComputer(format!("Executing archive query {:?}", beets_cmd))); let track: BeetsTrack = serde_json::from_str(str::from_utf8(&output.stdout).unwrap()).unwrap();
Ok(Artifact::BeetsTrack(serde_json::from_str(str::from_utf8(&output.stdout).unwrap()).unwrap()))
Ok(track.into())
} else { } else {
log::error!("Unable to execute query!");
Err(()) Err(())
//messages.push(ConversationEntry::ShipComputer("Unable to execute query!".into()));
} }
} }
} }
+111 -43
View File
@@ -3,46 +3,109 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlite::OpenFlags; use sqlite::OpenFlags;
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Artist {
pub enum Artifact { pub name: String,
Bandcamp(BandcampResult), #[serde(skip_serializing_if = "Option::is_none")]
BeetsTrack(serde_json::Value) pub bio: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>
} }
#[derive(Debug, Serialize, Deserialize, Clone)] impl PartialEq for Artist {
pub enum BandcampResult { fn eq(&self, other: &Self) -> bool {
Artist { name: String, bio: Option<String>, location: Option<String> }, if self.name != other.name {
Album { title: String, about: Option<String>, credits: Option<String>, release_date: DateTime<Utc>, artist: String } return false;
}
impl Into<BandcampResult> for bandcamp::Artist {
fn into(self) -> BandcampResult {
BandcampResult::Artist { name: self.name, bio: self.bio, location: self.location }
}
}
impl Into<BandcampResult> 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
} }
true
}
fn ne(&self, other: &Self) -> bool {
!self.eq(other)
} }
} }
impl Into<Artifact> for bandcamp::Album { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
fn into(self) -> Artifact { pub struct Album {
Artifact::Bandcamp(self.into()) 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>>,
}
#[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>
}
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<Artifact> for bandcamp::Artist { impl Into<Artifact> for bandcamp::Artist {
fn into(self) -> Artifact { fn into(self) -> Artifact {
Artifact::Bandcamp(self.into()) Artifact::Artist(Artist { name: self.name, bio: self.bio, location: self.location })
}
}
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)
})
} }
} }
@@ -51,14 +114,6 @@ pub struct BandcampQueryArgs {
pub query: String 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)] #[derive(Debug)]
pub enum MixxxError { pub enum MixxxError {
Sql(sqlite::Error) Sql(sqlite::Error)
@@ -73,9 +128,10 @@ impl From<sqlite::Error> for MixxxError {
pub struct MixxxDB(()); pub struct MixxxDB(());
impl MixxxDB { impl MixxxDB {
pub fn load(episode_number: u32) -> Result<Vec<MixxxTrack>, MixxxError> { pub fn load(episode_number: u32) -> Result<Vec<Artifact>, MixxxError> {
let mut ret = vec![]; let mut ret = vec![];
let playlist_name = format!("BFF.fm - Episode {}", episode_number); 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 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 query = "SELECT id FROM Playlists WHERE name = ? ORDER BY id DESC LIMIT 1";
let mut statement = connection.prepare(query)?; let mut statement = connection.prepare(query)?;
@@ -89,12 +145,24 @@ impl MixxxDB {
let artist = track.try_read::<&str, _>("artist").unwrap_or("Unknown Artist"); let artist = track.try_read::<&str, _>("artist").unwrap_or("Unknown Artist");
let album = track.try_read::<&str, _>("album").unwrap_or("Unknown Album"); let album = track.try_read::<&str, _>("album").unwrap_or("Unknown Album");
let bpm = track.try_read::<f64, _>("bpm").unwrap_or(0.); let bpm = track.try_read::<f64, _>("bpm").unwrap_or(0.);
ret.push(MixxxTrack { ret.push(Artifact::Track(Track {
artist: artist.into(), artist: Some(artist.into()),
album: album.into(), album: Some(album.into()),
title: title.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) Ok(ret)
+10 -7
View File
@@ -253,7 +253,7 @@ impl App {
let status_line = Line::from_iter([ let status_line = Line::from_iter([
Span::from(format!("Episode {}", self.scene.direction.episode_number)).style(ratatui::style::Color::LightBlue), Span::from(format!("Episode {}", self.scene.direction.episode_number)).style(ratatui::style::Color::LightBlue),
Span::from(" | ").style(ratatui::style::Color::DarkGray), 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(" | ").style(ratatui::style::Color::DarkGray),
Span::from(format!("Time Remaining: {}", formatted_time)).style(time_style), Span::from(format!("Time Remaining: {}", formatted_time)).style(time_style),
Span::from(" | ").style(ratatui::style::Color::DarkGray), Span::from(" | ").style(ratatui::style::Color::DarkGray),
@@ -539,12 +539,15 @@ async fn main() {
let mut terminal: Terminal<CrosstermBackend<std::io::Stdout>> = ratatui::init(); let mut terminal: Terminal<CrosstermBackend<std::io::Stdout>> = ratatui::init();
let saved_session = if let Ok(save_data) = std::fs::read_to_string("save.json") { 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) { match serde_json::from_str(&save_data) {
log::info!("Loaded session from save.json"); Ok(ret) => {
ret log::info!("Loaded session from save.json");
} else { ret
log::warn!("Could not load saved session!"); },
SaveData::default() Err(err) => {
log::error!("Could not load saved session! {:?}", err);
SaveData::default()
}
} }
} else { } else {
log::info!("Creating new session in save.json"); log::info!("Creating new session in save.json");
+21 -6
View File
@@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{Serializer, ser::CompactFormatter}; use serde_json::{Serializer, ser::CompactFormatter};
use tokio::sync::{RwLock, mpsc, watch}; 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"); const SYSTEM_PROMPT: &str = include_str!("system-prompt.txt");
@@ -110,7 +110,13 @@ impl Session {
let artifact_count = json_results.len(); 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())); 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 { ToolResults {
result: Some(format!("{} artifacts were added to the archive.", artifact_count)), 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) => { PredictionAction::SetEpisodeNumber(num) => {
session.direction.episode_number = num; session.direction.episode_number = num;
if let Err(err) = session.direction.reload_mixxx_playlist() { match MixxxDB::load(num) {
session.log(format!("Failed to load mixxx playlist: {:?}.", err)); Err(err) => session.log(format!("Failed to load mixxx playlist: {:?}.", err)),
} else { Ok(playlist) => {
session.log("Mixxx playlist reloaded."); 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 false
}, },
+1
View File
@@ -7,6 +7,7 @@ pub enum ConversationEntry {
Eva(String), Eva(String),
ShipComputer(String), ShipComputer(String),
StageDirection(String), StageDirection(String),
#[serde(skip)]
SystemMessage(String) SystemMessage(String)
} }
+3 -13
View File
@@ -1,8 +1,7 @@
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize}; 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; pub mod conversation;
@@ -12,7 +11,6 @@ pub struct StageDirection {
#[serde(skip)] #[serde(skip)]
pub end_time: DateTime<Utc>, pub end_time: DateTime<Utc>,
pub narrative: String, pub narrative: String,
pub current_playlist: Vec<MixxxTrack>
} }
impl StageDirection { impl StageDirection {
@@ -27,22 +25,14 @@ impl Default for StageDirection {
episode_number: 0, episode_number: 0,
end_time: Utc::now() + Duration::hours(2), end_time: Utc::now() + Duration::hours(2),
narrative: Default::default(), narrative: Default::default(),
current_playlist: Default::default(),
} }
} }
} }
#[derive(Debug, Default, Serialize, Deserialize, Clone)] #[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct Scenery { pub struct Scenery {
pub artifacts: Vec<Artifact> pub artifacts: Vec<Artifact>,
} pub current_playlist: Vec<Artifact>
impl StageDirection {
pub fn reload_mixxx_playlist(&mut self) -> Result<(), MixxxError> {
self.current_playlist = MixxxDB::load(self.episode_number)?;
Ok(())
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]