use std::sync::Arc;
mod backend;
#[derive(Debug, Default, Clone)]
pub struct ImportReport {
pub ran: bool,
pub tracks: usize,
pub albums: usize,
pub playlists: usize,
pub favorites: usize,
pub servers: usize,
}
pub use config::Source;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Page {
pub offset: u32,
pub limit: u32,
}
#[derive(Clone, Debug, Default)]
pub struct QueueSnapshot {
pub version: u8,
pub queue: Vec<reader::Track>,
pub current_queue_index: usize,
pub progress_secs: u64,
pub shuffle_order: Vec<usize>,
pub shuffle_enabled: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TrackSort {
#[default]
ArtistAlbum,
Title,
Artist,
Album,
DateAdded,
PlayCount,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TrackFilter {
pub source: Source,
pub sort: TrackSort,
pub search: String,
}
impl TrackFilter {
pub fn new(source: Source) -> Self {
Self {
source,
..Default::default()
}
}
}
#[derive(Debug, Clone)]
pub enum DbError {
Backend(String),
Serde(String),
Io(String),
}
impl std::fmt::Display for DbError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DbError::Backend(e) => write!(f, "db backend: {e}"),
DbError::Serde(e) => write!(f, "db serde: {e}"),
DbError::Io(e) => write!(f, "db io: {e}"),
}
}
}
impl std::error::Error for DbError {}
impl From<serde_json::Error> for DbError {
fn from(e: serde_json::Error) -> Self {
DbError::Serde(e.to_string())
}
}
impl From<sqlx::Error> for DbError {
fn from(e: sqlx::Error) -> Self {
DbError::Backend(e.to_string())
}
}
impl From<sqlx::migrate::MigrateError> for DbError {
fn from(e: sqlx::migrate::MigrateError) -> Self {
DbError::Backend(e.to_string())
}
}
pub type ArtistImages = (
std::collections::HashMap<String, std::path::PathBuf>,
std::collections::HashMap<String, reader::ArtistImageRef>,
);
#[async_trait::async_trait]
pub trait ReadStore: Send + Sync {
async fn load_config(&self) -> Result<Option<config::AppConfig>, DbError>;
async fn tracks_page(
&self,
filter: &TrackFilter,
page: Page,
) -> Result<Vec<reader::Track>, DbError>;
async fn tracks_count(&self, filter: &TrackFilter) -> Result<u32, DbError>;
async fn album_tracks(
&self,
source: &Source,
album_id: &str,
) -> Result<Vec<reader::Track>, DbError>;
async fn artist_tracks(
&self,
source: &Source,
artist: &str,
) -> Result<Vec<reader::Track>, DbError>;
async fn genre_tracks(
&self,
source: &Source,
genre: &str,
) -> Result<Vec<reader::Track>, DbError>;
async fn folder_tracks(&self, prefix: &str) -> Result<Vec<reader::Track>, DbError>;
async fn recently_played(&self, source: &Source, limit: u32) -> Result<Vec<String>, DbError>;
async fn artist_sample_tracks(
&self,
source: &Source,
limit: u32,
) -> Result<Vec<reader::Track>, DbError>;
async fn top_genre(&self, source: &Source) -> Result<Option<String>, DbError>;
async fn search_corpus(&self, source: &Source) -> Result<Vec<reader::Track>, DbError>;
async fn tracks_by_keys(
&self,
source: &Source,
keys: &[String],
) -> Result<Vec<reader::Track>, DbError>;
async fn artists(&self, source: &Source) -> Result<Vec<(String, u32)>, DbError>;
async fn genres(&self, source: &Source) -> Result<Vec<String>, DbError>;
async fn album(
&self,
source: &Source,
album_id: &str,
) -> Result<Option<reader::Album>, DbError>;
async fn artist_images(&self) -> Result<ArtistImages, DbError>;
async fn albums(&self, source: &Source) -> Result<Vec<reader::Album>, DbError>;
async fn load_queue(&self) -> Result<QueueSnapshot, DbError>;
async fn load_playlists(&self, source: &Source) -> Result<reader::PlaylistStore, DbError>;
async fn load_server(&self, id: &str) -> Result<Option<config::MusicServer>, DbError>;
async fn meta_get(&self, cache_key: &str, kind: &str) -> Result<Option<String>, DbError>;
async fn favorites(&self, server_id: &str) -> Result<Vec<String>, DbError>;
async fn is_favorite(&self, server_id: &str, ref_: &str) -> Result<bool, DbError>;
async fn dirty_favorites(&self, server_id: &str) -> Result<Vec<String>, DbError>;
async fn dirty_unlikes(&self, server_id: &str) -> Result<Vec<String>, DbError>;
}
#[async_trait::async_trait]
pub trait Storage: ReadStore {
async fn save_config(&self, cfg: &config::AppConfig) -> Result<(), DbError>;
async fn import_legacy_json(
&self,
config_dir: &std::path::Path,
) -> Result<ImportReport, DbError>;
async fn finalize_migration(&self, config_dir: &std::path::Path) -> Result<usize, DbError>;
async fn delete_tracks(&self, source: &Source, keys: &[String]) -> Result<u64, DbError>;
async fn delete_album(&self, source: &Source, album_id: &str) -> Result<(), DbError>;
async fn prune_source(
&self,
source: &Source,
keep_track_keys: &[String],
keep_album_ids: &[String],
) -> Result<(), DbError>;
async fn set_artist_image(
&self,
artist_norm: &str,
kind: &str,
image_ref: Option<&str>,
) -> Result<(), DbError>;
async fn update_album_cover(
&self,
source: &Source,
album_id: &str,
cover_path: Option<&str>,
manual: bool,
) -> Result<(), DbError>;
async fn upsert_playlist_meta(
&self,
source: &Source,
pl_id: &str,
name: &str,
cover_path: Option<&str>,
image_tag: Option<&str>,
) -> Result<(), DbError>;
async fn delete_playlist(&self, source: &Source, pl_id: &str) -> Result<(), DbError>;
async fn set_playlist_tracks(
&self,
source: &Source,
pl_id: &str,
refs: &[String],
) -> Result<(), DbError>;
async fn add_playlist_tracks(
&self,
source: &Source,
pl_id: &str,
refs: &[String],
) -> Result<(), DbError>;
async fn remove_playlist_tracks(
&self,
source: &Source,
pl_id: &str,
refs: &[String],
) -> Result<(), DbError>;
async fn upsert_playlist_tracks_page(
&self,
source: &Source,
pl_id: &str,
refs: &[String],
start_position: i64,
epoch: i64,
) -> Result<(), DbError>;
async fn sweep_playlist_tracks(
&self,
source: &Source,
pl_id: &str,
epoch: i64,
) -> Result<(), DbError>;
async fn create_folder(&self, id: &str, name: &str) -> Result<(), DbError>;
async fn rename_folder(&self, id: &str, name: &str) -> Result<(), DbError>;
async fn delete_folder(&self, id: &str) -> Result<(), DbError>;
async fn set_playlist_folder(
&self,
playlist_ref: &str,
folder_id: Option<&str>,
) -> Result<(), DbError>;
async fn bump_listen_count(&self, track_uid: &str) -> Result<(), DbError>;
async fn push_recent(&self, source: &Source, track_key: &str) -> Result<(), DbError>;
async fn set_offline_track(&self, id: &str, path: Option<&str>) -> Result<(), DbError>;
async fn save_queue(&self, snap: &QueueSnapshot) -> Result<(), DbError>;
async fn meta_put(&self, cache_key: &str, kind: &str, payload: &str) -> Result<(), DbError>;
async fn debug_reset(&self, db_path: &std::path::Path) -> Result<(), DbError>;
async fn debug_load_release(
&self,
release_path: &std::path::Path,
db_path: &std::path::Path,
) -> Result<(), DbError>;
async fn debug_seed_synthetic(&self, n: u32) -> Result<(), DbError>;
async fn debug_info(&self) -> Result<String, DbError>;
async fn debug_vacuum(&self) -> Result<(), DbError>;
async fn set_favorite(&self, server_id: &str, ref_: &str, on: bool) -> Result<(), DbError>;
async fn clear_favorite_dirty(&self, server_id: &str, ref_: &str) -> Result<(), DbError>;
async fn replace_favorites_clean(
&self,
server_id: &str,
refs: &[String],
) -> Result<(), DbError>;
async fn upsert_favorites_page(
&self,
server_id: &str,
refs: &[String],
start_rank: i64,
epoch: i64,
) -> Result<(), DbError>;
async fn sweep_favorites(&self, server_id: &str, epoch: i64) -> Result<(), DbError>;
async fn upsert_tracks(&self, source: &Source, tracks: &[reader::Track])
-> Result<(), DbError>;
async fn upsert_albums(&self, source: &Source, albums: &[reader::Album])
-> Result<(), DbError>;
}
#[derive(Clone)]
pub struct Db(Arc<dyn Storage>);
impl std::ops::Deref for Db {
type Target = dyn Storage;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
#[derive(Clone)]
pub struct ReadDb(std::sync::Arc<dyn ReadStore>);
impl std::ops::Deref for ReadDb {
type Target = dyn ReadStore;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
impl Db {
pub fn reads(&self) -> ReadDb {
ReadDb(self.0.clone())
}
}
pub async fn init(db_path: &std::path::Path) -> Result<Db, DbError> {
let native = backend::Native::open(db_path).await?;
Ok(Db(Arc::new(native)))
}
pub fn default_db_path() -> std::path::PathBuf {
if let Ok(p) = std::env::var("KOPUZ_DB_PATH") {
return std::path::PathBuf::from(p);
}
let name = if cfg!(debug_assertions) {
"kopuz-debug.db"
} else {
"kopuz.db"
};
config_dir().join(name)
}
pub fn peek_config(db_path: &std::path::Path) -> Option<config::AppConfig> {
if !db_path.exists() {
return None;
}
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.ok()?;
rt.block_on(async {
let opts = sqlx::sqlite::SqliteConnectOptions::new()
.filename(db_path)
.create_if_missing(false)
.read_only(true);
use sqlx::ConnectOptions;
let mut conn = opts.connect().await.ok()?;
let json: Option<String> = sqlx::query_scalar!("SELECT json FROM app_config WHERE id = 1")
.fetch_optional(&mut conn)
.await
.ok()
.flatten();
json.and_then(|j| serde_json::from_str(&j).ok())
})
}
pub fn release_db_path() -> std::path::PathBuf {
config_dir().join("kopuz.db")
}
pub fn config_dir() -> std::path::PathBuf {
directories::ProjectDirs::from("com", "temidaradev", "kopuz")
.map(|d| d.config_dir().to_path_buf())
.unwrap_or_else(|| std::path::PathBuf::from("./config"))
}