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:
vicrodh committed 2026-01-17 17:09:49 -06:00
1 parent 7631462522
commit cac5a0c173
15 files changed
+24 -2420

No files matched your search

-317
View File
@@ -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**
-505
View File
@@ -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
View File
@@ -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 ✅
-511
View File
@@ -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
-174
View File
@@ -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,
}
+3 -47
View File
@@ -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())
-8
View File
@@ -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,
-15
View File
@@ -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,
+10 -51
View File
@@ -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;
+1 -6
View File
@@ -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",
+1 -6
View File
@@ -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",