feat(cache): add locale support for artist cache

- Modify cached_artists table to include locale in primary key
- Update get_artist and set_artist methods to consider locale
- Add get_locale() method to QobuzClient for cache key generation
- Ensure artist descriptions respect language settings

cp
This commit is contained in:
vicrodh committed 2026-01-15 10:02:25 -06:00
1 parent d0a30c7175
commit a5200d5d05
6 files changed
+147 -13

No files matched your search

+6 -1
View File
@@ -51,7 +51,12 @@ impl QobuzClient {
*self.locale.write().await = locale;
}
/// Get the current locale
/// Get the current locale (public for cache key generation)
pub async fn get_locale(&self) -> String {
self.locale.read().await.clone()
}
/// Get the current locale (internal use)
async fn locale(&self) -> String {
self.locale.read().await.clone()
}
+10 -8
View File
@@ -63,9 +63,11 @@ impl ApiCache {
CREATE INDEX IF NOT EXISTS idx_cached_albums_fetched ON cached_albums(fetched_at);
CREATE TABLE IF NOT EXISTS cached_artists (
artist_id INTEGER PRIMARY KEY,
artist_id INTEGER NOT NULL,
locale TEXT NOT NULL,
data TEXT NOT NULL,
fetched_at INTEGER NOT NULL
fetched_at INTEGER NOT NULL,
PRIMARY KEY (artist_id, locale)
);
CREATE INDEX IF NOT EXISTS idx_cached_artists_fetched ON cached_artists(fetched_at);
@@ -165,15 +167,15 @@ impl ApiCache {
// ============ Artist Cache ============
/// Get a cached artist if it exists and hasn't expired
pub fn get_artist(&self, artist_id: u64, ttl_secs: Option<i64>) -> Result<Option<String>, String> {
pub fn get_artist(&self, artist_id: u64, locale: &str, ttl_secs: Option<i64>) -> Result<Option<String>, String> {
let ttl = ttl_secs.unwrap_or(DEFAULT_TTL_SECS);
let min_fetched_at = Self::current_timestamp() - ttl;
let result: Option<String> = self
.conn
.query_row(
"SELECT data FROM cached_artists WHERE artist_id = ? AND fetched_at > ?",
params![artist_id, min_fetched_at],
"SELECT data FROM cached_artists WHERE artist_id = ? AND locale = ? AND fetched_at > ?",
params![artist_id, locale, min_fetched_at],
|row| row.get(0),
)
.optional()
@@ -183,12 +185,12 @@ impl ApiCache {
}
/// Cache an artist response
pub fn set_artist(&self, artist_id: u64, data: &str) -> Result<(), String> {
pub fn set_artist(&self, artist_id: u64, locale: &str, data: &str) -> Result<(), String> {
let fetched_at = Self::current_timestamp();
self.conn
.execute(
"INSERT OR REPLACE INTO cached_artists (artist_id, data, fetched_at) VALUES (?, ?, ?)",
params![artist_id, data, fetched_at],
"INSERT OR REPLACE INTO cached_artists (artist_id, locale, data, fetched_at) VALUES (?, ?, ?, ?)",
params![artist_id, locale, data, fetched_at],
)
.map_err(|e| format!("Failed to cache artist: {}", e))?;
Ok(())
+10 -4
View File
@@ -137,18 +137,24 @@ pub async fn get_artist(
) -> Result<Artist, String> {
log::info!("Command: get_artist {}", artist_id);
// Get current locale
let locale = {
let client = state.client.lock().await;
client.get_locale().await
};
// Check cache first
{
let cache = cache_state.cache.lock().await;
if let Some(cached_data) = cache.get_artist(artist_id, None)? {
log::debug!("Cache hit for artist {}", artist_id);
if let Some(cached_data) = cache.get_artist(artist_id, &locale, None)? {
log::debug!("Cache hit for artist {} (locale: {})", artist_id, locale);
return serde_json::from_str(&cached_data)
.map_err(|e| format!("Failed to parse cached artist: {}", e));
}
}
// Cache miss - fetch from API
log::debug!("Cache miss for artist {}, fetching from API", artist_id);
log::debug!("Cache miss for artist {} (locale: {}), fetching from API", artist_id, locale);
let client = state.client.lock().await;
let artist = client
.get_artist(artist_id, true)
@@ -160,7 +166,7 @@ pub async fn get_artist(
let cache = cache_state.cache.lock().await;
let json = serde_json::to_string(&artist)
.map_err(|e| format!("Failed to serialize artist: {}", e))?;
cache.set_artist(artist_id, &json)?;
cache.set_artist(artist_id, &locale, &json)?;
}
Ok(artist)
+5
View File
@@ -391,6 +391,11 @@ pub fn run() {
library::commands::library_set_album_hidden,
library::commands::library_get_hidden_albums,
library::commands::library_backfill_downloads,
// Artist images commands
library::commands::library_get_artist_image,
library::commands::library_get_artist_images,
library::commands::library_cache_artist_image,
library::commands::library_set_custom_artist_image,
// Chromecast casting commands
cast::commands::cast_start_discovery,
cast::commands::cast_stop_discovery,
+61
View File
@@ -947,3 +947,64 @@ pub async fn library_backfill_downloads(
Ok(report)
}
// === Artist Images Management ===
#[derive(serde::Serialize)]
pub struct ArtistImageInfo {
pub artist_name: String,
pub image_url: Option<String>,
pub source: Option<String>,
pub custom_image_path: Option<String>,
}
/// Get cached artist image
#[tauri::command]
pub async fn library_get_artist_image(
artist_name: String,
state: State<'_, LibraryState>,
) -> Result<Option<ArtistImageInfo>, String> {
let db = state.db.lock().await;
db.get_artist_image(&artist_name).map_err(|e| e.to_string())
}
/// Get multiple artist images at once
#[tauri::command]
pub async fn library_get_artist_images(
artist_names: Vec<String>,
state: State<'_, LibraryState>,
) -> Result<Vec<ArtistImageInfo>, String> {
let db = state.db.lock().await;
let mut results = Vec::new();
for name in artist_names {
if let Ok(Some(info)) = db.get_artist_image(&name) {
results.push(info);
}
}
Ok(results)
}
/// Cache artist image from Qobuz
#[tauri::command]
pub async fn library_cache_artist_image(
artist_name: String,
image_url: String,
source: String,
state: State<'_, LibraryState>,
) -> Result<(), String> {
let db = state.db.lock().await;
db.cache_artist_image(&artist_name, Some(&image_url), &source, None)
.map_err(|e| e.to_string())
}
/// Set custom artist image
#[tauri::command]
pub async fn library_set_custom_artist_image(
artist_name: String,
custom_image_path: String,
state: State<'_, LibraryState>,
) -> Result<(), String> {
let db = state.db.lock().await;
db.cache_artist_image(&artist_name, None, "custom", Some(&custom_image_path))
.map_err(|e| e.to_string())
}
+55
View File
@@ -118,6 +118,18 @@ impl LibraryDatabase {
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
-- Artist images cache (Qobuz/Discogs images and custom uploads)
CREATE TABLE IF NOT EXISTS artist_images (
artist_name TEXT PRIMARY KEY,
image_url TEXT,
source TEXT NOT NULL,
custom_image_path TEXT,
fetched_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_artist_images_fetched ON artist_images(fetched_at);
"#,
)
.map_err(|e| LibraryError::Database(format!("Failed to create schema: {}", e)))?;
@@ -1535,4 +1547,47 @@ impl LibraryDatabase {
.map_err(|e| LibraryError::Database(format!("Failed to remove all Qobuz downloads: {}", e)))?;
Ok(count)
}
// === Artist Images Management ===
/// Get cached artist image
pub fn get_artist_image(&self, artist_name: &str) -> Result<Option<crate::library::commands::ArtistImageInfo>, LibraryError> {
let result = self.conn.query_row(
"SELECT artist_name, image_url, source, custom_image_path FROM artist_images WHERE artist_name = ?1",
params![artist_name],
|row| {
Ok(crate::library::commands::ArtistImageInfo {
artist_name: row.get(0)?,
image_url: row.get(1)?,
source: row.get(2)?,
custom_image_path: row.get(3)?,
})
}
).optional()
.map_err(|e| LibraryError::Database(format!("Failed to get artist image: {}", e)))?;
Ok(result)
}
/// Cache artist image
pub fn cache_artist_image(
&self,
artist_name: &str,
image_url: Option<&str>,
source: &str,
custom_image_path: Option<&str>,
) -> Result<(), LibraryError> {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
self.conn.execute(
"INSERT OR REPLACE INTO artist_images
(artist_name, image_url, source, custom_image_path, fetched_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![artist_name, image_url, source, custom_image_path, now, now],
)
.map_err(|e| LibraryError::Database(format!("Failed to cache artist image: {}", e)))?;
Ok(())
}
}