add per-user isolation support to all remaining modules
Add new_at() constructors, Option<Store> wrappers, new_empty()/init_at()/ teardown() methods, and update all commands to unwrap Option for: - config: audio_settings, download_settings, favorites_cache, favorites_preferences, legal_settings, playback_preferences, remote_control_settings, subscription_state, tray_settings - data: api_cache, artist_blacklist, artist_vectors, library, listenbrainz, lyrics, musicbrainz, offline, offline_cache, radio_engine, reco_store, updates
This commit is contained in:
1 parent
48dac0ec3a
commit
aab3e67641
29 files changed
+1095
-350
No files matched your search
@@ -11,7 +11,7 @@ use tokio::sync::Mutex;
|
||||
|
||||
/// API cache state shared across commands
|
||||
pub struct ApiCacheState {
|
||||
pub cache: Arc<Mutex<ApiCache>>,
|
||||
pub cache: Arc<Mutex<Option<ApiCache>>>,
|
||||
}
|
||||
|
||||
impl ApiCacheState {
|
||||
@@ -30,9 +30,32 @@ impl ApiCacheState {
|
||||
log::info!("API cache initialized at {:?}", db_path);
|
||||
|
||||
Ok(Self {
|
||||
cache: Arc::new(Mutex::new(cache)),
|
||||
cache: Arc::new(Mutex::new(Some(cache))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
cache: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_at(&self, base_dir: &Path) -> Result<(), String> {
|
||||
let cache_dir = base_dir.join("cache");
|
||||
std::fs::create_dir_all(&cache_dir)
|
||||
.map_err(|e| format!("Failed to create cache directory: {}", e))?;
|
||||
let db_path = cache_dir.join("api_cache.db");
|
||||
let new_cache = ApiCache::new(&db_path)?;
|
||||
log::info!("API cache initialized at {:?}", db_path);
|
||||
let mut guard = self.cache.blocking_lock();
|
||||
*guard = Some(new_cache);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) {
|
||||
let mut guard = self.cache.blocking_lock();
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Default TTL for cached items (24 hours)
|
||||
|
||||
@@ -24,11 +24,12 @@ pub mod service;
|
||||
pub use models::{BlacklistSettings, BlacklistedArtist};
|
||||
pub use service::BlacklistService;
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Thread-safe state wrapper for the blacklist service
|
||||
pub struct BlacklistState {
|
||||
pub service: Mutex<BlacklistService>,
|
||||
pub service: Mutex<Option<BlacklistService>>,
|
||||
}
|
||||
|
||||
impl BlacklistState {
|
||||
@@ -45,16 +46,39 @@ impl BlacklistState {
|
||||
let service = BlacklistService::new(&db_path)?;
|
||||
|
||||
Ok(Self {
|
||||
service: Mutex::new(service),
|
||||
service: Mutex::new(Some(service)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
service: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_at(&self, base_dir: &Path) -> Result<(), String> {
|
||||
std::fs::create_dir_all(base_dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
let db_path = base_dir.join("artist_blacklist.db");
|
||||
let new_service = BlacklistService::new(&db_path)?;
|
||||
let mut guard = self.service.lock().map_err(|_| "Failed to acquire lock")?;
|
||||
*guard = Some(new_service);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) {
|
||||
if let Ok(mut guard) = self.service.lock() {
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an artist is blacklisted - thread-safe O(1) operation
|
||||
#[inline]
|
||||
pub fn is_blacklisted(&self, artist_id: u64) -> bool {
|
||||
self.service
|
||||
.lock()
|
||||
.map(|s| s.is_blacklisted(artist_id))
|
||||
.ok()
|
||||
.and_then(|guard| guard.as_ref().map(|s| s.is_blacklisted(artist_id)))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -63,60 +87,59 @@ impl BlacklistState {
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.service
|
||||
.lock()
|
||||
.map(|s| s.is_enabled())
|
||||
.ok()
|
||||
.and_then(|guard| guard.as_ref().map(|s| s.is_enabled()))
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// Add an artist to the blacklist
|
||||
pub fn add(&self, artist_id: u64, artist_name: &str, notes: Option<&str>) -> Result<(), String> {
|
||||
self.service
|
||||
.lock()
|
||||
.map_err(|_| "Failed to acquire lock")?
|
||||
let guard = self.service.lock().map_err(|_| "Failed to acquire lock")?;
|
||||
guard.as_ref().ok_or("No active session - please log in")?
|
||||
.add(artist_id, artist_name, notes)
|
||||
}
|
||||
|
||||
/// Remove an artist from the blacklist
|
||||
pub fn remove(&self, artist_id: u64) -> Result<(), String> {
|
||||
self.service
|
||||
.lock()
|
||||
.map_err(|_| "Failed to acquire lock")?
|
||||
let guard = self.service.lock().map_err(|_| "Failed to acquire lock")?;
|
||||
guard.as_ref().ok_or("No active session - please log in")?
|
||||
.remove(artist_id)
|
||||
}
|
||||
|
||||
/// Get all blacklisted artists
|
||||
pub fn get_all(&self) -> Result<Vec<BlacklistedArtist>, String> {
|
||||
self.service
|
||||
.lock()
|
||||
.map_err(|_| "Failed to acquire lock")?
|
||||
let guard = self.service.lock().map_err(|_| "Failed to acquire lock")?;
|
||||
guard.as_ref().ok_or("No active session - please log in")?
|
||||
.get_all()
|
||||
}
|
||||
|
||||
/// Set enabled state
|
||||
pub fn set_enabled(&self, enabled: bool) -> Result<(), String> {
|
||||
self.service
|
||||
.lock()
|
||||
.map_err(|_| "Failed to acquire lock")?
|
||||
let guard = self.service.lock().map_err(|_| "Failed to acquire lock")?;
|
||||
guard.as_ref().ok_or("No active session - please log in")?
|
||||
.set_enabled(enabled)
|
||||
}
|
||||
|
||||
/// Get blacklist settings
|
||||
pub fn get_settings(&self) -> Result<BlacklistSettings, String> {
|
||||
self.service
|
||||
.lock()
|
||||
.map(|s| s.get_settings())
|
||||
.map_err(|_| "Failed to acquire lock".to_string())
|
||||
let guard = self.service.lock().map_err(|_| "Failed to acquire lock")?;
|
||||
Ok(guard.as_ref().ok_or("No active session - please log in")?
|
||||
.get_settings())
|
||||
}
|
||||
|
||||
/// Get count of blacklisted artists
|
||||
pub fn count(&self) -> usize {
|
||||
self.service.lock().map(|s| s.count()).unwrap_or(0)
|
||||
self.service
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| guard.as_ref().map(|s| s.count()))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Clear all blacklisted artists
|
||||
pub fn clear_all(&self) -> Result<(), String> {
|
||||
self.service
|
||||
.lock()
|
||||
.map_err(|_| "Failed to acquire lock")?
|
||||
let guard = self.service.lock().map_err(|_| "Failed to acquire lock")?;
|
||||
guard.as_ref().ok_or("No active session - please log in")?
|
||||
.clear_all()
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,11 @@ use super::weights::RelationshipWeights;
|
||||
/// Builder for constructing artist vectors from multiple data sources
|
||||
pub struct ArtistVectorBuilder {
|
||||
/// Vector store for persistence
|
||||
store: Arc<Mutex<ArtistVectorStore>>,
|
||||
store: Arc<Mutex<Option<ArtistVectorStore>>>,
|
||||
/// MusicBrainz client for relationship data
|
||||
mb_client: Arc<MusicBrainzClient>,
|
||||
/// MusicBrainz cache
|
||||
mb_cache: Arc<Mutex<MusicBrainzCache>>,
|
||||
mb_cache: Arc<Mutex<Option<MusicBrainzCache>>>,
|
||||
/// Qobuz client for similar artists
|
||||
qobuz_client: Arc<Mutex<QobuzClient>>,
|
||||
/// Configurable weights
|
||||
@@ -46,9 +46,9 @@ pub struct BuildResult {
|
||||
impl ArtistVectorBuilder {
|
||||
/// Create a new builder with the given dependencies
|
||||
pub fn new(
|
||||
store: Arc<Mutex<ArtistVectorStore>>,
|
||||
store: Arc<Mutex<Option<ArtistVectorStore>>>,
|
||||
mb_client: Arc<MusicBrainzClient>,
|
||||
mb_cache: Arc<Mutex<MusicBrainzCache>>,
|
||||
mb_cache: Arc<Mutex<Option<MusicBrainzCache>>>,
|
||||
qobuz_client: Arc<Mutex<QobuzClient>>,
|
||||
weights: RelationshipWeights,
|
||||
) -> Self {
|
||||
@@ -88,7 +88,8 @@ impl ArtistVectorBuilder {
|
||||
// 1. Get or create index for this artist
|
||||
log::info!("[VectorBuilder] Acquiring store lock...");
|
||||
{
|
||||
let mut store = self.store.lock().await;
|
||||
let mut guard__ = self.store.lock().await;
|
||||
let store = guard__.as_mut().ok_or("No active session - please log in")?;
|
||||
log::info!("[VectorBuilder] Store lock acquired, creating index");
|
||||
store.get_or_create_idx(artist_mbid, artist_name)?;
|
||||
}
|
||||
@@ -130,7 +131,8 @@ impl ArtistVectorBuilder {
|
||||
// 4. Persist the vectors (using saved vectors to avoid deadlock)
|
||||
log::info!("[VectorBuilder] Persisting vectors...");
|
||||
{
|
||||
let mut store = self.store.lock().await;
|
||||
let mut guard__ = self.store.lock().await;
|
||||
let store = guard__.as_mut().ok_or("No active session - please log in")?;
|
||||
|
||||
// Store MB relationships
|
||||
if let Some(mb_vec) = mb_vec_to_store {
|
||||
@@ -161,7 +163,8 @@ impl ArtistVectorBuilder {
|
||||
// Try cache first
|
||||
log::info!("[VectorBuilder] Acquiring mb_cache lock...");
|
||||
let cached = {
|
||||
let cache = self.mb_cache.lock().await;
|
||||
let guard__ = self.mb_cache.lock().await;
|
||||
let cache = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
log::info!("[VectorBuilder] mb_cache lock acquired, checking relations");
|
||||
cache.get_artist_relations(artist_mbid)?
|
||||
};
|
||||
@@ -181,7 +184,8 @@ impl ArtistVectorBuilder {
|
||||
|
||||
// Cache it
|
||||
{
|
||||
let cache = self.mb_cache.lock().await;
|
||||
let guard__ = self.mb_cache.lock().await;
|
||||
let cache = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
cache.set_artist_relations(artist_mbid, &extracted)?;
|
||||
}
|
||||
|
||||
@@ -192,7 +196,8 @@ impl ArtistVectorBuilder {
|
||||
let mut count = 0;
|
||||
|
||||
// Get store for index lookups
|
||||
let mut store = self.store.lock().await;
|
||||
let mut guard__ = self.store.lock().await;
|
||||
let store = guard__.as_mut().ok_or("No active session - please log in")?;
|
||||
|
||||
// Process members (band → person)
|
||||
for member in &relations.members {
|
||||
@@ -241,7 +246,8 @@ impl ArtistVectorBuilder {
|
||||
|
||||
let mut vector = SparseVector::new();
|
||||
let mut count = 0;
|
||||
let mut store = self.store.lock().await;
|
||||
let mut guard__ = self.store.lock().await;
|
||||
let store = guard__.as_mut().ok_or("No active session - please log in")?;
|
||||
|
||||
for artist in similar.items {
|
||||
// We need to resolve Qobuz artist to MBID
|
||||
@@ -271,7 +277,8 @@ impl ArtistVectorBuilder {
|
||||
|
||||
// Check if we have a fresh vector
|
||||
let has_fresh = {
|
||||
let store = self.store.lock().await;
|
||||
let guard__ = self.store.lock().await;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.has_fresh_vector(artist_mbid, max_age_secs)
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ const VECTOR_TTL_SECS: i64 = 7 * 24 * 60 * 60;
|
||||
|
||||
/// Shared state wrapper for Tauri
|
||||
pub struct ArtistVectorStoreState {
|
||||
pub store: Arc<Mutex<ArtistVectorStore>>,
|
||||
pub store: Arc<Mutex<Option<ArtistVectorStore>>>,
|
||||
}
|
||||
|
||||
impl ArtistVectorStoreState {
|
||||
@@ -36,9 +36,32 @@ impl ArtistVectorStoreState {
|
||||
log::info!("Artist vector store initialized at {:?}", db_path);
|
||||
|
||||
Ok(Self {
|
||||
store: Arc::new(Mutex::new(store)),
|
||||
store: Arc::new(Mutex::new(Some(store))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
store: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_at(&self, base_dir: &Path) -> Result<(), String> {
|
||||
let cache_dir = base_dir.join("cache");
|
||||
std::fs::create_dir_all(&cache_dir)
|
||||
.map_err(|e| format!("Failed to create cache directory: {}", e))?;
|
||||
let db_path = cache_dir.join("artist_vectors.db");
|
||||
let new_store = ArtistVectorStore::new(&db_path)?;
|
||||
log::info!("Artist vector store initialized at {:?}", db_path);
|
||||
let mut guard = self.store.blocking_lock();
|
||||
*guard = Some(new_store);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) {
|
||||
let mut guard = self.store.blocking_lock();
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Artist vector store with SQLite backend
|
||||
|
||||
@@ -94,7 +94,7 @@ pub struct SuggestionResult {
|
||||
/// Playlist suggestions engine
|
||||
pub struct SuggestionsEngine {
|
||||
/// Vector store for similarity lookups
|
||||
store: Arc<Mutex<ArtistVectorStore>>,
|
||||
store: Arc<Mutex<Option<ArtistVectorStore>>>,
|
||||
/// Vector builder for lazy construction
|
||||
builder: Arc<ArtistVectorBuilder>,
|
||||
/// Qobuz client for track search
|
||||
@@ -106,7 +106,7 @@ pub struct SuggestionsEngine {
|
||||
impl SuggestionsEngine {
|
||||
/// Create a new suggestions engine
|
||||
pub fn new(
|
||||
store: Arc<Mutex<ArtistVectorStore>>,
|
||||
store: Arc<Mutex<Option<ArtistVectorStore>>>,
|
||||
builder: Arc<ArtistVectorBuilder>,
|
||||
qobuz_client: Arc<Mutex<QobuzClient>>,
|
||||
config: SuggestionConfig,
|
||||
@@ -184,7 +184,8 @@ impl SuggestionsEngine {
|
||||
let step3_start = Instant::now();
|
||||
let exclude_vec: Vec<String> = playlist_artist_mbids.to_vec();
|
||||
let similar_artists = {
|
||||
let store = self.store.lock().await;
|
||||
let guard__ = self.store.lock().await;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
// Use direct relationship lookup instead of vector similarity
|
||||
// This finds members, collaborators, etc. from the MusicBrainz data
|
||||
store.get_all_related_artists(&playlist_artist_mbids, &exclude_vec, self.config.max_artists)?
|
||||
@@ -396,7 +397,8 @@ impl SuggestionsEngine {
|
||||
artist_mbids: &[String],
|
||||
) -> Result<SparseVector, String> {
|
||||
let mut combined = SparseVector::new();
|
||||
let store = self.store.lock().await;
|
||||
let guard__ = self.store.lock().await;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
|
||||
for mbid in artist_mbids {
|
||||
if let Some(vector) = store.get_vector(mbid) {
|
||||
@@ -506,10 +508,14 @@ impl SuggestionsEngine {
|
||||
Some(name) => name.to_string(),
|
||||
None => {
|
||||
// Try to get name from store
|
||||
let store = self.store.lock().await;
|
||||
let guard__ = self.store.lock().await;
|
||||
if let Some(store) = guard__.as_ref() {
|
||||
store
|
||||
.get_artist_name(artist_mbid)
|
||||
.unwrap_or_else(|| artist_mbid.to_string())
|
||||
} else {
|
||||
artist_mbid.to_string()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
use crate::audio::{AlsaPlugin, AudioBackendType};
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -55,15 +56,11 @@ pub struct AudioSettingsStore {
|
||||
}
|
||||
|
||||
impl AudioSettingsStore {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
|
||||
std::fs::create_dir_all(&data_dir)
|
||||
fn open_at(dir: &Path, db_name: &str) -> Result<Self, String> {
|
||||
std::fs::create_dir_all(dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
|
||||
let db_path = data_dir.join("audio_settings.db");
|
||||
let db_path = dir.join(db_name);
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open audio settings database: {}", e))?;
|
||||
|
||||
@@ -97,6 +94,17 @@ impl AudioSettingsStore {
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
Self::open_at(&data_dir, "audio_settings.db")
|
||||
}
|
||||
|
||||
pub fn new_at(base_dir: &Path) -> Result<Self, String> {
|
||||
Self::open_at(base_dir, "audio_settings.db")
|
||||
}
|
||||
|
||||
pub fn get_settings(&self) -> Result<AudioSettings, String> {
|
||||
self.conn
|
||||
.query_row(
|
||||
@@ -267,15 +275,36 @@ impl AudioSettingsStore {
|
||||
|
||||
/// Thread-safe wrapper
|
||||
pub struct AudioSettingsState {
|
||||
pub store: Arc<Mutex<AudioSettingsStore>>,
|
||||
pub store: Arc<Mutex<Option<AudioSettingsStore>>>,
|
||||
}
|
||||
|
||||
impl AudioSettingsState {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
store: Arc::new(Mutex::new(AudioSettingsStore::new()?)),
|
||||
store: Arc::new(Mutex::new(Some(AudioSettingsStore::new()?))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
store: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_at(&self, base_dir: &Path) -> Result<(), String> {
|
||||
let new_store = AudioSettingsStore::new_at(base_dir)?;
|
||||
let mut guard = self.store.lock()
|
||||
.map_err(|_| "Failed to lock audio settings store".to_string())?;
|
||||
*guard = Some(new_store);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) -> Result<(), String> {
|
||||
let mut guard = self.store.lock()
|
||||
.map_err(|_| "Failed to lock audio settings store".to_string())?;
|
||||
*guard = None;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Tauri commands
|
||||
@@ -283,7 +312,8 @@ impl AudioSettingsState {
|
||||
pub fn get_audio_settings(
|
||||
state: tauri::State<'_, AudioSettingsState>,
|
||||
) -> Result<AudioSettings, String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.get_settings()
|
||||
}
|
||||
|
||||
@@ -304,7 +334,8 @@ pub fn set_audio_output_device(
|
||||
normalized_device
|
||||
);
|
||||
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_output_device(normalized_device.as_deref())
|
||||
}
|
||||
|
||||
@@ -313,7 +344,8 @@ pub fn set_audio_exclusive_mode(
|
||||
state: tauri::State<'_, AudioSettingsState>,
|
||||
enabled: bool,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_exclusive_mode(enabled)
|
||||
}
|
||||
|
||||
@@ -322,7 +354,8 @@ pub fn set_audio_dac_passthrough(
|
||||
state: tauri::State<'_, AudioSettingsState>,
|
||||
enabled: bool,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_dac_passthrough(enabled)
|
||||
}
|
||||
|
||||
@@ -331,7 +364,8 @@ pub fn set_audio_sample_rate(
|
||||
state: tauri::State<'_, AudioSettingsState>,
|
||||
rate: Option<u32>,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_sample_rate(rate)
|
||||
}
|
||||
|
||||
@@ -340,7 +374,8 @@ pub fn set_audio_backend_type(
|
||||
state: tauri::State<'_, AudioSettingsState>,
|
||||
backend_type: Option<AudioBackendType>,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_backend_type(backend_type)
|
||||
}
|
||||
|
||||
@@ -349,7 +384,8 @@ pub fn set_audio_alsa_plugin(
|
||||
state: tauri::State<'_, AudioSettingsState>,
|
||||
plugin: Option<AlsaPlugin>,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_alsa_plugin(plugin)
|
||||
}
|
||||
|
||||
@@ -358,7 +394,8 @@ pub fn set_audio_alsa_hardware_volume(
|
||||
state: tauri::State<'_, AudioSettingsState>,
|
||||
enabled: bool,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_alsa_hardware_volume(enabled)
|
||||
}
|
||||
|
||||
@@ -367,7 +404,8 @@ pub fn set_audio_stream_first_track(
|
||||
state: tauri::State<'_, AudioSettingsState>,
|
||||
enabled: bool,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_stream_first_track(enabled)
|
||||
}
|
||||
|
||||
@@ -376,7 +414,8 @@ pub fn set_audio_stream_buffer_seconds(
|
||||
state: tauri::State<'_, AudioSettingsState>,
|
||||
seconds: u8,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_stream_buffer_seconds(seconds)
|
||||
}
|
||||
|
||||
@@ -385,7 +424,8 @@ pub fn set_audio_streaming_only(
|
||||
state: tauri::State<'_, AudioSettingsState>,
|
||||
enabled: bool,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_streaming_only(enabled)
|
||||
}
|
||||
|
||||
@@ -394,7 +434,8 @@ pub fn set_audio_limit_quality_to_device(
|
||||
state: tauri::State<'_, AudioSettingsState>,
|
||||
enabled: bool,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_limit_quality_to_device(enabled)
|
||||
}
|
||||
|
||||
@@ -403,6 +444,7 @@ pub fn set_audio_device_max_sample_rate(
|
||||
state: tauri::State<'_, AudioSettingsState>,
|
||||
rate: Option<u32>,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_device_max_sample_rate(rate)
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -33,15 +34,11 @@ pub struct DownloadSettingsStore {
|
||||
}
|
||||
|
||||
impl DownloadSettingsStore {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
|
||||
std::fs::create_dir_all(&data_dir)
|
||||
fn open_at(dir: &Path, db_name: &str) -> Result<Self, String> {
|
||||
std::fs::create_dir_all(dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
|
||||
let db_path = data_dir.join("download_settings.db");
|
||||
let db_path = dir.join(db_name);
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open download settings database: {}", e))?;
|
||||
|
||||
@@ -64,6 +61,17 @@ impl DownloadSettingsStore {
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
Self::open_at(&data_dir, "download_settings.db")
|
||||
}
|
||||
|
||||
pub fn new_at(base_dir: &Path) -> Result<Self, String> {
|
||||
Self::open_at(base_dir, "download_settings.db")
|
||||
}
|
||||
|
||||
pub fn get_settings(&self) -> Result<DownloadSettings, String> {
|
||||
self.conn
|
||||
.query_row(
|
||||
@@ -100,11 +108,15 @@ impl DownloadSettingsStore {
|
||||
}
|
||||
}
|
||||
|
||||
pub type DownloadSettingsState = Arc<Mutex<DownloadSettingsStore>>;
|
||||
pub type DownloadSettingsState = Arc<Mutex<Option<DownloadSettingsStore>>>;
|
||||
|
||||
pub fn create_download_settings_state() -> Result<DownloadSettingsState, String> {
|
||||
let store = DownloadSettingsStore::new()?;
|
||||
Ok(Arc::new(Mutex::new(store)))
|
||||
Ok(Arc::new(Mutex::new(Some(store))))
|
||||
}
|
||||
|
||||
pub fn create_empty_download_settings_state() -> DownloadSettingsState {
|
||||
Arc::new(Mutex::new(None))
|
||||
}
|
||||
|
||||
// Tauri commands
|
||||
@@ -114,7 +126,8 @@ pub fn get_download_settings(
|
||||
state: tauri::State<DownloadSettingsState>,
|
||||
) -> Result<DownloadSettings, String> {
|
||||
log::info!("Command: get_download_settings");
|
||||
let store = state.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard = state.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.get_settings()
|
||||
}
|
||||
|
||||
@@ -134,7 +147,8 @@ pub fn set_download_root(
|
||||
return Err("Path is not a directory".to_string());
|
||||
}
|
||||
|
||||
let store = state.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard = state.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_download_root(&path)
|
||||
}
|
||||
|
||||
@@ -144,7 +158,8 @@ pub fn set_show_downloads_in_library(
|
||||
state: tauri::State<DownloadSettingsState>,
|
||||
) -> Result<(), String> {
|
||||
log::info!("Command: set_show_downloads_in_library to: {}", show);
|
||||
let store = state.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard = state.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_show_in_library(show)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
use rusqlite::{params, Connection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// Represents a cached favorite track
|
||||
@@ -37,15 +38,11 @@ pub struct FavoritesCacheStore {
|
||||
}
|
||||
|
||||
impl FavoritesCacheStore {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
|
||||
std::fs::create_dir_all(&data_dir)
|
||||
fn open_at(dir: &Path, db_name: &str) -> Result<Self, String> {
|
||||
std::fs::create_dir_all(dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
|
||||
let db_path = data_dir.join("favorites_cache.db");
|
||||
let db_path = dir.join(db_name);
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open favorites cache database: {}", e))?;
|
||||
|
||||
@@ -80,6 +77,17 @@ impl FavoritesCacheStore {
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
Self::open_at(&data_dir, "favorites_cache.db")
|
||||
}
|
||||
|
||||
pub fn new_at(base_dir: &Path) -> Result<Self, String> {
|
||||
Self::open_at(base_dir, "favorites_cache.db")
|
||||
}
|
||||
|
||||
// ============ Track favorites ============
|
||||
|
||||
pub fn get_favorite_track_ids(&self) -> Result<Vec<i64>, String> {
|
||||
@@ -298,16 +306,37 @@ impl FavoritesCacheStore {
|
||||
// ============ Tauri State ============
|
||||
|
||||
pub struct FavoritesCacheState {
|
||||
pub store: Arc<Mutex<FavoritesCacheStore>>,
|
||||
pub store: Arc<Mutex<Option<FavoritesCacheStore>>>,
|
||||
}
|
||||
|
||||
impl FavoritesCacheState {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let store = FavoritesCacheStore::new()?;
|
||||
Ok(Self {
|
||||
store: Arc::new(Mutex::new(store)),
|
||||
store: Arc::new(Mutex::new(Some(store))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
store: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_at(&self, base_dir: &Path) -> Result<(), String> {
|
||||
let new_store = FavoritesCacheStore::new_at(base_dir)?;
|
||||
let mut guard = self.store.lock()
|
||||
.map_err(|_| "Failed to lock favorites cache store".to_string())?;
|
||||
*guard = Some(new_store);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) -> Result<(), String> {
|
||||
let mut guard = self.store.lock()
|
||||
.map_err(|_| "Failed to lock favorites cache store".to_string())?;
|
||||
*guard = None;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Tauri Commands ============
|
||||
@@ -317,10 +346,11 @@ impl FavoritesCacheState {
|
||||
pub fn get_cached_favorite_tracks(
|
||||
state: tauri::State<FavoritesCacheState>,
|
||||
) -> Result<Vec<i64>, String> {
|
||||
let store = state
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock favorites cache store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.get_favorite_track_ids()
|
||||
}
|
||||
|
||||
@@ -329,10 +359,11 @@ pub fn get_cached_favorite_tracks(
|
||||
pub fn get_cached_favorite_albums(
|
||||
state: tauri::State<FavoritesCacheState>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let store = state
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock favorites cache store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.get_favorite_album_ids()
|
||||
}
|
||||
|
||||
@@ -341,10 +372,11 @@ pub fn get_cached_favorite_albums(
|
||||
pub fn get_cached_favorite_artists(
|
||||
state: tauri::State<FavoritesCacheState>,
|
||||
) -> Result<Vec<i64>, String> {
|
||||
let store = state
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock favorites cache store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.get_favorite_artist_ids()
|
||||
}
|
||||
|
||||
@@ -354,10 +386,11 @@ pub fn cache_favorite_track(
|
||||
track_id: i64,
|
||||
state: tauri::State<FavoritesCacheState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock favorites cache store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.add_favorite_track(track_id)
|
||||
}
|
||||
|
||||
@@ -367,10 +400,11 @@ pub fn uncache_favorite_track(
|
||||
track_id: i64,
|
||||
state: tauri::State<FavoritesCacheState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock favorites cache store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.remove_favorite_track(track_id)
|
||||
}
|
||||
|
||||
@@ -380,10 +414,11 @@ pub fn cache_favorite_album(
|
||||
album_id: String,
|
||||
state: tauri::State<FavoritesCacheState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock favorites cache store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.add_favorite_album(&album_id)
|
||||
}
|
||||
|
||||
@@ -393,10 +428,11 @@ pub fn uncache_favorite_album(
|
||||
album_id: String,
|
||||
state: tauri::State<FavoritesCacheState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock favorites cache store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.remove_favorite_album(&album_id)
|
||||
}
|
||||
|
||||
@@ -406,10 +442,11 @@ pub fn cache_favorite_artist(
|
||||
artist_id: i64,
|
||||
state: tauri::State<FavoritesCacheState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock favorites cache store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.add_favorite_artist(artist_id)
|
||||
}
|
||||
|
||||
@@ -419,10 +456,11 @@ pub fn uncache_favorite_artist(
|
||||
artist_id: i64,
|
||||
state: tauri::State<FavoritesCacheState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock favorites cache store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.remove_favorite_artist(artist_id)
|
||||
}
|
||||
|
||||
@@ -432,10 +470,11 @@ pub fn sync_cached_favorite_tracks(
|
||||
track_ids: Vec<i64>,
|
||||
state: tauri::State<FavoritesCacheState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock favorites cache store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.sync_favorite_tracks(&track_ids)
|
||||
}
|
||||
|
||||
@@ -445,10 +484,11 @@ pub fn sync_cached_favorite_albums(
|
||||
album_ids: Vec<String>,
|
||||
state: tauri::State<FavoritesCacheState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock favorites cache store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.sync_favorite_albums(&album_ids)
|
||||
}
|
||||
|
||||
@@ -458,10 +498,11 @@ pub fn sync_cached_favorite_artists(
|
||||
artist_ids: Vec<i64>,
|
||||
state: tauri::State<FavoritesCacheState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock favorites cache store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.sync_favorite_artists(&artist_ids)
|
||||
}
|
||||
|
||||
@@ -470,9 +511,10 @@ pub fn sync_cached_favorite_artists(
|
||||
pub fn clear_favorites_cache(
|
||||
state: tauri::State<FavoritesCacheState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock favorites cache store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.clear_all()
|
||||
}
|
||||
@@ -35,15 +35,11 @@ pub struct FavoritesPreferencesStore {
|
||||
}
|
||||
|
||||
impl FavoritesPreferencesStore {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
|
||||
std::fs::create_dir_all(&data_dir)
|
||||
fn open_at(dir: &Path, db_name: &str) -> Result<Self, String> {
|
||||
std::fs::create_dir_all(dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
|
||||
let db_path = data_dir.join("favorites_preferences.db");
|
||||
let db_path = dir.join(db_name);
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open favorites preferences database: {}", e))?;
|
||||
|
||||
@@ -74,6 +70,17 @@ impl FavoritesPreferencesStore {
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
Self::open_at(&data_dir, "favorites_preferences.db")
|
||||
}
|
||||
|
||||
pub fn new_at(base_dir: &Path) -> Result<Self, String> {
|
||||
Self::open_at(base_dir, "favorites_preferences.db")
|
||||
}
|
||||
|
||||
fn favorites_icon_dir() -> Result<PathBuf, String> {
|
||||
let cache_dir = dirs::cache_dir()
|
||||
.ok_or("Could not determine cache directory")?
|
||||
@@ -206,26 +213,48 @@ impl FavoritesPreferencesStore {
|
||||
}
|
||||
|
||||
pub struct FavoritesPreferencesState {
|
||||
pub store: Arc<Mutex<FavoritesPreferencesStore>>,
|
||||
pub store: Arc<Mutex<Option<FavoritesPreferencesStore>>>,
|
||||
}
|
||||
|
||||
impl FavoritesPreferencesState {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let store = FavoritesPreferencesStore::new()?;
|
||||
Ok(Self {
|
||||
store: Arc::new(Mutex::new(store)),
|
||||
store: Arc::new(Mutex::new(Some(store))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
store: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_at(&self, base_dir: &Path) -> Result<(), String> {
|
||||
let new_store = FavoritesPreferencesStore::new_at(base_dir)?;
|
||||
let mut guard = self.store.lock()
|
||||
.map_err(|_| "Failed to lock favorites preferences store".to_string())?;
|
||||
*guard = Some(new_store);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) -> Result<(), String> {
|
||||
let mut guard = self.store.lock()
|
||||
.map_err(|_| "Failed to lock favorites preferences store".to_string())?;
|
||||
*guard = None;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_favorites_preferences(
|
||||
state: tauri::State<FavoritesPreferencesState>,
|
||||
) -> Result<FavoritesPreferences, String> {
|
||||
let store = state
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock favorites preferences store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.get_preferences()
|
||||
}
|
||||
|
||||
@@ -234,10 +263,11 @@ pub fn save_favorites_preferences(
|
||||
prefs: FavoritesPreferences,
|
||||
state: tauri::State<FavoritesPreferencesState>,
|
||||
) -> Result<FavoritesPreferences, String> {
|
||||
let store = state
|
||||
let guard = state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock favorites preferences store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.save_preferences(prefs)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use log::info;
|
||||
|
||||
@@ -27,15 +28,11 @@ pub struct LegalSettingsStore {
|
||||
}
|
||||
|
||||
impl LegalSettingsStore {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
|
||||
std::fs::create_dir_all(&data_dir)
|
||||
fn open_at(dir: &Path, db_name: &str) -> Result<Self, String> {
|
||||
std::fs::create_dir_all(dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
|
||||
let db_path = data_dir.join("legal_settings.db");
|
||||
let db_path = dir.join(db_name);
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open legal settings database: {}", e))?;
|
||||
|
||||
@@ -58,6 +55,17 @@ impl LegalSettingsStore {
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
Self::open_at(&data_dir, "legal_settings.db")
|
||||
}
|
||||
|
||||
pub fn new_at(base_dir: &Path) -> Result<Self, String> {
|
||||
Self::open_at(base_dir, "legal_settings.db")
|
||||
}
|
||||
|
||||
pub fn get_settings(&self) -> Result<LegalSettings, String> {
|
||||
self.conn
|
||||
.query_row(
|
||||
@@ -86,7 +94,16 @@ impl LegalSettingsStore {
|
||||
}
|
||||
}
|
||||
|
||||
pub type LegalSettingsState = Arc<Mutex<LegalSettingsStore>>;
|
||||
pub type LegalSettingsState = Arc<Mutex<Option<LegalSettingsStore>>>;
|
||||
|
||||
pub fn create_legal_settings_state() -> Result<LegalSettingsState, String> {
|
||||
let store = LegalSettingsStore::new()?;
|
||||
Ok(Arc::new(Mutex::new(Some(store))))
|
||||
}
|
||||
|
||||
pub fn create_empty_legal_settings_state() -> LegalSettingsState {
|
||||
Arc::new(Mutex::new(None))
|
||||
}
|
||||
|
||||
// Tauri commands
|
||||
|
||||
@@ -94,7 +111,8 @@ pub type LegalSettingsState = Arc<Mutex<LegalSettingsStore>>;
|
||||
pub fn get_legal_settings(
|
||||
state: tauri::State<'_, LegalSettingsState>,
|
||||
) -> Result<LegalSettings, String> {
|
||||
let store = state.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard = state.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.get_settings()
|
||||
}
|
||||
|
||||
@@ -102,7 +120,8 @@ pub fn get_legal_settings(
|
||||
pub fn get_qobuz_tos_accepted(
|
||||
state: tauri::State<'_, LegalSettingsState>,
|
||||
) -> Result<bool, String> {
|
||||
let store = state.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard = state.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
let settings = store.get_settings()?;
|
||||
Ok(settings.qobuz_tos_accepted)
|
||||
}
|
||||
@@ -112,6 +131,7 @@ pub fn set_qobuz_tos_accepted(
|
||||
state: tauri::State<'_, LegalSettingsState>,
|
||||
accepted: bool,
|
||||
) -> Result<(), String> {
|
||||
let store = state.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard = state.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_qobuz_tos_accepted(accepted)
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use log::info;
|
||||
|
||||
@@ -64,15 +65,11 @@ pub struct PlaybackPreferencesStore {
|
||||
}
|
||||
|
||||
impl PlaybackPreferencesStore {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
|
||||
std::fs::create_dir_all(&data_dir)
|
||||
fn open_at(dir: &Path, db_name: &str) -> Result<Self, String> {
|
||||
std::fs::create_dir_all(dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
|
||||
let db_path = data_dir.join("playback_preferences.db");
|
||||
let db_path = dir.join(db_name);
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open playback preferences database: {}", e))?;
|
||||
|
||||
@@ -118,6 +115,17 @@ impl PlaybackPreferencesStore {
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
Self::open_at(&data_dir, "playback_preferences.db")
|
||||
}
|
||||
|
||||
pub fn new_at(base_dir: &Path) -> Result<Self, String> {
|
||||
Self::open_at(base_dir, "playback_preferences.db")
|
||||
}
|
||||
|
||||
pub fn get_preferences(&self) -> Result<PlaybackPreferences, String> {
|
||||
self.conn
|
||||
.query_row(
|
||||
@@ -158,27 +166,54 @@ impl PlaybackPreferencesStore {
|
||||
|
||||
/// Global state wrapper for thread-safe access
|
||||
pub struct PlaybackPreferencesState {
|
||||
store: Arc<Mutex<PlaybackPreferencesStore>>,
|
||||
pub store: Arc<Mutex<Option<PlaybackPreferencesStore>>>,
|
||||
}
|
||||
|
||||
impl PlaybackPreferencesState {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let store = PlaybackPreferencesStore::new()?;
|
||||
Ok(Self {
|
||||
store: Arc::new(Mutex::new(store)),
|
||||
store: Arc::new(Mutex::new(Some(store))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
store: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_at(&self, base_dir: &Path) -> Result<(), String> {
|
||||
let new_store = PlaybackPreferencesStore::new_at(base_dir)?;
|
||||
let mut guard = self.store.lock()
|
||||
.map_err(|_| "Failed to lock playback preferences store".to_string())?;
|
||||
*guard = Some(new_store);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) -> Result<(), String> {
|
||||
let mut guard = self.store.lock()
|
||||
.map_err(|_| "Failed to lock playback preferences store".to_string())?;
|
||||
*guard = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_preferences(&self) -> Result<PlaybackPreferences, String> {
|
||||
self.store.lock().unwrap().get_preferences()
|
||||
let guard = self.store.lock().map_err(|_| "Failed to lock playback preferences store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.get_preferences()
|
||||
}
|
||||
|
||||
pub fn set_autoplay_mode(&self, mode: AutoplayMode) -> Result<(), String> {
|
||||
self.store.lock().unwrap().set_autoplay_mode(mode)
|
||||
let guard = self.store.lock().map_err(|_| "Failed to lock playback preferences store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_autoplay_mode(mode)
|
||||
}
|
||||
|
||||
pub fn set_show_context_icon(&self, show: bool) -> Result<(), String> {
|
||||
self.store.lock().unwrap().set_show_context_icon(show)
|
||||
let guard = self.store.lock().map_err(|_| "Failed to lock playback preferences store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_show_context_icon(show)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use base64::Engine;
|
||||
use rand::RngCore;
|
||||
use rusqlite::{params, Connection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -35,15 +36,11 @@ pub struct RemoteControlSettingsStore {
|
||||
}
|
||||
|
||||
impl RemoteControlSettingsStore {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
|
||||
std::fs::create_dir_all(&data_dir)
|
||||
fn open_at(dir: &Path, db_name: &str) -> Result<Self, String> {
|
||||
std::fs::create_dir_all(dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
|
||||
let db_path = data_dir.join("remote_control_settings.db");
|
||||
let db_path = dir.join(db_name);
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open remote control settings database: {}", e))?;
|
||||
|
||||
@@ -69,6 +66,17 @@ impl RemoteControlSettingsStore {
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
Self::open_at(&data_dir, "remote_control_settings.db")
|
||||
}
|
||||
|
||||
pub fn new_at(base_dir: &Path) -> Result<Self, String> {
|
||||
Self::open_at(base_dir, "remote_control_settings.db")
|
||||
}
|
||||
|
||||
pub fn get_settings(&self) -> Result<RemoteControlSettings, String> {
|
||||
let mut settings = self.conn
|
||||
.query_row(
|
||||
@@ -146,49 +154,75 @@ impl RemoteControlSettingsStore {
|
||||
|
||||
/// Global state wrapper for thread-safe access
|
||||
pub struct RemoteControlSettingsState {
|
||||
store: Arc<Mutex<RemoteControlSettingsStore>>,
|
||||
pub store: Arc<Mutex<Option<RemoteControlSettingsStore>>>,
|
||||
}
|
||||
|
||||
impl RemoteControlSettingsState {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
store: Arc::new(Mutex::new(RemoteControlSettingsStore::new()?)),
|
||||
store: Arc::new(Mutex::new(Some(RemoteControlSettingsStore::new()?))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
store: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_at(&self, base_dir: &Path) -> Result<(), String> {
|
||||
let new_store = RemoteControlSettingsStore::new_at(base_dir)?;
|
||||
let mut guard = self.store.lock()
|
||||
.map_err(|_| "Failed to lock remote control settings store".to_string())?;
|
||||
*guard = Some(new_store);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) -> Result<(), String> {
|
||||
let mut guard = self.store.lock()
|
||||
.map_err(|_| "Failed to lock remote control settings store".to_string())?;
|
||||
*guard = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_settings(&self) -> Result<RemoteControlSettings, String> {
|
||||
self.store
|
||||
let guard = self.store
|
||||
.lock()
|
||||
.map_err(|e| format!("Lock error: {}", e))?
|
||||
.get_settings()
|
||||
.map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.get_settings()
|
||||
}
|
||||
|
||||
pub fn set_enabled(&self, enabled: bool) -> Result<(), String> {
|
||||
self.store
|
||||
let guard = self.store
|
||||
.lock()
|
||||
.map_err(|e| format!("Lock error: {}", e))?
|
||||
.set_enabled(enabled)
|
||||
.map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_enabled(enabled)
|
||||
}
|
||||
|
||||
pub fn set_port(&self, port: u16) -> Result<(), String> {
|
||||
self.store
|
||||
let guard = self.store
|
||||
.lock()
|
||||
.map_err(|e| format!("Lock error: {}", e))?
|
||||
.set_port(port)
|
||||
.map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_port(port)
|
||||
}
|
||||
|
||||
pub fn set_secure(&self, secure: bool) -> Result<(), String> {
|
||||
self.store
|
||||
let guard = self.store
|
||||
.lock()
|
||||
.map_err(|e| format!("Lock error: {}", e))?
|
||||
.set_secure(secure)
|
||||
.map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_secure(secure)
|
||||
}
|
||||
|
||||
pub fn regenerate_token(&self) -> Result<String, String> {
|
||||
self.store
|
||||
let guard = self.store
|
||||
.lock()
|
||||
.map_err(|e| format!("Lock error: {}", e))?
|
||||
.regenerate_token()
|
||||
.map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.regenerate_token()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,15 +284,11 @@ pub struct AllowedOriginsStore {
|
||||
}
|
||||
|
||||
impl AllowedOriginsStore {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
|
||||
std::fs::create_dir_all(&data_dir)
|
||||
fn open_at(dir: &Path, db_name: &str) -> Result<Self, String> {
|
||||
std::fs::create_dir_all(dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
|
||||
let db_path = data_dir.join("remote_control_settings.db");
|
||||
let db_path = dir.join(db_name);
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open allowed origins database: {}", e))?;
|
||||
|
||||
@@ -290,6 +320,17 @@ impl AllowedOriginsStore {
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
Self::open_at(&data_dir, "remote_control_settings.db")
|
||||
}
|
||||
|
||||
pub fn new_at(base_dir: &Path) -> Result<Self, String> {
|
||||
Self::open_at(base_dir, "remote_control_settings.db")
|
||||
}
|
||||
|
||||
/// Get all allowed origins
|
||||
pub fn get_origins(&self) -> Result<Vec<AllowedOrigin>, String> {
|
||||
let mut stmt = self.conn
|
||||
@@ -372,48 +413,77 @@ impl AllowedOriginsStore {
|
||||
|
||||
/// Global state wrapper for thread-safe access to allowed origins
|
||||
pub struct AllowedOriginsState {
|
||||
store: Arc<Mutex<AllowedOriginsStore>>,
|
||||
pub store: Arc<Mutex<Option<AllowedOriginsStore>>>,
|
||||
}
|
||||
|
||||
impl AllowedOriginsState {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
store: Arc::new(Mutex::new(AllowedOriginsStore::new()?)),
|
||||
store: Arc::new(Mutex::new(Some(AllowedOriginsStore::new()?))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
store: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_at(&self, base_dir: &Path) -> Result<(), String> {
|
||||
let new_store = AllowedOriginsStore::new_at(base_dir)?;
|
||||
let mut guard = self.store.lock()
|
||||
.map_err(|_| "Failed to lock allowed origins store".to_string())?;
|
||||
*guard = Some(new_store);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) -> Result<(), String> {
|
||||
let mut guard = self.store.lock()
|
||||
.map_err(|_| "Failed to lock allowed origins store".to_string())?;
|
||||
*guard = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_origins(&self) -> Result<Vec<AllowedOrigin>, String> {
|
||||
self.store
|
||||
let guard = self.store
|
||||
.lock()
|
||||
.map_err(|e| format!("Lock error: {}", e))?
|
||||
.get_origins()
|
||||
.map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.get_origins()
|
||||
}
|
||||
|
||||
pub fn is_origin_allowed(&self, origin: &str) -> bool {
|
||||
self.store
|
||||
.lock()
|
||||
.map(|guard| {
|
||||
guard.as_ref()
|
||||
.map(|s| s.is_origin_allowed(origin))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn add_origin(&self, origin: &str) -> Result<AllowedOrigin, String> {
|
||||
self.store
|
||||
let guard = self.store
|
||||
.lock()
|
||||
.map_err(|e| format!("Lock error: {}", e))?
|
||||
.add_origin(origin)
|
||||
.map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.add_origin(origin)
|
||||
}
|
||||
|
||||
pub fn remove_origin(&self, id: i64) -> Result<(), String> {
|
||||
self.store
|
||||
let guard = self.store
|
||||
.lock()
|
||||
.map_err(|e| format!("Lock error: {}", e))?
|
||||
.remove_origin(id)
|
||||
.map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.remove_origin(id)
|
||||
}
|
||||
|
||||
pub fn restore_defaults(&self) -> Result<(), String> {
|
||||
self.store
|
||||
let guard = self.store
|
||||
.lock()
|
||||
.map_err(|e| format!("Lock error: {}", e))?
|
||||
.restore_defaults()
|
||||
.map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.restore_defaults()
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
const GRACE_PERIOD_SECS: i64 = 3 * 24 * 60 * 60; // 3 days
|
||||
@@ -37,15 +38,11 @@ pub struct SubscriptionStateStore {
|
||||
}
|
||||
|
||||
impl SubscriptionStateStore {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
|
||||
std::fs::create_dir_all(&data_dir)
|
||||
fn open_at(dir: &Path, db_name: &str) -> Result<Self, String> {
|
||||
std::fs::create_dir_all(dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
|
||||
let db_path = data_dir.join("subscription_state.db");
|
||||
let db_path = dir.join(db_name);
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open subscription state database: {}", e))?;
|
||||
|
||||
@@ -64,6 +61,17 @@ impl SubscriptionStateStore {
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
Self::open_at(&data_dir, "subscription_state.db")
|
||||
}
|
||||
|
||||
pub fn new_at(base_dir: &Path) -> Result<Self, String> {
|
||||
Self::open_at(base_dir, "subscription_state.db")
|
||||
}
|
||||
|
||||
pub fn get_state(&self) -> Result<SubscriptionState, String> {
|
||||
self.conn
|
||||
.query_row(
|
||||
@@ -138,10 +146,13 @@ impl SubscriptionStateStore {
|
||||
}
|
||||
}
|
||||
|
||||
pub type SubscriptionStateState = Arc<Mutex<SubscriptionStateStore>>;
|
||||
pub type SubscriptionStateState = Arc<Mutex<Option<SubscriptionStateStore>>>;
|
||||
|
||||
pub fn create_subscription_state() -> Result<SubscriptionStateState, String> {
|
||||
let store = SubscriptionStateStore::new()?;
|
||||
Ok(Arc::new(Mutex::new(store)))
|
||||
Ok(Arc::new(Mutex::new(Some(store))))
|
||||
}
|
||||
|
||||
pub fn create_empty_subscription_state() -> SubscriptionStateState {
|
||||
Arc::new(Mutex::new(None))
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use log::info;
|
||||
|
||||
@@ -35,15 +36,11 @@ pub struct TraySettingsStore {
|
||||
}
|
||||
|
||||
impl TraySettingsStore {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
|
||||
std::fs::create_dir_all(&data_dir)
|
||||
fn open_at(dir: &Path, db_name: &str) -> Result<Self, String> {
|
||||
std::fs::create_dir_all(dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
|
||||
let db_path = data_dir.join("tray_settings.db");
|
||||
let db_path = dir.join(db_name);
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open tray settings database: {}", e))?;
|
||||
|
||||
@@ -69,6 +66,17 @@ impl TraySettingsStore {
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let data_dir = dirs::data_dir()
|
||||
.ok_or("Could not determine data directory")?
|
||||
.join("qbz");
|
||||
Self::open_at(&data_dir, "tray_settings.db")
|
||||
}
|
||||
|
||||
pub fn new_at(base_dir: &Path) -> Result<Self, String> {
|
||||
Self::open_at(base_dir, "tray_settings.db")
|
||||
}
|
||||
|
||||
pub fn get_settings(&self) -> Result<TraySettings, String> {
|
||||
self.conn
|
||||
.query_row(
|
||||
@@ -121,31 +129,60 @@ impl TraySettingsStore {
|
||||
|
||||
/// Global state wrapper for thread-safe access
|
||||
pub struct TraySettingsState {
|
||||
store: Arc<Mutex<TraySettingsStore>>,
|
||||
pub store: Arc<Mutex<Option<TraySettingsStore>>>,
|
||||
}
|
||||
|
||||
impl TraySettingsState {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let store = TraySettingsStore::new()?;
|
||||
Ok(Self {
|
||||
store: Arc::new(Mutex::new(store)),
|
||||
store: Arc::new(Mutex::new(Some(store))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
store: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_at(&self, base_dir: &Path) -> Result<(), String> {
|
||||
let new_store = TraySettingsStore::new_at(base_dir)?;
|
||||
let mut guard = self.store.lock()
|
||||
.map_err(|_| "Failed to lock tray settings store".to_string())?;
|
||||
*guard = Some(new_store);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) -> Result<(), String> {
|
||||
let mut guard = self.store.lock()
|
||||
.map_err(|_| "Failed to lock tray settings store".to_string())?;
|
||||
*guard = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_settings(&self) -> Result<TraySettings, String> {
|
||||
self.store.lock().unwrap().get_settings()
|
||||
let guard = self.store.lock().map_err(|_| "Failed to lock tray settings store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.get_settings()
|
||||
}
|
||||
|
||||
pub fn set_enable_tray(&self, value: bool) -> Result<(), String> {
|
||||
self.store.lock().unwrap().set_enable_tray(value)
|
||||
let guard = self.store.lock().map_err(|_| "Failed to lock tray settings store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_enable_tray(value)
|
||||
}
|
||||
|
||||
pub fn set_minimize_to_tray(&self, value: bool) -> Result<(), String> {
|
||||
self.store.lock().unwrap().set_minimize_to_tray(value)
|
||||
let guard = self.store.lock().map_err(|_| "Failed to lock tray settings store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_minimize_to_tray(value)
|
||||
}
|
||||
|
||||
pub fn set_close_to_tray(&self, value: bool) -> Result<(), String> {
|
||||
self.store.lock().unwrap().set_close_to_tray(value)
|
||||
let guard = self.store.lock().map_err(|_| "Failed to lock tray settings store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_close_to_tray(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ pub use models::*;
|
||||
pub use tag_sidecar::*;
|
||||
pub use scanner::{LibraryScanner, ScanResult};
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -56,7 +56,30 @@ pub fn init_library_state() -> Result<LibraryState, LibraryError> {
|
||||
let db = LibraryDatabase::open(&db_path)?;
|
||||
|
||||
Ok(LibraryState {
|
||||
db: Arc::new(Mutex::new(db)),
|
||||
db: Arc::new(Mutex::new(Some(db))),
|
||||
scan_progress: Arc::new(Mutex::new(ScanProgress::default())),
|
||||
scan_cancel: Arc::new(AtomicBool::new(false)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Initialize library state with no database (for deferred init)
|
||||
pub fn init_library_state_empty() -> LibraryState {
|
||||
LibraryState {
|
||||
db: Arc::new(Mutex::new(None)),
|
||||
scan_progress: Arc::new(Mutex::new(ScanProgress::default())),
|
||||
scan_cancel: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize library state at a specific directory
|
||||
pub fn init_library_state_at(base_dir: &Path) -> Result<LibraryState, LibraryError> {
|
||||
std::fs::create_dir_all(base_dir)
|
||||
.map_err(|e| LibraryError::Database(format!("Failed to create directory: {}", e)))?;
|
||||
let db_path = base_dir.join("library.db");
|
||||
let db = LibraryDatabase::open(&db_path)?;
|
||||
|
||||
Ok(LibraryState {
|
||||
db: Arc::new(Mutex::new(Some(db))),
|
||||
scan_progress: Arc::new(Mutex::new(ScanProgress::default())),
|
||||
scan_cancel: Arc::new(AtomicBool::new(false)),
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@ const QUEUE_MAX_SIZE: i64 = 500;
|
||||
|
||||
/// ListenBrainz cache state for Tauri
|
||||
pub struct ListenBrainzCacheState {
|
||||
pub cache: Arc<Mutex<ListenBrainzCache>>,
|
||||
pub cache: Arc<Mutex<Option<ListenBrainzCache>>>,
|
||||
}
|
||||
|
||||
impl ListenBrainzCacheState {
|
||||
@@ -37,9 +37,32 @@ impl ListenBrainzCacheState {
|
||||
log::info!("ListenBrainz cache initialized at {:?}", db_path);
|
||||
|
||||
Ok(Self {
|
||||
cache: Arc::new(Mutex::new(cache)),
|
||||
cache: Arc::new(Mutex::new(Some(cache))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
cache: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_at(&self, base_dir: &Path) -> Result<(), String> {
|
||||
let cache_dir = base_dir.join("cache");
|
||||
std::fs::create_dir_all(&cache_dir)
|
||||
.map_err(|e| format!("Failed to create cache directory: {}", e))?;
|
||||
let db_path = cache_dir.join("listenbrainz_cache.db");
|
||||
let new_cache = ListenBrainzCache::new(&db_path)?;
|
||||
log::info!("ListenBrainz cache initialized at {:?}", db_path);
|
||||
let mut guard = self.cache.blocking_lock();
|
||||
*guard = Some(new_cache);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) {
|
||||
let mut guard = self.cache.blocking_lock();
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// ListenBrainz SQLite cache
|
||||
|
||||
@@ -38,13 +38,14 @@ pub use models::{
|
||||
SubmitListensPayload, TrackMetadata, UserInfo,
|
||||
};
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
/// Shared state wrapper for Tauri
|
||||
pub struct ListenBrainzSharedState {
|
||||
pub client: Arc<Mutex<ListenBrainzClient>>,
|
||||
pub cache: Arc<Mutex<ListenBrainzCache>>,
|
||||
pub cache: Arc<Mutex<Option<ListenBrainzCache>>>,
|
||||
}
|
||||
|
||||
impl ListenBrainzSharedState {
|
||||
@@ -53,10 +54,14 @@ impl ListenBrainzSharedState {
|
||||
|
||||
// Check for saved credentials and enabled state
|
||||
let (token, user_name, enabled) = {
|
||||
let cache = cache_state.cache.blocking_lock();
|
||||
let cache_guard = cache_state.cache.blocking_lock();
|
||||
if let Some(cache) = cache_guard.as_ref() {
|
||||
let (token, user_name) = cache.get_credentials().unwrap_or((None, None));
|
||||
let enabled = cache.is_enabled().unwrap_or(true);
|
||||
(token, user_name, enabled)
|
||||
} else {
|
||||
(None, None, true)
|
||||
}
|
||||
};
|
||||
|
||||
// Create client with saved config
|
||||
@@ -76,4 +81,45 @@ impl ListenBrainzSharedState {
|
||||
cache: cache_state.cache,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
let client = ListenBrainzClient::with_config(ListenBrainzConfig {
|
||||
enabled: true,
|
||||
token: None,
|
||||
user_name: None,
|
||||
});
|
||||
Self {
|
||||
client: Arc::new(Mutex::new(client)),
|
||||
cache: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_at(&self, base_dir: &Path) -> Result<(), String> {
|
||||
let cache_dir = base_dir.join("cache");
|
||||
std::fs::create_dir_all(&cache_dir)
|
||||
.map_err(|e| format!("Failed to create cache directory: {}", e))?;
|
||||
let db_path = cache_dir.join("listenbrainz_cache.db");
|
||||
let new_cache = ListenBrainzCache::new(&db_path)?;
|
||||
log::info!("ListenBrainz cache initialized at {:?}", db_path);
|
||||
|
||||
// Restore credentials from new cache
|
||||
let (token, user_name) = {
|
||||
let (token, user_name) = new_cache.get_credentials().unwrap_or((None, None));
|
||||
(token, user_name)
|
||||
};
|
||||
|
||||
let mut guard = self.cache.blocking_lock();
|
||||
*guard = Some(new_cache);
|
||||
|
||||
if token.is_some() && user_name.is_some() {
|
||||
log::info!("ListenBrainz: restored session for user {:?}", user_name);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) {
|
||||
let mut guard = self.cache.blocking_lock();
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,8 @@ pub async fn lyrics_get(
|
||||
|
||||
// Try cache by track_id first, then by key
|
||||
{
|
||||
let db = state.db.lock().await;
|
||||
let db_opt__ = state.db.lock().await;
|
||||
let db = db_opt__.as_ref().ok_or("No active session - please log in")?;
|
||||
if let Some(id) = track_id {
|
||||
if let Ok(Some(payload)) = db.get_by_track_id(id) {
|
||||
return Ok(Some(payload));
|
||||
@@ -52,7 +53,8 @@ pub async fn lyrics_get(
|
||||
cached: false,
|
||||
};
|
||||
|
||||
let db = state.db.lock().await;
|
||||
let db_opt__ = state.db.lock().await;
|
||||
let db = db_opt__.as_ref().ok_or("No active session - please log in")?;
|
||||
db.upsert(&cache_key, &payload)?;
|
||||
return Ok(Some(payload));
|
||||
}
|
||||
@@ -70,7 +72,8 @@ pub async fn lyrics_get(
|
||||
cached: false,
|
||||
};
|
||||
|
||||
let db = state.db.lock().await;
|
||||
let db_opt__ = state.db.lock().await;
|
||||
let db = db_opt__.as_ref().ok_or("No active session - please log in")?;
|
||||
db.upsert(&cache_key, &payload)?;
|
||||
return Ok(Some(payload));
|
||||
}
|
||||
@@ -80,7 +83,8 @@ pub async fn lyrics_get(
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn lyrics_clear_cache(state: State<'_, LyricsState>) -> Result<(), String> {
|
||||
let db = state.db.lock().await;
|
||||
let db_opt__ = state.db.lock().await;
|
||||
let db = db_opt__.as_ref().ok_or("No active session - please log in")?;
|
||||
db.clear()
|
||||
}
|
||||
|
||||
@@ -94,7 +98,8 @@ pub struct LyricsCacheStats {
|
||||
#[tauri::command]
|
||||
pub async fn lyrics_get_cache_stats(state: State<'_, LyricsState>) -> Result<LyricsCacheStats, String> {
|
||||
let entries = {
|
||||
let db = state.db.lock().await;
|
||||
let db_opt__ = state.db.lock().await;
|
||||
let db = db_opt__.as_ref().ok_or("No active session - please log in")?;
|
||||
db.count_entries()?
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ pub mod commands;
|
||||
pub mod providers;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
@@ -51,7 +52,7 @@ impl LyricsProvider {
|
||||
|
||||
/// Lyrics state shared across commands
|
||||
pub struct LyricsState {
|
||||
pub db: Arc<Mutex<LyricsCacheDb>>,
|
||||
pub db: Arc<Mutex<Option<LyricsCacheDb>>>,
|
||||
}
|
||||
|
||||
impl LyricsState {
|
||||
@@ -68,9 +69,31 @@ impl LyricsState {
|
||||
let db = LyricsCacheDb::new(&db_path)?;
|
||||
|
||||
Ok(Self {
|
||||
db: Arc::new(Mutex::new(db)),
|
||||
db: Arc::new(Mutex::new(Some(db))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
db: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_at(&self, base_dir: &Path) -> Result<(), String> {
|
||||
let cache_dir = base_dir.join("lyrics");
|
||||
std::fs::create_dir_all(&cache_dir)
|
||||
.map_err(|e| format!("Failed to create lyrics cache directory: {}", e))?;
|
||||
let db_path = cache_dir.join("lyrics.db");
|
||||
let new_db = LyricsCacheDb::new(&db_path)?;
|
||||
let mut guard = self.db.blocking_lock();
|
||||
*guard = Some(new_db);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) {
|
||||
let mut guard = self.db.blocking_lock();
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_cache_key(title: &str, artist: &str, duration_secs: Option<u64>) -> String {
|
||||
|
||||
@@ -24,7 +24,7 @@ const RELATIONS_TTL_SECS: i64 = 7 * 24 * 60 * 60;
|
||||
|
||||
/// MusicBrainz cache state shared across commands
|
||||
pub struct MusicBrainzCacheState {
|
||||
pub cache: Arc<Mutex<MusicBrainzCache>>,
|
||||
pub cache: Arc<Mutex<Option<MusicBrainzCache>>>,
|
||||
}
|
||||
|
||||
impl MusicBrainzCacheState {
|
||||
@@ -43,9 +43,32 @@ impl MusicBrainzCacheState {
|
||||
log::info!("MusicBrainz cache initialized at {:?}", db_path);
|
||||
|
||||
Ok(Self {
|
||||
cache: Arc::new(Mutex::new(cache)),
|
||||
cache: Arc::new(Mutex::new(Some(cache))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
cache: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_at(&self, base_dir: &Path) -> Result<(), String> {
|
||||
let cache_dir = base_dir.join("cache");
|
||||
std::fs::create_dir_all(&cache_dir)
|
||||
.map_err(|e| format!("Failed to create cache directory: {}", e))?;
|
||||
let db_path = cache_dir.join("musicbrainz_cache.db");
|
||||
let new_cache = MusicBrainzCache::new(&db_path)?;
|
||||
log::info!("MusicBrainz cache initialized at {:?}", db_path);
|
||||
let mut guard = self.cache.blocking_lock();
|
||||
*guard = Some(new_cache);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) {
|
||||
let mut guard = self.cache.blocking_lock();
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// MusicBrainz SQLite cache
|
||||
|
||||
@@ -28,13 +28,14 @@ pub use models::{
|
||||
ResolvedTrack, Tag,
|
||||
};
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
/// Shared state wrapper for Tauri
|
||||
pub struct MusicBrainzSharedState {
|
||||
pub client: Arc<MusicBrainzClient>,
|
||||
pub cache: Arc<Mutex<MusicBrainzCache>>,
|
||||
pub cache: Arc<Mutex<Option<MusicBrainzCache>>>,
|
||||
}
|
||||
|
||||
impl MusicBrainzSharedState {
|
||||
@@ -45,4 +46,28 @@ impl MusicBrainzSharedState {
|
||||
cache: cache_state.cache,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
client: Arc::new(MusicBrainzClient::new()),
|
||||
cache: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_at(&self, base_dir: &Path) -> Result<(), String> {
|
||||
let cache_dir = base_dir.join("cache");
|
||||
std::fs::create_dir_all(&cache_dir)
|
||||
.map_err(|e| format!("Failed to create cache directory: {}", e))?;
|
||||
let db_path = cache_dir.join("musicbrainz_cache.db");
|
||||
let new_cache = MusicBrainzCache::new(&db_path)?;
|
||||
log::info!("MusicBrainz cache initialized at {:?}", db_path);
|
||||
let mut guard = self.cache.blocking_lock();
|
||||
*guard = Some(new_cache);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) {
|
||||
let mut guard = self.cache.blocking_lock();
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,17 @@ impl OfflineStore {
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
|
||||
let db_path = data_dir.join("offline_settings.db");
|
||||
Self::open_at(&db_path)
|
||||
}
|
||||
|
||||
pub fn new_at(base_dir: &std::path::Path) -> Result<Self, String> {
|
||||
std::fs::create_dir_all(base_dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
let db_path = base_dir.join("offline_settings.db");
|
||||
Self::open_at(&db_path)
|
||||
}
|
||||
|
||||
fn open_at(db_path: &std::path::Path) -> Result<Self, String> {
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|e| format!("Failed to open offline settings database: {}", e))?;
|
||||
|
||||
@@ -503,15 +514,34 @@ impl OfflineStore {
|
||||
|
||||
/// Thread-safe wrapper for OfflineStore
|
||||
pub struct OfflineState {
|
||||
pub store: Arc<Mutex<OfflineStore>>,
|
||||
pub store: Arc<Mutex<Option<OfflineStore>>>,
|
||||
}
|
||||
|
||||
impl OfflineState {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
store: Arc::new(Mutex::new(OfflineStore::new()?)),
|
||||
store: Arc::new(Mutex::new(Some(OfflineStore::new()?))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
store: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_at(&self, base_dir: &std::path::Path) -> Result<(), String> {
|
||||
let new_store = OfflineStore::new_at(base_dir)?;
|
||||
let mut guard = self.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
*guard = Some(new_store);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) {
|
||||
if let Ok(mut guard) = self.store.lock() {
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Counter for alternating between neutral endpoint and Qobuz checks.
|
||||
@@ -595,7 +625,8 @@ pub mod commands {
|
||||
offline_state: State<'_, OfflineState>,
|
||||
) -> Result<OfflineStatus, String> {
|
||||
let settings = {
|
||||
let store = offline_state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = offline_state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.get_settings()?
|
||||
};
|
||||
|
||||
@@ -631,7 +662,8 @@ pub mod commands {
|
||||
pub fn get_offline_settings(
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<OfflineSettings, String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.get_settings()
|
||||
}
|
||||
|
||||
@@ -643,7 +675,8 @@ pub mod commands {
|
||||
app_handle: AppHandle,
|
||||
) -> Result<OfflineStatus, String> {
|
||||
{
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_manual_offline_mode(enabled)?;
|
||||
}
|
||||
|
||||
@@ -662,7 +695,8 @@ pub mod commands {
|
||||
enabled: bool,
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_show_partial_playlists(enabled)
|
||||
}
|
||||
|
||||
@@ -672,7 +706,8 @@ pub mod commands {
|
||||
enabled: bool,
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_allow_cast_while_offline(enabled)
|
||||
}
|
||||
|
||||
@@ -682,7 +717,8 @@ pub mod commands {
|
||||
enabled: bool,
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_allow_immediate_scrobbling(enabled)
|
||||
}
|
||||
|
||||
@@ -692,7 +728,8 @@ pub mod commands {
|
||||
enabled: bool,
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_allow_accumulated_scrobbling(enabled)
|
||||
}
|
||||
|
||||
@@ -702,7 +739,8 @@ pub mod commands {
|
||||
enabled: bool,
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_show_network_folders_in_manual_offline(enabled)
|
||||
}
|
||||
|
||||
@@ -724,7 +762,8 @@ pub mod commands {
|
||||
local_track_paths: Vec<String>,
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<i64, String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.create_pending_playlist(&name, description.as_deref(), is_public, &track_ids, &local_track_paths)
|
||||
}
|
||||
|
||||
@@ -733,7 +772,8 @@ pub mod commands {
|
||||
pub fn get_pending_playlists(
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<Vec<PendingPlaylist>, String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.get_pending_playlists()
|
||||
}
|
||||
|
||||
@@ -745,7 +785,8 @@ pub mod commands {
|
||||
local_track_paths: Vec<String>,
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.add_tracks_to_pending_playlist(pending_id, &qobuz_track_ids, &local_track_paths)
|
||||
}
|
||||
|
||||
@@ -754,7 +795,8 @@ pub mod commands {
|
||||
pub fn get_pending_playlist_count(
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<u32, String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.get_pending_playlist_count()
|
||||
}
|
||||
|
||||
@@ -765,7 +807,8 @@ pub mod commands {
|
||||
qobuz_playlist_id: u64,
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.update_qobuz_playlist_id(pending_id, qobuz_playlist_id)
|
||||
}
|
||||
|
||||
@@ -776,7 +819,8 @@ pub mod commands {
|
||||
qobuz_playlist_id: u64,
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.mark_playlist_synced(pending_id, qobuz_playlist_id)
|
||||
}
|
||||
|
||||
@@ -786,7 +830,8 @@ pub mod commands {
|
||||
pending_id: i64,
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.delete_pending_playlist(pending_id)
|
||||
}
|
||||
|
||||
@@ -801,7 +846,8 @@ pub mod commands {
|
||||
timestamp: i64,
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<i64, String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.queue_scrobble(&artist, &track, album.as_deref(), timestamp)
|
||||
}
|
||||
|
||||
@@ -811,7 +857,8 @@ pub mod commands {
|
||||
limit: Option<u32>,
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<Vec<QueuedScrobble>, String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.get_queued_scrobbles(limit.unwrap_or(50))
|
||||
}
|
||||
|
||||
@@ -821,7 +868,8 @@ pub mod commands {
|
||||
ids: Vec<i64>,
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.mark_scrobbles_sent(&ids)
|
||||
}
|
||||
|
||||
@@ -830,7 +878,8 @@ pub mod commands {
|
||||
pub fn get_queued_scrobble_count(
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<u32, String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.get_queued_scrobble_count()
|
||||
}
|
||||
|
||||
@@ -840,7 +889,8 @@ pub mod commands {
|
||||
older_than_days: Option<u32>,
|
||||
state: State<'_, OfflineState>,
|
||||
) -> Result<u32, String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let guard__ = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
let store = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
store.cleanup_sent_scrobbles(older_than_days.unwrap_or(7))
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ async fn post_process_cached_track(
|
||||
current_path: &str,
|
||||
offline_root: &str,
|
||||
qobuz_client: &crate::api::QobuzClient,
|
||||
library_db: Arc<tokio::sync::Mutex<crate::library::database::LibraryDatabase>>,
|
||||
library_db: Arc<tokio::sync::Mutex<Option<crate::library::database::LibraryDatabase>>>,
|
||||
) -> Result<String, String> {
|
||||
log::info!("Post-processing cached track {}", track_id);
|
||||
|
||||
@@ -79,7 +79,8 @@ async fn post_process_cached_track(
|
||||
};
|
||||
|
||||
// 7. ALWAYS insert into local library DB (visibility controlled by toggle)
|
||||
let lib_guard = library_db.lock().await;
|
||||
let lib_opt__ = library_db.lock().await;
|
||||
let lib_guard = lib_opt__.as_ref().ok_or("No active session - please log in")?;
|
||||
|
||||
// Generate album_group_key: album|album_artist
|
||||
let album_artist = metadata.album_artist.as_ref().unwrap_or(&metadata.artist);
|
||||
@@ -147,7 +148,8 @@ pub async fn cache_track_for_offline(
|
||||
|
||||
// Insert into database as queued
|
||||
{
|
||||
let db = cache_state.db.lock().await;
|
||||
let guard__ = cache_state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
db.insert_track(&track_info, &file_path_str)?;
|
||||
}
|
||||
|
||||
@@ -166,12 +168,13 @@ pub async fn cache_track_for_offline(
|
||||
Ok(permit) => permit,
|
||||
Err(err) => {
|
||||
log::error!("Failed to acquire cache slot for track {}: {}", track_id, err);
|
||||
let db_guard = db.lock().await;
|
||||
if let Some(db_guard) = db.lock().await.as_ref() {
|
||||
let _ = db_guard.update_status(
|
||||
track_id,
|
||||
OfflineCacheStatus::Failed,
|
||||
Some("Failed to start caching"),
|
||||
);
|
||||
}
|
||||
let _ = app.emit("offline:caching_failed", serde_json::json!({
|
||||
"trackId": track_id,
|
||||
"error": "Failed to acquire cache slot"
|
||||
@@ -182,9 +185,10 @@ pub async fn cache_track_for_offline(
|
||||
|
||||
// Update status to caching
|
||||
{
|
||||
let db_guard = db.lock().await;
|
||||
if let Some(db_guard) = db.lock().await.as_ref() {
|
||||
let _ = db_guard.update_status(track_id, OfflineCacheStatus::Downloading, None);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = app.emit("offline:caching_started", serde_json::json!({
|
||||
"trackId": track_id
|
||||
@@ -202,12 +206,13 @@ pub async fn cache_track_for_offline(
|
||||
Ok(s) => s.url,
|
||||
Err(e) => {
|
||||
log::error!("Failed to get stream URL for track {}: {}", track_id, e);
|
||||
let db_guard = db.lock().await;
|
||||
if let Some(db_guard) = db.lock().await.as_ref() {
|
||||
let _ = db_guard.update_status(
|
||||
track_id,
|
||||
OfflineCacheStatus::Failed,
|
||||
Some(&format!("Failed to get stream URL: {}", e)),
|
||||
);
|
||||
}
|
||||
let _ = app.emit("offline:caching_failed", serde_json::json!({
|
||||
"trackId": track_id,
|
||||
"error": e.to_string()
|
||||
@@ -224,9 +229,10 @@ pub async fn cache_track_for_offline(
|
||||
Ok(size) => {
|
||||
log::info!("Caching complete for track {}: {} bytes", track_id, size);
|
||||
{
|
||||
let db_guard = db.lock().await;
|
||||
if let Some(db_guard) = db.lock().await.as_ref() {
|
||||
let _ = db_guard.mark_complete(track_id, size);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = app.emit("offline:caching_completed", serde_json::json!({
|
||||
"trackId": track_id,
|
||||
@@ -247,10 +253,11 @@ pub async fn cache_track_for_offline(
|
||||
).await {
|
||||
Ok(new_path) => {
|
||||
// Update database with new path
|
||||
let db_guard = db.lock().await;
|
||||
if let Some(db_guard) = db.lock().await.as_ref() {
|
||||
if let Err(e) = db_guard.update_file_path(track_id, &new_path) {
|
||||
log::error!("Failed to update path for track {}: {}", track_id, e);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = app.emit("offline:caching_processed", serde_json::json!({
|
||||
"trackId": track_id,
|
||||
@@ -265,8 +272,9 @@ pub async fn cache_track_for_offline(
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Caching failed for track {}: {}", track_id, e);
|
||||
let db_guard = db.lock().await;
|
||||
if let Some(db_guard) = db.lock().await.as_ref() {
|
||||
let _ = db_guard.update_status(track_id, OfflineCacheStatus::Failed, Some(&e));
|
||||
}
|
||||
let _ = app.emit("offline:caching_failed", serde_json::json!({
|
||||
"trackId": track_id,
|
||||
"error": e
|
||||
@@ -284,7 +292,8 @@ pub async fn is_track_cached(
|
||||
track_id: u64,
|
||||
cache_state: State<'_, OfflineCacheState>,
|
||||
) -> Result<bool, String> {
|
||||
let db = cache_state.db.lock().await;
|
||||
let guard__ = cache_state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
db.is_cached(track_id)
|
||||
}
|
||||
|
||||
@@ -294,7 +303,8 @@ pub async fn get_cached_track_path(
|
||||
track_id: u64,
|
||||
cache_state: State<'_, OfflineCacheState>,
|
||||
) -> Result<Option<String>, String> {
|
||||
let db = cache_state.db.lock().await;
|
||||
let guard__ = cache_state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
db.get_file_path(track_id)
|
||||
}
|
||||
|
||||
@@ -304,7 +314,8 @@ pub async fn get_cached_track(
|
||||
track_id: u64,
|
||||
cache_state: State<'_, OfflineCacheState>,
|
||||
) -> Result<Option<CachedTrackInfo>, String> {
|
||||
let db = cache_state.db.lock().await;
|
||||
let guard__ = cache_state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
db.get_track(track_id)
|
||||
}
|
||||
|
||||
@@ -313,7 +324,8 @@ pub async fn get_cached_track(
|
||||
pub async fn get_cached_tracks(
|
||||
cache_state: State<'_, OfflineCacheState>,
|
||||
) -> Result<Vec<CachedTrackInfo>, String> {
|
||||
let db = cache_state.db.lock().await;
|
||||
let guard__ = cache_state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
db.get_all_tracks()
|
||||
}
|
||||
|
||||
@@ -322,8 +334,9 @@ pub async fn get_cached_tracks(
|
||||
pub async fn get_offline_cache_stats(
|
||||
cache_state: State<'_, OfflineCacheState>,
|
||||
) -> Result<OfflineCacheStats, String> {
|
||||
let db = cache_state.db.lock().await;
|
||||
let limit = *cache_state.limit_bytes.lock().await;
|
||||
let guard__ = cache_state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
db.get_stats(&cache_state.get_cache_path(), limit)
|
||||
}
|
||||
|
||||
@@ -336,7 +349,9 @@ pub async fn remove_cached_track(
|
||||
) -> Result<(), String> {
|
||||
log::info!("Command: remove_cached_track {}", track_id);
|
||||
|
||||
let db = cache_state.db.lock().await;
|
||||
{
|
||||
let guard__ = cache_state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
|
||||
// Get file path and delete from DB
|
||||
if let Some(file_path) = db.delete_track(track_id)? {
|
||||
@@ -352,10 +367,11 @@ pub async fn remove_cached_track(
|
||||
cleanup_empty_folder(album_dir, &cache_state.cache_dir);
|
||||
}
|
||||
}
|
||||
drop(db);
|
||||
}
|
||||
|
||||
// Also remove from library if it was added
|
||||
let library_db = library_state.db.lock().await;
|
||||
let guard__ = library_state.db.lock().await;
|
||||
let library_db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
let _ = library_db.remove_qobuz_cached_track(track_id);
|
||||
|
||||
Ok(())
|
||||
@@ -413,11 +429,11 @@ pub async fn purge_all_cached_files(
|
||||
cache_state: &OfflineCacheState,
|
||||
library_state: &crate::library::commands::LibraryState,
|
||||
) -> Result<(), String> {
|
||||
let db = cache_state.db.lock().await;
|
||||
|
||||
// Get all file paths and clear DB
|
||||
let paths = db.clear_all()?;
|
||||
drop(db);
|
||||
let paths = {
|
||||
let guard__ = cache_state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
db.clear_all()?
|
||||
};
|
||||
|
||||
// Delete all files
|
||||
for path in paths {
|
||||
@@ -458,7 +474,8 @@ pub async fn purge_all_cached_files(
|
||||
}
|
||||
|
||||
// Remove all Qobuz cached tracks from library
|
||||
let library_db = library_state.db.lock().await;
|
||||
let guard__ = library_state.db.lock().await;
|
||||
let library_db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
let removed_count = library_db
|
||||
.remove_all_qobuz_cached_tracks()
|
||||
.map_err(|e| format!("Failed to remove cached tracks from library: {}", e))?;
|
||||
@@ -515,7 +532,8 @@ pub async fn open_album_folder(
|
||||
) -> Result<(), String> {
|
||||
log::info!("Command: open_album_folder for album_id: {}", album_id);
|
||||
|
||||
let db = cache_state.db.lock().await;
|
||||
let guard__ = cache_state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
|
||||
// Get tracks for this album
|
||||
let tracks = db.get_all_tracks()?;
|
||||
@@ -556,7 +574,8 @@ pub async fn open_track_folder(
|
||||
) -> Result<(), String> {
|
||||
log::info!("Command: open_track_folder for track_id: {}", track_id);
|
||||
|
||||
let db = cache_state.db.lock().await;
|
||||
let guard__ = cache_state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
|
||||
// Get the track's file path
|
||||
let file_path = db.get_file_path(track_id)?
|
||||
@@ -583,7 +602,8 @@ pub async fn check_album_fully_cached(
|
||||
album_id: String,
|
||||
cache_state: State<'_, OfflineCacheState>,
|
||||
) -> Result<bool, String> {
|
||||
let db = cache_state.db.lock().await;
|
||||
let guard__ = cache_state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
|
||||
// Get all tracks for this album
|
||||
let tracks = db.get_all_tracks()?;
|
||||
@@ -611,7 +631,8 @@ async fn evict_if_needed(
|
||||
cache_state: &OfflineCacheState,
|
||||
limit_bytes: u64,
|
||||
) -> Result<(), String> {
|
||||
let db = cache_state.db.lock().await;
|
||||
let guard__ = cache_state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
let stats = db.get_stats(&cache_state.get_cache_path(), Some(limit_bytes))?;
|
||||
|
||||
if stats.total_size_bytes <= limit_bytes {
|
||||
@@ -746,15 +767,21 @@ pub async fn sync_offline_cache_to_library(
|
||||
) -> Result<SyncResult, String> {
|
||||
log::info!("Command: sync_offline_cache_to_library");
|
||||
|
||||
let cache_db = cache_state.db.lock().await;
|
||||
let library_db = library_state.db.lock().await;
|
||||
// Get ready tracks from cache (scoped to drop lock before library access)
|
||||
let ready_tracks = {
|
||||
let guard__ = cache_state.db.lock().await;
|
||||
let cache_db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
cache_db.get_ready_tracks_for_sync()?
|
||||
};
|
||||
|
||||
// Get all ready tracks from offline cache
|
||||
let ready_tracks = cache_db.get_ready_tracks_for_sync()?;
|
||||
let mut synced = 0u32;
|
||||
let mut already_present = 0u32;
|
||||
let mut errors = 0u32;
|
||||
|
||||
let mut synced = 0;
|
||||
let mut already_present = 0;
|
||||
let mut errors = 0;
|
||||
// Process tracks with library lock (scoped)
|
||||
{
|
||||
let guard__ = library_state.db.lock().await;
|
||||
let library_db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
|
||||
for track in ready_tracks {
|
||||
// Check if track already exists in library
|
||||
@@ -790,6 +817,7 @@ pub async fn sync_offline_cache_to_library(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Sync complete: {} synced, {} already present, {} errors",
|
||||
|
||||
@@ -73,7 +73,7 @@ async fn migrate_single_track(
|
||||
legacy_path: PathBuf,
|
||||
offline_root: &str,
|
||||
qobuz_client: &QobuzClient,
|
||||
library_db: Arc<Mutex<LibraryDatabase>>,
|
||||
library_db: Arc<Mutex<Option<LibraryDatabase>>>,
|
||||
) -> Result<String, String> {
|
||||
log::info!("Migrating track {}", track_id);
|
||||
|
||||
@@ -128,7 +128,8 @@ async fn migrate_single_track(
|
||||
};
|
||||
|
||||
// 7. Insert into local library DB
|
||||
let lib_guard = library_db.lock().await;
|
||||
let lib_opt__ = library_db.lock().await;
|
||||
let lib_guard = lib_opt__.as_ref().ok_or("No active session - please log in")?;
|
||||
|
||||
let album_artist = metadata.album_artist.as_ref().unwrap_or(&metadata.artist);
|
||||
let album_group_key = format!("{}|{}", metadata.album, album_artist);
|
||||
@@ -162,7 +163,7 @@ pub async fn migrate_legacy_cached_files(
|
||||
tracks_dir: PathBuf,
|
||||
offline_root: String,
|
||||
qobuz_client: Arc<Mutex<QobuzClient>>,
|
||||
library_db: Arc<Mutex<LibraryDatabase>>,
|
||||
library_db: Arc<Mutex<Option<LibraryDatabase>>>,
|
||||
) -> MigrationStatus {
|
||||
let total = track_ids.len();
|
||||
let mut status = MigrationStatus {
|
||||
|
||||
@@ -129,7 +129,7 @@ pub struct TrackCacheInfo {
|
||||
|
||||
/// Offline cache state manager
|
||||
pub struct OfflineCacheState {
|
||||
pub db: Arc<Mutex<OfflineCacheDb>>,
|
||||
pub db: Arc<Mutex<Option<OfflineCacheDb>>>,
|
||||
pub fetcher: Arc<StreamFetcher>,
|
||||
pub cache_dir: PathBuf,
|
||||
/// Cache limit in bytes (None = unlimited)
|
||||
@@ -160,7 +160,7 @@ impl OfflineCacheState {
|
||||
let default_limit = Some(2 * 1024 * 1024 * 1024u64);
|
||||
|
||||
let state = Self {
|
||||
db: Arc::new(Mutex::new(db)),
|
||||
db: Arc::new(Mutex::new(Some(db))),
|
||||
fetcher: Arc::new(StreamFetcher::new()),
|
||||
cache_dir: cache_dir.clone(),
|
||||
limit_bytes: Arc::new(Mutex::new(default_limit)),
|
||||
@@ -172,6 +172,41 @@ impl OfflineCacheState {
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
let cache_dir = dirs::cache_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join("qbz")
|
||||
.join("audio");
|
||||
Self {
|
||||
db: Arc::new(Mutex::new(None)),
|
||||
fetcher: Arc::new(StreamFetcher::new()),
|
||||
cache_dir,
|
||||
limit_bytes: Arc::new(Mutex::new(Some(2 * 1024 * 1024 * 1024u64))),
|
||||
cache_semaphore: Arc::new(Semaphore::new(3)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_at(&self, cache_base_dir: &std::path::Path) -> Result<(), String> {
|
||||
let cache_dir = cache_base_dir.join("audio");
|
||||
std::fs::create_dir_all(&cache_dir)
|
||||
.map_err(|e| format!("Failed to create cache directory: {}", e))?;
|
||||
std::fs::create_dir_all(cache_dir.join("tracks"))
|
||||
.map_err(|e| format!("Failed to create tracks directory: {}", e))?;
|
||||
std::fs::create_dir_all(cache_dir.join("artwork"))
|
||||
.map_err(|e| format!("Failed to create artwork directory: {}", e))?;
|
||||
let db_path = cache_dir.join("index.db");
|
||||
let new_db = OfflineCacheDb::new(&db_path)?;
|
||||
let mut guard = self.db.blocking_lock();
|
||||
*guard = Some(new_db);
|
||||
log::info!("Offline cache initialized at: {:?}", cache_dir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) {
|
||||
let mut guard = self.db.blocking_lock();
|
||||
*guard = None;
|
||||
}
|
||||
|
||||
/// Get the path for a track's audio file
|
||||
pub fn track_file_path(&self, track_id: u64, format: &str) -> PathBuf {
|
||||
self.cache_dir.join("tracks").join(format!("{}.{}", track_id, format))
|
||||
|
||||
@@ -70,6 +70,14 @@ impl RadioDb {
|
||||
Self::open(&db_path)
|
||||
}
|
||||
|
||||
pub fn open_at(base_dir: &Path) -> Result<Self, String> {
|
||||
let radio_dir = base_dir.join("radio");
|
||||
std::fs::create_dir_all(&radio_dir)
|
||||
.map_err(|e| format!("Failed to create radio data directory: {}", e))?;
|
||||
let db_path = radio_dir.join("radio_engine.db");
|
||||
Self::open(&db_path)
|
||||
}
|
||||
|
||||
pub fn open(path: &Path) -> Result<Self, String> {
|
||||
let conn = Connection::open(path)
|
||||
.map_err(|e| format!("Failed to open radio database: {}", e))?;
|
||||
|
||||
@@ -42,7 +42,8 @@ pub async fn reco_log_event(
|
||||
event.item_type.as_str()
|
||||
);
|
||||
|
||||
let db = state.db.lock().await;
|
||||
let guard__ = state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
db.insert_event(&event)
|
||||
}
|
||||
|
||||
@@ -59,7 +60,8 @@ pub async fn reco_get_home(
|
||||
let limit_top_artists = limit_top_artists.unwrap_or(10);
|
||||
let limit_favorites = limit_favorites.unwrap_or(12);
|
||||
|
||||
let db = state.db.lock().await;
|
||||
let guard__ = state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
|
||||
let recently_played_album_ids = db.get_recent_album_ids(limit_recent_albums)?;
|
||||
let continue_listening_track_ids = db.get_recent_track_ids(limit_continue_tracks)?;
|
||||
@@ -92,7 +94,8 @@ pub async fn reco_train_scores(
|
||||
let now_ts = current_timestamp();
|
||||
let since_ts = now_ts.saturating_sub(lookback_days * 86_400);
|
||||
|
||||
let mut db = state.db.lock().await;
|
||||
let mut guard__ = state.db.lock().await;
|
||||
let db = guard__.as_mut().ok_or("No active session - please log in")?;
|
||||
let events = db.get_events_since(since_ts, Some(max_events))?;
|
||||
let total_events = events.len();
|
||||
let total_favorite_events = events
|
||||
@@ -148,7 +151,8 @@ pub async fn reco_get_home_ml(
|
||||
let limit_top_artists = limit_top_artists.unwrap_or(10);
|
||||
let limit_favorites = limit_favorites.unwrap_or(12);
|
||||
|
||||
let db = state.db.lock().await;
|
||||
let guard__ = state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
let has_scores = db.has_scores("all")?;
|
||||
|
||||
// HYBRID APPROACH: Always get fresh recent plays first, then supplement with scored
|
||||
@@ -402,7 +406,8 @@ pub async fn get_playlist_suggestions(
|
||||
state: State<'_, RecoState>,
|
||||
) -> Result<Vec<u64>, String> {
|
||||
let limit = limit.unwrap_or(10);
|
||||
let db = state.db.lock().await;
|
||||
let guard__ = state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
|
||||
// Get user's top scored tracks (get more than needed for filtering)
|
||||
let fetch_limit = (limit * 5).max(50);
|
||||
@@ -484,8 +489,12 @@ pub async fn reco_backfill_genres(
|
||||
let batch_size = batch_size.unwrap_or(50);
|
||||
log::info!("Command: reco_backfill_genres batch_size={}", batch_size);
|
||||
|
||||
let db = reco_state.db.lock().await;
|
||||
let album_ids = db.get_album_ids_without_genre(batch_size)?;
|
||||
// Get album IDs first (scoped lock to avoid Send issue)
|
||||
let album_ids = {
|
||||
let guard__ = reco_state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
db.get_album_ids_without_genre(batch_size)?
|
||||
};
|
||||
|
||||
if album_ids.is_empty() {
|
||||
log::info!("No albums need genre backfill");
|
||||
@@ -499,52 +508,48 @@ pub async fn reco_backfill_genres(
|
||||
|
||||
log::info!("Backfilling genres for {} albums", album_ids.len());
|
||||
|
||||
let client = app_state.client.lock().await;
|
||||
let mut albums_processed = 0u32;
|
||||
let mut events_updated = 0u64;
|
||||
let mut albums_failed = 0u32;
|
||||
|
||||
for album_id in &album_ids {
|
||||
// Fetch album from Qobuz API
|
||||
// Fetch album from Qobuz API (lock client, then drop before DB)
|
||||
let genre_info = {
|
||||
let client = app_state.client.lock().await;
|
||||
match client.get_album(album_id).await {
|
||||
Ok(album) => {
|
||||
if let Some(genre) = album.genre {
|
||||
// Update all events with this album_id
|
||||
match db.update_genre_for_album(album_id, genre.id) {
|
||||
Ok(updated) => {
|
||||
events_updated += updated;
|
||||
albums_processed += 1;
|
||||
log::debug!(
|
||||
"Updated {} events for album {} with genre {} ({})",
|
||||
updated,
|
||||
album_id,
|
||||
genre.id,
|
||||
genre.name
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to update genre for album {}: {}", album_id, e);
|
||||
albums_failed += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Album has no genre, mark as processed by setting genre_id to 0
|
||||
match db.update_genre_for_album(album_id, 0) {
|
||||
Ok(updated) => {
|
||||
events_updated += updated;
|
||||
albums_processed += 1;
|
||||
log::debug!("Album {} has no genre, set to 0", album_id);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to update genre for album {}: {}", album_id, e);
|
||||
albums_failed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(album) => Some(album.genre.map(|g| (g.id, g.name))),
|
||||
Err(e) => {
|
||||
log::warn!("Failed to fetch album {}: {}", album_id, e);
|
||||
albums_failed += 1;
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(genre_opt) = genre_info {
|
||||
let genre_id = genre_opt.as_ref().map(|(id, _)| *id).unwrap_or(0);
|
||||
let genre_name = genre_opt.as_ref().map(|(_, name)| name.as_str()).unwrap_or("none");
|
||||
|
||||
// Re-acquire DB lock for update
|
||||
let guard__ = reco_state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
match db.update_genre_for_album(album_id, genre_id) {
|
||||
Ok(updated) => {
|
||||
events_updated += updated;
|
||||
albums_processed += 1;
|
||||
if genre_id > 0 {
|
||||
log::debug!(
|
||||
"Updated {} events for album {} with genre {} ({})",
|
||||
updated, album_id, genre_id, genre_name
|
||||
);
|
||||
} else {
|
||||
log::debug!("Album {} has no genre, set to 0", album_id);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to update genre for album {}: {}", album_id, e);
|
||||
albums_failed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,10 +558,15 @@ pub async fn reco_backfill_genres(
|
||||
}
|
||||
|
||||
// Check how many albums still need backfill
|
||||
let albums_remaining = {
|
||||
let guard__ = reco_state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
let remaining = db.get_album_ids_without_genre(1)?;
|
||||
let albums_remaining = if remaining.is_empty() { 0 } else {
|
||||
// Get actual count
|
||||
if remaining.is_empty() {
|
||||
0
|
||||
} else {
|
||||
db.get_album_ids_without_genre(10000)?.len() as u32
|
||||
}
|
||||
};
|
||||
|
||||
log::info!(
|
||||
@@ -580,7 +590,8 @@ pub async fn reco_backfill_genres(
|
||||
pub async fn reco_needs_genre_backfill(
|
||||
reco_state: State<'_, RecoState>,
|
||||
) -> Result<bool, String> {
|
||||
let db = reco_state.db.lock().await;
|
||||
let guard__ = reco_state.db.lock().await;
|
||||
let db = guard__.as_ref().ok_or("No active session - please log in")?;
|
||||
let albums = db.get_album_ids_without_genre(1)?;
|
||||
Ok(!albums.is_empty())
|
||||
}
|
||||
@@ -6,6 +6,7 @@ pub mod commands;
|
||||
pub mod db;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
@@ -78,7 +79,7 @@ pub struct HomeSeeds {
|
||||
|
||||
/// Recommendation store state shared across commands
|
||||
pub struct RecoState {
|
||||
pub db: Arc<Mutex<RecoStoreDb>>,
|
||||
pub db: Arc<Mutex<Option<RecoStoreDb>>>,
|
||||
}
|
||||
|
||||
impl RecoState {
|
||||
@@ -95,7 +96,29 @@ impl RecoState {
|
||||
let db = RecoStoreDb::new(&db_path)?;
|
||||
|
||||
Ok(Self {
|
||||
db: Arc::new(Mutex::new(db)),
|
||||
db: Arc::new(Mutex::new(Some(db))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
db: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_at(&self, base_dir: &Path) -> Result<(), String> {
|
||||
let reco_dir = base_dir.join("reco");
|
||||
std::fs::create_dir_all(&reco_dir)
|
||||
.map_err(|e| format!("Failed to create reco directory: {}", e))?;
|
||||
let db_path = reco_dir.join("events.db");
|
||||
let new_db = RecoStoreDb::new(&db_path)?;
|
||||
let mut guard = self.db.blocking_lock();
|
||||
*guard = Some(new_db);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) {
|
||||
let mut guard = self.db.blocking_lock();
|
||||
*guard = None;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use log::{info, warn};
|
||||
use reqwest::Client;
|
||||
use rusqlite::{params, Connection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -97,8 +98,19 @@ impl UpdatesStore {
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
|
||||
let db_path = data_dir.join("updates.db");
|
||||
Self::open_at(&db_path)
|
||||
}
|
||||
|
||||
pub fn new_at(base_dir: &Path) -> Result<Self, String> {
|
||||
std::fs::create_dir_all(base_dir)
|
||||
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||
let db_path = base_dir.join("updates.db");
|
||||
Self::open_at(&db_path)
|
||||
}
|
||||
|
||||
fn open_at(db_path: &Path) -> Result<Self, String> {
|
||||
let conn =
|
||||
Connection::open(&db_path).map_err(|e| format!("Failed to open updates database: {}", e))?;
|
||||
Connection::open(db_path).map_err(|e| format!("Failed to open updates database: {}", e))?;
|
||||
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS update_preferences (
|
||||
@@ -262,27 +274,52 @@ impl UpdatesStore {
|
||||
|
||||
/// Thread-safe state wrapper
|
||||
pub struct UpdatesState {
|
||||
store: Arc<Mutex<UpdatesStore>>,
|
||||
store: Arc<Mutex<Option<UpdatesStore>>>,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl UpdatesState {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let store = UpdatesStore::new()?;
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(NETWORK_TIMEOUT_SECONDS))
|
||||
.user_agent("qbz-nix-updates")
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to create updates HTTP client: {}", e))?;
|
||||
let client = Self::build_client()?;
|
||||
Ok(Self {
|
||||
store: Arc::new(Mutex::new(store)),
|
||||
store: Arc::new(Mutex::new(Some(store))),
|
||||
client,
|
||||
})
|
||||
}
|
||||
|
||||
fn with_store<T>(&self, f: impl FnOnce(&UpdatesStore) -> T) -> T {
|
||||
pub fn new_empty() -> Result<Self, String> {
|
||||
let client = Self::build_client()?;
|
||||
Ok(Self {
|
||||
store: Arc::new(Mutex::new(None)),
|
||||
client,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init_at(&self, base_dir: &Path) -> Result<(), String> {
|
||||
let new_store = UpdatesStore::new_at(base_dir)?;
|
||||
let mut guard = self.store.lock().expect("UpdatesStore mutex poisoned");
|
||||
*guard = Some(new_store);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn teardown(&self) {
|
||||
let mut guard = self.store.lock().expect("UpdatesStore mutex poisoned");
|
||||
*guard = None;
|
||||
}
|
||||
|
||||
fn build_client() -> Result<Client, String> {
|
||||
Client::builder()
|
||||
.timeout(Duration::from_secs(NETWORK_TIMEOUT_SECONDS))
|
||||
.user_agent("qbz-nix-updates")
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to create updates HTTP client: {}", e))
|
||||
}
|
||||
|
||||
fn with_store<T>(&self, f: impl FnOnce(&UpdatesStore) -> T) -> Result<T, String> {
|
||||
let guard = self.store.lock().expect("UpdatesStore mutex poisoned");
|
||||
f(&guard)
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
Ok(f(store))
|
||||
}
|
||||
|
||||
fn current_version(&self) -> String {
|
||||
@@ -411,7 +448,7 @@ fn select_latest_valid_update(
|
||||
}
|
||||
let is_blocked = state.with_store(|store| {
|
||||
store.is_release_acknowledged(&info.version) || store.is_release_ignored(&info.version)
|
||||
});
|
||||
}).unwrap_or(false);
|
||||
if is_blocked {
|
||||
continue;
|
||||
}
|
||||
@@ -424,7 +461,7 @@ fn select_latest_valid_update(
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_update_preferences(state: tauri::State<UpdatesState>) -> Result<UpdatePreferences, String> {
|
||||
state.with_store(|store| store.get_preferences())
|
||||
state.with_store(|store| store.get_preferences())?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -432,7 +469,7 @@ pub fn set_update_check_on_launch(
|
||||
enabled: bool,
|
||||
state: tauri::State<UpdatesState>,
|
||||
) -> Result<(), String> {
|
||||
state.with_store(|store| store.set_check_on_launch(enabled))
|
||||
state.with_store(|store| store.set_check_on_launch(enabled))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -440,47 +477,47 @@ pub fn set_show_whats_new_on_launch(
|
||||
enabled: bool,
|
||||
state: tauri::State<UpdatesState>,
|
||||
) -> Result<(), String> {
|
||||
state.with_store(|store| store.set_show_whats_new_on_launch(enabled))
|
||||
state.with_store(|store| store.set_show_whats_new_on_launch(enabled))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn acknowledge_release(version: String, state: tauri::State<UpdatesState>) -> Result<(), String> {
|
||||
state.with_store(|store| store.acknowledge_release(&version))
|
||||
state.with_store(|store| store.acknowledge_release(&version))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn ignore_release(version: String, state: tauri::State<UpdatesState>) -> Result<(), String> {
|
||||
state.with_store(|store| store.ignore_release(&version))
|
||||
state.with_store(|store| store.ignore_release(&version))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn is_release_acknowledged(version: String, state: tauri::State<UpdatesState>) -> bool {
|
||||
pub fn is_release_acknowledged(version: String, state: tauri::State<UpdatesState>) -> Result<bool, String> {
|
||||
state.with_store(|store| store.is_release_acknowledged(&version))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn is_release_ignored(version: String, state: tauri::State<UpdatesState>) -> bool {
|
||||
pub fn is_release_ignored(version: String, state: tauri::State<UpdatesState>) -> Result<bool, String> {
|
||||
state.with_store(|store| store.is_release_ignored(&version))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn has_whats_new_been_shown(version: String, state: tauri::State<UpdatesState>) -> bool {
|
||||
pub fn has_whats_new_been_shown(version: String, state: tauri::State<UpdatesState>) -> Result<bool, String> {
|
||||
state.with_store(|store| store.has_whats_new_been_shown(&version))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn mark_whats_new_shown(version: String, state: tauri::State<UpdatesState>) -> Result<(), String> {
|
||||
state.with_store(|store| store.mark_whats_new_shown(&version))
|
||||
state.with_store(|store| store.mark_whats_new_shown(&version))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn has_flatpak_welcome_been_shown(state: tauri::State<UpdatesState>) -> bool {
|
||||
pub fn has_flatpak_welcome_been_shown(state: tauri::State<UpdatesState>) -> Result<bool, String> {
|
||||
state.with_store(|store| store.has_flatpak_welcome_been_shown())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn mark_flatpak_welcome_shown(state: tauri::State<UpdatesState>) -> Result<(), String> {
|
||||
state.with_store(|store| store.mark_flatpak_welcome_shown())
|
||||
state.with_store(|store| store.mark_flatpak_welcome_shown())?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -496,7 +533,7 @@ pub async fn check_for_updates(
|
||||
let current_version = state.current_version();
|
||||
|
||||
if mode == "launch" {
|
||||
let prefs = match state.with_store(|store| store.get_preferences()) {
|
||||
let prefs = match state.with_store(|store| store.get_preferences()).and_then(|r| r) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
warn!("[Updates] Failed to read preferences: {}", e);
|
||||
|
||||
Reference in new issue
Block a user