From f62a99feb676eb6771e088695cd3dbc2430dd33d Mon Sep 17 00:00:00 2001 From: skybldev Date: Fri, 23 Oct 2026 17:53:16 -0400 Subject: [PATCH] feat: add more scanned properties, switch over to diesel from rusqlite (with first migration) --- .env | 1 + Cargo.lock | 249 ++++++++++++++---- Cargo.toml | 3 +- diesel.toml | 9 + .../down.sql | 3 + .../up.sql | 30 +++ src/library_scan.rs | 14 + src/main.rs | 84 +++--- src/models.rs | 67 +++++ src/schema.rs | 37 +++ src/types.rs | 16 +- 11 files changed, 415 insertions(+), 98 deletions(-) create mode 100644 .env create mode 100644 diesel.toml create mode 100644 migrations/2026-10-23-174301-0000_create_artists_collections_tracks/down.sql create mode 100644 migrations/2026-10-23-174301-0000_create_artists_collections_tracks/up.sql create mode 100644 src/models.rs create mode 100644 src/schema.rs diff --git a/.env b/.env new file mode 100644 index 0000000..e89f9ac --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=data/tuneparty.db \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index dfeaea6..81d5880 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,12 +8,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "bitflags" -version = "2.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" - [[package]] name = "bumpalo" version = "3.20.3" @@ -51,6 +45,41 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[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", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + [[package]] name = "data-encoding" version = "2.11.1" @@ -58,16 +87,77 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] -name = "fallible-iterator" -version = "0.3.0" +name = "deranged" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" [[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" +name = "diesel" +version = "2.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +checksum = "ebe9f7eaef33febd60290c5a9f3b0571d03c4c04a24739cd64808ce30e65a5d3" +dependencies = [ + "diesel_derives", + "downcast-rs", + "libsqlite3-sys", + "sqlite-wasm-rs", + "time", +] + +[[package]] +name = "diesel_derives" +version = "2.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecbd51fb6c020672543641167efa4e6417ff7ad76849ed556ace3595e72de03a" +dependencies = [ + "diesel_table_macro_syntax", + "dsl_auto_type", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe2444076b48641147115697648dc743c2c00b61adade0f01ce67133c7babe8c" +dependencies = [ + "syn 2.0.119", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + +[[package]] +name = "dsl_auto_type" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd122633e4bef06db27737f21d3738fb89c8f6d5360d6d9d7635dda142a7757e" +dependencies = [ + "darling", + "either", + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" [[package]] name = "find-msvc-tools" @@ -86,6 +176,12 @@ dependencies = [ "zlib-rs", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.2.0" @@ -102,22 +198,16 @@ dependencies = [ ] [[package]] -name = "hashbrown" -version = "0.17.1" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -dependencies = [ - "foldhash", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "hashlink" -version = "0.12.2" +name = "ident_case" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb" -dependencies = [ - "hashbrown 0.17.1", -] +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "js-sys" @@ -162,7 +252,7 @@ checksum = "fdbca1b60ba7cea959ffc84ac27f3dbdbacd6dfda168c7eabbc00e48654bb9fd" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.9", ] [[package]] @@ -181,6 +271,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "ogg_pager" version = "0.7.2" @@ -208,6 +304,12 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.107" @@ -232,31 +334,36 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" dependencies = [ - "hashbrown 0.16.1", + "hashbrown", "thiserror", ] -[[package]] -name = "rusqlite" -version = "0.40.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3" -dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", - "sqlite-wasm-rs", -] - [[package]] name = "rustversion" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "shlex" version = "2.0.1" @@ -269,12 +376,6 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" -[[package]] -name = "smallvec" -version = "1.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" - [[package]] name = "sqlite-wasm-rs" version = "0.5.5" @@ -287,6 +388,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "3.0.9" @@ -315,15 +433,46 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.9", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", ] [[package]] name = "tuneparty_pre1" version = "0.1.0" dependencies = [ + "diesel", + "dotenvy", "lofty", - "rusqlite", ] [[package]] @@ -370,7 +519,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 3.0.9", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index a8039e4..6f5ed03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,6 @@ version = "0.1.0" edition = "2024" [dependencies] +diesel = { version = "2.3.14", features = ["sqlite", "returning_clauses_for_sqlite_3_35"] } +dotenvy = "0.15.7" lofty = "0.25.4" -rusqlite = "0.40.2" diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..a0d61bf --- /dev/null +++ b/diesel.toml @@ -0,0 +1,9 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] + +[migrations_directory] +dir = "migrations" diff --git a/migrations/2026-10-23-174301-0000_create_artists_collections_tracks/down.sql b/migrations/2026-10-23-174301-0000_create_artists_collections_tracks/down.sql new file mode 100644 index 0000000..f368c57 --- /dev/null +++ b/migrations/2026-10-23-174301-0000_create_artists_collections_tracks/down.sql @@ -0,0 +1,3 @@ +DROP TABLE artists; +DROP TABLE collections; +DROP TABLE tracks; \ No newline at end of file diff --git a/migrations/2026-10-23-174301-0000_create_artists_collections_tracks/up.sql b/migrations/2026-10-23-174301-0000_create_artists_collections_tracks/up.sql new file mode 100644 index 0000000..d6de1a7 --- /dev/null +++ b/migrations/2026-10-23-174301-0000_create_artists_collections_tracks/up.sql @@ -0,0 +1,30 @@ +CREATE TABLE artists ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + original_name TEXT NOT NULL, + display_name TEXT NOT NULL +); + +CREATE TABLE collections ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + collection_type TEXT NOT NULL, + original_name TEXT NOT NULL, + display_name TEXT NOT NULL +); + +CREATE TABLE tracks ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + artist_id INTEGER NOT NULL, + collection_id INTEGER, + original_title TEXT, + display_title TEXT, + track_number INTEGER, + genre TEXT, + track_datetime TEXT, + length_sec INTEGER NOT NULL, + CONSTRAINT fk_tracks_artist + FOREIGN KEY (artist_id) + REFERENCES artists (id), + CONSTRAINT fk_tracks_collection + FOREIGN KEY (collection_id) + REFERENCES collections (id) +); \ No newline at end of file diff --git a/src/library_scan.rs b/src/library_scan.rs index c614b9d..fec7281 100644 --- a/src/library_scan.rs +++ b/src/library_scan.rs @@ -46,18 +46,31 @@ pub fn scan(path: &Path) -> Result, ScanError> { path: entry_path.clone(), source, })?; + + let props = tagged_file.properties(); + + let track_length_sec = u32::try_from(props.duration().as_secs()) + .unwrap_or(u32::MAX); if let Some(tags) = tagged_file.primary_tag() { Ok(ScannedTrackMetadata { artist: tags.artist().as_deref().map(|tag| String::from(tag)), collection: tags.album().as_deref().map(|tag| String::from(tag)), title: tags.title().as_deref().map(|tag| String::from(tag)), + number: tags.track(), + genre: tags.genre().as_deref().map(|tag| String::from(tag)), + datetime: tags.date(), + length_sec: track_length_sec }) } else { Ok(ScannedTrackMetadata { artist: None, collection: None, title: None, + number: None, + genre: None, + datetime: None, + length_sec: track_length_sec }) } }(); @@ -76,3 +89,4 @@ pub fn scan(path: &Path) -> Result, ScanError> { Ok(tracks) } + diff --git a/src/main.rs b/src/main.rs index 0bb7e39..f20aba0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,57 +1,58 @@ use std::{fs::metadata, path::Path}; -use lofty::ogg::tag; -use rusqlite::{Connection, Result}; +use diesel::prelude::*; +use dotenvy::dotenv; +use std::env; mod library_scan; mod types; +mod models; +mod schema; -fn main() -> Result<()> { - println!("Hello, world!"); +fn open_db_file() -> SqliteConnection { + dotenv().ok(); - let conn = Connection::open_in_memory()?; + let database_url = env::var("DATABASE_URL") + .expect("DATABASE_URL missing"); - println!("Initializing database..."); + SqliteConnection::establish(&database_url) + .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)) +} - conn.execute( - "CREATE TABLE artist ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL - )", - (), - )?; - - conn.execute( - "CREATE TABLE collection ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL - )", - (), - )?; - - conn.execute( - "CREATE TABLE track ( - id INTEGER PRIMARY KEY, - artist INTEGER, - collection INTEGER, - title TEXT - )", - (), - )?; - - println!("Database initialized."); +fn open_db_in_memory() -> SqliteConnection { + SqliteConnection::establish(":memory:") + .unwrap_or_else(|_| panic!("Error starting in-memory database")) +} +fn import_scan(conn: &mut SqliteConnection) { for track in library_scan::scan(Path::new("./test/fixtures")).expect("oh no") { println!("-- {}", track.path.into_string().unwrap()); match track.metadata { Ok(metadata) => { - println!("artist: {}", metadata.artist.unwrap_or("unknown".into())); + println!("artist: {}", metadata.artist.as_deref().unwrap_or("unknown")); println!( "collection: {}", - metadata.collection.unwrap_or("unknown".into()) + metadata.collection.as_deref().unwrap_or("unknown") ); - println!("title: {}", metadata.title.unwrap_or("unknown".into())); + println!("title: {}", metadata.title.as_deref().unwrap_or("unknown")); + println!( + "number: {}", + metadata + .number + .map(|value| value.to_string()) + .unwrap_or_else(|| "unknown".to_owned()) + ); + println!("genre: {}", metadata.genre.as_deref().unwrap_or("unknown")); + println!( + "datetime: {}", + metadata + .datetime + .as_ref() + .map(|value| format!("{value:?}")) + .unwrap_or_else(|| "unknown".to_owned()) + ); + println!("length_sec: {}", metadata.length_sec); } Err(e) => { eprint!("{e}"); @@ -59,6 +60,13 @@ fn main() -> Result<()> { } println!(""); } - - Ok(()) +} + +fn main() { + println!("Hello, world!"); + + let conn = &mut open_db_in_memory(); + + import_scan(conn); + } diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..187692f --- /dev/null +++ b/src/models.rs @@ -0,0 +1,67 @@ +use diesel::prelude::*; + +use crate::schema::{artists, collections, tracks}; + +#[derive(Debug, Clone, Queryable, Selectable, Identifiable)] +#[diesel(table_name = artists)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct Artist { + pub id: i32, + pub original_name: String, + pub display_name: String, +} + +#[derive(Debug, Clone, Queryable, Selectable, Identifiable)] +#[diesel(table_name = collections)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct Collection { + pub id: i32, + pub collection_type: String, + pub original_name: String, + pub display_name: String, +} + +#[derive(Debug, Clone, Queryable, Selectable, Identifiable, Associations)] +#[diesel(table_name = tracks)] +#[diesel(belongs_to(Artist, foreign_key = artist_id))] +#[diesel(belongs_to(Collection, foreign_key = collection_id))] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct Track { + pub id: i32, + pub artist_id: i32, + pub collection_id: Option, + pub original_title: Option, + pub display_title: Option, + pub track_number: Option, + pub genre: Option, + pub track_datetime: Option, + pub length_sec: i32, +} + +#[derive(Debug, Clone, Insertable)] +#[diesel(table_name = artists)] +pub struct NewArtist<'a> { + pub original_name: &'a str, + pub display_name: &'a str, +} + +#[derive(Debug, Clone, Insertable)] +#[diesel(table_name = collections)] +pub struct NewCollection<'a> { + pub collection_type: &'a str, + pub original_name: &'a str, + pub display_name: &'a str, +} + +#[derive(Debug, Clone, Insertable)] +#[diesel(table_name = tracks)] +pub struct NewTrack<'a> { + pub artist_id: i32, + pub collection_id: Option, + pub original_title: Option<&'a str>, + pub display_title: Option<&'a str>, + pub track_number: Option, + pub genre: Option<&'a str>, + pub track_datetime: Option<&'a str>, + pub length_sec: i32, +} \ No newline at end of file diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..691f6c9 --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,37 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + artists (id) { + id -> Integer, + original_name -> Text, + display_name -> Text, + } +} + +diesel::table! { + collections (id) { + id -> Integer, + collection_type -> Text, + original_name -> Text, + display_name -> Text, + } +} + +diesel::table! { + tracks (id) { + id -> Integer, + artist_id -> Integer, + collection_id -> Nullable, + original_title -> Nullable, + display_title -> Nullable, + track_number -> Nullable, + genre -> Nullable, + track_datetime -> Nullable, + length_sec -> Integer, + } +} + +diesel::joinable!(tracks -> artists (artist_id)); +diesel::joinable!(tracks -> collections (collection_id)); + +diesel::allow_tables_to_appear_in_same_query!(artists, collections, tracks,); diff --git a/src/types.rs b/src/types.rs index 162fa56..891e6c7 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,7 +1,9 @@ pub mod library { use std::path::PathBuf; - use crate::types::errors; + use lofty::tag::items::Timestamp; + +use crate::types::errors; #[derive(Debug)] struct Artist { @@ -15,19 +17,15 @@ pub mod library { name: String, } - #[derive(Debug)] - struct Track { - id: u32, - artist: Option, - collection: Option, - title: Option, - } - #[derive(Debug)] pub struct ScannedTrackMetadata { pub artist: Option, pub collection: Option, pub title: Option, + pub number: Option, + pub genre: Option, + pub datetime: Option, + pub length_sec: u32 } #[derive(Debug)]