Files
lidify/docs/handoff/spotify-import-code-reference.md
2025-12-25 18:58:06 -06:00

191 lines
5.2 KiB
Markdown

# 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
```typescript
// 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
```typescript
// 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
```typescript
// 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 timeout
- `downloadTrack()` - Lines ~300-400: Download single file with 180s timeout
- `searchAndDownloadBatch()` - Lines ~525-600: Parallel search, concurrent download
- `downloadBestMatch()` - 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):
1. Exact normalized title + artist first word
2. Stripped title (remove remaster/remix suffixes)
3. Contains search
4. Fuzzy artist + title
5. StartsWith search
6. 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 API
- `jobStatus`: 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 audio
- `handleRetryPendingTrack()` - Calls retry API, shows toast
- `handleRemovePendingTrack()` - Removes from playlist
Pending track rendering: Lines ~555-650
## Database Queries
### Get Playlist with Pending Tracks
```typescript
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
```typescript
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)
```typescript
// 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
```typescript
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
```typescript
try {
await soulseekService.ensureConnected();
} catch (err) {
// Credentials not configured or connection failed
return { success: false, error: "Soulseek connection failed" };
}
```
### Download Retry Logic
```typescript
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:
```typescript
import { sessionLog } from "../utils/playlistLogger";
sessionLog("SOULSEEK", "Message here"); // INFO level
sessionLog("SOULSEEK", "Error message", "ERROR");
sessionLog("SOULSEEK", "Warning", "WARN");
```
Job-specific logging:
```typescript
import { createPlaylistLogger } from "../utils/playlistLogger";
const logger = createPlaylistLogger(jobId);
logger.info("Message");
logger.error("Error");
logger.debug("Debug info");
```