5.2 KiB
Spotify Import - Code Reference
Quick reference to key code sections for the next agent.
Backend Entry Points
Preview Playlist
File: backend/src/routes/spotify.ts
Endpoint: POST /spotify/preview
Handler: Lines ~50-120
// Fetches Spotify playlist, searches MusicBrainz for albums
const preview = await spotifyImportService.previewPlaylist(url);
// Returns: matchedTracks, unmatchedTracks, albumsToDownload
Execute Import
File: backend/src/routes/spotify.ts
Endpoint: POST /spotify/import
Handler: Lines ~130-200
// Starts async import job
const job = await spotifyImportService.executeImport(preview, userId, playlistName);
// Returns: jobId for status polling
Retry Pending Track
File: backend/src/routes/playlists.ts
Endpoint: POST /playlists/:id/pending/:trackId/retry
Handler: Lines ~630-745
// Non-blocking retry flow:
// 1. Search Soulseek (15s timeout)
// 2. Return immediately with success/failure
// 3. Download in background
// 4. Trigger library scan after download
Core Import Logic
spotifyImportService.executeImport()
File: backend/src/services/spotifyImport.ts
Function: Lines ~150-350
Key sections:
- Lines ~180-220: Download albums via Lidarr or Soulseek
- Lines ~230-280: Wait for downloads, handle failures
- Lines ~290-350: Create playlist, match tracks, store pending
Soulseek Download Flow
File: backend/src/services/soulseek.ts
Key methods:
searchTrack()- Lines ~150-250: Search with 15s timeoutdownloadTrack()- Lines ~300-400: Download single file with 180s timeoutsearchAndDownloadBatch()- Lines ~525-600: Parallel search, concurrent downloaddownloadBestMatch()- Lines ~465-520: Download from pre-searched results
Track Matching
File: backend/src/services/spotifyImport.ts
Function: matchTrackToLibrary() - Lines ~400-500
Matching strategies (in order):
- Exact normalized title + artist first word
- Stripped title (remove remaster/remix suffixes)
- Contains search
- Fuzzy artist + title
- StartsWith search
- Last resort fuzzy
Pending Track Reconciliation
File: backend/src/services/spotifyImport.ts
Function: reconcilePendingTracks() - Lines ~550-650
Called after library scan to match pending tracks to newly added files.
Frontend Components
Import Wizard
File: frontend/app/import/spotify/page.tsx
Key state:
step: "url" | "preview" | "importing" | "complete"preview: PreviewResult from APIjobStatus: Polling status during import
Playlist Detail - Pending Tracks
File: frontend/app/playlist/[id]/page.tsx
Key handlers (Lines ~100-160):
handlePlayPreview()- Fetches fresh Deezer URL, plays audiohandleRetryPendingTrack()- Calls retry API, shows toasthandleRemovePendingTrack()- Removes from playlist
Pending track rendering: Lines ~555-650
Database Queries
Get Playlist with Pending Tracks
const playlist = await prisma.playlist.findUnique({
where: { id: playlistId },
include: {
items: { include: { track: { include: { album: { include: { artist: true }} }} }},
pendingTracks: { orderBy: { sort: 'asc' } }
}
});
Create Pending Track
await prisma.playlistPendingTrack.create({
data: {
playlistId,
spotifyArtist: track.artist,
spotifyTitle: track.title,
spotifyAlbum: resolvedAlbum,
spotifyTrackId: track.spotifyId,
deezerPreviewUrl: previewUrl,
sort: index
}
});
Reconcile Pending Track (convert to real track)
// Delete pending, add real track
await prisma.$transaction([
prisma.playlistPendingTrack.delete({ where: { id: pendingId } }),
prisma.playlistItem.create({
data: { playlistId, trackId: matchedTrack.id, sort: pending.sort }
})
]);
Configuration Check
const settings = await getSystemSettings();
// Key fields:
// - settings.downloadSource: "soulseek" | "lidarr"
// - settings.soulseekFallback: "none" | "failed" | "always"
// - settings.musicPath: where files are downloaded
// - settings.soulseekUsername / soulseekPassword
// - settings.lidarrUrl / lidarrApiKey
Error Handling Patterns
Soulseek Connection
try {
await soulseekService.ensureConnected();
} catch (err) {
// Credentials not configured or connection failed
return { success: false, error: "Soulseek connection failed" };
}
Download Retry Logic
const matchesToTry = allMatches.slice(0, MAX_DOWNLOAD_RETRIES); // 3 attempts
for (const match of matchesToTry) {
const result = await this.downloadTrack(match, destPath);
if (result.success) return { success: true, filePath: destPath };
// Try next user on failure
}
return { success: false, error: "All attempts failed" };
Logging
Session logging for debugging:
import { sessionLog } from "../utils/playlistLogger";
sessionLog("SOULSEEK", "Message here"); // INFO level
sessionLog("SOULSEEK", "Error message", "ERROR");
sessionLog("SOULSEEK", "Warning", "WARN");
Job-specific logging:
import { createPlaylistLogger } from "../utils/playlistLogger";
const logger = createPlaylistLogger(jobId);
logger.info("Message");
logger.error("Error");
logger.debug("Debug info");