- 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>
- Replace apk yt-dlp with pip3 install to get current version with ytmsearch support
- Split mainArtist on comma in downloadTrack so multi-artist tracks
always land in the primary artist folder (e.g. "Artist1, Artist2" → Artist1/)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Switch to YouTube Music search (ytmsearch3) as primary source with
fallback to ytsearch5 if no result found
- Add duration filter (±30s/+45s) and live-stream exclusion via
--match-filter when iTunes trackTimeMillis is available
- Send trackTimeMillis from frontend in download payload
- Pass durationMs through downloadTrack and downloadAlbumTracks
- Use first artist only (split on comma) for folder path on multi-artist tracks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
new Image() onload fires when bytes are downloaded but GPU decode of the
blurred layer hasn't happened yet on mobile. Double rAF defers the opacity
swap until after the browser has actually painted the new background.
will-change:opacity keeps the layers on dedicated compositor layers so
the blur doesn't need to be recomputed on every opacity transition.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renders immediately with available data, then enriches with complete
metadata (genre, bitrate, path, plays, etc.) from getSong response.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_syncNpDirect is called 2-3x per skip from different call sites;
the crossfade now no-ops when the cover URL hasn't changed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two-layer opacity crossfade replaces the broken background-image transition.
New image loads on the hidden layer, then both opacity values swap smoothly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- renderQueue: in-place active-class update when queue unchanged — no DOM rebuild, no image reload
- updatePlayerUI: update img.src directly instead of replacing innerHTML
- _syncNpDirect: same img.src update for now-playing cover
- navigate('home'): always reloads home data so new albums appear without restart
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Collects tracks not matched in Navidrome search during playlist creation
and saves them as 'missing' in the notification — expandable list in the
bell panel labeled 'nicht in Navidrome gefunden'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bell icon in sidebar and mobile header with unread badge
- On download complete/error: saves notification to localStorage (survives reload)
- Playlist/album downloads: parses job log to list which tracks failed
- Panel toggles open with animation, click-outside closes it
- Each notification dismissable with X; "clear all" button
- Notifications capped at 100 entries in localStorage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>