1mod albums;
2pub mod api_keys;
3pub mod app_passwords;
4mod artists;
5pub mod auth;
6pub mod batch;
7mod bookmarks;
8pub mod dsp;
9mod favourites;
10pub mod history;
11pub mod lyrics;
12mod play_queues;
13pub mod playback_state;
14pub mod playlists;
15mod ratings;
16mod scan_cache;
17pub mod scrobbling;
18mod search;
19pub mod shares;
20pub mod smart;
21pub(crate) mod sources;
22mod stats;
23pub mod tracks;
24pub mod uids;
25
26use std::path::PathBuf;
27
28pub use albums::*;
30pub use artists::*;
31pub use auth::LOCAL_USER;
32pub use batch::*;
33pub use bookmarks::*;
34pub use favourites::*;
35pub use history::*;
36pub use lyrics::*;
37pub use play_queues::*;
38pub use playback_state::*;
39pub use playlists::*;
40pub use ratings::*;
41pub use scan_cache::*;
42pub use search::*;
43pub use sources::sources_of_track;
44pub use stats::*;
45pub use tracks::*;
46pub use uids::*;
47
48pub fn write_transaction(
57 conn: &rusqlite::Connection,
58) -> rusqlite::Result<rusqlite::Transaction<'_>> {
59 rusqlite::Transaction::new_unchecked(conn, rusqlite::TransactionBehavior::Immediate)
60}
61
62pub fn atomically<T, E: From<rusqlite::Error>>(
70 conn: &rusqlite::Connection,
71 f: impl FnOnce() -> Result<T, E>,
72) -> Result<T, E> {
73 let (begin, commit, rollback) = if conn.is_autocommit() {
74 ("BEGIN IMMEDIATE", "COMMIT", "ROLLBACK")
75 } else {
76 (
77 "SAVEPOINT atomically",
78 "RELEASE atomically",
79 "ROLLBACK TO atomically; RELEASE atomically",
80 )
81 };
82 conn.execute_batch(begin)?;
83 let result = f();
84 match &result {
85 Ok(_) => conn.execute_batch(commit)?,
86 Err(_) => {
90 if let Err(e) = conn.execute_batch(rollback) {
91 log::warn!("rollback after a failed transaction: {e}");
92 }
93 }
94 }
95 result
96}
97
98pub fn json_list<T: serde::Serialize>(items: &[T]) -> String {
103 serde_json::to_string(items).unwrap_or_else(|_| "[]".into())
104}
105
106pub fn folder_prefix_range(folder: &std::path::Path) -> (String, String) {
119 let prefix = format!(
120 "{}{}",
121 folder
122 .to_string_lossy()
123 .trim_end_matches(std::path::MAIN_SEPARATOR),
124 std::path::MAIN_SEPARATOR
125 );
126 let upper = format!("{prefix}\u{10FFFF}");
127 (prefix, upper)
128}
129
130#[derive(Debug, Clone)]
133pub struct ArtistRow {
134 pub id: i64,
135 pub name: String,
136 pub sort_name: Option<String>,
137 pub remote_id: Option<String>,
138 pub album_count: i64,
142 pub track_count: i64,
143}
144
145#[derive(Debug, Clone)]
146pub struct AlbumRow {
147 pub id: i64,
148 pub title: String,
149 pub artist_id: i64,
150 pub artist_name: String,
151 pub date: Option<String>,
152 pub total_discs: Option<i32>,
153 pub total_tracks: Option<i32>,
154 pub codec: Option<String>,
155 pub label: Option<String>,
156 pub remote_id: Option<String>,
157 pub added_at: Option<String>,
160 pub on_device: Option<OnDevice>,
163}
164
165#[derive(Debug, Clone)]
166pub struct TrackRow {
167 pub id: i64,
168 pub album_id: Option<i64>,
169 pub artist_id: Option<i64>,
170 pub artist_name: String,
171 pub album_artist_name: String,
172 pub album_title: String,
173 pub disc: Option<i32>,
174 pub track_number: Option<i32>,
175 pub title: String,
176 pub duration_ms: Option<i64>,
177 pub path: Option<String>,
178 pub codec: Option<String>,
179 pub sample_rate: Option<i32>,
180 pub bit_depth: Option<i32>,
181 pub channels: Option<i32>,
182 pub bitrate: Option<i32>,
183 pub genre: Option<String>,
184 pub source: String,
185 pub remote_id: Option<String>,
186 pub cached_path: Option<String>,
187}
188
189#[derive(Debug, Clone)]
191pub enum PlaybackSource {
192 Local(PathBuf),
193 Cached(PathBuf),
194 Remote(String),
195}
196
197#[derive(Debug, Clone, Default)]
198pub struct LibraryStats {
199 pub total_tracks: i64,
200 pub local_tracks: i64,
201 pub remote_tracks: i64,
202 pub cached_tracks: i64,
203 pub total_albums: i64,
204 pub total_artists: i64,
205}
206
207#[derive(Debug, Clone, PartialEq)]
209pub struct TrackMeta {
210 pub title: String,
211 pub artist: String,
212 pub album_artist: Option<String>,
213 pub album: String,
214 pub date: Option<String>,
215 pub disc: Option<i32>,
216 pub track_number: Option<i32>,
217 pub genre: Option<String>,
218 pub label: Option<String>,
219 pub duration_ms: Option<i64>,
220 pub codec: Option<String>,
221 pub sample_rate: Option<i32>,
222 pub bit_depth: Option<i32>,
223 pub channels: Option<i32>,
224 pub bitrate: Option<i32>,
225 pub size_bytes: Option<i64>,
226 pub mtime: Option<i64>,
227 pub path: Option<String>,
228 pub source: String,
229 pub remote_id: Option<String>,
230 pub remote_url: Option<String>,
231 pub album_remote_id: Option<String>,
237 pub artist_remote_id: Option<String>,
238 pub mbid: Option<String>,
243 pub album_mbid: Option<String>,
244 pub album_added_at: Option<String>,
248}
249
250#[cfg(test)]
252pub fn sample_meta(title: &str, artist: &str, album: &str) -> TrackMeta {
253 TrackMeta {
254 title: title.into(),
255 artist: artist.into(),
256 album_artist: Some(artist.into()),
257 album: album.into(),
258 date: Some("2024".into()),
259 disc: Some(1),
260 track_number: Some(1),
261 genre: Some("Electronic".into()),
262 label: None,
263 duration_ms: Some(240_000),
264 codec: Some("FLAC".into()),
265 sample_rate: Some(44100),
266 bit_depth: Some(16),
267 channels: Some(2),
268 bitrate: Some(1000),
269 size_bytes: Some(30_000_000),
270 mtime: Some(1700000000),
271 path: Some(format!("/music/{}/{}.flac", album, title)),
272 source: "local".into(),
273 remote_id: None,
274 album_remote_id: None,
275 artist_remote_id: None,
276 mbid: None,
277 album_mbid: None,
278 remote_url: None,
279 album_added_at: None,
280 }
281}
282
283#[cfg(test)]
284mod write_transaction_tests {
285 use std::time::Duration;
286
287 fn open(path: &std::path::Path) -> rusqlite::Connection {
288 let conn = rusqlite::Connection::open(path).unwrap();
289 conn.pragma_update(None, "journal_mode", "wal").unwrap();
290 conn.busy_timeout(Duration::from_secs(5)).unwrap();
291 conn
292 }
293
294 #[test]
299 fn a_write_transaction_waits_where_a_deferred_one_fails() {
300 let dir = tempfile::tempdir().unwrap();
301 let path = dir.path().join("t.db");
302 open(&path)
303 .execute_batch("CREATE TABLE t (x INTEGER)")
304 .unwrap();
305
306 let deferred = open(&path);
307 let tx = deferred.unchecked_transaction().unwrap();
308 let _: i64 = tx
309 .query_row("SELECT COUNT(*) FROM t", [], |r| r.get(0))
310 .unwrap();
311 open(&path).execute("INSERT INTO t VALUES (1)", []).unwrap();
312 let err = tx.execute("INSERT INTO t VALUES (2)", []).unwrap_err();
313 assert!(
314 err.to_string().contains("locked") || err.to_string().contains("busy"),
315 "{err}"
316 );
317 drop(tx);
318
319 let path_for_writer = path.clone();
320 let writer = std::thread::spawn(move || {
321 let conn = open(&path_for_writer);
322 conn.execute_batch("BEGIN IMMEDIATE; INSERT INTO t VALUES (3)")
323 .unwrap();
324 std::thread::sleep(Duration::from_millis(200));
325 conn.execute_batch("COMMIT").unwrap();
326 });
327 std::thread::sleep(Duration::from_millis(50));
328 let conn = open(&path);
329 let tx = super::write_transaction(&conn).unwrap();
330 let _: i64 = tx
331 .query_row("SELECT COUNT(*) FROM t", [], |r| r.get(0))
332 .unwrap();
333 tx.execute("INSERT INTO t VALUES (4)", []).unwrap();
334 tx.commit().unwrap();
335 writer.join().unwrap();
336
337 let n: i64 = conn
338 .query_row("SELECT COUNT(*) FROM t", [], |r| r.get(0))
339 .unwrap();
340 assert_eq!(n, 3);
341 }
342}