artifacts: rewrite the artifacts model to be source agnostic for now
This commit is contained in:
+30
-4
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ pub enum ConversationEntry {
|
||||
Eva(String),
|
||||
ShipComputer(String),
|
||||
StageDirection(String),
|
||||
#[serde(skip)]
|
||||
SystemMessage(String)
|
||||
}
|
||||
|
||||
|
||||
+3
-13
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user