Commit Graph
104 Commits
Author SHA1 Message Date
HoneyandClaude Sonnet 4.6 5559857c15 feat: metadata editor with MusicBrainz autofill + cover art upload
- Info modal gets an edit pencil button that switches to edit form
- Editable fields: title, artist, album, genre, year, track#, disc#, total tracks
- Cover art: upload your own image or auto-fetch from Cover Art Archive via MusicBrainz
- MusicBrainz button queries /api/musicbrainz/lookup and autofills all fields
- Save POSTs to /api/edit-metadata which rewrites tags via ffmpeg and triggers Navidrome rescan
- Backend: GET /api/musicbrainz/lookup + POST /api/edit-metadata endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 19:22:11 +02:00
HoneyandClaude Sonnet 4.6 4f83eebe79 feat: sort genre pills by total play count via backend
Page loads instantly with song-count order, then silently re-sorts
once the backend aggregates play counts across all songs. Result cached
20 min server-side to avoid repeated full-library fetches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 12:02:25 +02:00
HoneyandClaude Sonnet 4.6 b9a5c6468f refactor: dedup buildTrackList, extract topsong row helper, split romaji to lib
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 11:36:06 +02:00
HoneyandClaude Sonnet 4.6 5bcb8b09a8 fix: romaji index routes moved after app init + kuromoji in root package.json
- app.get/delete for /api/romaji-index were placed before const app = express()
  causing ReferenceError on startup — moved into the ROUTES section
- kuromoji moved from backend/package.json to root package.json so Docker
  picks it up during the existing npm install step
- Fixed dicPath resolution to use path.dirname(require.resolve(...))

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:26:46 +02:00
HoneyandClaude Sonnet 4.6 aa239f8e6a feat: romaji index for Japanese kanji song search
Backend:
- Add /api/romaji-index endpoint using kuromoji morphological analyzer
- Fetches all Navidrome songs, converts kanji → hiragana readings → romaji
- Only indexes songs with Japanese characters; caches for 20 min
- /api/romaji-index DELETE clears cache (force rebuild)

Frontend:
- fetchRomajiIndex() in api.js: lazy-fetches and caches the index
- doLocalSearch: when query is romaji (ASCII), queries romaji index in
  parallel with Navidrome search3 and merges+deduplicates results

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:15:07 +02:00
Honey 11dec04fea fix: improve Spotify importer metadata — copy iTunes durationMs, skip redundant backend lookup when frontend already enriched track 2026-05-14 17:58:36 +02:00
HoneyandClaude Sonnet 4.6 74a452c037 fix: use playlist/album artworkUrl as fallback when track has no cover
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 10:41:01 +02:00
HoneyandClaude Sonnet 4.6 6bedd3ed28 fix: proxy iTunes API through backend to resolve CORS errors
Frontend was calling itunes.apple.com directly; browsers block it (no CORS headers).
Added /api/itunes/:endpoint proxy on the backend and updated all itunesSearch/Lookup
calls in api.js to route through it when backendUrl is configured.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 23:23:40 +02:00
HoneyandClaude Sonnet 4.6 d4c41a9f92 fix: enforce Spotify rate limit cooldown server- and client-side
Server tracks _spRateLimitUntil and rejects retries instantly without
hitting Spotify, returning the remaining wait time. Retrying before the
window expired was resetting Spotify's timer (19s → 7s → 48s).

Frontend now shows a live countdown with a note not to retry early.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 20:22:10 +02:00
HoneyandClaude Sonnet 4.6 5265aa1f33 fix: replace SpotipyFree with curl_cffi, remove dead Node.js TOTP code
SpotipyFree crashes with KeyError: 'ownerV2' — Spotify changed their
internal API response format and the library is out of date.

curl_cffi presents a real Chrome TLS fingerprint so open.spotify.com/api/
server-time responds (plain Node.js TLS is filtered by Spotify's WAF).
Node.js TOTP fallback always failed on this for the same reason — removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 20:17:15 +02:00
HoneyandClaude Sonnet 4.6 0f1e6ff531 refactor: remove dead code from backend and lib
- Remove unused spotifyClientId/spotifyClientSecret from config
- Remove AUDIO_EXTS from metadata exports (internal-only)
- Remove tryYtdlp wrapper in ytdlp.js (direct spawnYtdlp calls)
- Remove _spArtistName wrapper in server (direct _spAllArtists calls)
- Fix stale comment: primary resolver is SpotipyFree, not curl_cffi

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 18:18:50 +02:00
HoneyandClaude Sonnet 4.6 06b78be199 fix: full stderr logging per-line, scan embed page for GQL hash
- Log each Python stderr line separately (no 500-char truncation) so
  we can see exactly what's happening in the bundle scan
- Try embed page (open.spotify.com/embed/...) first for GQL hashes —
  focused app, fewer bundles, GQL chunks directly referenced
- Suppress noisy bundle-fetch errors to keep logs readable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:26:51 +02:00
HoneyandClaude Sonnet 4.6 8bf481a2da fix: replace spotdl CLI with direct curl_cffi Chrome TLS impersonation
spotdl save hangs for 60s initializing its full pipeline. Rewrite
spotify_resolve.py to use curl_cffi (already installed) which
impersonates Chrome's TLS fingerprint directly. TOTP token fetched
in Python too. RATE_LIMITED propagates without TOTP fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:21:32 +02:00
HoneyandClaude Sonnet 4.6 eacd68aea7 fix: remove Spotify credential requirement — no API keys needed
spotdl (SpotAPI) works without developer credentials. Remove the
health-check warning, credential error UI, and Client Credentials
fallback that were blocking the importer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:01:35 +02:00
HoneyandClaude Sonnet 4.6 9b197e119a fix: strip ?si= from Spotify URL, improve spotdl error logging
spotdl doesn't handle tracking params in URLs. Also separate stdout/stderr
in Python script and always try parsing stdout before checking exit code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 14:55:41 +02:00
HoneyandClaude Sonnet 4.6 3c85deab40 feat: use spotdl (TzurSoffer fork) as primary Spotify resolver
Replaces rate-limited api.spotify.com calls with spotdl's SpotAPI
approach, which uses Spotify's internal browser endpoints — no
developer credentials needed, no api.spotify.com rate limits.
TOTP anonymous token kept as automatic fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 14:49:19 +02:00
HoneyandClaude Sonnet 4.6 8cc6653898 fix: add Spotify web-player headers to API calls, pace paginated requests
Without app-platform/spotify-app-version headers, requests look like
bot traffic and get rate-limited aggressively. Add 300ms delay between
playlist track pages to avoid burst throttling.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 00:08:44 +02:00
HoneyandClaude Sonnet 4.6 859f4a458a fix: fail fast on Spotify 429 instead of retrying into timeout
Retrying with wait times (23s + 60s + 60s) exceeded the 120s global
timeout and showed a generic timeout error. Now returns immediately
with a RATE_LIMITED error and shows the user how long to wait.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 00:05:41 +02:00
HoneyandClaude Sonnet 4.6 fbb00baa11 feat: TOTP-based anonymous Spotify token (no developer credentials needed)
Dynamically extracts Spotify's TOTP secret from their web-player JS bundle,
computes a 6-digit HMAC-SHA1 code synced to open.spotify.com/api/server-time,
and requests a token from open.spotify.com/api/token — completely bypassing
the rate-limited Client Credentials token. Client Credentials kept as fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 23:12:29 +02:00
HoneyandClaude Sonnet 4.6 61271bff27 fix: use anonymous clienttoken as primary Spotify auth, Client Credentials as fallback
Developer Client ID is 24h rate-banned. clienttoken.spotify.com uses
Spotify's own web-player client ID (separate rate-limit bucket).
Also: fast-fail when Retry-After > 60s and invalidate the cached token
so the next request immediately fetches a fresh one.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 18:59:55 +02:00
HoneyandClaude Sonnet 4.6 3633b4c5fa fix: handle 429 rate-limit within timeout window
- Cap per-retry wait at 15s (Spotify's Retry-After can be very long)
- Increase global resolve timeout from 25s to 120s so retries can complete
- Log rate-limit waits for visibility

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 18:15:24 +02:00
HoneyandClaude Sonnet 4.6 bb52717c95 fix: force HTTP/1.1 via ALPNProtocols to prevent h2 framing mismatch
api.spotify.com negotiates HTTP/2 via TLS ALPN but https.request uses
HTTP/1.1 framing — the protocol mismatch silently hangs the connection.
wget on Alpine doesn't advertise h2 so it works fine. ALPNProtocols:
['http/1.1'] forces HTTP/1.1 on both ends.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 18:01:12 +02:00
HoneyandClaude Sonnet 4.6 d2cb1bed68 fix: add User-Agent, dual timeout, done-guard, and response logging to _fetchTimeout
- User-Agent header (Spotify may throttle anonymous requests)
- req.setTimeout + socket.setTimeout for belt-and-suspenders
- done() guard prevents double-resolve/reject
- [sp-http] log shows status code of each Spotify response
  so logs reveal whether the hang is before or after HTTP headers arrive

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 17:50:18 +02:00
HoneyandClaude Sonnet 4.6 f4766acc74 fix: switch Spotify HTTP to https module with socket.setTimeout for OS-level timeout
undici cannot abort a TCP connect in progress even with AbortController.
Node's https.request + socket.setTimeout fires at the OS socket level
and aborts within ms even when the TCP SYN is silently dropped.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 17:45:10 +02:00
HoneyandClaude Sonnet 4.6 d2620519aa fix: replace AbortSignal.timeout with AbortController+setTimeout for reliable fetch timeouts
undici (Node.js native fetch) silently ignores AbortSignal.timeout when
TCP connect is blocked. Using AbortController driven by Node's setTimeout
guarantees the abort fires and the hanging request is cancelled within 10s.
Adds [spotify] token ok log to confirm when credentials work.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 23:43:31 +02:00
HoneyandClaude Sonnet 4.6 76271c1e54 fix: 25s backend timeout + 30s frontend timeout on Spotify resolve
Extracts resolve logic into _resolveSpotify() and wraps it in
Promise.race with a 25s timer so the backend always sends a response
even if undici's AbortSignal.timeout silently fails on blocked sockets.
Also reduces frontend timeout from 90s to 30s.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 23:39:07 +02:00
HoneyandClaude Sonnet 4.6 f7e4059f2d fix: friendly error for Spotify editorial playlist 403
Backend returns {error:'EDITORIAL_PLAYLIST'} on 403 playlist routes.
Frontend shows a step-by-step workaround card instead of a red error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 23:22:56 +02:00
HoneyandClaude Sonnet 4.6 468827571a feat: switch Spotify auth to official Client Credentials Flow
Replaces the broken open.spotify.com/get_access_token scrape with
accounts.spotify.com/api/token (Basic auth, client_credentials grant).
Adds SPOTIFY_CLIENT_ID/SECRET to config, docker-compose, and .env.example.
Health endpoint now reports spotify.configured; frontend disables the
importer and shows a setup guide when credentials are missing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:57:01 +02:00
Honey 7c1c444bf2 fix: 429 retry, safety counter, local file filter in Spotify pagination 2026-05-09 20:58:16 +02:00
Honey b7f09058a9 fix: use sp_t cookie trick for Spotify anonymous token (spotdl approach) 2026-05-09 20:55:00 +02:00
Honey 6c4caebc69 fix: regex accessToken directly from embed HTML instead of brittle JSON path 2026-05-09 20:17:37 +02:00
Honey 60c74a60a2 fix: get Spotify token from embed page instead of blocked get_access_token endpoint 2026-05-09 20:02:18 +02:00
Honey f218b66ad2 fix: replace Spotify embed scraper with anonymous token + real API
Uses open.spotify.com/get_access_token for an anonymous bearer token,
then paginates through the full Spotify API — removes the 100-track
embed limit for playlists and albums.
2026-05-09 19:58:07 +02:00
Honey eb4d6fcbf9 fix: CROS fix, Mini Bar fix switches Audiobook and Music Player, Chapter Track mode added, Delted Playlist mode from Audiobooks 2026-05-03 15:14:19 +02:00
Honey 88999d30ca fix: Backend path error fix 2026-05-03 14:55:36 +02:00
Honey 28f5b83751 fix. Cors Fix and Audiobookshelf Setting Design Fix 2026-05-03 14:49:08 +02:00
HoneyandClaude Sonnet 4.6 d70f649b90 fix: ABS proxy via backend to bypass CORS + settings button layout
- Add /api/abs-proxy/* endpoint in the backend that forwards all ABS API
  calls server-side (X-ABS-URL + X-ABS-Token headers). Browser never
  touches ABS directly for API calls — no CORS issue.
- abs.js routes through proxy when backendUrl is set; falls back to direct
  if no backend (requires user to configure CORS on ABS server)
- Audio streaming still uses direct <audio src> (HTML media bypasses CORS)
- Settings: status text above buttons, buttons full-width flex row

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 14:44:16 +02:00
HoneyandClaude Sonnet 4.6 ac8d26bf9d feat: split backend into lib/ modules + add squid.wtf (Qobuz) download backend
- backend/lib/config.js  — CONFIG from env (adds DOWNLOAD_BACKEND, SQUID_QUALITY)
- backend/lib/jobs.js    — jobs Map, updateJob(), log()
- backend/lib/metadata.js — iTunes, artwork, lyrics, sanitize helpers
- backend/lib/ytdlp.js   — yt-dlp download cascade + shared embedMetadata()
- backend/lib/squid.js   — ALTCHA PoW solver, Qobuz search/download via squid.wtf
- backend/lib/downloader.js — backend selector: ytdlp | squid | auto (squid-first + fallback)
- novasonic-server.js thinned to Express routes only, imports from lib/
- docker-compose: mount full backend/ dir, install deps outside read-only volume
- .env.example: document DOWNLOAD_BACKEND and SQUID_QUALITY

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 20:44:26 +02:00
HoneyandClaude Sonnet 4.6 2d71fdce6a fix: skip artist channel results, live notification list update
- Add --match-filter 'webpage_url !*= /browse/' to baseArgs so yt-dlp
  skips YTM artist channel pages returned as search results (previously
  caused recursive channel-upload fetches that wasted time)
- Call _renderNotifList() in _addNotif so playlist-created notifications
  appear immediately without needing a page reload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 19:59:04 +02:00
HoneyandClaude Sonnet 4.6 f831e1fd8f fix: use exact expected filename instead of newest-file heuristic
Parallel downloads to the same album dir (e.g. two tracks from same
album) both resolved to the last-written file, causing wrong metadata
to be embedded. Now each download targets its known output path
(trackPrefix + safeTitle + audioFmt) directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 18:25:51 +02:00
HoneyandClaude Sonnet 4.6 3962a90b0d fix: delete cover.jpg and leftover webm/m4a after ffmpeg embed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 18:09:12 +02:00
HoneyandClaude Sonnet 4.6 252d2d3071 feat: download playlist tracks in parallel batches (MAX_PARALLEL)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 18:03:29 +02:00
HoneyandClaude Sonnet 4.6 c1a740240a fix: normalize diacritics in playlist artist matching
Use Unicode property escapes to strip only symbols (☆, !, etc.)
while keeping Unicode letters (ø, ä, é) — fixes "Kløn" becoming
"Kln" instead of staying "Kløn" in the search query.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 17:54:50 +02:00
HoneyandClaude Sonnet 4.6 afcbf81547 fix: strip parens from keywords, top-10 search, clean search query
- buildTitleKeywords: strip parenthetical tags before picking words so
  "Eleven Night (Jersey Club)" → keywords ["Eleven","Night"] instead of
  ["Eleven","Jersey"] — "Jersey" was never in the YT video title
- Cascade: add top-10 steps (3+4) with duration still active before
  dropping duration filter, so "Whispers" (position 6-10) can be found
  without falling through to the unfiltered last resort
- search query: strip non-ASCII chars from artist name so "☆NoAki☆"
  becomes "NoAki" in the query → cleaner YTM search ranking

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 16:49:53 +02:00
HoneyandClaude Sonnet 4.6 51d2ea257f fix: use top-2 title keywords for precise match-filter
Single-word filter was too loose: "Echoes of Eternity" produced only
"Eternity" which matched unrelated tracks. Now the top 2 longest words
are used so the filter becomes e.g.
  title ~= (?i)Eternity & title ~= (?i)Echoes?
  title ~= (?i)Amethyst & title ~= (?i)Flowers?
Both words must appear — drastically reduces false matches.
Trailing-s stays optional (Whispers? matches Whisper/Whispers).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 16:10:05 +02:00
HoneyandClaude Sonnet 4.6 2d1c9029c5 fix: add YTM 'Songs only' filter param to search URL
Without &params=EgWKAQII..., yt-dlp gets a mixed feed of videos,
channels and playlists alongside tracks — same query that shows the
correct song first in the browser "Titel" tab returned nursery rhyme
videos and unrelated channels. This param is YTM's internal 'Songs'
filter (equivalent to clicking the Titel tab).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 15:38:35 +02:00
HoneyandClaude Sonnet 4.6 971cadd8fa fix: match-filter trailing-s optional + playlist search fallback for special chars
- buildTitleKeywords: append '?' to keywords ending in 's' so regex
  "Whispers?" matches both "Whisper" (YouTube title) and "Whispers"
  (iTunes title) — fixes NoAki Whispers downloading wrong track
- _createNdPlaylist: if artist+title search returns no title match,
  retry with title-only query so tracks with special chars in artist
  name (e.g. "Kløn") are still found in Navidrome

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 14:57:51 +02:00
HoneyandClaude Sonnet 4.6 0a7591a690 fix: remove artist filter + fix playlist scan timing
backend: drop unreliable artist *= filter entirely — yt-dlp artist field
is empty for many label-distributed YTM tracks (NoAki, etc.), causing
correct results to be rejected in Steps 1-2 and falling through to Step 3
anyway. Search query already targets the right artist; title keyword +
duration are sufficient. Simplifies cascade from 8→8 steps but with fewer
wasted filter attempts.

frontend: add 3s delay before polling getScanStatus (gives Navidrome time
to actually start the scan), and 5s buffer after scan completes (lets DB
writes settle). Fallback to 30s fixed wait if scan API is unavailable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 14:15:09 +02:00
HoneyandClaude Sonnet 4.6 7a0a7ca455 fix: case-insensitive title filter + scan before playlist creation
backend: replace title *= (case-sensitive) with title ~= (?i) regex so
"MEMORIES" and "Echoes of Eternity" match regardless of YT title casing.
Refactor buildTitleKeyword → buildTitleKeywords returning array.

frontend: trigger Navidrome startScan after download completes and wait
for scan to finish before running _createNdPlaylist, so newly downloaded
tracks are indexed and searchable when the playlist is built.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 13:40:20 +02:00
HoneyandClaude Sonnet 4.6 42658b3cbf fix(backend): improve yt-dlp match filters for accuracy
- artistFilterWord: use longest \w+ run instead of stripping special chars,
  fixes artists like N!GHT where stripping gave NGHT (not a substring)
- buildTitleKeyword: new fn extracts most distinctive title word, added as
  title *= filter to all cascade steps to block unrelated fallback downloads
- Mix version specificity: Radio/Extended/Club Mix titles also filter on the
  qualifier word so plain versions are rejected
- Switch uploader *= to artist *= so label-uploaded tracks (Monstercat etc.)
  match by music metadata artist tag, not channel name

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 12:46:20 +02:00