···
155
155
"futures-lite",
156
156
"parking",
157
157
"polling",
158
158
-
"rustix 1.1.2",
158
158
+
"rustix 1.1.4",
159
159
"slab",
160
160
"windows-sys 0.61.0",
161
161
]
···
186
186
"cfg-if",
187
187
"event-listener 5.4.1",
188
188
"futures-lite",
189
189
-
"rustix 1.1.2",
189
189
+
"rustix 1.1.4",
190
190
]
191
191
192
192
[[package]]
···
212
212
"cfg-if",
213
213
"futures-core",
214
214
"futures-io",
215
215
-
"rustix 1.1.2",
215
215
+
"rustix 1.1.4",
216
216
"signal-hook-registry",
217
217
"slab",
218
218
"windows-sys 0.61.0",
···
1226
1226
"simd-json",
1227
1227
"specta",
1228
1228
"sqlx",
1229
1229
+
"tempfile",
1229
1230
"time",
1230
1231
"tokio",
1231
1232
"trash",
···
1582
1583
source = "registry+https://github.com/rust-lang/crates.io-index"
1583
1584
checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55"
1584
1585
dependencies = [
1585
1585
-
"rustix 1.1.2",
1586
1586
+
"rustix 1.1.4",
1586
1587
"windows-targets 0.52.6",
1587
1588
]
1588
1589
···
2387
2388
2388
2389
[[package]]
2389
2390
name = "libc"
2390
2390
-
version = "0.2.175"
2391
2391
+
version = "0.2.186"
2391
2392
source = "registry+https://github.com/rust-lang/crates.io-index"
2392
2392
-
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
2393
2393
+
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
2393
2394
2394
2395
[[package]]
2395
2396
name = "libloading"
···
2466
2467
2467
2468
[[package]]
2468
2469
name = "linux-raw-sys"
2469
2469
-
version = "0.11.0"
2470
2470
+
version = "0.12.1"
2470
2471
source = "registry+https://github.com/rust-lang/crates.io-index"
2471
2471
-
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
2472
2472
+
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
2472
2473
2473
2474
[[package]]
2474
2475
name = "litemap"
···
3519
3520
"concurrent-queue",
3520
3521
"hermit-abi",
3521
3522
"pin-project-lite",
3522
3522
-
"rustix 1.1.2",
3523
3523
+
"rustix 1.1.4",
3523
3524
"windows-sys 0.61.0",
3524
3525
]
3525
3526
···
3980
3981
3981
3982
[[package]]
3982
3983
name = "rustix"
3983
3983
-
version = "1.1.2"
3984
3984
+
version = "1.1.4"
3984
3985
source = "registry+https://github.com/rust-lang/crates.io-index"
3985
3985
-
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
3986
3986
+
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
3986
3987
dependencies = [
3987
3988
"bitflags 2.9.4",
3988
3989
"errno",
3989
3990
"libc",
3990
3990
-
"linux-raw-sys 0.11.0",
3991
3991
+
"linux-raw-sys 0.12.1",
3991
3992
"windows-sys 0.61.0",
3992
3993
]
3993
3994
···
5273
5274
5274
5275
[[package]]
5275
5276
name = "tempfile"
5276
5276
-
version = "3.22.0"
5277
5277
+
version = "3.27.0"
5277
5278
source = "registry+https://github.com/rust-lang/crates.io-index"
5278
5278
-
checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53"
5279
5279
+
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
5279
5280
dependencies = [
5280
5281
"fastrand",
5281
5281
-
"getrandom 0.3.3",
5282
5282
+
"getrandom 0.4.2",
5282
5283
"once_cell",
5283
5283
-
"rustix 1.1.2",
5284
5284
+
"rustix 1.1.4",
5284
5285
"windows-sys 0.61.0",
5285
5286
]
5286
5287
···
6938
6939
"hex",
6939
6940
"libc",
6940
6941
"ordered-stream",
6941
6941
-
"rustix 1.1.2",
6942
6942
+
"rustix 1.1.4",
6942
6943
"serde",
6943
6944
"serde_repr",
6944
6945
"tracing",
···
59
59
mimalloc = "0.1.48"
60
60
specta = { version = "=2.0.0-rc.22", features = ["derive"] }
61
61
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] }
62
62
+
tempfile = "3.27.0"
63
63
+
rusqlite = "0.40.1"
62
64
63
65
[target.'cfg(not(target_os = "android"))'.dependencies]
64
66
trash = "5.1"
···
1
1
+
use std::{env, path::PathBuf};
1
2
extern crate napi_build;
2
3
3
4
fn main() {
5
5
+
// Path to SQLite file relative to the crate root
6
6
+
let db_path = PathBuf::from("src-native/appdata/Library/Library-typegen.sqlite");
7
7
+
let abs_path = env::current_dir().unwrap().join(&db_path);
8
8
+
let database_url = format!("sqlite:///{}", abs_path.display());
9
9
+
println!("cargo:rustc-env=DATABASE_URL={}", database_url);
10
10
+
4
11
#[cfg(not(target_os = "android"))]
5
12
napi_build::setup();
6
13
}
···
1
1
use crate::{
2
2
library::{Paths, load_old_library_json},
3
3
-
library_types::Library,
3
3
+
library_types::{Library, TrackList, TrackLists},
4
4
};
5
5
use anyhow::{Context, Result};
6
6
use sqlx::{
7
7
ConnectOptions, Connection, Sqlite, migrate::MigrateDatabase, sqlite::SqliteConnectOptions,
8
8
};
9
9
+
use tempfile::TempDir;
9
10
10
11
pub async fn migrate_to_sqlite(paths: &Paths) -> Result<()> {
11
12
let old_library = match load_old_library_json(&paths.library_json)? {
···
15
16
Some(old_library) => old_library,
16
17
};
17
18
18
18
-
Sqlite::create_database(&paths.library_sqlite)
19
19
+
let tmp_dir = TempDir::new().context("failed to create temp dir")?;
20
20
+
let tmp_db = tmp_dir.path().join("Library.sqlite");
21
21
+
22
22
+
Sqlite::create_database(&tmp_db.to_str().unwrap())
19
23
.await
20
24
.context("Could not create library database")?;
21
21
-
22
25
let mut connection = SqliteConnectOptions::new()
23
23
-
.filename(&paths.library_sqlite)
26
26
+
.filename(&tmp_db)
24
27
.connect()
25
28
.await
26
29
.context("Error connecting to created library database")?;
···
30
33
.await
31
34
.context("Could not run database migrations")?;
32
35
33
33
-
todo!("Save library to database");
36
36
+
insert_library_into_db(&old_library, &mut connection)
37
37
+
.await
38
38
+
.context("could not insert library into database")?;
34
39
35
40
connection
36
41
.close()
37
42
.await
38
43
.context("Could not save/close database")?;
44
44
+
45
45
+
std::fs::rename(&tmp_db, &paths.library_sqlite)
46
46
+
.context("Failed to finalize sqlite database")?;
39
47
40
48
Ok(())
41
49
}
50
50
+
51
51
+
async fn insert_library_into_db(
52
52
+
library: &Library,
53
53
+
conn: &mut sqlx::SqliteConnection,
54
54
+
) -> anyhow::Result<()> {
55
55
+
let mut tx = conn.begin().await?;
56
56
+
57
57
+
// --- tracks ---
58
58
+
for (id, track) in library.get_tracks() {
59
59
+
conn.execute(
60
60
+
"INSERT INTO tracks (
61
61
+
id, filesize, duration_s, bitrate, sample_rate, file, modified_at, added_at, name, artist, imported_from, original_id, composer, sort_name, sort_artist, sort_composer, genre, rating_pct, year, bpm, comments, grouping, liked, disliked, disabled, compilation, album_name, album_artist, sort_album_name, sort_album_artist, track_num, track_count, disc_num, disc_count, imported_at, play_count, skip_count, volume
62
62
+
) VALUES (
63
63
+
:id,:filesize,:duration_s,:bitrate,:sample_rate,:file,:modified_at,:added_at,:name,:artist,:imported_from,:original_id,:composer,:sort_name,:sort_artist,:sort_composer,:genre,:rating_pct,:year,:bpm,:comments,:grouping,:liked,:disliked,:disabled,:compilation,:album_name,:album_artist,:sort_album_name,:sort_album_artist,:track_num,:track_count,:disc_num,:disc_count,:imported_at,:play_count,:skip_count,:volume
64
64
+
)",
65
65
+
named_params! {
66
66
+
":id": track.id,
67
67
+
":filesize": track.filesize,
68
68
+
":duration_s": track.duration_s,
69
69
+
":bitrate": track.bitrate,
70
70
+
":sample_rate": track.sample_rate,
71
71
+
":file": track.file,
72
72
+
":modified_at": track.modified_at,
73
73
+
":added_at": track.added_at,
74
74
+
":name": track.name,
75
75
+
":artist": track.artist,
76
76
+
":imported_from": track.imported_from,
77
77
+
":original_id": track.original_id,
78
78
+
":composer": track.composer,
79
79
+
":sort_name": track.sort_name,
80
80
+
":sort_artist": track.sort_artist,
81
81
+
":sort_composer": track.sort_composer,
82
82
+
":genre": track.genre,
83
83
+
":rating_pct": track.rating_pct,
84
84
+
":year": track.year,
85
85
+
":bpm": track.bpm,
86
86
+
":comments": track.comments,
87
87
+
":grouping": track.grouping,
88
88
+
":liked": track.liked,
89
89
+
":disliked": track.disliked,
90
90
+
":disabled": track.disabled,
91
91
+
":compilation": track.compilation,
92
92
+
":album_name": track.album_name,
93
93
+
":album_artist": track.album_artist,
94
94
+
":sort_album_name": track.sort_album_name,
95
95
+
":sort_album_artist": track.sort_album_artist,
96
96
+
":track_num": track.track_num,
97
97
+
":track_count": track.track_count,
98
98
+
":disc_num": track.disc_num,
99
99
+
":disc_count": track.disc_count,
100
100
+
":imported_at": track.imported_at,
101
101
+
":play_count": track.play_count,
102
102
+
":skip_count": track.skip_count,
103
103
+
":volume": track.volume,
104
104
+
105
105
+
},
106
106
+
)?;
107
107
+
sqlx::query!(
108
108
+
r#"
109
109
+
INSERT INTO tracks (
110
110
+
id
111
111
+
filesize
112
112
+
duration_s
113
113
+
bitrate
114
114
+
sample_rate
115
115
+
file
116
116
+
modified_at
117
117
+
added_at
118
118
+
name
119
119
+
artist
120
120
+
imported_from
121
121
+
original_id
122
122
+
composer
123
123
+
sort_name
124
124
+
sort_artist
125
125
+
sort_composer
126
126
+
genre
127
127
+
rating_pct
128
128
+
year
129
129
+
bpm
130
130
+
comments
131
131
+
grouping
132
132
+
liked
133
133
+
disliked
134
134
+
disabled
135
135
+
compilation
136
136
+
album_name
137
137
+
album_artist
138
138
+
sort_album_name
139
139
+
sort_album_artist
140
140
+
track_num
141
141
+
track_count
142
142
+
disc_num
143
143
+
disc_count
144
144
+
imported_at
145
145
+
play_count
146
146
+
skip_count
147
147
+
volume
148
148
+
) VALUES (
149
149
+
?1, ?2, ?3, ?4, ?5, ?6,
150
150
+
?7, ?8, ?9, ?10,
151
151
+
?11, ?12, ?13,
152
152
+
?14, ?15, ?16,
153
153
+
?17, ?18, ?19, ?20, ?21, ?22,
154
154
+
?23, ?24, ?25, ?26,
155
155
+
?27, ?28, ?29, ?30,
156
156
+
?31, ?32, ?33, ?34,
157
157
+
?35, ?36, ?37, ?38
158
158
+
)
159
159
+
"#,
160
160
+
id,
161
161
+
track.size,
162
162
+
track.duration,
163
163
+
track.bitrate,
164
164
+
track.sampleRate,
165
165
+
track.file,
166
166
+
track.dateModified,
167
167
+
track.dateAdded,
168
168
+
track.name,
169
169
+
track.artist,
170
170
+
track.importedFrom,
171
171
+
track.originalId,
172
172
+
track.composer,
173
173
+
track.sortName,
174
174
+
track.sortArtist,
175
175
+
track.sortComposer,
176
176
+
track.genre,
177
177
+
track.rating,
178
178
+
track.year,
179
179
+
track.bpm,
180
180
+
track.comments,
181
181
+
track.grouping,
182
182
+
track.liked,
183
183
+
track.disliked,
184
184
+
track.disabled,
185
185
+
track.compilation,
186
186
+
track.albumName,
187
187
+
track.albumArtist,
188
188
+
track.sortAlbumName,
189
189
+
track.sortAlbumArtist,
190
190
+
track.trackNum,
191
191
+
track.trackCount,
192
192
+
track.discNum,
193
193
+
track.discCount,
194
194
+
track.dateImported,
195
195
+
track.playCount,
196
196
+
track.skipCount,
197
197
+
track.volume,
198
198
+
)
199
199
+
.execute(&mut *tx)
200
200
+
.await
201
201
+
.with_context(|| format!("Failed to insert track {id}"))?;
202
202
+
203
203
+
// // Individual play timestamps (known exact times)
204
204
+
// if let Some(plays) = &track.plays {
205
205
+
// for &date in plays {
206
206
+
// sqlx::query!(
207
207
+
// "INSERT INTO plays (track_id, date, date_range_to) VALUES (?1, ?2, NULL)",
208
208
+
// id,
209
209
+
// date,
210
210
+
// )
211
211
+
// .execute(&mut *tx)
212
212
+
// .await?;
213
213
+
// }
214
214
+
// }
215
215
+
216
216
+
// // Imported play ranges (CountObject = fromDate..toDate with a count,
217
217
+
// // but the schema only stores one row per range — store as a range row
218
218
+
// // and ignore count since the schema has no count column)
219
219
+
// if let Some(imported) = &track.playsImported {
220
220
+
// for co in imported {
221
221
+
// sqlx::query!(
222
222
+
// "INSERT INTO plays (track_id, date, date_range_to) VALUES (?1, ?2, ?3)",
223
223
+
// id,
224
224
+
// co.fromDate,
225
225
+
// co.toDate,
226
226
+
// )
227
227
+
// .execute(&mut *tx)
228
228
+
// .await?;
229
229
+
// }
230
230
+
// }
231
231
+
232
232
+
// // Individual skip timestamps
233
233
+
// if let Some(skips) = &track.skips {
234
234
+
// for &date in skips {
235
235
+
// sqlx::query!(
236
236
+
// "INSERT INTO skips (track_id, date, date_range_to) VALUES (?1, ?2, NULL)",
237
237
+
// id,
238
238
+
// date,
239
239
+
// )
240
240
+
// .execute(&mut *tx)
241
241
+
// .await?;
242
242
+
// }
243
243
+
// }
244
244
+
245
245
+
// // Imported skip ranges
246
246
+
// if let Some(imported) = &track.skipsImported {
247
247
+
// for co in imported {
248
248
+
// sqlx::query!(
249
249
+
// "INSERT INTO skips (track_id, date, date_range_to) VALUES (?1, ?2, ?3)",
250
250
+
// id,
251
251
+
// co.fromDate,
252
252
+
// co.toDate,
253
253
+
// )
254
254
+
// .execute(&mut *tx)
255
255
+
// .await?;
256
256
+
// }
257
257
+
// }
258
258
+
// }
259
259
+
260
260
+
// // --- track_lists (folders, playlists, specials) ---
261
261
+
// // We need parent_id, which requires a first pass to build the parent map.
262
262
+
// let parent_map = build_parent_map(&library.trackLists);
263
263
+
264
264
+
// for (list_id, tracklist) in &library.trackLists {
265
265
+
// let parent_id = parent_map.get(list_id.as_str()).map(|s| s.as_str());
266
266
+
// let position = get_position_in_parent(list_id, &library.trackLists);
267
267
+
268
268
+
// match tracklist {
269
269
+
// TrackList::Special(s) => {
270
270
+
// let name = s.name.to_string();
271
271
+
// sqlx::query!(
272
272
+
// r#"INSERT INTO track_lists
273
273
+
// (id, type, parent_id, position, name, description, liked, disliked,
274
274
+
// imported_from, original_id, imported_at, created_at)
275
275
+
// VALUES (?1,'special',?2,?3,?4,'',0,0,NULL,NULL,NULL,?5)"#,
276
276
+
// s.id,
277
277
+
// parent_id,
278
278
+
// position,
279
279
+
// name,
280
280
+
// s.dateCreated,
281
281
+
// )
282
282
+
// .execute(&mut *tx)
283
283
+
// .await?;
284
284
+
// }
285
285
+
// TrackList::Folder(f) => {
286
286
+
// sqlx::query!(
287
287
+
// r#"INSERT INTO track_lists
288
288
+
// (id, type, parent_id, position, name, description, liked, disliked,
289
289
+
// imported_from, original_id, imported_at, created_at)
290
290
+
// VALUES (?1,'folder',?2,?3,?4,?5,?6,?7,?8,?9,?10,?11)"#,
291
291
+
// f.id,
292
292
+
// parent_id,
293
293
+
// position,
294
294
+
// f.name,
295
295
+
// f.description,
296
296
+
// f.liked,
297
297
+
// f.disliked,
298
298
+
// f.importedFrom,
299
299
+
// f.originalId,
300
300
+
// f.dateImported,
301
301
+
// f.dateCreated,
302
302
+
// )
303
303
+
// .execute(&mut *tx)
304
304
+
// .await?;
305
305
+
// }
306
306
+
// TrackList::Playlist(p) => {
307
307
+
// sqlx::query!(
308
308
+
// r#"INSERT INTO track_lists
309
309
+
// (id, type, parent_id, position, name, description, liked, disliked,
310
310
+
// imported_from, original_id, imported_at, created_at)
311
311
+
// VALUES (?1,'playlist',?2,?3,?4,?5,?6,?7,?8,?9,?10,?11)"#,
312
312
+
// p.id,
313
313
+
// parent_id,
314
314
+
// position,
315
315
+
// p.name,
316
316
+
// p.description,
317
317
+
// p.liked,
318
318
+
// p.disliked,
319
319
+
// p.importedFrom,
320
320
+
// p.originalId,
321
321
+
// p.dateImported,
322
322
+
// p.dateCreated,
323
323
+
// )
324
324
+
// .execute(&mut *tx)
325
325
+
// .await?;
326
326
+
327
327
+
// // playlist_tracks rows
328
328
+
// for (pos, track_id) in p.get_track_ids().iter().enumerate() {
329
329
+
// let pos = pos as i64;
330
330
+
// sqlx::query!(
331
331
+
// "INSERT INTO playlist_tracks (track_list_id, track_id, position)
332
332
+
// VALUES (?1, ?2, ?3)",
333
333
+
// p.id,
334
334
+
// track_id,
335
335
+
// pos,
336
336
+
// )
337
337
+
// .execute(&mut *tx)
338
338
+
// .await?;
339
339
+
// }
340
340
+
// }
341
341
+
// }
342
342
+
}
343
343
+
344
344
+
// // --- play_times (v1 and v2) ---
345
345
+
// for (track_id, started_at, duration) in &library.v1PlayTime {
346
346
+
// sqlx::query!(
347
347
+
// "INSERT INTO play_times (track_id, started_at, duration, is_v1) VALUES (?1,?2,?3,1)",
348
348
+
// track_id,
349
349
+
// started_at,
350
350
+
// duration,
351
351
+
// )
352
352
+
// .execute(&mut *tx)
353
353
+
// .await?;
354
354
+
// }
355
355
+
// for (track_id, started_at, duration) in &library.playTime {
356
356
+
// sqlx::query!(
357
357
+
// "INSERT INTO play_times (track_id, started_at, duration, is_v1) VALUES (?1,?2,?3,0)",
358
358
+
// track_id,
359
359
+
// started_at,
360
360
+
// duration,
361
361
+
// )
362
362
+
// .execute(&mut *tx)
363
363
+
// .await?;
364
364
+
// }
365
365
+
366
366
+
tx.commit()
367
367
+
.await
368
368
+
.context("Failed to commit migration transaction")?;
369
369
+
Ok(())
370
370
+
}
371
371
+
372
372
+
// /// Returns a map of child_id -> parent_id for every tracklist entry.
373
373
+
// fn build_parent_map(track_lists: &TrackLists) -> std::collections::HashMap<String, String> {
374
374
+
// let mut map = std::collections::HashMap::new();
375
375
+
// for (parent_id, tl) in track_lists {
376
376
+
// let children = match tl {
377
377
+
// TrackList::Folder(f) => &f.children,
378
378
+
// TrackList::Special(s) => &s.children,
379
379
+
// TrackList::Playlist(_) => continue,
380
380
+
// };
381
381
+
// for child_id in children {
382
382
+
// map.insert(child_id.clone(), parent_id.clone());
383
383
+
// }
384
384
+
// }
385
385
+
// map
386
386
+
// }
387
387
+
388
388
+
// /// Returns the 0-based position of `id` in its parent's children list, or None.
389
389
+
// fn get_position_in_parent(id: &str, track_lists: &TrackLists) -> Option<i64> {
390
390
+
// for tl in track_lists.values() {
391
391
+
// let children = match tl {
392
392
+
// TrackList::Folder(f) => &f.children,
393
393
+
// TrackList::Special(s) => &s.children,
394
394
+
// TrackList::Playlist(_) => continue,
395
395
+
// };
396
396
+
// if let Some(pos) = children.iter().position(|c| c == id) {
397
397
+
// return Some(pos as i64);
398
398
+
// }
399
399
+
// }
400
400
+
// None
401
401
+
// }