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>
- Duration: ±15% (min 15s) instead of -40%/+50%
- Variant filter: reject sped/speedup/slowed/reverb/nightcore/karaoke/
parody/remix/cover/acoustic/instrumental unless original title has keyword
- Cascade: YTM+YT tried at each level (dur+artist+variant → no artist → no dur → no filter)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Strips special chars from artist name (☆NoAki☆ → NoAki) and adds
uploader *= check so wrong-artist results are rejected early.
Falls back to duration-only, then no-filter if artist filter yields nothing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous order let wrong YTM results through on the no-filter fallback.
New order: YTM+filter → YT+filter → YTM no filter → YT no filter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ytmsearch1: is not a valid yt-dlp scheme — use full music.youtube.com URL with --playlist-items 1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
e.target in dragstart is always the draggable item element, never the
handle child — so closest('.pl-drag-handle') always returned null and
every drag was cancelled. Now mousedown on handle sets fromHandle=true
and item.draggable=true; dragstart checks the bool instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
draggable=true must be set before any mouse event (not on mousedown).
Firefox requires setData() for drag to work. Removed duplicate dragend
listener that was resetting draggable=false and conflicting with the first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
official audio consistently breaks remixes and lesser-known artists.
Duration filter is the primary quality check.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drag handle appears on hover, moves items in DOM, syncs order to
songs array, and saves to Navidrome via createPlaylist 800ms after
last drop (debounced to avoid rapid API calls).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
closePlModal() resets S.plModalSongs=[] before ids was computed,
so no songs were ever added to the playlist.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remix titles (remix/edit/flip/vip/etc.) are specific enough without suffix.
Normal tracks get 'official audio' to prefer audio over music videos.
Original official-audio issue was only for remixes — now caught by regex.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adding 'audio' shifts results for lesser-known artists (Kanro, etc.).
Plain 'Artist - Title' matches what users find manually on YouTube.
Duration filter is the reliable quality check against wrong songs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remixes already have '(Remix)' etc. in title so no suffix needed.
Normal tracks get 'audio' to filter piano covers and tutorials.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'official audio' biased towards artist's main channel (wrong for remixes).
No suffix caused piano covers to rank top. 'audio' filters covers/tutorials
without over-specifying to official channels.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
For remixes and edits, 'official audio' biases YouTube search towards the
artist's official channel (wrong song) instead of the specific version.
Duration filter is now the quality check — the plain query is more accurate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
--max-downloads 1 exits with code 101 which varies by yt-dlp version — caused
ffmpeg to never run, leaving untagged files. Use ytsearch1: + --match-filter instead:
filters top result by duration, exits 0 in all cases, falls back cleanly if no match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
--playlist-items 1 in baseArgs caused filter to only check result #1 (filter useless).
--max-downloads 1 exits with code 101 which we treated as an error — ffmpeg never ran,
leaving untagged files (→ Unknown Artist/Album in Navidrome). Now code 101 resolves
successfully and filtered search uses commonArgs without --playlist-items.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When iTunes/Spotify provides a track duration, search 5 YouTube results
and keep only those within 60–150% of the expected length (filters remixes,
prayer videos, etc.). Falls back to ytsearch1: if no match is found.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>