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:
vicrodh committed 2026-02-05 23:11:07 -06:00
1 parent 48dac0ec3a
commit aab3e67641
29 files changed
+1095 -350

No files matched your search

+25 -2
View File
@@ -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)
+47 -24
View File
@@ -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()
}
}
+18 -11
View File
@@ -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)
};
+25 -2
View File
@@ -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
+11 -5
View File
@@ -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()
}
}
};
+64 -22
View File
@@ -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)
}
+27 -12
View File
@@ -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)
}
+64 -22
View File
@@ -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()
}
+41 -11
View File
@@ -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)
}
+31 -11
View File
@@ -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)
}
+47 -12
View File
@@ -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)
}
}
+115 -45
View File
@@ -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()
}
}
+20 -9
View File
@@ -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))
}
+50 -13
View File
@@ -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)
}
}
+25 -2
View File
@@ -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)),
})
+25 -2
View File
@@ -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
+48 -2
View File
@@ -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;
}
}
+10 -5
View File
@@ -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()?
};
+25 -2
View File
@@ -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 {
+25 -2
View File
@@ -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
+26 -1
View File
@@ -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;
}
}
+72 -22
View File
@@ -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))
}
}
+62 -34
View File
@@ -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",
+4 -3
View File
@@ -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 {
+37 -2
View File
@@ -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))
+8
View File
@@ -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))?;
+58 -47
View File
@@ -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())
}
+25 -2
View File
@@ -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;
}
}
+60 -23
View File
@@ -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);