7.7 KiB
Spotify Import Feature - Handoff Document
Overview
The Spotify Import feature allows users to import playlists from Spotify into Lidify. It searches for matching tracks on Soulseek (and optionally Lidarr), downloads them, creates a local playlist, and matches downloaded tracks to the playlist.
Current State
What Works
- Spotify Playlist Parsing: Fetches playlist metadata via Spotify embed API
- Soulseek Downloads: Direct P2P downloads with retry logic (tries up to 3 different users)
- Parallel Processing: Searches run in parallel, downloads limited to concurrency of 4
- Track Matching: Multiple matching strategies (exact, fuzzy, contains, startsWith)
- Pending Track System: Tracks that fail to download are stored as "pending" with:
- Deezer preview playback (30s samples)
- Manual retry button
- Remove button
- Retry Functionality: Non-blocking retry - returns immediately, downloads in background
- Reconciliation: After library scan, pending tracks are automatically matched to downloaded files
What Needs Testing
- Lidarr Integration: Download source can be set to "lidarr" but needs end-to-end testing
- Lidarr + Soulseek Fallback: When
downloadSource: "lidarr"andsoulseekFallback: "failed", should try Lidarr first then fall back to Soulseek - Activity Panel Integration: Downloads should show progress in the activity panel
- Edge Cases: Various artist name formats, special characters, live recordings filtering
Architecture
Flow
1. User pastes Spotify playlist URL
2. Frontend calls POST /spotify/preview with URL
3. Backend fetches playlist via Spotify embed API
4. Backend searches MusicBrainz for album MBIDs
5. Preview returned to user showing matched/unmatched tracks
6. User confirms import
7. Frontend calls POST /spotify/import
8. Backend:
a. For each album, either:
- Sends to Lidarr (if enabled)
- Downloads directly via Soulseek
b. Waits for downloads to complete
c. Runs library scan
d. Matches tracks to playlist
e. Creates pending entries for unmatched tracks
9. User sees playlist with matched tracks + failed/pending tracks
Key Files
Backend Routes
-
backend/src/routes/spotify.ts- Main import endpointsPOST /spotify/preview- Parse and preview playlistPOST /spotify/import- Execute import jobGET /spotify/import/:jobId/status- Check job status
-
backend/src/routes/playlists.ts- Playlist management + pending track handlingGET /playlists/:id/pending/:trackId/preview- Get fresh Deezer preview URLPOST /playlists/:id/pending/:trackId/retry- Retry downloading a failed trackDELETE /playlists/:id/pending/:trackId- Remove pending track from playlistPOST /playlists/:id/pending/reconcile- Manually trigger reconciliation
Backend Services
-
backend/src/services/spotifyImport.ts- Core import logicpreviewPlaylist()- Parse Spotify URL and match to MusicBrainzexecuteImport()- Run the full import jobreconcilePendingTracks()- Match pending tracks to library after scan
-
backend/src/services/soulseek.ts- Direct Soulseek P2P clientsearchTrack()- Search for a track (15s timeout)downloadTrack()- Download a single filesearchAndDownload()- Search + download with retrysearchAndDownloadBatch()- Parallel search, concurrent downloaddownloadBestMatch()- Download from pre-searched results (used by retry)
-
backend/src/services/lidarr.ts- Lidarr integrationsearchAlbum()- Search for album by MBIDaddAlbum()- Add album to Lidarr for downloadgetDownloadQueue()- Check download progress
-
backend/src/services/deezer.ts- Deezer API for previewsgetTrackPreview()- Get 30s preview URL for a track
-
backend/src/services/musicbrainz.ts- MusicBrainz lookupssearchRecordingByISRC()- Find recording by ISRCsearchRecording()- Search by artist/titlegetReleaseDetails()- Get album details
Frontend
frontend/app/import/spotify/page.tsx- Import wizard UIfrontend/app/playlist/[id]/page.tsx- Playlist detail with pending track handlingfrontend/lib/api.ts- API client methods
Database Schema (relevant tables)
model Playlist {
id String @id @default(cuid())
name String
userId String
isPublic Boolean @default(false)
spotifyUrl String? // Original Spotify URL
items PlaylistItem[]
pendingTracks PlaylistPendingTrack[]
}
model PlaylistPendingTrack {
id String @id @default(cuid())
playlistId String
spotifyArtist String
spotifyTitle String
spotifyAlbum String
spotifyTrackId String?
deezerPreviewUrl String?
sort Int
createdAt DateTime @default(now())
}
Known Issues
1. Album Name Shows "Unknown Album"
Problem: Pending tracks sometimes show "Unknown Album" instead of the real album name.
Cause: Spotify embed API sometimes returns "Unknown Album" for track.album.
Fix Applied: Now uses resolved album name from albumsToDownload (MusicBrainz data) instead of Spotify embed data.
File: backend/src/services/spotifyImport.ts line ~280
2. Deezer Preview URLs Expire
Problem: Deezer preview URLs have timestamps and expire quickly.
Fix Applied: Added endpoint to fetch fresh preview URL on demand.
File: backend/src/routes/playlists.ts - GET /:id/pending/:trackId/preview
3. Retry Button Was Hanging
Problem: Clicking retry would hang for up to 180s (download timeout).
Fix Applied: Made retry non-blocking - search first (15s), return immediately, download in background.
File: backend/src/routes/playlists.ts - POST /:id/pending/:trackId/retry
4. Missing Files After Scan (Unresolved)
Problem: During testing, original downloaded files disappeared from disk, causing scan to remove 7 tracks. Status: Unknown cause - not a code bug. Files were deleted externally. Need to monitor in future tests.
Testing Checklist
Soulseek-Only Mode (Current Focus)
- Basic playlist import with Soulseek
- Track matching after download
- Pending track display for failed downloads
- Deezer preview playback
- Retry button functionality
- Remove pending track
- Toast notifications for retry status
- Activity panel shows download progress
- Verify files persist after download
Lidarr Mode (Needs Testing)
- Set
downloadSource: "lidarr"in settings - Import playlist - should send albums to Lidarr
- Lidarr downloads complete
- Library scan picks up Lidarr downloads
- Tracks match to playlist
Lidarr + Soulseek Fallback (Needs Testing)
- Set
downloadSource: "lidarr",soulseekFallback: "failed" - Import playlist with mix of albums (some in Lidarr, some not)
- Albums not in Lidarr should fall back to Soulseek
- Both sources' downloads get matched
Configuration
System settings relevant to import (in SystemSettings table):
downloadSource: "soulseek" | "lidarr"
soulseekFallback: "none" | "failed" | "always"
soulseekUsername: string
soulseekPassword: string (encrypted)
lidarrEnabled: boolean
lidarrUrl: string
lidarrApiKey: string (encrypted)
musicPath: string (e.g., "C:/Users/kevin/Music")
Logs
Import logs are written to: docs/logs/playlists/import_<jobId>_<timestamp>.log
Session log for Soulseek activity: docs/logs/playlists/session.log
Next Steps
- Run fresh import test with Soulseek
- Verify files persist and scan works correctly
- Test Lidarr-only mode
- Test Lidarr + Soulseek fallback
- Add activity panel integration for download progress
- Consider adding notification when background retry completes