diff --git a/migrations/2023-06-15-080643_create_samples/down.sql b/migrations/2023-06-15-080643_create_samples/down.sql new file mode 100644 index 0000000..84a838d --- /dev/null +++ b/migrations/2023-06-15-080643_create_samples/down.sql @@ -0,0 +1,3 @@ +DROP TABLE samples_tags; +DROP TABLE tags; +DROP TABLE samples; diff --git a/migrations/2023-06-15-080643_create_samples/up.sql b/migrations/2023-06-15-080643_create_samples/up.sql new file mode 100644 index 0000000..8dd7c26 --- /dev/null +++ b/migrations/2023-06-15-080643_create_samples/up.sql @@ -0,0 +1,20 @@ +CREATE TABLE samples ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name VARCHAR NOT NULL, + path VARCHAR NOT NULL, + bpm REAL, + key VARCHAR +); + +CREATE TABLE tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name VARCHAR NOT NULL +); + +CREATE TABLE samples_tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + sample_id INTEGER NOT NULL, + tag_id INTEGER NOT NULL, + FOREIGN KEY (sample_id) REFERENCES samples (id), + FOREIGN KEY (tag_id) REFERENCES tags (id) +); diff --git a/src/bin/add_sample.rs b/src/bin/add_sample.rs new file mode 100644 index 0000000..fbb7034 --- /dev/null +++ b/src/bin/add_sample.rs @@ -0,0 +1,14 @@ +use sample_amp::{tag_terms::TagSuggestions, *}; +use std::{env, path::Path}; + +fn main() { + let suggestions = + TagSuggestions::read_from_file("tag_terms.txt").expect("expected tag file to load"); + + let connection = &mut establish_connection(); + + let file_str = env::args().nth(1).expect("Need just one argument."); + let tags = suggestions.get_suggestions(&file_str); + let sample = add_sample(connection, Path::new(&file_str), tags); + dbg!(sample); +} diff --git a/src/bin/list_samples.rs b/src/bin/list_samples.rs new file mode 100644 index 0000000..00ca965 --- /dev/null +++ b/src/bin/list_samples.rs @@ -0,0 +1,7 @@ +fn main() { + let connection = &mut establish_connection(); + println!( + "Found tags: {}", + list_tags(connection).iter().map(|t| &t.name).join(", ") + ); +} diff --git a/src/bin/list_tags.rs b/src/bin/list_tags.rs new file mode 100644 index 0000000..0a7c70c --- /dev/null +++ b/src/bin/list_tags.rs @@ -0,0 +1,10 @@ +use itertools::Itertools; +use sample_amp::{establish_connection, list_tags}; + +fn main() { + let connection = &mut establish_connection(); + println!( + "Found tags: {}", + list_tags(connection).iter().map(|t| &t.name).join(", ") + ); +} diff --git a/src/lib.rs b/src/lib.rs index 552f7be..9ab9099 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,9 @@ use diesel::prelude::*; use dotenvy::dotenv; -use std::env; +use models::{NewSample, Sample, Tag}; +use std::{env, path::Path}; -use crate::models::{Library, NewLibrary}; +use crate::models::{Library, NewLibrary, NewSampleTag, NewTag, SampleTag}; pub mod models; pub mod schema; @@ -27,3 +28,80 @@ pub fn create_library(conn: &mut SqliteConnection, name: &str) -> Library { .get_result(conn) .expect("Error saving new post") } + +pub fn get_tag<'a>(conn: &mut SqliteConnection, tag: &'a str) -> Option { + use crate::schema::tags::dsl::*; + tags.filter(name.eq(&tag)) + .select(Tag::as_select()) + .first(conn) + .ok() +} + +pub fn list_tags(conn: &mut SqliteConnection) -> Vec { + use crate::schema::tags::dsl::*; + tags.select(Tag::as_select()) + .get_results(conn) + .expect("can't list tags") +} + +pub fn samples_with_tag<'a>(conn: &mut SqliteConnection, tag: &'a str) -> Vec { + let tag = get_tag(conn, tag).expect("Expected tag to exist..."); + + let sample_ids = SampleTag::belonging_to(&tag).select(schema::samples_tags::sample_id); + schema::samples::table + .filter(schema::samples::id.eq_any(sample_ids)) + .select(Sample::as_select()) + .get_results(conn) + .expect("expected sample") +} + +pub fn add_sample<'a>( + conn: &mut SqliteConnection, + path: &Path, + tags: impl Iterator, +) -> (Sample, Vec) { + use crate::schema::{samples, samples_tags, tags}; + + let name = path.file_name().expect("expected file name"); + let new_sample = NewSample { + name: &name.to_string_lossy(), + path: &path.to_string_lossy(), + bpm: None, + key: None, + }; + + let sample = diesel::insert_into(samples::table) + .values(&new_sample) + .returning(Sample::as_returning()) + .get_result(conn) + .expect("Error saving new sample"); + + let tags: Vec = tags + .into_iter() + .map(|tag_name| { + let tag = get_tag(conn, tag_name).unwrap_or_else(|| { + let tag = NewTag { name: tag_name }; + diesel::insert_into(tags::table) + .values(&tag) + .returning(Tag::as_returning()) + .get_result(conn) + .expect("Error saving tag") + }); + + let sample_tag = NewSampleTag { + tag_id: tag.id, + sample_id: sample.id, + }; + + diesel::insert_into(samples_tags::table) + .values(&sample_tag) + .returning(SampleTag::as_returning()) + .get_result(conn) + .expect("Error adding sample_tag"); + + tag + }) + .collect(); + + (sample, tags) +} diff --git a/src/models.rs b/src/models.rs index 34c60fa..864641a 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,7 +1,7 @@ -use super::schema::libraries; +use super::schema::{libraries, samples, samples_tags, tags}; use diesel::prelude::*; -#[derive(Queryable, Selectable)] +#[derive(Debug, Queryable, Selectable)] #[diesel(table_name = libraries)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct Library { @@ -9,8 +9,57 @@ pub struct Library { pub name: String, } -#[derive(Insertable)] +#[derive(Debug, Insertable)] #[diesel(table_name = libraries)] pub struct NewLibrary<'a> { pub name: &'a str, } + +#[derive(Debug, Identifiable, Queryable, Selectable)] +#[diesel(table_name = samples)] +pub struct Sample { + pub id: i32, + pub name: String, + pub path: String, + pub bpm: Option, + pub key: Option, // For now this seems to be reasonable, instead of creating a giant enum. +} + +#[derive(Debug, Insertable)] +#[diesel(table_name = samples)] +pub struct NewSample<'a> { + pub name: &'a str, + pub path: &'a str, + pub bpm: Option, + pub key: Option<&'a str>, +} + +#[derive(Debug, Identifiable, Queryable, Selectable)] +#[diesel(table_name = tags)] +pub struct Tag { + pub id: i32, + pub name: String, +} + +#[derive(Debug, Insertable)] +#[diesel(table_name = tags)] +pub struct NewTag<'a> { + pub name: &'a str, +} + +#[derive(Debug, Identifiable, Associations, Queryable, Selectable)] +#[diesel(belongs_to(Sample))] +#[diesel(belongs_to(Tag))] +#[diesel(table_name = samples_tags)] +pub struct SampleTag { + pub id: i32, + pub sample_id: i32, + pub tag_id: i32, +} + +#[derive(Debug, Insertable)] +#[diesel(table_name = samples_tags)] +pub struct NewSampleTag { + pub sample_id: i32, + pub tag_id: i32, +} diff --git a/src/schema.rs b/src/schema.rs index b9c4e5f..47c66b0 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -6,3 +6,33 @@ diesel::table! { name -> Text, } } + +diesel::table! { + samples (id) { + id -> Integer, + name -> Text, + path -> Text, + bpm -> Nullable, + key -> Nullable, + } +} + +diesel::table! { + samples_tags (id) { + id -> Integer, + sample_id -> Integer, + tag_id -> Integer, + } +} + +diesel::table! { + tags (id) { + id -> Integer, + name -> Text, + } +} + +diesel::joinable!(samples_tags -> samples (sample_id)); +diesel::joinable!(samples_tags -> tags (tag_id)); + +diesel::allow_tables_to_appear_in_same_query!(libraries, samples, samples_tags, tags,); diff --git a/src/tag_terms.rs b/src/tag_terms.rs index ca9c77f..35250fd 100644 --- a/src/tag_terms.rs +++ b/src/tag_terms.rs @@ -54,13 +54,10 @@ impl TagSuggestions { self.map.get(input).unwrap_or(input) } - pub fn get_suggestions<'a>( - &'a self, - input_list: impl IntoIterator, - ) -> impl Iterator { - input_list - .into_iter() - .filter_map(|i| self.map.get(i)) + pub fn get_suggestions<'a>(&'a self, input: &'a str) -> impl Iterator { + input + .split([' ', std::path::MAIN_SEPARATOR, '_', '-', '.']) // todo: pascal case detection + .filter_map(|i| self.map.get(&i.to_lowercase())) .unique() } }