feat(labels): follow / unfollow labels (mirrors Follow Artist)
Adds the same follow flow we already have for artists, applied to labels. Confirmed against the inferred OpenAPI spec (qbz-nix-docs/qobuz-api-inferred-openapi-v9.7.0.3.yaml) — the existing /favorite/create, /favorite/delete and /favorite/getUserFavorites endpoints already accept fav_type='label' and label_ids, so no new Qobuz endpoints were needed. Backend - favorites_cache: new favorite_labels SQLite table + add / remove / sync / get methods alongside the artist counterparts. clear_all drops the new table on logout. - commands_v2::favorites: 4 new commands — v2_get_cached_favorite_labels, v2_sync_cached_favorite_labels, v2_cache_favorite_label, v2_uncache_favorite_label. Same wiring as the artist commands. - lib.rs invoke_handler registers the four new commands. Frontend - src/lib/stores/labelFavoritesStore.ts: clone of artistFavoritesStore — same in-memory Set, same isLoaded flag, toggle calls /favorite/create with fav_type=label, sync paginates /favorite/getUserFavorites?type=labels. - routes/+page.svelte: loadLabelFavorites() called alongside the other three after login is confirmed. - LabelView.svelte: heart-shape Follow button in the label header info column, mirroring ArtistDetailView's .artist-actions treatment. Reactive to the store via labelFavoritesVersion bump so any future Favorites view that toggles a label will keep this view in sync. - i18n: label.follow / label.unfollow in all five locales. cc
This commit is contained in:
11 files changed
+457
-5
No files matched your search
@@ -351,3 +351,79 @@ pub async fn v2_uncache_favorite_artist(
|
||||
.remove_favorite_artist(artistId)
|
||||
.map_err(|e| RuntimeError::Internal(e))
|
||||
}
|
||||
|
||||
// ============ Label favorites cache (mirrors artist) ============
|
||||
|
||||
/// Get cached favorite labels (V2)
|
||||
#[tauri::command]
|
||||
pub async fn v2_get_cached_favorite_labels(
|
||||
cache_state: State<'_, crate::config::favorites_cache::FavoritesCacheState>,
|
||||
) -> Result<Vec<i64>, RuntimeError> {
|
||||
let guard = cache_state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| RuntimeError::Internal("Failed to lock favorites cache".to_string()))?;
|
||||
let store = guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| RuntimeError::Internal("No active session".to_string()))?;
|
||||
store
|
||||
.get_favorite_label_ids()
|
||||
.map_err(|e| RuntimeError::Internal(e))
|
||||
}
|
||||
|
||||
/// Sync cached favorite labels (V2) — replaces the entire cached set
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn v2_sync_cached_favorite_labels(
|
||||
labelIds: Vec<i64>,
|
||||
cache_state: State<'_, crate::config::favorites_cache::FavoritesCacheState>,
|
||||
) -> Result<(), RuntimeError> {
|
||||
let guard = cache_state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| RuntimeError::Internal("Failed to lock favorites cache".to_string()))?;
|
||||
let store = guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| RuntimeError::Internal("No active session".to_string()))?;
|
||||
store
|
||||
.sync_favorite_labels(&labelIds)
|
||||
.map_err(|e| RuntimeError::Internal(e))
|
||||
}
|
||||
|
||||
/// Cache a single favorite label (V2)
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn v2_cache_favorite_label(
|
||||
labelId: i64,
|
||||
cache_state: State<'_, crate::config::favorites_cache::FavoritesCacheState>,
|
||||
) -> Result<(), RuntimeError> {
|
||||
let guard = cache_state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| RuntimeError::Internal("Failed to lock favorites cache".to_string()))?;
|
||||
let store = guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| RuntimeError::Internal("No active session".to_string()))?;
|
||||
store
|
||||
.add_favorite_label(labelId)
|
||||
.map_err(|e| RuntimeError::Internal(e))
|
||||
}
|
||||
|
||||
/// Uncache a favorite label (V2)
|
||||
#[tauri::command]
|
||||
#[allow(non_snake_case)]
|
||||
pub async fn v2_uncache_favorite_label(
|
||||
labelId: i64,
|
||||
cache_state: State<'_, crate::config::favorites_cache::FavoritesCacheState>,
|
||||
) -> Result<(), RuntimeError> {
|
||||
let guard = cache_state
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| RuntimeError::Internal("Failed to lock favorites cache".to_string()))?;
|
||||
let store = guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| RuntimeError::Internal("No active session".to_string()))?;
|
||||
store
|
||||
.remove_favorite_label(labelId)
|
||||
.map_err(|e| RuntimeError::Internal(e))
|
||||
}
|
||||
@@ -33,6 +33,12 @@ pub struct CachedFavoriteArtist {
|
||||
pub artist_id: i64,
|
||||
}
|
||||
|
||||
/// Represents a cached favorite label
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CachedFavoriteLabel {
|
||||
pub label_id: i64,
|
||||
}
|
||||
|
||||
pub struct FavoritesCacheStore {
|
||||
conn: Connection,
|
||||
}
|
||||
@@ -77,6 +83,18 @@ impl FavoritesCacheStore {
|
||||
)
|
||||
.map_err(|e| format!("Failed to create favorite_artists table: {}", e))?;
|
||||
|
||||
// Labels — added as part of the Follow Label feature; same shape as
|
||||
// favorite_artists. CREATE IF NOT EXISTS is the migration story
|
||||
// for existing databases (no separate ALTER needed).
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS favorite_labels (
|
||||
label_id INTEGER PRIMARY KEY,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| format!("Failed to create favorite_labels table: {}", e))?;
|
||||
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
@@ -296,6 +314,74 @@ impl FavoritesCacheStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============ Label favorites ============
|
||||
|
||||
pub fn get_favorite_label_ids(&self) -> Result<Vec<i64>, String> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT label_id FROM favorite_labels")
|
||||
.map_err(|e| format!("Failed to prepare query: {}", e))?;
|
||||
|
||||
let rows = stmt
|
||||
.query_map([], |row| row.get(0))
|
||||
.map_err(|e| format!("Failed to query favorite labels: {}", e))?;
|
||||
|
||||
let mut ids = Vec::new();
|
||||
for row in rows {
|
||||
ids.push(row.map_err(|e| format!("Failed to read row: {}", e))?);
|
||||
}
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
pub fn is_label_favorite(&self, label_id: i64) -> Result<bool, String> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT 1 FROM favorite_labels WHERE label_id = ?1")
|
||||
.map_err(|e| format!("Failed to prepare query: {}", e))?;
|
||||
|
||||
let exists = stmt
|
||||
.exists(params![label_id])
|
||||
.map_err(|e| format!("Failed to check favorite: {}", e))?;
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
pub fn add_favorite_label(&self, label_id: i64) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute(
|
||||
"INSERT OR IGNORE INTO favorite_labels (label_id) VALUES (?1)",
|
||||
params![label_id],
|
||||
)
|
||||
.map_err(|e| format!("Failed to add favorite label: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_favorite_label(&self, label_id: i64) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute(
|
||||
"DELETE FROM favorite_labels WHERE label_id = ?1",
|
||||
params![label_id],
|
||||
)
|
||||
.map_err(|e| format!("Failed to remove favorite label: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sync_favorite_labels(&self, label_ids: &[i64]) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute("DELETE FROM favorite_labels", [])
|
||||
.map_err(|e| format!("Failed to clear favorite labels: {}", e))?;
|
||||
|
||||
for &label_id in label_ids {
|
||||
self.conn
|
||||
.execute(
|
||||
"INSERT INTO favorite_labels (label_id) VALUES (?1)",
|
||||
params![label_id],
|
||||
)
|
||||
.map_err(|e| format!("Failed to insert favorite label: {}", e))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============ Clear all (for logout) ============
|
||||
|
||||
pub fn clear_all(&self) -> Result<(), String> {
|
||||
@@ -308,6 +394,9 @@ impl FavoritesCacheStore {
|
||||
self.conn
|
||||
.execute("DELETE FROM favorite_artists", [])
|
||||
.map_err(|e| format!("Failed to clear favorite artists: {}", e))?;
|
||||
self.conn
|
||||
.execute("DELETE FROM favorite_labels", [])
|
||||
.map_err(|e| format!("Failed to clear favorite labels: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1688,6 +1688,10 @@ pub fn run() {
|
||||
commands_v2::v2_sync_cached_favorite_artists,
|
||||
commands_v2::v2_cache_favorite_artist,
|
||||
commands_v2::v2_uncache_favorite_artist,
|
||||
commands_v2::v2_get_cached_favorite_labels,
|
||||
commands_v2::v2_sync_cached_favorite_labels,
|
||||
commands_v2::v2_cache_favorite_label,
|
||||
commands_v2::v2_uncache_favorite_label,
|
||||
commands_v2::v2_share_track_songlink,
|
||||
commands_v2::v2_share_album_songlink,
|
||||
commands_v2::v2_library_backfill_downloads,
|
||||
|
||||
Reference in new issue
Block a user