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",
]
[[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",
+1
View File
@@ -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"
+30 -4
View File
@@ -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<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 {
pub fn execute(self) -> Result<Artifact, ()> {
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()));
}
}
}
+111 -43
View File
@@ -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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum BandcampResult {
Artist { name: String, bio: Option<String>, location: Option<String> },
Album { title: String, about: Option<String>, credits: Option<String>, release_date: DateTime<Utc>, artist: String }
}
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
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<Artifact> 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<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 {
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
}
#[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<sqlite::Error> for MixxxError {
pub struct 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 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::<f64, _>("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)
+10 -7
View File
@@ -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<CrosstermBackend<std::io::Stdout>> = 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");
+21 -6
View File
@@ -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
},
+1
View File
@@ -7,6 +7,7 @@ pub enum ConversationEntry {
Eva(String),
ShipComputer(String),
StageDirection(String),
#[serde(skip)]
SystemMessage(String)
}
+3 -13
View File
@@ -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<Utc>,
pub narrative: String,
pub current_playlist: Vec<MixxxTrack>
}
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<Artifact>
}
impl StageDirection {
pub fn reload_mixxx_playlist(&mut self) -> Result<(), MixxxError> {
self.current_playlist = MixxxDB::load(self.episode_number)?;
Ok(())
}
pub artifacts: Vec<Artifact>,
pub current_playlist: Vec<Artifact>
}
#[derive(Debug, Clone)]