Remove unused API Keys UI and infrastructure
All API requests now go through Cloudflare Workers proxy. User-provided credentials were collected but never used. Removed: - src-tauri/src/api_keys.rs (entire module) - API Keys section from Settings UI - ApiKeysState and related commands - User credential parameters from providers - Simplified DiscogsClient (removed with_user_credentials) - Cleaned up imports and translations Moved documentation: - CHANGELOG-api-keys-refactor.md - HANDOFF-MERGE.md - MERGE-PREP.md - POST-MERGE-CLEANUP.md Moved to: qbz-nix-docs/feature-api-keys-refactor/ This cleanup reduces codebase by ~715 lines and removes confusing UI that promised functionality that didn't work.
This commit is contained in:
1 parent
7631462522
commit
cac5a0c173
15 files changed
+24
-2420
No files matched your search
@@ -1,317 +0,0 @@
|
|||||||
# API Keys Refactor - Changelog
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This branch removes all embedded API credentials from the QBZ binary to achieve Flathub compliance and eliminate security/technical debt. API keys are now managed server-side through a Cloudflare Workers proxy.
|
|
||||||
|
|
||||||
**Branch:** `feature/api-keys-refactor`
|
|
||||||
**Target:** `main`
|
|
||||||
**Impact:** Security, Privacy, Flathub Compliance
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔐 Security & Compliance
|
|
||||||
|
|
||||||
### Removed Embedded API Keys
|
|
||||||
- ❌ Removed Last.fm API secret from binary
|
|
||||||
- ❌ Removed Spotify client credentials from binary
|
|
||||||
- ❌ Removed Tidal client credentials from binary
|
|
||||||
- ❌ Removed Discogs API credentials from binary
|
|
||||||
|
|
||||||
### New Architecture
|
|
||||||
- ✅ Cloudflare Workers proxy handles all API authentication
|
|
||||||
- ✅ Client only knows proxy URL (no secrets exposed)
|
|
||||||
- ✅ Rate limiting and User-Agent validation server-side
|
|
||||||
- ✅ Ready for Flathub submission
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 New Features
|
|
||||||
|
|
||||||
### 1. Discogs Artwork Selection UI
|
|
||||||
**Enhanced album artwork management with Discogs integration**
|
|
||||||
|
|
||||||
- 🎨 **Visual artwork selector** in album edit modal
|
|
||||||
- 🔍 **Smart search**: Uses catalog number if available, falls back to artist + album
|
|
||||||
- 🖼️ **Multi-release support**: Shows images from top 2 release variants
|
|
||||||
- 📄 **Carousel navigation**: Browse 4 images per page with prev/next controls
|
|
||||||
- 📋 **Release metadata**: Each image shows release title and year
|
|
||||||
- ✨ **Automatic download**: Select image → Save → automatically cached locally
|
|
||||||
|
|
||||||
**User Benefits:**
|
|
||||||
- Audiophiles with catalog numbers get precise results
|
|
||||||
- See which release variant each artwork comes from
|
|
||||||
- Easy to find and set correct album covers
|
|
||||||
|
|
||||||
### 2. Catalog Number Support
|
|
||||||
**Precise music metadata for audiophile libraries**
|
|
||||||
|
|
||||||
- 🏷️ **Reads catalog number** from audio file tags (CATALOGNUMBER field)
|
|
||||||
- 💾 **Stores in database** for all tracks and albums
|
|
||||||
- 🎯 **Prioritizes in Discogs search** for accurate results
|
|
||||||
- 👁️ **Displays in UI** on album detail view (e.g., "Cat# MFSL 1-395")
|
|
||||||
|
|
||||||
**User Benefits:**
|
|
||||||
- Professional/audiophile collections with catalog numbers get better search results
|
|
||||||
- Can identify specific pressings and editions
|
|
||||||
- More accurate metadata overall
|
|
||||||
|
|
||||||
### 3. URI Scheme Registration
|
|
||||||
**Deep linking support for OAuth flows**
|
|
||||||
|
|
||||||
- 🔗 **Registered `qbz://` URI scheme** for OAuth callbacks
|
|
||||||
- 🌐 **Enables web-based authentication** flows
|
|
||||||
- 🔄 **Future-proof** for additional integrations
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Technical Changes
|
|
||||||
|
|
||||||
### Cloudflare Workers Proxy
|
|
||||||
**Location:** `/home/blitzkriegfc/Personal/qbz/qbz-api-proxy`
|
|
||||||
|
|
||||||
#### Endpoints Implemented
|
|
||||||
|
|
||||||
**Last.fm**
|
|
||||||
- `POST /lastfm/auth.getMobileSession` - Mobile session authentication
|
|
||||||
- `GET /lastfm/*` - All Last.fm API methods (read-only)
|
|
||||||
- `POST /lastfm/*` - Write operations (scrobbles, love, etc.)
|
|
||||||
- Automatic signature generation with API secret
|
|
||||||
|
|
||||||
**Spotify**
|
|
||||||
- `POST /spotify/token` - OAuth token exchange
|
|
||||||
- `POST /spotify/refresh` - Token refresh
|
|
||||||
- Client credentials managed server-side
|
|
||||||
|
|
||||||
**Tidal**
|
|
||||||
- `POST /tidal/token` - OAuth token exchange
|
|
||||||
- `POST /tidal/refresh` - Token refresh
|
|
||||||
- Client credentials managed server-side
|
|
||||||
|
|
||||||
**Discogs**
|
|
||||||
- `GET /discogs/search` - Database search
|
|
||||||
- `GET /discogs/release/{id}` - Full release details with all images
|
|
||||||
- `GET /discogs/image` - Authenticated image download proxy
|
|
||||||
|
|
||||||
#### Security Features
|
|
||||||
- Rate limiting (configurable per client IP)
|
|
||||||
- User-Agent validation (rejects non-QBZ clients)
|
|
||||||
- CORS headers for web compatibility
|
|
||||||
- Request logging for monitoring
|
|
||||||
|
|
||||||
### Rust Backend Changes
|
|
||||||
|
|
||||||
#### Last.fm Client (`src-tauri/src/lastfm/client.rs`)
|
|
||||||
- Removed `api_secret` field and signature generation
|
|
||||||
- All requests now go through proxy
|
|
||||||
- Simplified authentication flow
|
|
||||||
- Maintains same public API for backward compatibility
|
|
||||||
|
|
||||||
#### Playlist Import (`src-tauri/src/playlist_import/`)
|
|
||||||
- Spotify import uses proxy for OAuth
|
|
||||||
- Tidal import uses proxy for OAuth
|
|
||||||
- No client credentials needed in binary
|
|
||||||
|
|
||||||
#### Discogs Integration (`src-tauri/src/discogs/mod.rs`)
|
|
||||||
- New `search_artwork_options()` method with catalog number support
|
|
||||||
- New `get_release_details()` for full image sets
|
|
||||||
- Intelligent image interleaving (4 from release #1, 4 from #2, 2 from others)
|
|
||||||
- Duplicate filtering by URL
|
|
||||||
- All requests through proxy (no credentials in binary)
|
|
||||||
|
|
||||||
#### Database Schema (`src-tauri/src/library/database.rs`)
|
|
||||||
- Added `catalog_number` column to `local_tracks` table
|
|
||||||
- Migration runs automatically on first launch
|
|
||||||
- Included in album aggregation queries
|
|
||||||
|
|
||||||
#### Metadata Extraction (`src-tauri/src/library/metadata.rs`)
|
|
||||||
- Reads `CatalogNumber` tag using lofty
|
|
||||||
- Extracts from FLAC, MP3, ALAC, etc.
|
|
||||||
|
|
||||||
### Frontend Changes
|
|
||||||
|
|
||||||
#### LocalLibraryView Component
|
|
||||||
- New Discogs artwork selector UI
|
|
||||||
- Carousel navigation for image options
|
|
||||||
- Release title and year display
|
|
||||||
- Catalog number display in album details
|
|
||||||
- Improved button consistency
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Database Migrations
|
|
||||||
|
|
||||||
### Migration: Add catalog_number Column
|
|
||||||
```sql
|
|
||||||
ALTER TABLE local_tracks ADD COLUMN catalog_number TEXT;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Impact:**
|
|
||||||
- Runs automatically on app startup
|
|
||||||
- Non-destructive (only adds column)
|
|
||||||
- Existing data unaffected
|
|
||||||
- New scans will populate catalog numbers
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Testing Notes
|
|
||||||
|
|
||||||
### What Was Tested
|
|
||||||
- ✅ Last.fm authentication and scrobbling through proxy
|
|
||||||
- ✅ Spotify playlist import with OAuth through proxy
|
|
||||||
- ✅ Tidal playlist import with OAuth through proxy
|
|
||||||
- ✅ Discogs artwork search with catalog numbers
|
|
||||||
- ✅ Multi-release image fetching and display
|
|
||||||
- ✅ Carousel navigation with 4+ images
|
|
||||||
- ✅ Artwork download and caching
|
|
||||||
- ✅ Database migration (catalog_number)
|
|
||||||
|
|
||||||
### Known Issues
|
|
||||||
- ⚠️ **429 Rate Limits**: Discogs API occasionally returns "Too Many Requests"
|
|
||||||
- **Impact**: User must retry after a few seconds
|
|
||||||
- **Frequency**: Occasional, especially when fetching detailed release info
|
|
||||||
- **Severity**: Low (transient error, retry works)
|
|
||||||
- **Mitigation**: Proxy uses rate limiting, but Discogs has strict limits
|
|
||||||
- **Future Fix**: Could add request caching or debouncing
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚨 Breaking Changes
|
|
||||||
|
|
||||||
### For Developers
|
|
||||||
- `LastFmClient::new()` no longer requires `api_secret` parameter
|
|
||||||
- `LastFmClient::with_credentials()` signature changed (removed `api_secret`)
|
|
||||||
- Cloudflare Workers proxy must be deployed for app to function
|
|
||||||
- Environment variables needed for proxy deployment (see `.dev.vars.example`)
|
|
||||||
|
|
||||||
### For Users
|
|
||||||
**None** - Changes are transparent to end users. All existing functionality works the same.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🌐 Deployment Requirements
|
|
||||||
|
|
||||||
### Cloudflare Workers
|
|
||||||
The proxy must be deployed to Cloudflare Workers with these secrets:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Last.fm
|
|
||||||
LAST_FM_API_KEY=your_key_here
|
|
||||||
LAST_FM_API_SHARED_SECRET=your_secret_here
|
|
||||||
|
|
||||||
# Spotify
|
|
||||||
SPOTIFY_API_CLIENT_ID=your_client_id_here
|
|
||||||
SPOTIFY_API_CLIENT_SECRET=your_client_secret_here
|
|
||||||
|
|
||||||
# Tidal
|
|
||||||
TIDAL_API_CLIENT_ID=your_client_id_here
|
|
||||||
TIDAL_API_CLIENT_SECRET=your_client_secret_here
|
|
||||||
|
|
||||||
# Discogs
|
|
||||||
DISCOGS_API_CLIENT_KEY=your_key_here
|
|
||||||
DISCOGS_API_CLIENT_SECRET=your_secret_here
|
|
||||||
```
|
|
||||||
|
|
||||||
**Deployment:**
|
|
||||||
```bash
|
|
||||||
cd qbz-api-proxy
|
|
||||||
npm install
|
|
||||||
wrangler deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
**URL:** `https://qbz-api-proxy.blitzkriegfc.workers.dev`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Migration Guide
|
|
||||||
|
|
||||||
### Merging to Main
|
|
||||||
|
|
||||||
1. **Ensure proxy is deployed** and responding
|
|
||||||
2. **Test all integrations** post-merge:
|
|
||||||
- Last.fm login and scrobbling
|
|
||||||
- Spotify playlist import
|
|
||||||
- Tidal playlist import
|
|
||||||
- Discogs artwork fetching
|
|
||||||
3. **Database migration runs automatically** on first launch
|
|
||||||
4. **No user action required** - changes are transparent
|
|
||||||
|
|
||||||
### Rollback Plan
|
|
||||||
If issues arise, revert commits and redeploy previous version. Database migration is additive only (safe to rollback).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Future Enhancements
|
|
||||||
|
|
||||||
### Potential Improvements
|
|
||||||
- [ ] **Request caching** in proxy to reduce 429 errors
|
|
||||||
- [ ] **Artwork preview** before download
|
|
||||||
- [ ] **Batch artwork fetching** for multiple albums
|
|
||||||
- [ ] **MusicBrainz integration** using same proxy pattern
|
|
||||||
- [ ] **User-provided API keys** as optional override
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📄 Files Changed
|
|
||||||
|
|
||||||
### New Files
|
|
||||||
- `qbz-api-proxy/` - Complete Cloudflare Workers proxy project
|
|
||||||
- `CHANGELOG-api-keys-refactor.md` - This file
|
|
||||||
|
|
||||||
### Modified Files
|
|
||||||
**Rust Backend:**
|
|
||||||
- `src-tauri/src/lastfm/client.rs`
|
|
||||||
- `src-tauri/src/lastfm/commands.rs`
|
|
||||||
- `src-tauri/src/playlist_import/spotify.rs`
|
|
||||||
- `src-tauri/src/playlist_import/tidal.rs`
|
|
||||||
- `src-tauri/src/discogs/mod.rs`
|
|
||||||
- `src-tauri/src/library/models.rs`
|
|
||||||
- `src-tauri/src/library/database.rs`
|
|
||||||
- `src-tauri/src/library/metadata.rs`
|
|
||||||
- `src-tauri/src/library/cue_parser.rs`
|
|
||||||
- `src-tauri/src/library/commands.rs`
|
|
||||||
- `src-tauri/src/lib.rs`
|
|
||||||
|
|
||||||
**Frontend:**
|
|
||||||
- `src/lib/components/views/LocalLibraryView.svelte`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 👥 Credits
|
|
||||||
|
|
||||||
- **Architecture Design**: API proxy pattern for credential management
|
|
||||||
- **Discogs Integration**: Multi-release artwork selection with metadata
|
|
||||||
- **Catalog Number Support**: Audiophile-grade metadata handling
|
|
||||||
- **UI/UX**: Carousel navigation and visual artwork selector
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📅 Timeline
|
|
||||||
|
|
||||||
- **Started**: API keys refactor planning
|
|
||||||
- **Completed**: Full integration with all services migrated to proxy
|
|
||||||
- **Ready for Merge**: Pending main branch audio configuration updates
|
|
||||||
- **Target Merge**: After main stabilizes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Pre-Merge Checklist
|
|
||||||
|
|
||||||
- [x] All API keys removed from binary
|
|
||||||
- [x] Cloudflare Workers proxy deployed and tested
|
|
||||||
- [x] Last.fm integration working through proxy
|
|
||||||
- [x] Spotify integration working through proxy
|
|
||||||
- [x] Tidal integration working through proxy
|
|
||||||
- [x] Discogs integration working through proxy
|
|
||||||
- [x] Database migration tested
|
|
||||||
- [x] UI updates complete and tested
|
|
||||||
- [x] No breaking changes for end users
|
|
||||||
- [x] Documentation complete (this CHANGELOG)
|
|
||||||
- [ ] Wait for main branch audio config changes to stabilize
|
|
||||||
- [ ] Final integration test after merge to main
|
|
||||||
- [ ] Update app version number
|
|
||||||
- [ ] Tag release
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**End of Changelog**
|
|
||||||
@@ -1,505 +0,0 @@
|
|||||||
# 🚀 HANDOFF: API Keys Refactor - Ready to Merge
|
|
||||||
|
|
||||||
**Date:** January 17, 2026
|
|
||||||
**Branch:** `feature/api-keys-refactor`
|
|
||||||
**Status:** ✅ Ready for merge to `main`
|
|
||||||
**Working Tree:** Clean (verified)
|
|
||||||
**New commits to merge:** 1 commit (removes API key injection from workflows)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Pre-Merge Verification
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verify current location
|
|
||||||
pwd
|
|
||||||
# Should be: /home/blitzkriegfc/Personal/qbz/qbz-worktrees/feature-api-keys-refactor
|
|
||||||
|
|
||||||
# Verify branch
|
|
||||||
git branch
|
|
||||||
# Should show: * feature/api-keys-refactor
|
|
||||||
|
|
||||||
# Verify clean state
|
|
||||||
git status
|
|
||||||
# Should show: nothing to commit, working tree clean
|
|
||||||
|
|
||||||
# Verify commits ready
|
|
||||||
git log --oneline origin/main..HEAD
|
|
||||||
# Should show 3 commits (1 new + 1 merge + 1 doc update)
|
|
||||||
```
|
|
||||||
|
|
||||||
**All checks passed:** ✅
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚨 POST-MERGE ACTION REQUIRED
|
|
||||||
|
|
||||||
**After merging and testing, you MUST remove the API Keys UI from Settings.**
|
|
||||||
|
|
||||||
**Why?** The UI is non-functional - it accepts user credentials but completely ignores them. All API requests go through the Cloudflare Workers proxy. Leaving this UI visible will confuse users and create unnecessary support requests.
|
|
||||||
|
|
||||||
**What to do:**
|
|
||||||
1. Merge this branch ✅
|
|
||||||
2. Test all integrations ✅
|
|
||||||
3. **Follow `POST-MERGE-CLEANUP.md`** (30-45 min, removes ~715 lines)
|
|
||||||
4. Commit the cleanup
|
|
||||||
5. Done!
|
|
||||||
|
|
||||||
**Document:** See `POST-MERGE-CLEANUP.md` for step-by-step instructions with exact line numbers.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Current Status
|
|
||||||
|
|
||||||
**Previous merge (already in main):** 14 commits with API keys refactor, Discogs UI, and catalog number support ✅
|
|
||||||
|
|
||||||
**New commits to merge:**
|
|
||||||
|
|
||||||
```
|
|
||||||
b182d2d Remove API key injection from GitHub workflows
|
|
||||||
76ed4e0 Merge main into feature/api-keys-refactor (brings audio config updates)
|
|
||||||
2495636 Update HANDOFF document to emphasize API Keys UI cleanup requirement
|
|
||||||
```
|
|
||||||
|
|
||||||
**What this merge will add to main:**
|
|
||||||
- Removal of API key injection from GitHub workflows (release-linux.yml, release-flatpak.yml)
|
|
||||||
- Updated HANDOFF documentation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Merge Commands (Copy & Paste)
|
|
||||||
|
|
||||||
### Step 1: Ensure you're in the main worktree
|
|
||||||
```bash
|
|
||||||
cd /home/blitzkriegfc/Personal/qbz/qbz-nix
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Ensure main is up to date
|
|
||||||
```bash
|
|
||||||
git checkout main
|
|
||||||
git pull origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Merge feature branch (no fast-forward for clean history)
|
|
||||||
```bash
|
|
||||||
git merge --no-ff feature/api-keys-refactor -m "Merge feature/api-keys-refactor: Remove embedded API keys and add Discogs artwork selector
|
|
||||||
|
|
||||||
This major refactor achieves Flathub compliance by removing all embedded
|
|
||||||
API credentials from the binary. All API requests now go through a
|
|
||||||
Cloudflare Workers proxy for secure credential management.
|
|
||||||
|
|
||||||
New Features:
|
|
||||||
- Discogs artwork selection UI with multi-release support
|
|
||||||
- Catalog number support for audiophile libraries
|
|
||||||
- Carousel navigation for artwork options
|
|
||||||
- Release metadata display (title, year)
|
|
||||||
|
|
||||||
Technical Changes:
|
|
||||||
- Cloudflare Workers proxy for Last.fm, Spotify, Tidal, Discogs
|
|
||||||
- Database migration adds catalog_number column
|
|
||||||
- URI scheme registration (qbz://) for OAuth flows
|
|
||||||
- Rate limiting and User-Agent validation server-side
|
|
||||||
|
|
||||||
See CHANGELOG-api-keys-refactor.md for complete details.
|
|
||||||
See POST-MERGE-CLEANUP.md for removing legacy API Keys UI."
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Resolve conflicts (if any)
|
|
||||||
```bash
|
|
||||||
# If conflicts occur (likely in SettingsView.svelte due to audio config changes)
|
|
||||||
git status
|
|
||||||
# Check which files have conflicts
|
|
||||||
|
|
||||||
# For each conflicted file:
|
|
||||||
# 1. Open in editor
|
|
||||||
# 2. Look for <<<<<<< HEAD markers
|
|
||||||
# 3. Keep BOTH changes (main's audio config + feature's other changes)
|
|
||||||
# 4. Remove conflict markers
|
|
||||||
# 5. git add <file>
|
|
||||||
|
|
||||||
# After resolving all conflicts:
|
|
||||||
git merge --continue
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 5: Push to origin
|
|
||||||
```bash
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 6: Tag the release
|
|
||||||
```bash
|
|
||||||
git tag -a v1.2.0 -m "API Keys Refactor - Flathub Compliance
|
|
||||||
|
|
||||||
Major changes:
|
|
||||||
- Removed all embedded API credentials
|
|
||||||
- Added Cloudflare Workers proxy for credential management
|
|
||||||
- Added Discogs artwork selection UI
|
|
||||||
- Added catalog number support
|
|
||||||
- Database migration for catalog_number column
|
|
||||||
- Flathub submission now possible"
|
|
||||||
|
|
||||||
git push origin v1.2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 7: Cleanup (optional - can keep branch for reference)
|
|
||||||
```bash
|
|
||||||
# Delete local feature branch (optional)
|
|
||||||
git branch -d feature/api-keys-refactor
|
|
||||||
|
|
||||||
# Delete remote feature branch (optional)
|
|
||||||
git push origin --delete feature/api-keys-refactor
|
|
||||||
|
|
||||||
# Remove worktree (optional)
|
|
||||||
git worktree remove /home/blitzkriegfc/Personal/qbz/qbz-worktrees/feature-api-keys-refactor
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Post-Merge Testing Checklist
|
|
||||||
|
|
||||||
### Critical Tests (Must Pass)
|
|
||||||
- [ ] App launches without errors
|
|
||||||
- [ ] Database migration runs (check logs for catalog_number column)
|
|
||||||
- [ ] Last.fm login works
|
|
||||||
- [ ] Last.fm scrobbling works
|
|
||||||
- [ ] Spotify playlist import works
|
|
||||||
- [ ] Tidal playlist import works
|
|
||||||
- [ ] Discogs artwork search works
|
|
||||||
- [ ] Discogs carousel navigation works (4+ images)
|
|
||||||
- [ ] Artwork save works (select image → Save)
|
|
||||||
- [ ] Settings view intact (audio config + other sections)
|
|
||||||
|
|
||||||
### Regression Tests (Should Still Work)
|
|
||||||
- [ ] Audio playback
|
|
||||||
- [ ] Local library scanning
|
|
||||||
- [ ] Album/Artist browsing
|
|
||||||
- [ ] Search functionality
|
|
||||||
- [ ] Playlist management
|
|
||||||
- [ ] Downloads
|
|
||||||
|
|
||||||
### Open browser DevTools and check console for:
|
|
||||||
- [ ] No errors on startup
|
|
||||||
- [ ] No errors when fetching Discogs artwork
|
|
||||||
- [ ] Logs show: "Downloading Discogs artwork from: ..."
|
|
||||||
- [ ] Logs show: "Downloaded to: ..."
|
|
||||||
- [ ] Logs show: "Set album artwork successfully"
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 What Changed - Quick Summary
|
|
||||||
|
|
||||||
### 🔐 Security & Compliance
|
|
||||||
- ❌ Removed Last.fm API secret from binary
|
|
||||||
- ❌ Removed Spotify credentials from binary
|
|
||||||
- ❌ Removed Tidal credentials from binary
|
|
||||||
- ❌ Removed Discogs credentials from binary
|
|
||||||
- ✅ Cloudflare Workers proxy handles all credentials
|
|
||||||
- ✅ Flathub submission now possible
|
|
||||||
|
|
||||||
### ✨ New Features
|
|
||||||
1. **Discogs Artwork Selector**
|
|
||||||
- Visual UI in album edit modal
|
|
||||||
- Multi-release support (4 images from top 2 releases)
|
|
||||||
- Carousel navigation (4 per page)
|
|
||||||
- Shows release title and year
|
|
||||||
- Smart search (uses catalog number if available)
|
|
||||||
|
|
||||||
2. **Catalog Number Support**
|
|
||||||
- Reads from audio file tags (CATALOGNUMBER field)
|
|
||||||
- Stored in database
|
|
||||||
- Displayed in UI ("Cat# XXXXX")
|
|
||||||
- Prioritized in Discogs searches
|
|
||||||
|
|
||||||
### 🔧 Technical
|
|
||||||
- Cloudflare Workers proxy deployed: `https://qbz-api-proxy.blitzkriegfc.workers.dev`
|
|
||||||
- Database migration adds `catalog_number` column (automatic, non-destructive)
|
|
||||||
- URI scheme `qbz://` registered for OAuth
|
|
||||||
|
|
||||||
### 🧹 Post-Merge Cleanup Required
|
|
||||||
- ⚠️ **API Keys UI section in Settings must be removed** (non-functional)
|
|
||||||
- 📄 Complete instructions in `POST-MERGE-CLEANUP.md`
|
|
||||||
- 🗑️ Removes ~715 lines of dead code
|
|
||||||
- ⏱️ Estimated time: 30-45 minutes
|
|
||||||
- 🎯 **Do this within 1-2 days after merge**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ Known Issues
|
|
||||||
|
|
||||||
### 1. Discogs 429 Rate Limits
|
|
||||||
- **Severity:** Low
|
|
||||||
- **Frequency:** Occasional (when fetching detailed release info)
|
|
||||||
- **Impact:** User must retry after a few seconds
|
|
||||||
- **Status:** Acceptable (transient error)
|
|
||||||
- **User Experience:** Works on retry
|
|
||||||
|
|
||||||
### 2. 🚨 API Keys UI Still Visible (REQUIRES CLEANUP)
|
|
||||||
- **Severity:** Medium (confusing to users)
|
|
||||||
- **Why:** Legacy code left to avoid merge conflicts with audio config changes
|
|
||||||
- **Status:** **MUST be removed after merge** (see POST-MERGE-CLEANUP.md)
|
|
||||||
- **Impact:** Users see non-functional settings section
|
|
||||||
- **What it does:** Accepts input but credentials are completely ignored
|
|
||||||
- **Reality:** All requests go through Cloudflare Workers proxy
|
|
||||||
- **Action Required:** Follow `POST-MERGE-CLEANUP.md` within 1-2 days of merge
|
|
||||||
- **Estimated effort:** 30-45 minutes, removes ~715 lines of dead code
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📄 Important Documentation Files
|
|
||||||
|
|
||||||
### 1. CHANGELOG-api-keys-refactor.md
|
|
||||||
**Location:** `/home/blitzkriegfc/Personal/qbz/qbz-worktrees/feature-api-keys-refactor/CHANGELOG-api-keys-refactor.md`
|
|
||||||
|
|
||||||
**Contains:**
|
|
||||||
- Complete list of all changes
|
|
||||||
- Security improvements details
|
|
||||||
- New features with user benefits
|
|
||||||
- Technical architecture changes
|
|
||||||
- Database migrations
|
|
||||||
- Testing notes
|
|
||||||
- Breaking changes (none for users)
|
|
||||||
- Deployment requirements
|
|
||||||
|
|
||||||
**Read this:** Before announcing release to users
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. MERGE-PREP.md
|
|
||||||
**Location:** `/home/blitzkriegfc/Personal/qbz/qbz-worktrees/feature-api-keys-refactor/MERGE-PREP.md`
|
|
||||||
|
|
||||||
**Contains:**
|
|
||||||
- Pre-merge checklist (all ✅)
|
|
||||||
- Merge strategy recommendations
|
|
||||||
- Post-merge testing plan
|
|
||||||
- Rollback procedures
|
|
||||||
- Release notes template
|
|
||||||
- Success criteria
|
|
||||||
|
|
||||||
**Read this:** Before doing the merge (you're reading the HANDOFF now, so this is optional)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. POST-MERGE-CLEANUP.md
|
|
||||||
**Location:** `/home/blitzkriegfc/Personal/qbz/qbz-worktrees/feature-api-keys-refactor/POST-MERGE-CLEANUP.md`
|
|
||||||
|
|
||||||
**Contains:**
|
|
||||||
- Step-by-step instructions to remove API Keys UI
|
|
||||||
- 11 files to modify with line numbers
|
|
||||||
- Before/after code examples
|
|
||||||
- Verification steps
|
|
||||||
- Git commit message template
|
|
||||||
- Estimated 30-45 minutes to complete
|
|
||||||
|
|
||||||
**Read this:** After merge, when ready to clean up legacy code
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🌐 Cloudflare Workers Proxy
|
|
||||||
|
|
||||||
**Status:** ✅ Deployed and operational
|
|
||||||
**URL:** `https://qbz-api-proxy.blitzkriegfc.workers.dev`
|
|
||||||
**Repository:** `/home/blitzkriegfc/Personal/qbz/qbz-api-proxy`
|
|
||||||
|
|
||||||
**Endpoints Active:**
|
|
||||||
- Last.fm: `/lastfm/*`
|
|
||||||
- Spotify: `/spotify/token`, `/spotify/refresh`
|
|
||||||
- Tidal: `/tidal/token`, `/tidal/refresh`
|
|
||||||
- Discogs: `/discogs/search`, `/discogs/release/{id}`, `/discogs/image`
|
|
||||||
|
|
||||||
**Test Proxy:**
|
|
||||||
```bash
|
|
||||||
curl https://qbz-api-proxy.blitzkriegfc.workers.dev
|
|
||||||
# Should return: {"service":"QBZ API Proxy","version":"1.0.0",...}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 Release Notes Template
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## QBZ v1.2.0 - API Keys Refactor & Discogs Enhancement
|
|
||||||
|
|
||||||
### 🔐 Security & Flathub Compliance
|
|
||||||
We've completely removed all embedded API credentials from the application.
|
|
||||||
All API requests now go through a secure Cloudflare Workers proxy, making
|
|
||||||
QBZ ready for Flathub submission and improving overall security.
|
|
||||||
|
|
||||||
### ✨ New Features
|
|
||||||
|
|
||||||
**Discogs Artwork Selector**
|
|
||||||
Browse and select album artwork directly from Discogs! The new visual selector
|
|
||||||
shows multiple release variants with metadata, making it easy to find the
|
|
||||||
perfect cover for your albums.
|
|
||||||
|
|
||||||
- 🎨 Visual artwork browser in album edit modal
|
|
||||||
- 🖼️ See images from different release variants
|
|
||||||
- 📄 Navigate with carousel (4 images per page)
|
|
||||||
- 📋 View release titles and years
|
|
||||||
- 🏷️ Smart search using catalog numbers
|
|
||||||
|
|
||||||
**Catalog Number Support**
|
|
||||||
For audiophile and collector libraries with professional tagging:
|
|
||||||
- Automatically reads catalog numbers from audio file tags
|
|
||||||
- Displays in album details (e.g., "Cat# MFSL 1-395")
|
|
||||||
- Prioritized in Discogs searches for precise results
|
|
||||||
|
|
||||||
### 🔧 Technical Improvements
|
|
||||||
- Cloudflare Workers proxy for all external APIs
|
|
||||||
- Database migration adds catalog_number support
|
|
||||||
- Improved error handling and logging
|
|
||||||
- Rate limiting and validation server-side
|
|
||||||
|
|
||||||
### 🐛 Known Issues
|
|
||||||
- Occasional 429 rate limit errors from Discogs (retry after a few seconds)
|
|
||||||
|
|
||||||
### 🙏 Thanks
|
|
||||||
Special thanks to all users who requested catalog number support!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 Rollback Plan (If Needed)
|
|
||||||
|
|
||||||
If critical issues found after merge:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Find the merge commit
|
|
||||||
git log --oneline -10
|
|
||||||
|
|
||||||
# Revert the merge (safest option)
|
|
||||||
git revert -m 1 <merge-commit-hash>
|
|
||||||
git push origin main
|
|
||||||
|
|
||||||
# OR hard reset (use with EXTREME caution)
|
|
||||||
git reset --hard <commit-before-merge>
|
|
||||||
git push --force origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Support Checklist
|
|
||||||
|
|
||||||
If users report issues after release:
|
|
||||||
|
|
||||||
1. **Proxy Down**
|
|
||||||
- Check: https://qbz-api-proxy.blitzkriegfc.workers.dev
|
|
||||||
- Verify Cloudflare Workers dashboard
|
|
||||||
- Check environment variables are set
|
|
||||||
|
|
||||||
2. **Database Migration Failed**
|
|
||||||
- Check app logs for migration errors
|
|
||||||
- May need manual `ALTER TABLE local_tracks ADD COLUMN catalog_number TEXT;`
|
|
||||||
|
|
||||||
3. **API Integration Broken**
|
|
||||||
- Check proxy is responding
|
|
||||||
- Verify User-Agent is "QBZ/1.0.0"
|
|
||||||
- Check rate limits not exceeded
|
|
||||||
|
|
||||||
4. **UI Issues**
|
|
||||||
- Clear browser cache
|
|
||||||
- Check browser console for errors
|
|
||||||
- Verify Settings changes don't conflict with audio config
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Final Verification Before Merge
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# You are here
|
|
||||||
pwd
|
|
||||||
# → /home/blitzkriegfc/Personal/qbz/qbz-worktrees/feature-api-keys-refactor
|
|
||||||
|
|
||||||
# Branch is clean
|
|
||||||
git status
|
|
||||||
# → nothing to commit, working tree clean ✅
|
|
||||||
|
|
||||||
# All commits ready
|
|
||||||
git log --oneline origin/main..HEAD | wc -l
|
|
||||||
# → 13 ✅
|
|
||||||
|
|
||||||
# Proxy is up
|
|
||||||
curl -s https://qbz-api-proxy.blitzkriegfc.workers.dev | grep service
|
|
||||||
# → "service":"QBZ API Proxy" ✅
|
|
||||||
|
|
||||||
# Ready to merge!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Next Steps After Merge
|
|
||||||
|
|
||||||
### Immediate (Required)
|
|
||||||
1. ✅ **Test all integrations** - Use checklist above (critical)
|
|
||||||
|
|
||||||
### Short-term (Within 1-2 days)
|
|
||||||
2. 🧹 **REMOVE API Keys UI** - Follow `POST-MERGE-CLEANUP.md`
|
|
||||||
- **Why:** UI is non-functional (accepts input but ignores it)
|
|
||||||
- **Impact:** Removes ~715 lines of confusing dead code
|
|
||||||
- **Time:** 30-45 minutes
|
|
||||||
- **File:** See detailed instructions in `POST-MERGE-CLEANUP.md`
|
|
||||||
- **Note:** Left in this merge to avoid conflicts with audio config changes
|
|
||||||
|
|
||||||
3. 📢 **Announce release** - Use release notes template above
|
|
||||||
|
|
||||||
### Future
|
|
||||||
4. 🚀 **Submit to Flathub** - Now possible with no embedded secrets!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ IMPORTANT: API Keys UI Cleanup
|
|
||||||
|
|
||||||
**The API Keys section in Settings must be removed after merge.**
|
|
||||||
|
|
||||||
**Current State:**
|
|
||||||
- ✅ UI exists and looks functional
|
|
||||||
- ❌ User can enter credentials
|
|
||||||
- ❌ Credentials are saved to state
|
|
||||||
- ❌ **BUT credentials are completely ignored**
|
|
||||||
- ❌ All requests go through proxy (user keys never used)
|
|
||||||
|
|
||||||
**Why it's confusing to users:**
|
|
||||||
- Promises functionality that doesn't work
|
|
||||||
- Makes users think they need to configure something
|
|
||||||
- Wastes support time explaining "it doesn't actually do anything"
|
|
||||||
|
|
||||||
**Action Required:**
|
|
||||||
```bash
|
|
||||||
# After merge and testing, run:
|
|
||||||
cd /home/blitzkriegfc/Personal/qbz/qbz-nix
|
|
||||||
|
|
||||||
# Follow step-by-step instructions in:
|
|
||||||
cat POST-MERGE-CLEANUP.md
|
|
||||||
|
|
||||||
# Creates one commit removing ~715 lines of dead code
|
|
||||||
# Estimated time: 30-45 minutes
|
|
||||||
```
|
|
||||||
|
|
||||||
**What gets removed:**
|
|
||||||
- `src-tauri/src/api_keys.rs` - Entire module (175 lines)
|
|
||||||
- Settings UI section - API Keys collapsible section (~350 lines)
|
|
||||||
- Command registrations - 10 Tauri commands
|
|
||||||
- Provider parameters - Unused `ApiKeysState` params in 11 files
|
|
||||||
- Translations - API Keys strings in en.json and es.json
|
|
||||||
|
|
||||||
**Document:** `POST-MERGE-CLEANUP.md` has exact line numbers and before/after code.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Notes
|
|
||||||
|
|
||||||
- Feature branch can be kept for reference or deleted
|
|
||||||
- Worktree can remain or be removed
|
|
||||||
- CHANGELOG files will be in main after merge
|
|
||||||
- No user data affected by this merge
|
|
||||||
- All changes are backward compatible
|
|
||||||
- **API Keys UI removal is documented but not automated** (manual cleanup required)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Prepared by:** Claude Sonnet 4.5
|
|
||||||
**Handoff Date:** January 17, 2026
|
|
||||||
**Status:** ✅ READY TO MERGE
|
|
||||||
|
|
||||||
🚀 **You're all set! Just run the merge commands above.**
|
|
||||||
|
|
||||||
⚠️ **Don't forget:** Remove API Keys UI after testing (see POST-MERGE-CLEANUP.md)
|
|
||||||
-254
@@ -1,254 +0,0 @@
|
|||||||
# Merge Preparation: feature/api-keys-refactor → main
|
|
||||||
|
|
||||||
## 📊 Branch Status
|
|
||||||
|
|
||||||
**Branch:** `feature/api-keys-refactor`
|
|
||||||
**Base:** `main`
|
|
||||||
**Status:** ✅ Ready for merge (waiting for main to stabilize)
|
|
||||||
**Total Commits:** 11
|
|
||||||
**Working Tree:** Clean
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Commits Ready for Merge
|
|
||||||
|
|
||||||
```
|
|
||||||
81b65bd Add comprehensive CHANGELOG for API keys refactor
|
|
||||||
80ba2bf Fix Discogs artwork selection UI issues
|
|
||||||
732a489 Improve Discogs artwork selection with multi-release images
|
|
||||||
279d433 Add catalog number support for precise Discogs searches
|
|
||||||
2a5c181 Fix Discogs artwork selection to show unique images
|
|
||||||
dab5c28 Add Discogs artwork selection UI in album edit modal
|
|
||||||
dd70f0a Refactor Discogs integration to use Cloudflare Workers proxy
|
|
||||||
9099123 Refactor Spotify and Tidal playlist import to use proxy
|
|
||||||
179a6b7 Refactor Last.fm integration to use Cloudflare Workers proxy
|
|
||||||
3a49e3f Remove embedded Last.fm API secret for Flathub compliance
|
|
||||||
b7048fa feat: register qbz:// URI scheme for OAuth callbacks
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🌐 Cloudflare Workers Proxy Status
|
|
||||||
|
|
||||||
**Repository:** `qbz-api-proxy/`
|
|
||||||
**Branch:** `main`
|
|
||||||
**Status:** ✅ Deployed and operational
|
|
||||||
**URL:** `https://qbz-api-proxy.blitzkriegfc.workers.dev`
|
|
||||||
|
|
||||||
**Recent Commits:**
|
|
||||||
```
|
|
||||||
0346925 Add release details endpoint for Discogs
|
|
||||||
7b9137a Add Discogs API proxy handler
|
|
||||||
0a5a9d6 Enhance Last.fm proxy to return authorization URL with token
|
|
||||||
66f8f29 Fix environment variable names to match QBZ conventions
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Pre-Merge Checklist
|
|
||||||
|
|
||||||
### Code Quality
|
|
||||||
- [x] All commits have descriptive messages
|
|
||||||
- [x] No debug code or console.logs (except intentional logging)
|
|
||||||
- [x] No TODO/FIXME comments without issues
|
|
||||||
- [x] Code follows project conventions
|
|
||||||
- [x] No hardcoded credentials or secrets
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
- [x] Last.fm authentication works through proxy
|
|
||||||
- [x] Last.fm scrobbling works through proxy
|
|
||||||
- [x] Spotify playlist import works through proxy
|
|
||||||
- [x] Tidal playlist import works through proxy
|
|
||||||
- [x] Discogs artwork search works (with catalog numbers)
|
|
||||||
- [x] Discogs multi-release image fetching works
|
|
||||||
- [x] Carousel navigation works (4 images per page)
|
|
||||||
- [x] Artwork download and save works
|
|
||||||
- [x] Database migration runs successfully
|
|
||||||
- [x] Catalog number extraction from audio files works
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
- [x] CHANGELOG created and comprehensive
|
|
||||||
- [x] All new features documented
|
|
||||||
- [x] Breaking changes documented (none for users)
|
|
||||||
- [x] Deployment requirements documented
|
|
||||||
- [x] Known issues documented (429 rate limits)
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
- [x] Cloudflare Workers proxy deployed
|
|
||||||
- [x] All proxy endpoints tested and working
|
|
||||||
- [x] Rate limiting configured
|
|
||||||
- [x] Environment variables set correctly
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚨 Known Issues
|
|
||||||
|
|
||||||
### 1. Discogs 429 Rate Limiting
|
|
||||||
- **Severity:** Low
|
|
||||||
- **Frequency:** Occasional
|
|
||||||
- **Impact:** User must retry after a few seconds
|
|
||||||
- **Status:** Acceptable (transient error)
|
|
||||||
- **Future Fix:** Could add request caching
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 Merge Strategy
|
|
||||||
|
|
||||||
### Recommended Approach
|
|
||||||
```bash
|
|
||||||
# Ensure you're on main branch
|
|
||||||
git checkout main
|
|
||||||
|
|
||||||
# Pull latest changes
|
|
||||||
git pull origin main
|
|
||||||
|
|
||||||
# Merge feature branch (no fast-forward for clear history)
|
|
||||||
git merge --no-ff feature/api-keys-refactor
|
|
||||||
|
|
||||||
# Resolve any conflicts (unlikely, but check audio config changes)
|
|
||||||
|
|
||||||
# Push to origin
|
|
||||||
git push origin main
|
|
||||||
|
|
||||||
# Tag the release
|
|
||||||
git tag -a v1.2.0 -m "API Keys Refactor - Flathub Compliance"
|
|
||||||
git push origin v1.2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### Conflict Resolution
|
|
||||||
If conflicts arise (likely in audio config files):
|
|
||||||
1. Carefully review both changes
|
|
||||||
2. Prefer main's audio configuration changes
|
|
||||||
3. Keep feature branch's API refactoring
|
|
||||||
4. Test thoroughly after resolution
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 Post-Merge Testing Plan
|
|
||||||
|
|
||||||
### Critical Tests
|
|
||||||
1. **Launch app** - Should run without errors
|
|
||||||
2. **Database migration** - Check for catalog_number column
|
|
||||||
3. **Last.fm login** - Authenticate new user
|
|
||||||
4. **Last.fm scrobble** - Play track and verify scrobble
|
|
||||||
5. **Spotify import** - Import a playlist
|
|
||||||
6. **Tidal import** - Import a playlist
|
|
||||||
7. **Discogs search** - Search for artwork
|
|
||||||
8. **Discogs catalog** - Test with album that has catalog number
|
|
||||||
9. **Artwork save** - Select and save Discogs artwork
|
|
||||||
10. **Carousel** - Navigate through 8+ artwork options
|
|
||||||
|
|
||||||
### Regression Tests
|
|
||||||
- Audio playback still works
|
|
||||||
- Playlists still load
|
|
||||||
- Album view still works
|
|
||||||
- Artist view still works
|
|
||||||
- Search still works
|
|
||||||
- Settings still work
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 Release Notes Template
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## v1.2.0 - API Keys Refactor & Discogs Enhancement
|
|
||||||
|
|
||||||
### 🔐 Security & Compliance
|
|
||||||
- Removed all embedded API credentials from binary
|
|
||||||
- Flathub compliance achieved
|
|
||||||
- Server-side credential management via Cloudflare Workers
|
|
||||||
|
|
||||||
### ✨ New Features
|
|
||||||
- **Discogs Artwork Selector**: Visual UI for selecting album artwork
|
|
||||||
- Browse multiple release variants
|
|
||||||
- See release titles and years
|
|
||||||
- Carousel navigation for 4+ options
|
|
||||||
- **Catalog Number Support**: Professional metadata for audiophile libraries
|
|
||||||
- Automatic extraction from audio tags
|
|
||||||
- Prioritized in Discogs searches
|
|
||||||
- Displayed in album details
|
|
||||||
|
|
||||||
### 🔧 Technical Improvements
|
|
||||||
- Cloudflare Workers proxy for all external APIs
|
|
||||||
- Multi-release image fetching from Discogs
|
|
||||||
- Database migration for catalog numbers
|
|
||||||
- Improved error handling and logging
|
|
||||||
|
|
||||||
### 🐛 Known Issues
|
|
||||||
- Occasional 429 rate limit errors from Discogs (retry after a few seconds)
|
|
||||||
|
|
||||||
### 🙏 Thanks
|
|
||||||
Special thanks to all audiophile users who requested catalog number support!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Support Plan
|
|
||||||
|
|
||||||
### If Issues Arise Post-Merge
|
|
||||||
|
|
||||||
1. **Proxy Down**: Check Cloudflare Workers dashboard
|
|
||||||
2. **Database Migration Fails**: Check logs, may need manual ALTER TABLE
|
|
||||||
3. **API Errors**: Verify proxy environment variables are set
|
|
||||||
4. **UI Broken**: Clear browser cache, check console for errors
|
|
||||||
|
|
||||||
### Rollback Plan
|
|
||||||
```bash
|
|
||||||
# If critical issues found
|
|
||||||
git revert <merge-commit-hash>
|
|
||||||
git push origin main
|
|
||||||
|
|
||||||
# Or hard reset (use with caution)
|
|
||||||
git reset --hard <commit-before-merge>
|
|
||||||
git push --force origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📅 Timeline
|
|
||||||
|
|
||||||
- **Branch Created**: December 2024
|
|
||||||
- **Development**: January 2025
|
|
||||||
- **Ready for Merge**: January 17, 2026
|
|
||||||
- **Waiting On**: Main branch audio configuration stabilization
|
|
||||||
- **Target Merge**: TBD (when main is stable)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Success Criteria
|
|
||||||
|
|
||||||
Merge is successful when:
|
|
||||||
- [x] All commits merged cleanly
|
|
||||||
- [ ] App launches without errors
|
|
||||||
- [ ] Database migration runs
|
|
||||||
- [ ] All 4 API integrations work (Last.fm, Spotify, Tidal, Discogs)
|
|
||||||
- [ ] No user-facing regressions
|
|
||||||
- [ ] Flathub submission possible (no embedded secrets)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Notes for Reviewer
|
|
||||||
|
|
||||||
### Why This Matters
|
|
||||||
This refactor is **critical** for:
|
|
||||||
1. **Flathub submission** - Can't have embedded secrets
|
|
||||||
2. **Security** - API keys were visible in binary
|
|
||||||
3. **Flexibility** - Can update credentials without app release
|
|
||||||
4. **Rate limiting** - Server-side control
|
|
||||||
|
|
||||||
### What Changed
|
|
||||||
- **User Experience**: Transparent (no changes except new Discogs UI)
|
|
||||||
- **Architecture**: API credentials now server-side
|
|
||||||
- **Code**: Simplified clients, removed signature logic
|
|
||||||
- **Database**: Added catalog_number column
|
|
||||||
|
|
||||||
### What Didn't Change
|
|
||||||
- All existing features work exactly the same
|
|
||||||
- User data is unaffected
|
|
||||||
- Performance is similar (proxy adds ~50-100ms latency)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Prepared by:** Claude Sonnet 4.5
|
|
||||||
**Date:** January 17, 2026
|
|
||||||
**Branch Status:** Ready for merge ✅
|
|
||||||
@@ -1,511 +0,0 @@
|
|||||||
# Post-Merge Cleanup: Remove API Keys UI
|
|
||||||
|
|
||||||
**Context:** The API Keys section in Settings UI is non-functional legacy code. All API requests now go through Cloudflare Workers proxy. User-provided credentials are accepted but ignored.
|
|
||||||
|
|
||||||
**When to do this:** After merging to main and audio configuration changes are stable.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files to Delete Completely
|
|
||||||
|
|
||||||
### 1. Delete API Keys Store Module
|
|
||||||
```bash
|
|
||||||
rm src-tauri/src/api_keys.rs
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why:** Entire module is unused. User credentials are collected but never used.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files to Modify
|
|
||||||
|
|
||||||
### 2. src-tauri/src/lib.rs
|
|
||||||
|
|
||||||
**Remove these imports:**
|
|
||||||
```rust
|
|
||||||
// Line ~8
|
|
||||||
mod api_keys;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Remove state initialization:**
|
|
||||||
```rust
|
|
||||||
// Around line 162
|
|
||||||
let api_keys_state = api_keys::create_api_keys_state();
|
|
||||||
```
|
|
||||||
|
|
||||||
**Remove from .manage() call:**
|
|
||||||
```rust
|
|
||||||
// Around line 400-420 in the .manage() chain
|
|
||||||
.manage(api_keys_state)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Remove command registrations:**
|
|
||||||
```rust
|
|
||||||
// Around line 530-539 in invoke_handler![...]
|
|
||||||
api_keys::set_spotify_credentials,
|
|
||||||
api_keys::clear_spotify_credentials,
|
|
||||||
api_keys::has_spotify_user_credentials,
|
|
||||||
api_keys::set_tidal_credentials,
|
|
||||||
api_keys::clear_tidal_credentials,
|
|
||||||
api_keys::has_tidal_user_credentials,
|
|
||||||
api_keys::set_discogs_credentials,
|
|
||||||
api_keys::clear_discogs_credentials,
|
|
||||||
api_keys::has_discogs_user_credentials,
|
|
||||||
api_keys::get_embedded_credentials_status,
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. src-tauri/src/commands/playlist_import.rs
|
|
||||||
|
|
||||||
**Remove import:**
|
|
||||||
```rust
|
|
||||||
// Line 5
|
|
||||||
use crate::api_keys::ApiKeysState;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Remove state parameter from commands:**
|
|
||||||
|
|
||||||
**In `playlist_import_preview()` (line 13-42):**
|
|
||||||
|
|
||||||
Before:
|
|
||||||
```rust
|
|
||||||
pub async fn playlist_import_preview(
|
|
||||||
url: String,
|
|
||||||
api_keys: State<'_, ApiKeysState>,
|
|
||||||
) -> Result<ImportPlaylist, String> {
|
|
||||||
// Get user-provided credentials from state
|
|
||||||
let keys = api_keys.lock().await;
|
|
||||||
let spotify_creds = if keys.spotify.is_set() {
|
|
||||||
Some(ProviderCredentials { ... })
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let tidal_creds = if keys.tidal.is_set() {
|
|
||||||
Some(ProviderCredentials { ... })
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
drop(keys);
|
|
||||||
|
|
||||||
preview_public_playlist(&url, spotify_creds, tidal_creds)
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
After:
|
|
||||||
```rust
|
|
||||||
pub async fn playlist_import_preview(
|
|
||||||
url: String,
|
|
||||||
) -> Result<ImportPlaylist, String> {
|
|
||||||
preview_public_playlist(&url, None, None)
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**In `playlist_import_execute()` (line 45-90):**
|
|
||||||
|
|
||||||
Remove the same pattern:
|
|
||||||
- Remove `api_keys: State<'_, ApiKeysState>` parameter
|
|
||||||
- Remove credential extraction logic (lines 54-68)
|
|
||||||
- Pass `None, None` instead of `spotify_creds, tidal_creds`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. src-tauri/src/library/commands.rs
|
|
||||||
|
|
||||||
**Remove import:**
|
|
||||||
```rust
|
|
||||||
// Line 10
|
|
||||||
use crate::api_keys::ApiKeysState;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Remove state parameter from these commands:**
|
|
||||||
|
|
||||||
1. **`library_fetch_missing_artwork`** (line ~1168)
|
|
||||||
2. **`discogs_search_artist`** (line ~1187)
|
|
||||||
3. **`library_import_musicbrainz_tags`** (line ~1240)
|
|
||||||
4. **`library_set_artist_image`** (line ~1312)
|
|
||||||
|
|
||||||
**Pattern to follow for each:**
|
|
||||||
|
|
||||||
Before:
|
|
||||||
```rust
|
|
||||||
pub async fn command_name(
|
|
||||||
// ... other parameters
|
|
||||||
api_keys: State<'_, ApiKeysState>,
|
|
||||||
) -> Result<T, String> {
|
|
||||||
// Get Discogs client
|
|
||||||
let keys = api_keys.lock().await;
|
|
||||||
let discogs_client = if keys.discogs.is_set() {
|
|
||||||
DiscogsClient::with_user_credentials(
|
|
||||||
keys.discogs.client_id.clone(),
|
|
||||||
keys.discogs.client_secret.clone(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
DiscogsClient::new()
|
|
||||||
};
|
|
||||||
drop(keys);
|
|
||||||
|
|
||||||
// ... rest of function
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
After:
|
|
||||||
```rust
|
|
||||||
pub async fn command_name(
|
|
||||||
// ... other parameters
|
|
||||||
) -> Result<T, String> {
|
|
||||||
// Get Discogs client (proxy handles credentials)
|
|
||||||
let discogs_client = DiscogsClient::new();
|
|
||||||
|
|
||||||
// ... rest of function
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Specific locations:**
|
|
||||||
- Line ~1175-1182: Remove credential logic in `library_fetch_missing_artwork`
|
|
||||||
- Line ~1193-1200: Remove credential logic in `discogs_search_artist`
|
|
||||||
- Line ~1256-1263: Remove credential logic in `library_import_musicbrainz_tags`
|
|
||||||
- Line ~1320-1327: Remove credential logic in `library_set_artist_image`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. src-tauri/src/playlist_import/providers/mod.rs
|
|
||||||
|
|
||||||
**Simplify `fetch_playlist` function:**
|
|
||||||
|
|
||||||
Before (line ~43-61):
|
|
||||||
```rust
|
|
||||||
pub async fn fetch_playlist(
|
|
||||||
kind: ProviderKind,
|
|
||||||
spotify_creds: Option<ProviderCredentials>,
|
|
||||||
tidal_creds: Option<ProviderCredentials>,
|
|
||||||
) -> Result<ImportPlaylist, PlaylistImportError> {
|
|
||||||
match kind {
|
|
||||||
ProviderKind::Spotify { playlist_id } => {
|
|
||||||
spotify::fetch_playlist(&playlist_id, spotify_creds).await
|
|
||||||
}
|
|
||||||
ProviderKind::AppleMusic { storefront, playlist_id } => {
|
|
||||||
apple::fetch_playlist(&storefront, &playlist_id).await
|
|
||||||
}
|
|
||||||
ProviderKind::Tidal { playlist_id } => {
|
|
||||||
tidal::fetch_playlist(&playlist_id, tidal_creds).await
|
|
||||||
}
|
|
||||||
ProviderKind::Deezer { playlist_id } => deezer::fetch_playlist(&playlist_id).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
After:
|
|
||||||
```rust
|
|
||||||
pub async fn fetch_playlist(
|
|
||||||
kind: ProviderKind,
|
|
||||||
) -> Result<ImportPlaylist, PlaylistImportError> {
|
|
||||||
match kind {
|
|
||||||
ProviderKind::Spotify { playlist_id } => {
|
|
||||||
spotify::fetch_playlist(&playlist_id).await
|
|
||||||
}
|
|
||||||
ProviderKind::AppleMusic { storefront, playlist_id } => {
|
|
||||||
apple::fetch_playlist(&storefront, &playlist_id).await
|
|
||||||
}
|
|
||||||
ProviderKind::Tidal { playlist_id } => {
|
|
||||||
tidal::fetch_playlist(&playlist_id).await
|
|
||||||
}
|
|
||||||
ProviderKind::Deezer { playlist_id } => deezer::fetch_playlist(&playlist_id).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. src-tauri/src/playlist_import/providers/spotify.rs
|
|
||||||
|
|
||||||
**Simplify `fetch_playlist` signature:**
|
|
||||||
|
|
||||||
Before (line ~39-50):
|
|
||||||
```rust
|
|
||||||
pub async fn fetch_playlist(
|
|
||||||
playlist_id: &str,
|
|
||||||
user_creds: Option<ProviderCredentials>,
|
|
||||||
) -> Result<ImportPlaylist, PlaylistImportError> {
|
|
||||||
if let Ok(token) = get_app_token(user_creds).await {
|
|
||||||
if let Ok(playlist) = fetch_playlist_with_token(playlist_id, &token).await {
|
|
||||||
return Ok(playlist);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetch_playlist_from_embed(playlist_id).await
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
After:
|
|
||||||
```rust
|
|
||||||
pub async fn fetch_playlist(
|
|
||||||
playlist_id: &str,
|
|
||||||
) -> Result<ImportPlaylist, PlaylistImportError> {
|
|
||||||
if let Ok(token) = get_app_token().await {
|
|
||||||
if let Ok(playlist) = fetch_playlist_with_token(playlist_id, &token).await {
|
|
||||||
return Ok(playlist);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetch_playlist_from_embed(playlist_id).await
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Simplify `get_app_token` signature:**
|
|
||||||
|
|
||||||
Before (line ~252-280):
|
|
||||||
```rust
|
|
||||||
async fn get_app_token(_user_creds: Option<ProviderCredentials>) -> Result<String, PlaylistImportError> {
|
|
||||||
// Proxy handles credentials - user_creds ignored (compatibility)
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
After:
|
|
||||||
```rust
|
|
||||||
async fn get_app_token() -> Result<String, PlaylistImportError> {
|
|
||||||
// Proxy handles credentials
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 7. src-tauri/src/playlist_import/providers/tidal.rs
|
|
||||||
|
|
||||||
**Same changes as Spotify:**
|
|
||||||
|
|
||||||
1. Remove `user_creds` parameter from `fetch_playlist()` signature
|
|
||||||
2. Remove `_user_creds` parameter from `get_app_token()` signature
|
|
||||||
3. Update function calls to not pass credentials
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 8. src-tauri/src/discogs/mod.rs
|
|
||||||
|
|
||||||
**Simplify `DiscogsClient`:**
|
|
||||||
|
|
||||||
**Remove this method entirely (line ~51-69):**
|
|
||||||
```rust
|
|
||||||
pub fn with_user_credentials(
|
|
||||||
_user_key: Option<String>,
|
|
||||||
_user_secret: Option<String>,
|
|
||||||
) -> Self {
|
|
||||||
// ...entire method...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Update documentation comment (line ~46):**
|
|
||||||
|
|
||||||
Before:
|
|
||||||
```rust
|
|
||||||
/// Create a new Discogs client (proxy handles credentials)
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::with_user_credentials(None, None)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new Discogs client - compatibility method (proxy handles credentials)
|
|
||||||
pub fn with_user_credentials(...) -> Self { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
After:
|
|
||||||
```rust
|
|
||||||
/// Create a new Discogs client (proxy handles credentials)
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let mut headers = reqwest::header::HeaderMap::new();
|
|
||||||
headers.insert(
|
|
||||||
reqwest::header::USER_AGENT,
|
|
||||||
reqwest::header::HeaderValue::from_static("QBZ/1.0.0"),
|
|
||||||
);
|
|
||||||
|
|
||||||
let client = Client::builder()
|
|
||||||
.timeout(Duration::from_secs(10))
|
|
||||||
.default_headers(headers)
|
|
||||||
.build()
|
|
||||||
.expect("Failed to create HTTP client");
|
|
||||||
|
|
||||||
Self { client }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 9. src/lib/components/views/SettingsView.svelte
|
|
||||||
|
|
||||||
**Remove from sections array:**
|
|
||||||
```svelte
|
|
||||||
// Around line 121
|
|
||||||
{ id: 'api-keys', label: 'API Keys' },
|
|
||||||
```
|
|
||||||
|
|
||||||
**Remove state variables (line 262-271):**
|
|
||||||
```svelte
|
|
||||||
// API Keys section state
|
|
||||||
let apiKeysExpanded = $state(false);
|
|
||||||
let spotifyClientId = $state('');
|
|
||||||
let spotifyClientSecret = $state('');
|
|
||||||
let tidalClientId = $state('');
|
|
||||||
let tidalClientSecret = $state('');
|
|
||||||
let discogsConsumerKey = $state('');
|
|
||||||
let discogsConsumerSecret = $state('');
|
|
||||||
let embeddedStatus = $state({ spotify: false, tidal: false, discogs: false, lastfm: false });
|
|
||||||
let apiKeysSaving = $state(false);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Remove helper functions (find and delete):**
|
|
||||||
```svelte
|
|
||||||
function hasAnyUserCredentials() { ... }
|
|
||||||
async function loadApiKeysStatus() { ... }
|
|
||||||
async function saveSpotifyCredentials() { ... }
|
|
||||||
async function clearSpotifyCredentials() { ... }
|
|
||||||
async function saveTidalCredentials() { ... }
|
|
||||||
async function clearTidalCredentials() { ... }
|
|
||||||
async function saveDiscogsCredentials() { ... }
|
|
||||||
async function clearDiscogsCredentials() { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
**Remove from onMount:**
|
|
||||||
```svelte
|
|
||||||
// Around line 300-320 in onMount()
|
|
||||||
// Load embedded credentials status
|
|
||||||
loadApiKeysStatus();
|
|
||||||
```
|
|
||||||
|
|
||||||
**Remove entire section element (line ~1494-1650):**
|
|
||||||
```svelte
|
|
||||||
<!-- API Keys Section (collapsible) -->
|
|
||||||
<section class="section api-keys-section" bind:this={apiKeysSection}>
|
|
||||||
<!-- ...entire section... -->
|
|
||||||
</section>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Remove CSS styles (line ~2112-2300+):**
|
|
||||||
```css
|
|
||||||
/* API Keys section styles */
|
|
||||||
.api-keys-section { ... }
|
|
||||||
.section-title-btn { ... }
|
|
||||||
.keys-badge { ... }
|
|
||||||
.api-keys-info { ... }
|
|
||||||
.api-key-group { ... }
|
|
||||||
.api-key-header { ... }
|
|
||||||
.api-key-title { ... }
|
|
||||||
.status-badge { ... }
|
|
||||||
.api-key-desc { ... }
|
|
||||||
.api-key-fields { ... }
|
|
||||||
.input-group { ... }
|
|
||||||
.input-group label { ... }
|
|
||||||
.input-group input { ... }
|
|
||||||
.api-key-actions { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 10. src/lib/i18n/locales/en.json
|
|
||||||
|
|
||||||
**Remove API Keys translations:**
|
|
||||||
```json
|
|
||||||
// Find and remove entire "apiKeys" object under "settings.integrations"
|
|
||||||
"apiKeys": "API Keys",
|
|
||||||
"apiKeysDesc": "Optional custom API credentials...",
|
|
||||||
// ... etc
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 11. src/lib/i18n/locales/es.json
|
|
||||||
|
|
||||||
**Remove API Keys translations:**
|
|
||||||
```json
|
|
||||||
// Find and remove entire "apiKeys" object under "settings.integrations"
|
|
||||||
"apiKeys": "Claves API",
|
|
||||||
"apiKeysDesc": "Credenciales API personalizadas opcionales...",
|
|
||||||
// ... etc
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification Steps
|
|
||||||
|
|
||||||
After cleanup:
|
|
||||||
|
|
||||||
1. **Build check:**
|
|
||||||
```bash
|
|
||||||
cargo check
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Search for remnants:**
|
|
||||||
```bash
|
|
||||||
# Should return nothing
|
|
||||||
grep -r "api_keys" src-tauri/src/ --include="*.rs"
|
|
||||||
grep -r "ApiKeysState" src-tauri/src/ --include="*.rs"
|
|
||||||
grep -r "apiKeys" src/lib/ --include="*.svelte" --include="*.ts"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Test that app builds:**
|
|
||||||
```bash
|
|
||||||
npm run tauri build
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Verify Settings UI:**
|
|
||||||
- Open Settings
|
|
||||||
- "API Keys" section should be gone
|
|
||||||
- Audio settings should be intact
|
|
||||||
- No console errors
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Size Reduction Estimate
|
|
||||||
|
|
||||||
**Code removed:**
|
|
||||||
- `api_keys.rs`: ~175 lines
|
|
||||||
- Command registrations: ~10 lines
|
|
||||||
- State management: ~5 lines
|
|
||||||
- Settings UI: ~150 lines HTML + ~200 lines CSS
|
|
||||||
- Helper functions: ~120 lines
|
|
||||||
- Translations: ~40 lines
|
|
||||||
- Import cleanup: ~15 lines
|
|
||||||
|
|
||||||
**Total: ~715 lines of unused code removed**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Git Commit Message Template
|
|
||||||
|
|
||||||
```
|
|
||||||
Remove unused API Keys UI and infrastructure
|
|
||||||
|
|
||||||
All API requests now go through Cloudflare Workers proxy.
|
|
||||||
User-provided credentials were collected but never used.
|
|
||||||
|
|
||||||
Removed:
|
|
||||||
- src-tauri/src/api_keys.rs (entire module)
|
|
||||||
- API Keys section from Settings UI
|
|
||||||
- ApiKeysState and related commands
|
|
||||||
- User credential parameters from providers
|
|
||||||
- Simplified DiscogsClient (removed with_user_credentials)
|
|
||||||
- Cleaned up imports and translations
|
|
||||||
|
|
||||||
This cleanup reduces codebase by ~715 lines and removes
|
|
||||||
confusing UI that promised functionality that didn't work.
|
|
||||||
|
|
||||||
See: POST-MERGE-CLEANUP.md for complete removal instructions
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- **Do this AFTER** main branch audio configuration changes are merged
|
|
||||||
- **Do this in a separate commit** for clean history
|
|
||||||
- **Test thoroughly** after removal
|
|
||||||
- If proxy goes down in future, this can be reverted and implemented properly
|
|
||||||
- Keep this file in repo as documentation of what was removed and why
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Created:** January 17, 2026
|
|
||||||
**Status:** Pending execution post-merge
|
|
||||||
**Estimated time:** 30-45 minutes
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
//! User-provided API credentials store
|
|
||||||
//!
|
|
||||||
//! Manages runtime credentials that override embedded (build-time) credentials.
|
|
||||||
//! Users can provide their own API keys if the embedded ones stop working.
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
/// Credentials for a specific provider
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub struct ProviderCredentials {
|
|
||||||
pub client_id: Option<String>,
|
|
||||||
pub client_secret: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProviderCredentials {
|
|
||||||
pub fn is_set(&self) -> bool {
|
|
||||||
self.client_id.is_some() && self.client_secret.is_some()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Store for all user-provided API credentials
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct ApiKeysStore {
|
|
||||||
pub spotify: ProviderCredentials,
|
|
||||||
pub tidal: ProviderCredentials,
|
|
||||||
pub discogs: ProviderCredentials,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ApiKeysStore {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Thread-safe wrapper for the API keys store
|
|
||||||
pub type ApiKeysState = Arc<Mutex<ApiKeysStore>>;
|
|
||||||
|
|
||||||
pub fn create_api_keys_state() -> ApiKeysState {
|
|
||||||
Arc::new(Mutex::new(ApiKeysStore::new()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tauri commands
|
|
||||||
|
|
||||||
/// Set Spotify credentials
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn set_spotify_credentials(
|
|
||||||
client_id: String,
|
|
||||||
client_secret: String,
|
|
||||||
state: tauri::State<'_, ApiKeysState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
log::info!("Setting user-provided Spotify credentials");
|
|
||||||
let mut store = state.lock().await;
|
|
||||||
store.spotify = ProviderCredentials {
|
|
||||||
client_id: Some(client_id),
|
|
||||||
client_secret: Some(client_secret),
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear Spotify credentials
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn clear_spotify_credentials(
|
|
||||||
state: tauri::State<'_, ApiKeysState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
log::info!("Clearing user-provided Spotify credentials");
|
|
||||||
let mut store = state.lock().await;
|
|
||||||
store.spotify = ProviderCredentials::default();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if Spotify has user-provided credentials
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn has_spotify_user_credentials(
|
|
||||||
state: tauri::State<'_, ApiKeysState>,
|
|
||||||
) -> Result<bool, String> {
|
|
||||||
let store = state.lock().await;
|
|
||||||
Ok(store.spotify.is_set())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set Tidal credentials
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn set_tidal_credentials(
|
|
||||||
client_id: String,
|
|
||||||
client_secret: String,
|
|
||||||
state: tauri::State<'_, ApiKeysState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
log::info!("Setting user-provided Tidal credentials");
|
|
||||||
let mut store = state.lock().await;
|
|
||||||
store.tidal = ProviderCredentials {
|
|
||||||
client_id: Some(client_id),
|
|
||||||
client_secret: Some(client_secret),
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear Tidal credentials
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn clear_tidal_credentials(
|
|
||||||
state: tauri::State<'_, ApiKeysState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
log::info!("Clearing user-provided Tidal credentials");
|
|
||||||
let mut store = state.lock().await;
|
|
||||||
store.tidal = ProviderCredentials::default();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if Tidal has user-provided credentials
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn has_tidal_user_credentials(
|
|
||||||
state: tauri::State<'_, ApiKeysState>,
|
|
||||||
) -> Result<bool, String> {
|
|
||||||
let store = state.lock().await;
|
|
||||||
Ok(store.tidal.is_set())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set Discogs credentials
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn set_discogs_credentials(
|
|
||||||
consumer_key: String,
|
|
||||||
consumer_secret: String,
|
|
||||||
state: tauri::State<'_, ApiKeysState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
log::info!("Setting user-provided Discogs credentials");
|
|
||||||
let mut store = state.lock().await;
|
|
||||||
store.discogs = ProviderCredentials {
|
|
||||||
client_id: Some(consumer_key),
|
|
||||||
client_secret: Some(consumer_secret),
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear Discogs credentials
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn clear_discogs_credentials(
|
|
||||||
state: tauri::State<'_, ApiKeysState>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
log::info!("Clearing user-provided Discogs credentials");
|
|
||||||
let mut store = state.lock().await;
|
|
||||||
store.discogs = ProviderCredentials::default();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if Discogs has user-provided credentials
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn has_discogs_user_credentials(
|
|
||||||
state: tauri::State<'_, ApiKeysState>,
|
|
||||||
) -> Result<bool, String> {
|
|
||||||
let store = state.lock().await;
|
|
||||||
Ok(store.discogs.is_set())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check which providers have embedded credentials available
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn get_embedded_credentials_status() -> EmbeddedCredentialsStatus {
|
|
||||||
EmbeddedCredentialsStatus {
|
|
||||||
spotify: option_env!("SPOTIFY_API_CLIENT_ID").is_some()
|
|
||||||
&& option_env!("SPOTIFY_API_CLIENT_SECRET").is_some(),
|
|
||||||
tidal: option_env!("TIDAL_API_CLIENT_ID").is_some()
|
|
||||||
&& option_env!("TIDAL_API_CLIENT_SECRET").is_some(),
|
|
||||||
discogs: option_env!("DISCOGS_API_CLIENT_KEY").is_some()
|
|
||||||
&& option_env!("DISCOGS_API_CLIENT_SECRET").is_some(),
|
|
||||||
lastfm: option_env!("LAST_FM_API_KEY").is_some()
|
|
||||||
&& option_env!("LAST_FM_API_SHARED_SECRET").is_some(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
|
||||||
pub struct EmbeddedCredentialsStatus {
|
|
||||||
pub spotify: bool,
|
|
||||||
pub tidal: bool,
|
|
||||||
pub discogs: bool,
|
|
||||||
pub lastfm: bool,
|
|
||||||
}
|
|
||||||
@@ -2,41 +2,18 @@
|
|||||||
|
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
use crate::api_keys::ApiKeysState;
|
|
||||||
use crate::playlist_import::{
|
use crate::playlist_import::{
|
||||||
import_public_playlist, preview_public_playlist, ImportPlaylist, ImportSummary,
|
import_public_playlist, preview_public_playlist, ImportPlaylist, ImportSummary,
|
||||||
ProviderCredentials,
|
|
||||||
};
|
};
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn playlist_import_preview(
|
pub async fn playlist_import_preview(
|
||||||
url: String,
|
url: String,
|
||||||
api_keys: State<'_, ApiKeysState>,
|
|
||||||
) -> Result<ImportPlaylist, String> {
|
) -> Result<ImportPlaylist, String> {
|
||||||
log::info!("Command: playlist_import_preview {}", url);
|
log::info!("Command: playlist_import_preview {}", url);
|
||||||
|
|
||||||
// Get user-provided credentials from state
|
preview_public_playlist(&url, None, None)
|
||||||
let keys = api_keys.lock().await;
|
|
||||||
let spotify_creds = if keys.spotify.is_set() {
|
|
||||||
Some(ProviderCredentials {
|
|
||||||
client_id: keys.spotify.client_id.clone(),
|
|
||||||
client_secret: keys.spotify.client_secret.clone(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let tidal_creds = if keys.tidal.is_set() {
|
|
||||||
Some(ProviderCredentials {
|
|
||||||
client_id: keys.tidal.client_id.clone(),
|
|
||||||
client_secret: keys.tidal.client_secret.clone(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
drop(keys);
|
|
||||||
|
|
||||||
preview_public_playlist(&url, spotify_creds, tidal_creds)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
@@ -47,38 +24,17 @@ pub async fn playlist_import_execute(
|
|||||||
name_override: Option<String>,
|
name_override: Option<String>,
|
||||||
is_public: Option<bool>,
|
is_public: Option<bool>,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
api_keys: State<'_, ApiKeysState>,
|
|
||||||
) -> Result<ImportSummary, String> {
|
) -> Result<ImportSummary, String> {
|
||||||
log::info!("Command: playlist_import_execute {}", url);
|
log::info!("Command: playlist_import_execute {}", url);
|
||||||
|
|
||||||
// Get user-provided credentials from state
|
|
||||||
let keys = api_keys.lock().await;
|
|
||||||
let spotify_creds = if keys.spotify.is_set() {
|
|
||||||
Some(ProviderCredentials {
|
|
||||||
client_id: keys.spotify.client_id.clone(),
|
|
||||||
client_secret: keys.spotify.client_secret.clone(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let tidal_creds = if keys.tidal.is_set() {
|
|
||||||
Some(ProviderCredentials {
|
|
||||||
client_id: keys.tidal.client_id.clone(),
|
|
||||||
client_secret: keys.tidal.client_secret.clone(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
drop(keys);
|
|
||||||
|
|
||||||
let client = state.client.lock().await;
|
let client = state.client.lock().await;
|
||||||
import_public_playlist(
|
import_public_playlist(
|
||||||
&url,
|
&url,
|
||||||
&client,
|
&client,
|
||||||
name_override.as_deref(),
|
name_override.as_deref(),
|
||||||
is_public.unwrap_or(false),
|
is_public.unwrap_or(false),
|
||||||
spotify_creds,
|
None,
|
||||||
tidal_creds,
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
|
|||||||
@@ -66,14 +66,6 @@ struct ReleaseImage {
|
|||||||
impl DiscogsClient {
|
impl DiscogsClient {
|
||||||
/// Create a new Discogs client (proxy handles credentials)
|
/// Create a new Discogs client (proxy handles credentials)
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::with_user_credentials(None, None)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new Discogs client - compatibility method (proxy handles credentials)
|
|
||||||
pub fn with_user_credentials(
|
|
||||||
_user_key: Option<String>,
|
|
||||||
_user_secret: Option<String>,
|
|
||||||
) -> Self {
|
|
||||||
let mut headers = reqwest::header::HeaderMap::new();
|
let mut headers = reqwest::header::HeaderMap::new();
|
||||||
headers.insert(
|
headers.insert(
|
||||||
reqwest::header::USER_AGENT,
|
reqwest::header::USER_AGENT,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod api_cache;
|
pub mod api_cache;
|
||||||
pub mod api_keys;
|
|
||||||
pub mod audio;
|
pub mod audio;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod cast;
|
pub mod cast;
|
||||||
@@ -166,8 +165,6 @@ pub fn run() {
|
|||||||
// Initialize download settings state
|
// Initialize download settings state
|
||||||
let download_settings_state = config::download_settings::create_download_settings_state()
|
let download_settings_state = config::download_settings::create_download_settings_state()
|
||||||
.expect("Failed to initialize download settings");
|
.expect("Failed to initialize download settings");
|
||||||
// Initialize API keys state (for user-provided credentials)
|
|
||||||
let api_keys_state = api_keys::create_api_keys_state();
|
|
||||||
// Initialize offline mode state
|
// Initialize offline mode state
|
||||||
let offline_state = offline::OfflineState::new()
|
let offline_state = offline::OfflineState::new()
|
||||||
.expect("Failed to initialize offline state");
|
.expect("Failed to initialize offline state");
|
||||||
@@ -294,7 +291,6 @@ pub fn run() {
|
|||||||
.manage(session_store_state)
|
.manage(session_store_state)
|
||||||
.manage(audio_settings_state)
|
.manage(audio_settings_state)
|
||||||
.manage(download_settings_state)
|
.manage(download_settings_state)
|
||||||
.manage(api_keys_state)
|
|
||||||
.manage(offline_state)
|
.manage(offline_state)
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
// Auth commands
|
// Auth commands
|
||||||
@@ -558,17 +554,6 @@ pub fn run() {
|
|||||||
config::download_settings::set_download_root,
|
config::download_settings::set_download_root,
|
||||||
config::download_settings::set_show_downloads_in_library,
|
config::download_settings::set_show_downloads_in_library,
|
||||||
config::download_settings::validate_download_root,
|
config::download_settings::validate_download_root,
|
||||||
// API keys commands (user-provided credentials)
|
|
||||||
api_keys::set_spotify_credentials,
|
|
||||||
api_keys::clear_spotify_credentials,
|
|
||||||
api_keys::has_spotify_user_credentials,
|
|
||||||
api_keys::set_tidal_credentials,
|
|
||||||
api_keys::clear_tidal_credentials,
|
|
||||||
api_keys::has_tidal_user_credentials,
|
|
||||||
api_keys::set_discogs_credentials,
|
|
||||||
api_keys::clear_discogs_credentials,
|
|
||||||
api_keys::has_discogs_user_credentials,
|
|
||||||
api_keys::get_embedded_credentials_status,
|
|
||||||
// Offline mode commands
|
// Offline mode commands
|
||||||
offline::commands::get_offline_status,
|
offline::commands::get_offline_status,
|
||||||
offline::commands::get_offline_settings,
|
offline::commands::get_offline_settings,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use std::sync::Arc;
|
|||||||
use tauri::State;
|
use tauri::State;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::api_keys::ApiKeysState;
|
|
||||||
use crate::discogs::DiscogsClient;
|
use crate::discogs::DiscogsClient;
|
||||||
use crate::library::{
|
use crate::library::{
|
||||||
cue_to_tracks, get_artwork_cache_dir, CueParser, LibraryDatabase, LibraryFolder, LibraryScanner, LibraryStats,
|
cue_to_tracks, get_artwork_cache_dir, CueParser, LibraryDatabase, LibraryFolder, LibraryScanner, LibraryStats,
|
||||||
@@ -1188,21 +1187,11 @@ pub async fn playlist_increment_play_count(
|
|||||||
|
|
||||||
// === Discogs Artwork ===
|
// === Discogs Artwork ===
|
||||||
|
|
||||||
/// Check if Discogs credentials are configured (embedded or user-provided)
|
/// Check if Discogs credentials are configured (proxy handles credentials)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn discogs_has_credentials(
|
pub async fn discogs_has_credentials() -> Result<bool, String> {
|
||||||
api_keys: State<'_, ApiKeysState>,
|
// Proxy always provides credentials
|
||||||
) -> Result<bool, String> {
|
Ok(true)
|
||||||
// Check user-provided credentials first
|
|
||||||
let keys = api_keys.lock().await;
|
|
||||||
if keys.discogs.is_set() {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
drop(keys);
|
|
||||||
|
|
||||||
// Fall back to embedded/env credentials
|
|
||||||
let client = DiscogsClient::new();
|
|
||||||
Ok(client.has_credentials())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch missing artwork from Discogs for albums without artwork
|
/// Fetch missing artwork from Discogs for albums without artwork
|
||||||
@@ -1210,21 +1199,11 @@ pub async fn discogs_has_credentials(
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn library_fetch_missing_artwork(
|
pub async fn library_fetch_missing_artwork(
|
||||||
state: State<'_, LibraryState>,
|
state: State<'_, LibraryState>,
|
||||||
api_keys: State<'_, ApiKeysState>,
|
|
||||||
) -> Result<u32, String> {
|
) -> Result<u32, String> {
|
||||||
log::info!("Command: library_fetch_missing_artwork");
|
log::info!("Command: library_fetch_missing_artwork");
|
||||||
|
|
||||||
// Get user-provided credentials if available
|
// Get Discogs client (proxy handles credentials)
|
||||||
let keys = api_keys.lock().await;
|
let discogs = DiscogsClient::new();
|
||||||
let discogs = DiscogsClient::with_user_credentials(
|
|
||||||
keys.discogs.client_id.clone(),
|
|
||||||
keys.discogs.client_secret.clone(),
|
|
||||||
);
|
|
||||||
drop(keys);
|
|
||||||
|
|
||||||
if !discogs.has_credentials() {
|
|
||||||
return Err("Discogs credentials not configured".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let artwork_cache = get_artwork_cache_dir();
|
let artwork_cache = get_artwork_cache_dir();
|
||||||
let mut updated_count = 0u32;
|
let mut updated_count = 0u32;
|
||||||
@@ -1263,21 +1242,11 @@ pub async fn library_fetch_album_artwork(
|
|||||||
artist: String,
|
artist: String,
|
||||||
album: String,
|
album: String,
|
||||||
state: State<'_, LibraryState>,
|
state: State<'_, LibraryState>,
|
||||||
api_keys: State<'_, ApiKeysState>,
|
|
||||||
) -> Result<Option<String>, String> {
|
) -> Result<Option<String>, String> {
|
||||||
log::info!("Command: library_fetch_album_artwork {} - {}", artist, album);
|
log::info!("Command: library_fetch_album_artwork {} - {}", artist, album);
|
||||||
|
|
||||||
// Get user-provided credentials if available
|
// Get Discogs client (proxy handles credentials)
|
||||||
let keys = api_keys.lock().await;
|
let discogs = DiscogsClient::new();
|
||||||
let discogs = DiscogsClient::with_user_credentials(
|
|
||||||
keys.discogs.client_id.clone(),
|
|
||||||
keys.discogs.client_secret.clone(),
|
|
||||||
);
|
|
||||||
drop(keys);
|
|
||||||
|
|
||||||
if !discogs.has_credentials() {
|
|
||||||
return Err("Discogs credentials not configured".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let artwork_cache = get_artwork_cache_dir();
|
let artwork_cache = get_artwork_cache_dir();
|
||||||
|
|
||||||
@@ -1335,21 +1304,11 @@ pub async fn library_set_album_artwork(
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn discogs_search_artist(
|
pub async fn discogs_search_artist(
|
||||||
query: String,
|
query: String,
|
||||||
api_keys: State<'_, ApiKeysState>,
|
|
||||||
) -> Result<crate::discogs::SearchResponse, String> {
|
) -> Result<crate::discogs::SearchResponse, String> {
|
||||||
log::info!("Command: discogs_search_artist query={}", query);
|
log::info!("Command: discogs_search_artist query={}", query);
|
||||||
|
|
||||||
// Get user-provided credentials if available
|
// Get Discogs client (proxy handles credentials)
|
||||||
let keys = api_keys.lock().await;
|
let discogs = DiscogsClient::new();
|
||||||
let discogs = DiscogsClient::with_user_credentials(
|
|
||||||
keys.discogs.client_id.clone(),
|
|
||||||
keys.discogs.client_secret.clone(),
|
|
||||||
);
|
|
||||||
drop(keys);
|
|
||||||
|
|
||||||
if !discogs.has_credentials() {
|
|
||||||
return Err("Discogs credentials not configured".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
discogs.search_artist(&query).await
|
discogs.search_artist(&query).await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,21 +40,19 @@ pub fn detect_provider(url: &str) -> Result<ProviderKind, PlaylistImportError> {
|
|||||||
Err(PlaylistImportError::UnsupportedProvider(url.to_string()))
|
Err(PlaylistImportError::UnsupportedProvider(url.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch playlist with optional user-provided credentials
|
/// Fetch playlist (proxy handles credentials)
|
||||||
pub async fn fetch_playlist(
|
pub async fn fetch_playlist(
|
||||||
kind: ProviderKind,
|
kind: ProviderKind,
|
||||||
spotify_creds: Option<ProviderCredentials>,
|
|
||||||
tidal_creds: Option<ProviderCredentials>,
|
|
||||||
) -> Result<ImportPlaylist, PlaylistImportError> {
|
) -> Result<ImportPlaylist, PlaylistImportError> {
|
||||||
match kind {
|
match kind {
|
||||||
ProviderKind::Spotify { playlist_id } => {
|
ProviderKind::Spotify { playlist_id } => {
|
||||||
spotify::fetch_playlist(&playlist_id, spotify_creds).await
|
spotify::fetch_playlist(&playlist_id).await
|
||||||
}
|
}
|
||||||
ProviderKind::AppleMusic { storefront, playlist_id } => {
|
ProviderKind::AppleMusic { storefront, playlist_id } => {
|
||||||
apple::fetch_playlist(&storefront, &playlist_id).await
|
apple::fetch_playlist(&storefront, &playlist_id).await
|
||||||
}
|
}
|
||||||
ProviderKind::Tidal { playlist_id } => {
|
ProviderKind::Tidal { playlist_id } => {
|
||||||
tidal::fetch_playlist(&playlist_id, tidal_creds).await
|
tidal::fetch_playlist(&playlist_id).await
|
||||||
}
|
}
|
||||||
ProviderKind::Deezer { playlist_id } => deezer::fetch_playlist(&playlist_id).await,
|
ProviderKind::Deezer { playlist_id } => deezer::fetch_playlist(&playlist_id).await,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,8 @@ pub fn parse_playlist_id(url: &str) -> Option<String> {
|
|||||||
|
|
||||||
pub async fn fetch_playlist(
|
pub async fn fetch_playlist(
|
||||||
playlist_id: &str,
|
playlist_id: &str,
|
||||||
user_creds: Option<ProviderCredentials>,
|
|
||||||
) -> Result<ImportPlaylist, PlaylistImportError> {
|
) -> Result<ImportPlaylist, PlaylistImportError> {
|
||||||
if let Ok(token) = get_app_token(user_creds).await {
|
if let Ok(token) = get_app_token().await {
|
||||||
if let Ok(playlist) = fetch_playlist_with_token(playlist_id, &token).await {
|
if let Ok(playlist) = fetch_playlist_with_token(playlist_id, &token).await {
|
||||||
return Ok(playlist);
|
return Ok(playlist);
|
||||||
}
|
}
|
||||||
@@ -249,8 +248,8 @@ async fn fetch_playlist_from_embed(playlist_id: &str) -> Result<ImportPlaylist,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_app_token(_user_creds: Option<ProviderCredentials>) -> Result<String, PlaylistImportError> {
|
async fn get_app_token() -> Result<String, PlaylistImportError> {
|
||||||
// Proxy handles credentials - user_creds ignored (compatibility)
|
// Proxy handles credentials
|
||||||
let url = format!("{}/token", SPOTIFY_PROXY_URL);
|
let url = format!("{}/token", SPOTIFY_PROXY_URL);
|
||||||
|
|
||||||
let response: Value = reqwest::Client::builder()
|
let response: Value = reqwest::Client::builder()
|
||||||
|
|||||||
@@ -39,9 +39,8 @@ pub fn parse_playlist_id(url: &str) -> Option<String> {
|
|||||||
|
|
||||||
pub async fn fetch_playlist(
|
pub async fn fetch_playlist(
|
||||||
playlist_id: &str,
|
playlist_id: &str,
|
||||||
user_creds: Option<ProviderCredentials>,
|
|
||||||
) -> Result<ImportPlaylist, PlaylistImportError> {
|
) -> Result<ImportPlaylist, PlaylistImportError> {
|
||||||
let token = get_app_token(user_creds).await?;
|
let token = get_app_token().await?;
|
||||||
let country_code = env::var("TIDAL_COUNTRY_CODE").unwrap_or_else(|_| "US".to_string());
|
let country_code = env::var("TIDAL_COUNTRY_CODE").unwrap_or_else(|_| "US".to_string());
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
@@ -331,8 +330,8 @@ fn parse_duration_ms(value: &str) -> Option<u64> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_app_token(_user_creds: Option<ProviderCredentials>) -> Result<String, PlaylistImportError> {
|
async fn get_app_token() -> Result<String, PlaylistImportError> {
|
||||||
// Proxy handles credentials - user_creds ignored (compatibility)
|
// Proxy handles credentials
|
||||||
let url = format!("{}/token", TIDAL_PROXY_URL);
|
let url = format!("{}/token", TIDAL_PROXY_URL);
|
||||||
|
|
||||||
let response: Value = reqwest::Client::builder()
|
let response: Value = reqwest::Client::builder()
|
||||||
|
|||||||
@@ -104,7 +104,6 @@
|
|||||||
let integrationsSection: HTMLElement;
|
let integrationsSection: HTMLElement;
|
||||||
let storageSection: HTMLElement;
|
let storageSection: HTMLElement;
|
||||||
let lyricsSection: HTMLElement;
|
let lyricsSection: HTMLElement;
|
||||||
let apiKeysSection: HTMLElement;
|
|
||||||
let activeSection = $state('audio');
|
let activeSection = $state('audio');
|
||||||
|
|
||||||
// Navigation section definitions (static, refs resolved at click/scroll time)
|
// Navigation section definitions (static, refs resolved at click/scroll time)
|
||||||
@@ -118,7 +117,6 @@
|
|||||||
{ id: 'integrations', label: 'Integrations' },
|
{ id: 'integrations', label: 'Integrations' },
|
||||||
{ id: 'storage', label: 'Storage' },
|
{ id: 'storage', label: 'Storage' },
|
||||||
{ id: 'lyrics', label: 'Lyrics' },
|
{ id: 'lyrics', label: 'Lyrics' },
|
||||||
{ id: 'api-keys', label: 'API Keys' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Get section element by id (resolved at call time, not definition time)
|
// Get section element by id (resolved at call time, not definition time)
|
||||||
@@ -133,7 +131,6 @@
|
|||||||
case 'integrations': return integrationsSection;
|
case 'integrations': return integrationsSection;
|
||||||
case 'storage': return storageSection;
|
case 'storage': return storageSection;
|
||||||
case 'lyrics': return lyricsSection;
|
case 'lyrics': return lyricsSection;
|
||||||
case 'api-keys': return apiKeysSection;
|
|
||||||
default: return undefined;
|
default: return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,17 +264,6 @@
|
|||||||
let showLastfmConfig = $state(false);
|
let showLastfmConfig = $state(false);
|
||||||
let hasEmbeddedCredentials = $state(false);
|
let hasEmbeddedCredentials = $state(false);
|
||||||
|
|
||||||
// API Keys section state
|
|
||||||
let apiKeysExpanded = $state(false);
|
|
||||||
let spotifyClientId = $state('');
|
|
||||||
let spotifyClientSecret = $state('');
|
|
||||||
let tidalClientId = $state('');
|
|
||||||
let tidalClientSecret = $state('');
|
|
||||||
let discogsConsumerKey = $state('');
|
|
||||||
let discogsConsumerSecret = $state('');
|
|
||||||
let embeddedStatus = $state({ spotify: false, tidal: false, discogs: false, lastfm: false });
|
|
||||||
let apiKeysSaving = $state(false);
|
|
||||||
|
|
||||||
// Load saved settings on mount
|
// Load saved settings on mount
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Load theme
|
// Load theme
|
||||||
@@ -339,9 +325,6 @@
|
|||||||
// Load Last.fm state
|
// Load Last.fm state
|
||||||
loadLastfmState();
|
loadLastfmState();
|
||||||
|
|
||||||
// Load API keys state
|
|
||||||
loadApiKeysState();
|
|
||||||
|
|
||||||
// Load notification preferences
|
// Load notification preferences
|
||||||
loadToastsPreference();
|
loadToastsPreference();
|
||||||
toastsEnabled = getToastsEnabled();
|
toastsEnabled = getToastsEnabled();
|
||||||
@@ -513,76 +496,6 @@
|
|||||||
localStorage.setItem('qbz-lastfm-scrobbling', String(enabled));
|
localStorage.setItem('qbz-lastfm-scrobbling', String(enabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
// API Keys management
|
|
||||||
async function loadApiKeysState() {
|
|
||||||
try {
|
|
||||||
// Get embedded credentials status from backend
|
|
||||||
embeddedStatus = await invoke<typeof embeddedStatus>('get_embedded_credentials_status');
|
|
||||||
|
|
||||||
// Load user-provided credentials from localStorage
|
|
||||||
spotifyClientId = localStorage.getItem('qbz-spotify-client-id') || '';
|
|
||||||
spotifyClientSecret = localStorage.getItem('qbz-spotify-client-secret') || '';
|
|
||||||
tidalClientId = localStorage.getItem('qbz-tidal-client-id') || '';
|
|
||||||
tidalClientSecret = localStorage.getItem('qbz-tidal-client-secret') || '';
|
|
||||||
discogsConsumerKey = localStorage.getItem('qbz-discogs-consumer-key') || '';
|
|
||||||
discogsConsumerSecret = localStorage.getItem('qbz-discogs-consumer-secret') || '';
|
|
||||||
|
|
||||||
// Restore credentials to backend state if we have saved ones
|
|
||||||
if (spotifyClientId && spotifyClientSecret) {
|
|
||||||
await invoke('set_spotify_credentials', {
|
|
||||||
clientId: spotifyClientId,
|
|
||||||
clientSecret: spotifyClientSecret
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (tidalClientId && tidalClientSecret) {
|
|
||||||
await invoke('set_tidal_credentials', {
|
|
||||||
clientId: tidalClientId,
|
|
||||||
clientSecret: tidalClientSecret
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (discogsConsumerKey && discogsConsumerSecret) {
|
|
||||||
await invoke('set_discogs_credentials', {
|
|
||||||
consumerKey: discogsConsumerKey,
|
|
||||||
consumerSecret: discogsConsumerSecret
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load API keys state:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSaveSpotifyCredentials() {
|
|
||||||
if (!spotifyClientId || !spotifyClientSecret) return;
|
|
||||||
apiKeysSaving = true;
|
|
||||||
try {
|
|
||||||
await invoke('set_spotify_credentials', {
|
|
||||||
clientId: spotifyClientId,
|
|
||||||
clientSecret: spotifyClientSecret
|
|
||||||
});
|
|
||||||
localStorage.setItem('qbz-spotify-client-id', spotifyClientId);
|
|
||||||
localStorage.setItem('qbz-spotify-client-secret', spotifyClientSecret);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to save Spotify credentials:', err);
|
|
||||||
} finally {
|
|
||||||
apiKeysSaving = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleClearSpotifyCredentials() {
|
|
||||||
apiKeysSaving = true;
|
|
||||||
try {
|
|
||||||
await invoke('clear_spotify_credentials');
|
|
||||||
localStorage.removeItem('qbz-spotify-client-id');
|
|
||||||
localStorage.removeItem('qbz-spotify-client-secret');
|
|
||||||
spotifyClientId = '';
|
|
||||||
spotifyClientSecret = '';
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to clear Spotify credentials:', err);
|
|
||||||
} finally {
|
|
||||||
apiKeysSaving = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleShowDownloadsChange(enabled: boolean) {
|
async function handleShowDownloadsChange(enabled: boolean) {
|
||||||
try {
|
try {
|
||||||
await invoke('set_show_downloads_in_library', { show: enabled });
|
await invoke('set_show_downloads_in_library', { show: enabled });
|
||||||
@@ -592,79 +505,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveTidalCredentials() {
|
|
||||||
if (!tidalClientId || !tidalClientSecret) return;
|
|
||||||
apiKeysSaving = true;
|
|
||||||
try {
|
|
||||||
await invoke('set_tidal_credentials', {
|
|
||||||
clientId: tidalClientId,
|
|
||||||
clientSecret: tidalClientSecret
|
|
||||||
});
|
|
||||||
localStorage.setItem('qbz-tidal-client-id', tidalClientId);
|
|
||||||
localStorage.setItem('qbz-tidal-client-secret', tidalClientSecret);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to save Tidal credentials:', err);
|
|
||||||
} finally {
|
|
||||||
apiKeysSaving = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleClearTidalCredentials() {
|
|
||||||
apiKeysSaving = true;
|
|
||||||
try {
|
|
||||||
await invoke('clear_tidal_credentials');
|
|
||||||
localStorage.removeItem('qbz-tidal-client-id');
|
|
||||||
localStorage.removeItem('qbz-tidal-client-secret');
|
|
||||||
tidalClientId = '';
|
|
||||||
tidalClientSecret = '';
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to clear Tidal credentials:', err);
|
|
||||||
} finally {
|
|
||||||
apiKeysSaving = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSaveDiscogsCredentials() {
|
|
||||||
if (!discogsConsumerKey || !discogsConsumerSecret) return;
|
|
||||||
apiKeysSaving = true;
|
|
||||||
try {
|
|
||||||
await invoke('set_discogs_credentials', {
|
|
||||||
consumerKey: discogsConsumerKey,
|
|
||||||
consumerSecret: discogsConsumerSecret
|
|
||||||
});
|
|
||||||
localStorage.setItem('qbz-discogs-consumer-key', discogsConsumerKey);
|
|
||||||
localStorage.setItem('qbz-discogs-consumer-secret', discogsConsumerSecret);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to save Discogs credentials:', err);
|
|
||||||
} finally {
|
|
||||||
apiKeysSaving = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleClearDiscogsCredentials() {
|
|
||||||
apiKeysSaving = true;
|
|
||||||
try {
|
|
||||||
await invoke('clear_discogs_credentials');
|
|
||||||
localStorage.removeItem('qbz-discogs-consumer-key');
|
|
||||||
localStorage.removeItem('qbz-discogs-consumer-secret');
|
|
||||||
discogsConsumerKey = '';
|
|
||||||
discogsConsumerSecret = '';
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to clear Discogs credentials:', err);
|
|
||||||
} finally {
|
|
||||||
apiKeysSaving = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if any user credentials are configured
|
|
||||||
function hasAnyUserCredentials(): boolean {
|
|
||||||
return !!(
|
|
||||||
(spotifyClientId && spotifyClientSecret) ||
|
|
||||||
(tidalClientId && tidalClientSecret) ||
|
|
||||||
(discogsConsumerKey && discogsConsumerSecret)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleQualityChange(quality: string) {
|
function handleQualityChange(quality: string) {
|
||||||
streamingQuality = quality;
|
streamingQuality = quality;
|
||||||
localStorage.setItem('qbz-streaming-quality', quality);
|
localStorage.setItem('qbz-streaming-quality', quality);
|
||||||
@@ -1752,177 +1592,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- API Keys Section (collapsible) -->
|
|
||||||
<section class="section api-keys-section" bind:this={apiKeysSection}>
|
|
||||||
<button
|
|
||||||
class="section-title-btn"
|
|
||||||
onclick={() => apiKeysExpanded = !apiKeysExpanded}
|
|
||||||
>
|
|
||||||
{#if apiKeysExpanded}
|
|
||||||
<ChevronDown size={18} />
|
|
||||||
{:else}
|
|
||||||
<ChevronRight size={18} />
|
|
||||||
{/if}
|
|
||||||
<span>{$t('settings.integrations.apiKeys')}</span>
|
|
||||||
{#if hasAnyUserCredentials()}
|
|
||||||
<span class="keys-badge">Custom</span>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if apiKeysExpanded}
|
|
||||||
<div class="api-keys-info">
|
|
||||||
<p>
|
|
||||||
You don't need to configure anything here unless playlist import or album artwork features stop working.
|
|
||||||
If you experience issues, you can provide your own API credentials to restore functionality.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Spotify -->
|
|
||||||
<div class="api-key-group">
|
|
||||||
<div class="api-key-header">
|
|
||||||
<span class="api-key-title">Spotify</span>
|
|
||||||
{#if embeddedStatus.spotify}
|
|
||||||
<span class="status-badge active">Active</span>
|
|
||||||
{:else if spotifyClientId && spotifyClientSecret}
|
|
||||||
<span class="status-badge custom">Custom</span>
|
|
||||||
{:else}
|
|
||||||
<span class="status-badge inactive">Not configured</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<p class="api-key-desc">
|
|
||||||
Used for importing Spotify playlists.
|
|
||||||
<a href="https://developer.spotify.com/dashboard" target="_blank" rel="noopener">Create an app</a> to get credentials.
|
|
||||||
</p>
|
|
||||||
<div class="api-key-fields">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Client ID"
|
|
||||||
bind:value={spotifyClientId}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
placeholder="Client Secret"
|
|
||||||
bind:value={spotifyClientSecret}
|
|
||||||
/>
|
|
||||||
<div class="api-key-actions">
|
|
||||||
<button
|
|
||||||
class="save-btn"
|
|
||||||
onclick={handleSaveSpotifyCredentials}
|
|
||||||
disabled={!spotifyClientId || !spotifyClientSecret || apiKeysSaving}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
{#if spotifyClientId || spotifyClientSecret}
|
|
||||||
<button
|
|
||||||
class="clear-btn-small"
|
|
||||||
onclick={handleClearSpotifyCredentials}
|
|
||||||
disabled={apiKeysSaving}
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tidal -->
|
|
||||||
<div class="api-key-group">
|
|
||||||
<div class="api-key-header">
|
|
||||||
<span class="api-key-title">Tidal</span>
|
|
||||||
{#if embeddedStatus.tidal}
|
|
||||||
<span class="status-badge active">Active</span>
|
|
||||||
{:else if tidalClientId && tidalClientSecret}
|
|
||||||
<span class="status-badge custom">Custom</span>
|
|
||||||
{:else}
|
|
||||||
<span class="status-badge inactive">Not configured</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<p class="api-key-desc">
|
|
||||||
Used for importing Tidal playlists.
|
|
||||||
<a href="https://developer.tidal.com/" target="_blank" rel="noopener">Register</a> to get API credentials.
|
|
||||||
</p>
|
|
||||||
<div class="api-key-fields">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Client ID"
|
|
||||||
bind:value={tidalClientId}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
placeholder="Client Secret"
|
|
||||||
bind:value={tidalClientSecret}
|
|
||||||
/>
|
|
||||||
<div class="api-key-actions">
|
|
||||||
<button
|
|
||||||
class="save-btn"
|
|
||||||
onclick={handleSaveTidalCredentials}
|
|
||||||
disabled={!tidalClientId || !tidalClientSecret || apiKeysSaving}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
{#if tidalClientId || tidalClientSecret}
|
|
||||||
<button
|
|
||||||
class="clear-btn-small"
|
|
||||||
onclick={handleClearTidalCredentials}
|
|
||||||
disabled={apiKeysSaving}
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Discogs -->
|
|
||||||
<div class="api-key-group last">
|
|
||||||
<div class="api-key-header">
|
|
||||||
<span class="api-key-title">Discogs</span>
|
|
||||||
{#if embeddedStatus.discogs}
|
|
||||||
<span class="status-badge active">Active</span>
|
|
||||||
{:else if discogsConsumerKey && discogsConsumerSecret}
|
|
||||||
<span class="status-badge custom">Custom</span>
|
|
||||||
{:else}
|
|
||||||
<span class="status-badge inactive">Not configured</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<p class="api-key-desc">
|
|
||||||
Used for fetching album artwork for local library.
|
|
||||||
<a href="https://www.discogs.com/settings/developers" target="_blank" rel="noopener">Create an application</a> to get credentials.
|
|
||||||
</p>
|
|
||||||
<div class="api-key-fields">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Consumer Key"
|
|
||||||
bind:value={discogsConsumerKey}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
placeholder="Consumer Secret"
|
|
||||||
bind:value={discogsConsumerSecret}
|
|
||||||
/>
|
|
||||||
<div class="api-key-actions">
|
|
||||||
<button
|
|
||||||
class="save-btn"
|
|
||||||
onclick={handleSaveDiscogsCredentials}
|
|
||||||
disabled={!discogsConsumerKey || !discogsConsumerSecret || apiKeysSaving}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
{#if discogsConsumerKey || discogsConsumerSecret}
|
|
||||||
<button
|
|
||||||
class="clear-btn-small"
|
|
||||||
onclick={handleClearDiscogsCredentials}
|
|
||||||
disabled={apiKeysSaving}
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if isCheckingNetwork}
|
{#if isCheckingNetwork}
|
||||||
@@ -2383,188 +2052,6 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* API Keys section styles */
|
|
||||||
.api-keys-section {
|
|
||||||
padding-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
width: 100%;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title-btn:hover {
|
|
||||||
color: var(--accent-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.keys-badge {
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 500;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: var(--accent-primary);
|
|
||||||
color: white;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-keys-info {
|
|
||||||
margin: 16px 0;
|
|
||||||
padding: 12px;
|
|
||||||
background-color: var(--bg-tertiary);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-keys-info p {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-key-group {
|
|
||||||
padding: 16px 0;
|
|
||||||
border-bottom: 1px solid var(--bg-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-key-group.last {
|
|
||||||
border-bottom: none;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-key-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-key-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 500;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge.active {
|
|
||||||
background-color: rgba(29, 185, 84, 0.2);
|
|
||||||
color: #1db954;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge.custom {
|
|
||||||
background-color: rgba(99, 102, 241, 0.2);
|
|
||||||
color: #6366f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge.inactive {
|
|
||||||
background-color: var(--bg-tertiary);
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-key-desc {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin: 0 0 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-key-desc a {
|
|
||||||
color: var(--accent-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-key-desc a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-key-fields {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-key-fields input {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 150px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background-color: var(--bg-tertiary);
|
|
||||||
border: 1px solid var(--border-subtle);
|
|
||||||
border-radius: 6px;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-key-fields input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-key-fields input::placeholder {
|
|
||||||
color: var(--text-disabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-key-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-btn {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 150ms ease;
|
|
||||||
background-color: var(--accent-primary);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-btn:hover:not(:disabled) {
|
|
||||||
background-color: var(--accent-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-btn-small {
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 150ms ease;
|
|
||||||
background: none;
|
|
||||||
border: 1px solid var(--text-muted);
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-btn-small:hover:not(:disabled) {
|
|
||||||
border-color: #ff6b6b;
|
|
||||||
color: #ff6b6b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-btn-small:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Offline status indicator */
|
/* Offline status indicator */
|
||||||
.status-indicator {
|
.status-indicator {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
@@ -345,12 +345,7 @@
|
|||||||
"connect": "Connect",
|
"connect": "Connect",
|
||||||
"disconnect": "Disconnect",
|
"disconnect": "Disconnect",
|
||||||
"scrobbling": "Scrobbling",
|
"scrobbling": "Scrobbling",
|
||||||
"configureApi": "Configure API",
|
"configureApi": "Configure API"
|
||||||
"apiKeys": "API Keys",
|
|
||||||
"clientId": "Client ID",
|
|
||||||
"clientSecret": "Client Secret",
|
|
||||||
"consumerKey": "Consumer Key",
|
|
||||||
"consumerSecret": "Consumer Secret"
|
|
||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
"title": "Storage",
|
"title": "Storage",
|
||||||
|
|||||||
@@ -345,12 +345,7 @@
|
|||||||
"connect": "Conectar",
|
"connect": "Conectar",
|
||||||
"disconnect": "Desconectar",
|
"disconnect": "Desconectar",
|
||||||
"scrobbling": "Scrobbling",
|
"scrobbling": "Scrobbling",
|
||||||
"configureApi": "Configurar API",
|
"configureApi": "Configurar API"
|
||||||
"apiKeys": "Claves API",
|
|
||||||
"clientId": "Client ID",
|
|
||||||
"clientSecret": "Client Secret",
|
|
||||||
"consumerKey": "Consumer Key",
|
|
||||||
"consumerSecret": "Consumer Secret"
|
|
||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
"title": "Almacenamiento",
|
"title": "Almacenamiento",
|
||||||
|
|||||||
Reference in new issue
Block a user