Commit Graph
266 Commits
Author SHA1 Message Date
HoneyandClaude Sonnet 4.6 ee6df1e1fe fix: prevent music canplay callback from interrupting ABS playback start
When absPlayItem() paused S.audio, the stale canplay listener from the
last playCurrent() call still had a matching _playGen value and would
call S.audio.play() — triggering absInterrupt() which immediately paused
the ABS audio. Fix: export cancelMusicAutoplay() that bumps _playGen
before pausing, invalidating any pending canplay/guard callbacks.

Also shrink OK button and widen time input in sleep timer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 19:21:29 +02:00
HoneyandClaude Sonnet 4.6 f5343efd37 fix: add OK button back to audiobook sleep timer custom input
Mobile needs the button since there's no keyboard Enter key in all cases.
Button is compact and styled to match the accent color.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 19:13:31 +02:00
HoneyandClaude Sonnet 4.6 cf88d87791 fix: simplify audiobook sleep timer custom input; update README
- Remove OK button and expand-on-click from sleep timer custom row
- Always show label + plain number input (no spinners, Enter to confirm)
- Update README with all current features (Spotify importer, ABS, lyrics, filters, etc.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 19:04:12 +02:00
Honey 757c7045ba fix: normalize title comparison in iTunes lookup to handle Spotify dash-variants vs iTunes paren-variants 2026-05-14 18:53:06 +02:00
Honey 55370ad760 feat: show iTunes enrichment status indicator in Spotify importer 2026-05-14 18:01:34 +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
Honey e78df7bdb9 feat: add search filter bar to playlist detail and favorites views 2026-05-13 13:58:08 +02:00
Honey d5470a6440 fix: add Alben to mobile bottom nav; match sidebar order to mobile (Playlists before Favoriten) 2026-05-13 13:01:56 +02:00
Honey 47f1605965 feat: add sort filter chips to albums and favorites views 2026-05-13 12:53:34 +02:00
Honey 751cc05b89 fix: reset progress bar to 0:00 immediately on track change on web too 2026-05-13 12:46:58 +02:00
Honey 10e3e0eb1d fix: reset progress bar to 0:00 immediately on track skip/play in bridge mode 2026-05-13 12:45:55 +02:00
Honey a1a4416bb6 fix: restart skip grace period on isPlaying:true to suppress multi-fire buffering during seek 2026-05-13 12:44:27 +02:00
HoneyandClaude Sonnet 4.6 d87abd2e9e fix: suppress transient pause during seek in bridge mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 10:46:12 +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 ee05998840 fix: suppress transient isPlaying:false during native skip transitions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 09:42:07 +02:00
HoneyandClaude Sonnet 4.6 1e61d35018 fix: use isPlaying() in syncNowPlaying to handle bridge/native mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 02:00:26 +02:00
HoneyandClaude Sonnet 4.6 0c5fe53f13 fix: inject iTunes previewUrl into Spotify importer track items
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 01:56:14 +02:00
HoneyandClaude Sonnet 4.6 c8f82ceba5 fix: scrape Spotify OG tags as fallback for single track resolution
song_info returns GenericError for unauthenticated track lookups.
Fall back to scraping og:title/og:description/og:image from the public
Spotify track page — description format is "Song · Artist · Album · Year".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 01:48:37 +02:00
HoneyandClaude Sonnet 4.6 4f52e7b883 fix: album_info yields pages (flatten), track items use flat format; track GenericError handling
- Album: flatten generator pages with list comprehension, parse flat track.{name,artists,...} directly
- Track: try both URI and URL, report GenericError clearly instead of crashing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 01:44:56 +02:00
HoneyandClaude Sonnet 4.6 b74d0da294 fix: song_info needs URI not URL; album_info returns generator of track items
- Track: pass spotify:track:ID URI instead of full https:// URL
- Album: Public().album_info() yields track items (generator); metadata from PublicAlbum

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 01:40:31 +02:00
HoneyandClaude Sonnet 4.6 a401c7a607 fix: use Public().song_info() for tracks, Public().album_info() for albums
Both are the correct unauthenticated API methods on the Public class.
Song class requires a logged-in session and has no public equivalent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 01:36:33 +02:00
HoneyandClaude Sonnet 4.6 76785a4eaa debug: wrap Song init in try-except, log Public methods before any Song attempts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 01:21:19 +02:00
HoneyandClaude Sonnet 4.6 5d0c5ec6eb fix: album track wrapping + try multiple Song() call patterns for tracks
- Album: wrap item.get('track') as {itemV2: ...} so _parse_track navigates trackUnion correctly
- Track: try get_song_info(url) then get_song_info(); log Song/Public methods on failure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 01:15:43 +02:00
HoneyandClaude Sonnet 4.6 bbe4412f42 chore: remove unused showSetupError from ui.js (duplicate of _showSetupError in main.js)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 01:07:49 +02:00
HoneyandClaude Sonnet 4.6 67185b85e9 fix: track needs Public() client; fix album partner API structure parsing
- Song() requires a client instance as first arg; pass Public() for unauthenticated access
- Album response is data.albumUnion.{name,artists,coverArt,tracks.items} not flat keys
- Add per-field debug logging for track and album responses

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 01:01:35 +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 7b5928f809 fix: parse spotapi playlist response structure (data.playlistV2.content.items)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 23:17:03 +02:00
HoneyandClaude Sonnet 4.6 575ca385fd debug: log spotapi playlist page structure to diagnose 0-track response
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 23:13:17 +02:00
HoneyandClaude Sonnet 4.6 ddc6cb8cb2 fix: detect spotapi exports at runtime, lazy-import per type
PublicSong doesn't exist in the installed spotapi version. Now discovers
available class names at startup and picks the right one dynamically.
Missing track/album class no longer breaks playlist resolution.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:11:06 +02:00
HoneyandClaude Sonnet 4.6 4ec441af57 fix: switch to spotapi (api-partner.spotify.com) to avoid rate limits
api.spotify.com/v1/ aggressively rate-limits server IPs regardless of
TLS fingerprint or token. SpotipyFree was already hitting the correct
endpoint (api-partner.spotify.com) but crashed on KeyError: 'ownerV2'
due to its own outdated field parsing — not a network block.

spotapi uses the same internal GraphQL API (Spotify's web player API)
which is on a separate rate-limit bucket from the public REST endpoint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 21:02:28 +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 750089fc8e revert: remove proxy support, restore SpotipyFree-only resolver
IP is not actually blocked (api.spotify.com returns 401, not 403/timeout).
Previous rate limits were temporary. Reverting to the clean state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 20:04:10 +02:00
HoneyandClaude Sonnet 4.6 207e241600 fix: add SPOTIFY_PROXY support to bypass server IP ban
Spotify blocks API calls from server IPs (open.spotify.com/api/* times
out, api.spotify.com/v1/ returns 429). Setting SPOTIFY_PROXY routes all
Spotify calls through curl_cffi via that proxy, bypassing the IP ban.

Without proxy, SpotipyFree is still tried (works on non-banned IPs).
A clear tip is printed if SpotipyFree fails without a proxy configured.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:45:18 +02:00
HoneyandClaude Sonnet 4.6 7f7bedc15e fix: remove rate-limited REST fallback, explicitly install SpotipyFree
The curl_cffi fallback in spotify_resolve.py was silently catching
SpotipyFree ImportErrors and hitting api.spotify.com/v1/ directly —
the same rate-limited endpoint. Removed it entirely.

Also explicitly pip-install SpotipyFree before TzurSoffer's package
so the import is always available regardless of dependency resolution.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 18:38:23 +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 c36dbb3c94 fix: use SpotipyFree as primary resolver (SpotAPI + Chrome TLS fingerprint)
SpotipyFree wraps SpotAPI which uses tls_client to impersonate Chrome's
TLS fingerprint and hits Spotify's internal endpoints. This is exactly
what TzurSoffer's spotdl fork uses. Removes the complex GQL hash
extraction (which couldn't find hashes in lazy-loaded webpack chunks).
curl_cffi + TOTP kept as fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 17:09:33 +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 46eb8c826f fix: scan resource page JS for GQL hash (Spotify lazy-loads feature chunks)
fetchPlaylist/getAlbum/getTrack GraphQL hashes are not in homepage bundles
— they live in lazy-loaded chunks that only appear on the playlist/album/track
page. Fetch the specific page HTML to find the extra JS bundles, then scan
those for the persisted query hash. Also improved hash regex (no semicolon
restriction, wider window, fallback nearest-hex search).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 16:45:32 +02:00
HoneyandClaude Sonnet 4.6 e87d48d6b0 fix: use api-partner.spotify.com GraphQL instead of rate-limited REST API
api.spotify.com/v1 is IP-banned for 24h. Switch to Spotify's internal
partner API (api-partner.spotify.com/pathfinder/v1/query) which uses
separate infrastructure and rate limits. Persisted query hashes are
extracted dynamically from JS bundles alongside the TOTP secret.
Falls back to REST only if GQL hash not found or partner API fails.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:35:44 +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 b53910ed45 fix: switch backend to node:20-slim (Debian) for glibc compatibility
tls-client (used by SpotAPI/spotdl) bundles a native .so that requires
libresolv.so.2 from glibc. Alpine uses musl libc which doesn't provide
this — causing spotdl to crash on import. Debian-based image has glibc.
Also install yt-dlp via direct download (latest) instead of apk.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:07:50 +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