Fixes#158 -- fresh docker-compose install shows login instead of setup
wizard because Next.js rewrites were compiled with 127.0.0.1:3006 baked
in. Added NEXT_PUBLIC_BACKEND_URL as a Dockerfile build arg, defaulting
to http://backend:3006 in docker-compose.yml so the frontend container
proxies to the backend service correctly.
Audiobookshelf sync fixes:
- syncAudiobook returns boolean; skipped books no longer count as synced
- Sync notification now surfaces failed/skipped counts
- downloadCover has 10s fetch timeout (was unbounded)
- book.size changed to book.media?.size for audio-only size
Added sync button on audiobooks page so users can trigger a sync without
going through settings or the enrichment system.
Bump to v1.7.2
Fixes:
- Playlist track count now updates live after add/remove -- cache
invalidated at all addTrackToPlaylist call sites (closes#157)
- Remove stale restart-required modal from settings -- services
reinitialize live on save, modal was dead code (closes#158)
- YouTube / YouTube Music playlist import (closes#155)
- Lidarr MBID mismatch: album MBID passed to MB /artist endpoint (closes#156)
- Hardcoded port-3030 in API base URL detection (closes#154)
Dead code:
- Remove unused setIsBulkAdd / setIsAddingToPlaylist state (album page)
- Remove unused useSearchParams() call (settings page)
Bump versions to 1.7.1 in frontend/backend package.json, README, CHANGELOG
Remove:
- api-contracts.spec.ts (100% theater -- shape checks with no bug-catching value)
Add auth middleware unit tests (backend/src/middleware/__tests__/auth.test.ts):
- 14 tests covering real code branches: missing header, wrong scheme,
empty token, expired JWT, tokenVersion mismatch (password-change
invalidation), legacy token without tokenVersion claim, deleted user,
requireAdmin 401/403 distinction, requireAuthOrToken query param path
- All tests assert on observable behavior that would fail if specific
lines of production code were removed
Add supertest route integration tests with mocked Prisma:
- auth.route.test.ts: login shape (no passwordHash in response), token
claims verification, no username enumeration (identical 401 for wrong
password and nonexistent user), refresh tokenVersion mismatch, role
injection rejected, admin-only endpoint 403 enforcement
- playlists.route.test.ts: IDOR for GET/PUT/DELETE -- each test asserts
BOTH the 403 status AND that the DB write was not called (catches
ownership check removed or moved after the mutation)
Enhance security.spec.ts (e2e against real Docker stack):
- IDOR: add PUT and DELETE scenarios (not just read)
- All IDOR tests now verify state after failed attempt (re-fetch confirms
resource unchanged/still exists -- not just checking status code)
- Enforce 403 specifically (not [403, 404]) -- 404 masks broken auth checks
- Add img onerror XSS vector alongside script tag vector
- Input validation: verify playlist count unchanged after 400
- Error response: add prisma/ORM internals to leakage checks
Add test infrastructure:
- src/__mocks__/test-env.cjs: sets JWT_SECRET before module load (setupFiles)
- jest.config.js: add setupFiles entry
- supertest + @types/supertest added to backend devDependencies
- Playback watchdog monitors currentTime every 1s, recovers after 3s stall
with auto-resume and position preservation (addresses #145)
- Resume after page refresh loads stream for restored track/audiobook/podcast
- Network retry upgraded to exponential backoff (1s/2s/4s), 3 max attempts
- Stalled event 10s grace timer as supplementary recovery path
- ETag/Last-Modified headers on audio streams for browser cache validation
- Centralized USER_AGENT constant in config.ts (was hardcoded in 12 files)
- Version bump from 1.6.2 to 1.6.4 (fixes#152)
Add M3U/M3U8 file upload endpoint (POST /api/spotify/import/m3u) that
parses playlist files and matches entries against the local library using
file path, filename, exact metadata, and fuzzy metadata matching tiers.
Closes#121, #125, #136, #138. Partially addresses #139, #25, #108, #30.
Share links: generate shareable URLs for playlists/tracks/albums with
public playback page, token-based access, expiry, and play limits.
Mobile lyrics: replace album art with scrollable lyrics view when
active. Synced lyrics auto-scroll; plain lyrics freely scrollable.
Mobile double-tap: custom touch handler with 300ms window across all
7 track list components. Desktop double-click preserved.
Security: path traversal containment in getLocalImagePath/getResizedImagePath,
stream error handling via streamFileWithRangeSupport, scoped JSON body limit.
Enrichment: sequential audio/vibe phases (no simultaneous ML models),
heroUrl preservation in manual enrichment, scanner deep-to-shallow iteration.
UI: playlist inline rename, player queue/add-to-playlist buttons,
error toasts on silent catch blocks, activity panel listener stability,
dead handleSeek wrappers removed, query key standardization.
Media player fixes:
- Sync playbackState to "paused" on audio engine error events
- Set playbackState "none" when queue clears (no stale lock screen controls)
- Emit error instead of swallowing failed retry play() in background
- Prevent infinite network retry loop by exhausting retry counter
- Preserve current media on network errors for foreground recovery
- Clear error state on foreground return so user can manually retry
- Fix podcast progress bar reverting on pause (update state on save)
- Auto-resume playback after phone call/Siri interruption
- Remove silence-keepalive (caused Bluetooth/CarPlay audio theft)
- Fix duplicate "play" event (emit on "playing" not "play")
- Set Safari audioSession.type = "playback" for correct session category
Production cleanup:
- Remove swagger-jsdoc/swagger-ui-express and all 16 @openapi annotations
- Remove 20 debug console.log/warn statements (SSE, Store, QueueDebug)
- Remove unused react-virtuoso dependency
- Remove commented-out Vibe route from sidebar
- Remove dead pauseRef from useMediaSession
- Bump Subsonic rate limit to 1500 req/min for Symfonium compatibility
- feat(import): paginate Spotify/Deezer playlist imports for any size (#25)
Spotify: /v1/playlists/{id}/tracks with offset/limit=50, 429/401 handling
Deezer: /playlist/{id}/tracks with index/limit=100, 200ms rate delay
SSE progress: "Fetching tracks: X of Y..." during pagination
- feat(lidarr): configurable quality & metadata profiles (#8)
New DB fields, POST /system-settings/lidarr-profiles endpoint,
auto-populated dropdowns in Settings after successful connection test
- fix(subsonic): map #text to value in JSON responses for Symfonium (#126)
- fix(subsonic): add getBookmarks.view empty stub for Symfonium (#126)
- fix(artist): show 10 popular tracks instead of 5 (#91)
- feat(musicbrainz): configurable base URL via MUSICBRAINZ_BASE_URL env var (#63)
Fix 4 mobile/iOS playback bugs: infinite network retry loop, silence keepalive
running during active playback, play button failing outside gesture window, and
MediaSession handlers never registering after app restore. Add resumeWithGesture()
across 13 call sites.
Security hardening: safeError() across ~82 catch blocks to prevent error leakage,
SSRF protection on cover art proxy, login timing normalization, crypto.randomInt()
for device links, select clauses on user queries, metrics auth gate, registration
gate with rate limiting, admin role check fix.
Production cleanup: remove dead code/imports, fix all ESLint warnings, add cover
art fetch retry for transient network errors.
BullMQ enrichment migration
- Rewrote enrichment pipeline on BullMQ v5: artist, track, podcast workers with
pause/resume/stop support and Bull Board visibility
- Essentia publishes audio:analysis:complete events; CLAP subscribes reactively
instead of polling (eliminates scan delay between phases)
- Thread-safe DB pool in CLAP worker, all DB calls in run_in_executor
- Fixed psycopg2.pool submodule import crash on BullMQ vibe worker startup
- Silenced EnrichmentStateService disconnect error on already-closed Redis conn
Enrichment fixes
- lastfmTags NULL caused mood-tags phase to silently skip all tracks; migration
backfills NULL -> '{}' and sets column default
- Cover art fetch errors for temp-MBID albums (temp-* passed to Cover Art Archive)
- VIBE-VOCAB vocabulary JSON not copied to Docker image (TypeScript omits .json)
- Wired cleanupOldResolved() to run daily; added missing enrichment status indexes
- Fixed circuit breaker reset, orphan cleanup, podcast entityType, hang detection
Soulseek
- Track search-page downloads in activity tracker
- Use async fs.promises.access instead of synchronous existsSync
- Verify file exists on disk before emitting download:complete (#110)
Docker image size: 28.4 GB -> 12.2 GB
- Removed all CUDA/NVIDIA dependencies; switched to CPU-only PyTorch/TensorFlow
- tensorflow-cpu + essentia-tensorflow --no-deps (avoids GPU TF transitive dep)
- Fixed .dockerignore: **/node_modules and **/.next now excluded from build context
PWA / mobile
- Background audio session loss on iOS/Android: SilenceKeepalive singleton,
tryResume() in MediaSession play handler, direct track load on 'ended' event,
visibilitychange/pageshow foreground recovery
- Lock orientation to portrait for Android device lock (#117)
Discovery
- Retry All re-importing albums already in library: apply same three-level filter
as GET /current before creating download jobs; delete stale UnavailableAlbum
records for already-present albums. Closes#34
CI / release
- linux/arm64 added to release and nightly Docker builds (#87)
- Isolated release CI from nightly GHA cache (cache-from/cache-to removed from
docker-publish.yml to guarantee clean release builds)
- Redis vm.overcommit_memory=1 sysctl added to prod and server compose files
Other fixes
- Cross-artist album fallback by title+year prevents library splitting (#50)
- Retry temp-MBID artists after 24h not 7 days; hide temp MBIDs from API (#112)
- 3-attempt ECONNRESET retry on all Deezer getPlaylist call sites (#119)
- check response.ok on health probe — fetch does not throw on 5xx (#104)
- Z-index stacking hierarchy established (MiniPlayer through OverlayPlayer)
- API token display overflow on iPhone (min-w-0/overflow-hidden on flex container)
Three compounding bugs caused enrichment to deadlock after 12+ hours:
1. runFullEnrichment reset analysisStatus to pending without clearing
analysisRetryCount, silently orphaning tracks Python would never queue
2. queueAudioAnalysis had no retryCount filter, queuing tracks Python
ignores (retryCount >= MAX_RETRIES), causing processing timeouts that
fed the circuit breaker
3. Circuit breaker fired on permanentlyFailedCount > 0 (expected cleanup
behavior), making it unrecoverable — reopened immediately on every
HALF-OPEN attempt
Fixes: reset retryCount+error in runFullEnrichment, add lt:3 filter to
queueAudioAnalysis, restrict circuit breaker to resetCount > 0 only.
Bumps version to 1.5.2.
Critical for 1k+ production users - ensures smooth upgrades when pulling new Docker images.
Changes:
- Added migrate-safe.sh script that automatically baselines existing databases
- Updated docker-entrypoint.sh to use safe migration script
- Updated Dockerfile to include migration script in image
- Manually applied missing config migration (SystemSettings columns)
How it works for users:
1. User runs: docker compose pull
2. Container starts, runs migrate-safe.sh automatically
3. Script detects if database exists but isn't tracked (P3005 error)
4. If yes: baselines all existing migrations, then applies new ones
5. If no: runs normal migrate deploy
Safety:
- Zero data loss (only marks existing migrations as applied)
- Idempotent (safe to run multiple times)
- Graceful (continues even if status check unclear)
- Production-tested on existing database with 30 migrations
Result:
- Users can docker compose pull without manual migration commands
- Existing databases automatically get proper migration tracking
- New migrations apply cleanly after baselining
Replace slsk-client npm dep with vendored soulseek-ts source.
TypeScript, Promise-based, with download progress events,
queue position tracking, and NAT traversal via ConnectToPeer.
Replace setInterval with self-rescheduling setTimeout for reconciliation
(2min) and Lidarr cleanup (5min) cycles. setInterval fires unconditionally
regardless of whether the previous cycle completed, and withTimeout() never
cancels the underlying operation -- it just resolves via Promise.race while
the operation continues as a zombie. Over hours, hundreds of zombie operations
accumulate, starving the event loop and exhausting DB connections and sockets.
Each cycle now schedules its next run only after fully completing, making
pile-up impossible. Bump to v1.4.3.
Search:
- Full-text search with DB triggers for Artist/Album/Track searchVector
- Harden queryToTsquery for special chars, fix NaN LIMIT propagation
- Extract ILIKE fallback methods, normalize cache keys
- Parallelize discovery endpoint (Last.fm + iTunes concurrent)
- Remove double debounce in frontend search
- Separate similar artists endpoint (musically similar, not name-similar)
- Alias resolution banner ("Showing results for X, searched Y")
- Soulseek polling interval leak fix (useRef + AbortController)
- Typed API responses for discover and similar artists
- "/" keyboard shortcut to focus search bar
Streaming:
- Fix HTTP Range suffix parsing for Firefox/Safari (#84)
- Extract parseRangeHeader utility, use in audioStreaming + podcasts
- Clamp range end to file boundary per RFC 7233
Library:
- Server-side sorting for artists, albums, tracks endpoints
- Case-insensitive artist matching in playlists and discovery (#64)
Audio Analyzer:
- Replace 5s poll loop with Redis BRPOP (zero CPU while idle)
- Remove TensorFlow import from main process (~300MB RAM saved)
- Lazy worker pool creation (starts on first job, not at boot)
- Configurable MODEL_IDLE_TIMEOUT to unload models after idle period
- DB reconciliation on BRPOP timeout for missed queue items
- Lighter Dockerfile healthcheck (pgrep instead of TF import)
Cleanup:
- Delete dead useDebouncedValue hook, enrichment worker, mood bucket worker
- Move sort maps to module scope
- Bump version to 1.4.2
Thanks to @Allram for identifying and fixing the Range parsing issue (#84)
Closes#84, closes#64
- Run audio analysis and vibe embedding phases sequentially to prevent
resource contention (CPU/memory) from concurrent analyzers
- Auto-detect GPU availability in both audio analyzers (CUDA/ROCm)
- Fix false lite mode detection on startup by checking analyzer scripts
on disk before falling back to heartbeat/DB checks
- Fix Dockerfile NEXT_PUBLIC_BACKEND_URL and frontend rewrite proxy
- Route enrichment failures through notification system instead of
persistent error banner
- Remove playback error banner from player components
- Reduce enrichment cycle interval from 6h to 2h
- Comprehensive repo cleanup: remove 127 decorative comment dividers
across 17 files, clean verbose comments, harden .gitignore, remove
tracked docs from git
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Jest testing infrastructure (jest, ts-jest, @types/jest)
- Create DiscoveryBatchLogger class with addLog, info, warn, error methods
- Add max log entries trimming (100 entries)
- Add unit tests for logging behavior
- Create discovery module with index exports
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use CachedImage (memoized) instead of raw Image in library grids
- Add GPU acceleration hints (translateZ) to card containers
- Remove expensive shadow-lg from library card image containers
- Add hover scale transform to match homepage cards
- Fix pagination scroll to use correct scroll container (#main-content)
- Scroll after page change, not on click
- Remove standalone /artists and /albums pages (use /library tabs)
- Add NEXT_PUBLIC_BUILD_TYPE env var for nightly vs release builds
- Bump version to 1.3.6
Co-Authored-By: Claude <noreply@anthropic.com>