feat: add more scanned properties, switch over to diesel from rusqlite (with first migration)

This commit is contained in:
skybldev
2026-10-23 17:53:16 -04:00
parent 9b89ea803b
commit f62a99feb6
11 changed files with 415 additions and 98 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
DATABASE_URL=data/tuneparty.db

249
Cargo.lock generated
View File

@@ -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",
]

View File

@@ -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"

9
diesel.toml Normal file
View File

@@ -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"

View File

@@ -0,0 +1,3 @@
DROP TABLE artists;
DROP TABLE collections;
DROP TABLE tracks;

View File

@@ -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)
);

View File

@@ -46,18 +46,31 @@ pub fn scan(path: &Path) -> Result<Vec<ScannedTrack>, 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<Vec<ScannedTrack>, ScanError> {
Ok(tracks)
}

View File

@@ -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);
}

67
src/models.rs Normal file
View File

@@ -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<i32>,
pub original_title: Option<String>,
pub display_title: Option<String>,
pub track_number: Option<i32>,
pub genre: Option<String>,
pub track_datetime: Option<String>,
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<i32>,
pub original_title: Option<&'a str>,
pub display_title: Option<&'a str>,
pub track_number: Option<i32>,
pub genre: Option<&'a str>,
pub track_datetime: Option<&'a str>,
pub length_sec: i32,
}

37
src/schema.rs Normal file
View File

@@ -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<Integer>,
original_title -> Nullable<Text>,
display_title -> Nullable<Text>,
track_number -> Nullable<Integer>,
genre -> Nullable<Text>,
track_datetime -> Nullable<Text>,
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,);

View File

@@ -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<Artist>,
collection: Option<Collection>,
title: Option<String>,
}
#[derive(Debug)]
pub struct ScannedTrackMetadata {
pub artist: Option<String>,
pub collection: Option<String>,
pub title: Option<String>,
pub number: Option<u32>,
pub genre: Option<String>,
pub datetime: Option<Timestamp>,
pub length_sec: u32
}
#[derive(Debug)]