[READ-ONLY] Mirror of https://github.com/probablykasper/ferrum. Music library app for Mac, Linux and Windows ferrum.kasper.space
electron linux macos music music-library music-player napi windows
0

Configure Feed

Select the types of activity you want to include in your feed.

Add sqlite schema

Kasper (Jun 9, 2026, 6:24 AM +0200) 63ec0262 1d044681

+140 -18
+2
src-native/lib.rs
··· 26 26 mod itunes_import; 27 27 pub mod library; 28 28 pub mod library_types; 29 + #[cfg(feature = "napi-rs")] 30 + mod migrate; 29 31 pub mod page; 30 32 #[cfg(feature = "napi-rs")] 31 33 pub mod playlists;
+8 -18
src-native/library.rs
··· 3 3 #[cfg(feature = "napi-rs")] 4 4 use crate::data_js::get_data; 5 5 use crate::library_types::{ItemId, Library, SpecialTrackListName, TrackList, VersionedLibrary}; 6 + #[cfg(feature = "napi-rs")] 7 + use crate::migrate::migrate_to_sqlite; 6 8 use anyhow::{Context, Result, bail}; 7 9 use linked_hash_map::LinkedHashMap; 8 10 #[cfg(feature = "napi-rs")] ··· 10 12 use serde_json::{Value, json}; 11 13 use sqlx::SqliteConnection; 12 14 use sqlx::{ConnectOptions, sqlite::SqliteConnectOptions}; 13 - use sqlx::{Sqlite, migrate::MigrateDatabase}; 14 15 use std::fs::File; 15 16 use std::io::{ErrorKind, Read, Seek, SeekFrom}; 16 17 #[cfg(feature = "napi-rs")] ··· 67 68 68 69 let exists = Path::new(&library_sqlite).exists(); 69 70 if !exists { 70 - migrate_to_sqlite(paths); 71 + rt.block_on(migrate_to_sqlite(paths))?; 71 72 } 72 - let connection = rt 73 + let mut connection = rt 73 74 .block_on( 74 75 SqliteConnectOptions::new() 75 76 .filename(&paths.library_sqlite) ··· 77 78 ) 78 79 .context("Error connecting to library database")?; 79 80 81 + rt.block_on(sqlx::migrate!("./src-native/migrations").run(&mut connection)) 82 + .map_err(|e| anyhow::anyhow!("{:?}", e)) 83 + .context("Could not run database migrations")?; 84 + 80 85 println!("Open library: {}ms", now.elapsed().as_millis()); 81 86 Ok(connection) 82 - } 83 - 84 - #[cfg(feature = "napi-rs")] 85 - pub fn migrate_to_sqlite(paths: &Paths) -> Result<()> { 86 - let rt = Runtime::new().context("Error creating tokio runtime")?; 87 - let old_library = match load_old_library_json(&paths.library_json)? { 88 - None => { 89 - rt.block_on(Sqlite::create_database(&paths.library_sqlite)) 90 - .context("Could not create library database")?; 91 - return Ok(()); 92 - } 93 - Some(old_library) => old_library, 94 - }; 95 - 96 - todo!("Save database to disk after migrating"); 97 87 } 98 88 99 89 pub fn load_old_library_json(library_json: &str) -> Result<Option<Library>> {
+41
src-native/migrate.rs
··· 1 + use crate::{ 2 + library::{Paths, load_old_library_json}, 3 + library_types::Library, 4 + }; 5 + use anyhow::{Context, Result}; 6 + use sqlx::{ 7 + ConnectOptions, Connection, Sqlite, migrate::MigrateDatabase, sqlite::SqliteConnectOptions, 8 + }; 9 + 10 + pub async fn migrate_to_sqlite(paths: &Paths) -> Result<()> { 11 + let old_library = match load_old_library_json(&paths.library_json)? { 12 + None => { 13 + return Ok(()); 14 + } 15 + Some(old_library) => old_library, 16 + }; 17 + 18 + Sqlite::create_database(&paths.library_sqlite) 19 + .await 20 + .context("Could not create library database")?; 21 + 22 + let mut connection = SqliteConnectOptions::new() 23 + .filename(&paths.library_sqlite) 24 + .connect() 25 + .await 26 + .context("Error connecting to created library database")?; 27 + 28 + sqlx::migrate!("./src-native/migrations") 29 + .run(&mut connection) 30 + .await 31 + .context("Could not run database migrations")?; 32 + 33 + todo!("Save library to database"); 34 + 35 + connection 36 + .close() 37 + .await 38 + .context("Could not save/close database")?; 39 + 40 + Ok(()) 41 + }
+89
src-native/migrations/1_start.sql
··· 1 + create table tracks ( 2 + id TEXT PRIMARY KEY, 3 + filesize INTEGER NOT NULL, -- u64 (or i64 for js) 4 + duration_s REAL NOT NULL, -- f64 5 + bitrate REAL NOT NULL, -- f64 6 + sample_rate REAL NOT NULL, -- f64 7 + file TEXT NOT NULL, -- f64 8 + modified_at INTEGER NOT NULL, -- u64 ms since unix epoch 9 + added_at INTEGER NOT NULL, -- u64 ms since unix epoch 10 + name TEXT NOT NULL, 11 + artist TEXT NOT NULL, 12 + imported_from TEXT NULL, 13 + original_id TEXT NULL, -- Imported ID, like iTunes Persistent ID 14 + composer TEXT NULL, 15 + sort_name TEXT NULL, 16 + sort_artist TEXT NULL, 17 + sort_composer TEXT NULL, 18 + genre TEXT NULL, 19 + rating_pct INTEGER NULL, -- from 0 to 100 20 + year INTEGER NULL, -- i64 21 + bpm REAL NULL, -- f64 22 + comments TEXT NULL, 23 + grouping TEXT NULL, 24 + liked BOOLEAN NULL, 25 + disliked BOOLEAN NULL, 26 + disabled BOOLEAN NULL, 27 + compilation BOOLEAN NULL, 28 + album_name TEXT NULL, 29 + album_artist TEXT NULL, 30 + sort_album_name TEXT NULL, 31 + sort_album_artist TEXT NULL, 32 + track_num INTEGER NULL, -- u32 33 + track_count INTEGER NULL, -- u32 34 + disc_num INTEGER NULL, -- u32 35 + disc_count INTEGER NULL, -- u32 36 + imported_at INTEGER NULL, 37 + play_count INTEGER NULL, -- u32 38 + skip_count INTEGER NULL, -- u32 39 + volume INTEGER NULL -- from -100 to 100 40 + ); 41 + 42 + CREATE TABLE plays ( 43 + id TEXT PRIMARY KEY, 44 + track_id TEXT NOT NULL REFERENCES tracks(id), 45 + date INTEGER NULL, 46 + date_range_to INTEGER NULL, 47 + CHECK ((date IS NULL) != (date_range_to IS NULL)) 48 + ); 49 + 50 + CREATE TABLE skips ( 51 + id TEXT PRIMARY KEY, 52 + track_id TEXt NOT NULL REFERENCES tracks(id), 53 + date INTEGER NULL, 54 + date_range_to INTEGER NULL, 55 + CHECK ((date IS NULL) != (date_range_to IS NULL)) 56 + ); 57 + 58 + CREATE TABLE track_lists ( 59 + id TEXT PRIMARY KEY, 60 + type TEXT NOT NULL CHECK (type IN ('playlist', 'folder', 'special')), 61 + parent_id TEXT NULL REFERENCES track_lists(id), 62 + position INTEGER NULL, 63 + name TEXT NOT NULL, 64 + description TEXT NOT NULL, 65 + liked BOOLEAN NOT NULL DEFAULT 0, 66 + disliked BOOLEAN NOT NULL DEFAULT 0, 67 + imported_from TEXT NULL, -- For example "itunes" 68 + original_id TEXT NULL, -- For example iTunes Persistent ID 69 + imported_at INTEGER NULL, 70 + created_at INTEGER NOT NULL 71 + ); 72 + 73 + CREATE TABLE playlist_tracks ( 74 + track_list_id TEXT NOT NULL REFERENCES track_lists(id), 75 + track_id TEXT NOT NULL REFERENCES tracks(id), 76 + position INTEGER NOT NULL, 77 + PRIMARY KEY (track_list_id, position) 78 + ); 79 + 80 + CREATE TABLE play_times ( 81 + id TEXT PRIMARY KEY, 82 + track_id TEXT NOT NULL REFERENCES tracks(id), 83 + started_at INTEGER NOT NULL, 84 + duration INTEGER NOT NULL, 85 + -- v1 playtime has two issues: 86 + -- - some durations are double counted (or triple, etc.) 87 + -- - timestamps aren't updated after pausing 88 + is_v1 BOOLEAN NOT NULL DEFAULT 0 89 + );