mod albums;
pub mod api_keys;
mod artists;
pub mod auth;
pub mod batch;
mod favourites;
pub mod history;
pub mod lyrics;
pub mod playback_state;
pub mod playlists;
mod scan_cache;
mod search;
pub mod shares;
mod stats;
pub mod tracks;
pub mod uids;
use std::path::PathBuf;
pub use albums::*;
pub use artists::*;
pub use auth::LOCAL_USER;
pub use batch::*;
pub use favourites::*;
pub use history::*;
pub use lyrics::*;
pub use playback_state::*;
pub use playlists::*;
pub use scan_cache::*;
pub use search::*;
pub use stats::*;
pub use tracks::*;
pub use uids::*;
pub fn write_transaction(
conn: &rusqlite::Connection,
) -> rusqlite::Result<rusqlite::Transaction<'_>> {
rusqlite::Transaction::new_unchecked(conn, rusqlite::TransactionBehavior::Immediate)
}
pub fn atomically<T, E: From<rusqlite::Error>>(
conn: &rusqlite::Connection,
f: impl FnOnce() -> Result<T, E>,
) -> Result<T, E> {
let (begin, commit, rollback) = if conn.is_autocommit() {
("BEGIN IMMEDIATE", "COMMIT", "ROLLBACK")
} else {
(
"SAVEPOINT atomically",
"RELEASE atomically",
"ROLLBACK TO atomically; RELEASE atomically",
)
};
conn.execute_batch(begin)?;
let result = f();
match &result {
Ok(_) => conn.execute_batch(commit)?,
Err(_) => conn.execute_batch(rollback)?,
}
result
}
pub fn json_list<T: serde::Serialize>(items: &[T]) -> String {
serde_json::to_string(items).unwrap_or_else(|_| "[]".into())
}
pub fn folder_prefix_range(folder: &std::path::Path) -> (String, String) {
let prefix = format!(
"{}{}",
folder
.to_string_lossy()
.trim_end_matches(std::path::MAIN_SEPARATOR),
std::path::MAIN_SEPARATOR
);
let upper = format!("{prefix}\u{10FFFF}");
(prefix, upper)
}
#[derive(Debug, Clone)]
pub struct ArtistRow {
pub id: i64,
pub name: String,
pub sort_name: Option<String>,
pub remote_id: Option<String>,
pub album_count: i64,
pub track_count: i64,
}
#[derive(Debug, Clone)]
pub struct AlbumRow {
pub id: i64,
pub title: String,
pub artist_id: i64,
pub artist_name: String,
pub date: Option<String>,
pub total_discs: Option<i32>,
pub total_tracks: Option<i32>,
pub codec: Option<String>,
pub label: Option<String>,
pub remote_id: Option<String>,
pub added_at: Option<String>,
}
#[derive(Debug, Clone)]
pub struct TrackRow {
pub id: i64,
pub album_id: Option<i64>,
pub artist_id: Option<i64>,
pub artist_name: String,
pub album_artist_name: String,
pub album_title: String,
pub disc: Option<i32>,
pub track_number: Option<i32>,
pub title: String,
pub duration_ms: Option<i64>,
pub path: Option<String>,
pub codec: Option<String>,
pub sample_rate: Option<i32>,
pub bit_depth: Option<i32>,
pub channels: Option<i32>,
pub bitrate: Option<i32>,
pub genre: Option<String>,
pub source: String,
pub remote_id: Option<String>,
pub cached_path: Option<String>,
}
#[derive(Debug, Clone)]
pub enum PlaybackSource {
Local(PathBuf),
Cached(PathBuf),
Remote(String),
}
#[derive(Debug, Clone, Default)]
pub struct LibraryStats {
pub total_tracks: i64,
pub local_tracks: i64,
pub remote_tracks: i64,
pub cached_tracks: i64,
pub total_albums: i64,
pub total_artists: i64,
}
#[derive(Debug, Clone)]
pub struct TrackMeta {
pub title: String,
pub artist: String,
pub album_artist: Option<String>,
pub album: String,
pub date: Option<String>,
pub disc: Option<i32>,
pub track_number: Option<i32>,
pub genre: Option<String>,
pub label: Option<String>,
pub duration_ms: Option<i64>,
pub codec: Option<String>,
pub sample_rate: Option<i32>,
pub bit_depth: Option<i32>,
pub channels: Option<i32>,
pub bitrate: Option<i32>,
pub size_bytes: Option<i64>,
pub mtime: Option<i64>,
pub path: Option<String>,
pub source: String,
pub remote_id: Option<String>,
pub remote_url: Option<String>,
pub album_remote_id: Option<String>,
pub artist_remote_id: Option<String>,
pub mbid: Option<String>,
pub album_mbid: Option<String>,
pub album_added_at: Option<String>,
}
#[cfg(test)]
pub fn sample_meta(title: &str, artist: &str, album: &str) -> TrackMeta {
TrackMeta {
title: title.into(),
artist: artist.into(),
album_artist: Some(artist.into()),
album: album.into(),
date: Some("2024".into()),
disc: Some(1),
track_number: Some(1),
genre: Some("Electronic".into()),
label: None,
duration_ms: Some(240_000),
codec: Some("FLAC".into()),
sample_rate: Some(44100),
bit_depth: Some(16),
channels: Some(2),
bitrate: Some(1000),
size_bytes: Some(30_000_000),
mtime: Some(1700000000),
path: Some(format!("/music/{}/{}.flac", album, title)),
source: "local".into(),
remote_id: None,
album_remote_id: None,
artist_remote_id: None,
mbid: None,
album_mbid: None,
remote_url: None,
album_added_at: None,
}
}
#[cfg(test)]
mod write_transaction_tests {
use std::time::Duration;
fn open(path: &std::path::Path) -> rusqlite::Connection {
let conn = rusqlite::Connection::open(path).unwrap();
conn.pragma_update(None, "journal_mode", "wal").unwrap();
conn.busy_timeout(Duration::from_secs(5)).unwrap();
conn
}
#[test]
fn a_write_transaction_waits_where_a_deferred_one_fails() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.db");
open(&path)
.execute_batch("CREATE TABLE t (x INTEGER)")
.unwrap();
let deferred = open(&path);
let tx = deferred.unchecked_transaction().unwrap();
let _: i64 = tx
.query_row("SELECT COUNT(*) FROM t", [], |r| r.get(0))
.unwrap();
open(&path).execute("INSERT INTO t VALUES (1)", []).unwrap();
let err = tx.execute("INSERT INTO t VALUES (2)", []).unwrap_err();
assert!(
err.to_string().contains("locked") || err.to_string().contains("busy"),
"{err}"
);
drop(tx);
let path_for_writer = path.clone();
let writer = std::thread::spawn(move || {
let conn = open(&path_for_writer);
conn.execute_batch("BEGIN IMMEDIATE; INSERT INTO t VALUES (3)")
.unwrap();
std::thread::sleep(Duration::from_millis(200));
conn.execute_batch("COMMIT").unwrap();
});
std::thread::sleep(Duration::from_millis(50));
let conn = open(&path);
let tx = super::write_transaction(&conn).unwrap();
let _: i64 = tx
.query_row("SELECT COUNT(*) FROM t", [], |r| r.get(0))
.unwrap();
tx.execute("INSERT INTO t VALUES (4)", []).unwrap();
tx.commit().unwrap();
writer.join().unwrap();
let n: i64 = conn
.query_row("SELECT COUNT(*) FROM t", [], |r| r.get(0))
.unwrap();
assert_eq!(n, 3);
}
}