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:
1 parent
d0a30c7175
commit
a5200d5d05
6 files changed
+147
-13
No files matched your search
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user