playCurrent():
- Remove canplay listener from before play() — it caused a race where
both the listener and the initial play() fired simultaneously
- canplay is now only registered in the catch (failure path only),
eliminating the double-play race entirely
- AbortError: reload src+load() with _trackChanging=true (OS interruption
recovery); canplay fires once buffered and retries play()
- Other errors: reset _trackChanging, let canplay retry (not throttled
by Android background timers unlike setTimeout)
- Add 10s safety net to reset _trackChanging if Doze kills everything
(matches Kima Hub's reload failsafe)
main.js error handler:
- Skip when _trackChanging=true (playCurrent already recovering) to
prevent conflicting dual recovery attempts
- _onForeground: reset _trackChanging on play() failure so the flag
can't get stuck when foreground recovery also fails
index.html:
- Add crossorigin=anonymous: Navidrome and NovaSonic backend both send
Access-Control-Allow-Origin:* so this is safe, and it enables proper
cross-context HTTP caching for cover art and audio responses
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add preload="auto" playsinline to <audio> element: browser starts
buffering as soon as src is set, not only when play() is called —
critical for background where play() has no data to work with
- Fix AbortError handling: instead of silently returning (was swallowing
audio element bad-state errors), reload src + load() like Kima Hub
so the already-registered canplay listener retries play() once buffered
- Pre-warm next song's cover art in _preloadNext() using _cachedCoverUrl
so the lock screen notification never cold-starts artwork on first skip
- Move _coverCache to top of file so _preloadNext and updateMediaSession
share the same stable token URLs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cache cover art URLs per song so the same auth token is reused across
metadata updates; Samsung/Chrome can cache the artwork image and the
notification no longer visually reloads on every skip
- Add canplay listener as background-safe play() retry: the audio
element fires canplay from its internal state machine (not throttled
like setTimeout by Android Doze), so play() is retried as soon as
the browser has buffered enough data
- Set navigator.audioSession.type='playback' on each _tryPlay attempt
(not just on play event) so the OS audio session claim happens before
Chrome requests the play grant
- Guard _onForeground playbackState assignment with !_trackChanging to
prevent visibilitychange/pageshow from stomping 'playing' to 'paused'
during an in-progress track transition
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Set playbackState='playing' explicitly before MediaMetadata swap so
Samsung One UI never sees a gap where state is ambiguous during skip
- Add retry loop (500ms→1s→2s→4s→8s) in playCurrent() catch — handles
Android Doze rejecting play() in background; stops retrying if the
song changes mid-flight (AbortError)
- Fix error handler: set _trackChanging=true before audio.load() so
the load()'s pause event can't slip past the Samsung notification guard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Set navigator.audioSession.type = 'playback' on first play — tells Android
OS this is a music app, keeping background network alive through Doze mode
- Replace flat 2s error retry with exponential backoff (1s/2s/4s/8s)
- Add pageshow event alongside visibilitychange for Samsung/iOS PWA reliability
- Attempt audio resume on foreground if OS killed playback while backgrounded
- Switch preload from hidden audio element to link rel=prefetch (lighter,
avoids triggering two-audio-stream detection on Android)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the current song ends, Android's battery optimization briefly kills
background network access. By starting the next song's stream request while
the current track is still playing (and Chrome holds an active audio session),
the buffer is ready before the gap hits. The same URL is reused by the main
element so Chrome can serve from its in-memory buffer.
Also reset _trackChanging on play() failure to avoid permanently suppressing
pause events.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- playCurrent: set _trackChanging flag and update media session metadata
before changing src, so the pause event fired by src-swap does not
set playbackState='paused' and dismiss the Samsung cover screen notification
- Remove stalled->load() handler that silently paused audio with no recovery
- Error handler now retries regardless of paused state (load() causes paused)
- Add .catch() to audio.play() in playCurrent
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- Replace username/password login with API key (Bearer token) auth
- New absVerifyApiKey() verifies via GET /api/me instead of POST /login
- Settings shows single API Key field with hint (Profil → API Token in ABS)
- API key never pre-filled in settings for security
- Add "Bücher" button to mobile bottom-nav (bnav-library), hidden until connected
- _showAbsNavItem() helper keeps sidebar + bnav in sync on connect/disconnect
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Using the full artist name with Unicode symbols (e.g. ☆NoAki☆) in ytsearch
caused YouTube to return 0-1 completely irrelevant results, letting wrong songs
fall through to the no-filter fallback. Strip only \p{S} (Unicode symbol chars
like ☆ ★ ♥) but keep ASCII punctuation — so N!GHT stays "N!GHT" (important for
disambiguation) while ☆NoAki☆ becomes "NoAki" (searchable).
YTM URL still uses the original artist name with all chars intact.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use mainArtist (with special chars) for ytsearch queries so e.g. "N!GHT - Yume"
searches correctly instead of "NGHT - Yume" which YouTube misreads as "night"
and returns ambience videos.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- YTM URL now uses unstripped artist name (e.g. "N!GHT" instead of "NGHT") for
better search ranking on YouTube Music
- Steps 1-2 of cascade now include a soft channel ~= filter using the artist's
first word, preventing wrong-artist songs with matching titles from being
downloaded (e.g. a different "Moonlight" for N!GHT)
- Steps 3+ drop the channel filter so label-hosted tracks still get found
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cap Navidrome scan poll from 3 min to 60s, shrink post-scan buffer from 5s to 3s, and error fallback from 30s to 20s — playlist now appears sooner after downloads complete.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ytdlp.js: use \b word boundaries in all variant filter regexes so that
partial matches like "nightcoreals" containing "nightcore" no longer
falsely reject official videos; also fixes sped regression (!~= \bsped\b
instead of the too-narrow "sped up" pattern)
- squid.js: restructure verify payload to mirror challenge response layout
{ parameters, solution: { number, derivedKey }, signature }
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- squid.js: add top-level signature from challenge response to verify payload
(server added signature validation, causing HTTP 400 without it)
- ytdlp.js: switch variant filter from !*= to !~= (?i) so Remix/REMIX/remix
are all rejected, not just lowercase
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ytdlp.js: watch for [youtube:tab] in yt-dlp output during flat-playlist step;
return [] when channel expansion is detected so cascade falls through to ytsearch
- Revert player.js / main.js / state.js PWA _changingTrack changes (not effective)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The /api/altcha/challenge response is { parameters: { nonce, salt, ... } }.
Destructuring the top level left nonce/salt/keyPrefix as undefined, causing
Buffer.from(undefined) crash on every squid.wtf download attempt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
--match-filter 'webpage_url !*= /browse/' was applied to individual videos
AFTER yt-dlp expanded an artist channel into its uploads playlist — by then
each video had a /watch?v= URL, so the filter never fired.
New approach: run yt-dlp --flat-playlist --print webpage_url first to get
the raw search result URLs, filter /browse/ entries in Node.js, then pass
only the actual watch?v= video URLs to yt-dlp for download with --max-downloads 1.
The channel is never expanded; we get the real song from the search results.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
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>
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>
- 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>
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>
The normD regex used actual Unicode combining chars (U+0300-U+036F)
in the source which some build tools mangle, causing a SyntaxError at
runtime and silently killing playlist creation. Replaced with normA
that strips non-word/non-space chars via plain ASCII regex, which is
safe and achieves the same loose artist-first-word comparison.
Also fixed race condition in _pollAndCreatePlaylist — save lastStatus
before job is auto-deleted so the done check still works.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without ¶ms=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>
- 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>
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>
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>
- 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>
"Kløn, Renzyx" → search with "Kløn Alone", compare "kløn" == "kløn" (not "kløn,")
songCount raised to 20 so generic titles like "Alone" have better recall.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code 101 means --max-downloads was reached — the file was downloaded
successfully. Treating it as error caused the ffmpeg/metadata step to
be skipped, leaving files with no tags.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
--playlist-items 1-5 --max-downloads 1 on all filtered steps so yt-dlp
skips non-matching results (wrong artist, sped up, wrong duration) and
lands on the first result that actually passes all active filters.
Unfiltered last-resort fallbacks keep --playlist-items 1.
YouTube fallback uses ytsearch5: to match the 5-result window.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Playlist track search: use 'Artist Title' query + songCount=10 (was title-only + 5)
fixes songs like 'Dead' by Geoxor being missed due to common-word collision
- Missing tracks now show 'Artist – Title' instead of just title
- renderJobs: update existing cards instead of full innerHTML wipe every 1.5s
eliminates slideUp animation re-triggering on every poll tick
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Snapshot dir before each yt-dlp call so hasAudio() only detects
files added by the current attempt, not pre-existing files from
parallel jobs downloading to the same album directory.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>