The delete-song endpoint returned a bare "File not found on disk" with no
context, making path-resolution mismatches impossible to diagnose. Now
reports the Navidrome path, music root, and searched filename — matching the
detail the edit-metadata endpoint already provides.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ¶ms= Songs-tab filter re-ranked YTM results toward a same-named
artist's discography and excluded the official track (qNffDItRUeo) entirely,
even logged-out. The default blended search (what the browser shows) ranks
the exact track first. Removing the param so yt-dlp gets the same result set.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"MINA - recognize" skewed YTM ranking toward a same-named artist's full
discography, so the actual track (qNffDItRUeo) never appeared in results.
"MINA recognize" — matching how users type in the YTM search box — ranks
the exact official track first.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The YTM search (getYtmVideoUrls) ran without the web_music client, so yt-dlp
parsed the search page as a generic YouTube search and returned unrelated
videos instead of the native Songs shelf — the actual artist track (e.g.
qNffDItRUeo for MINA - recognize) never appeared in results. Apply the same
player_client=web_music,default arg to the search step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
yt-dlp's android_vr player client returns "This video is not available" for
many YouTube Music Art Track / topic uploads (e.g. MINA - recognize) that
play fine in the browser. Switch to player_client=web_music with default
fallback so these tracks become downloadable, fixing the cascade falling
through to wrong YouTube videos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
searchTitle (e.g. "recognize") is now passed to embedMetadata instead of
the raw title ("recognize - Single"), so the ID3 title tag matches the
actual song name. Album tag stays unchanged ("recognize - Single").
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
[youtube:tab] in stderr fires for all YTM search page processing, not just
artist channel expansion, causing channelExpanded=true for every search and
returning [] — bypassing all YTM cascade steps entirely. Fix: only check
stdout for /browse/UC (actual channel URL in results). Remove ytmsearch5:
scheme which is not supported by this yt-dlp version. Revert pool to 10.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ytmsearch5 (yt-dlp native YTM search) added as step 2 — returns different
result ordering than the manual YTM URL, recovering songs that the URL
approach ranks too low. YTM pool expanded from 10 → 20 results to reduce
fallthrough to YouTube.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
spawnYtdlp now always resolves so the hasAudio() cascade continues when a
single YTM result is geo-blocked or unavailable. Error is still logged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
YTM search used `title` directly, so "recognize - Single" was searched on
YouTube Music instead of "recognize" — causing no YTM results and falling
through to the less reliable YouTube search cascade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Matches spotdl behaviour; still covers YouTube intros/outros while
reducing false positives from similarly-lengthed wrong songs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Track path: always fetch iTunes when durationMs/trackNum missing (not just genre)
- Both paths: strip release suffixes from title before iTunes lookup
- Log "Album:" instead of "Album aus Spotify:" (also fires for iTunes global search)
- Log duration when fetched from iTunes so it's visible in job log
Ensures all importers have consistent yt-dlp search filters (duration filter
now always runs when iTunes finds the track).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- downloads.js: use durationMs (Spotify) before trackTimeMillis (iTunes)
- server: read durationMs from top-level body, not only from tracks[0]
- server: also grab durationMs + trackNum from iTunes in the Spotify path
Without durationMs the duration filter never ran, allowing a 58s fan video
to pass all text filters and be downloaded instead of the actual song.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Handles all Spotify trailing suffixes: Single, EP, Remaster, Deluxe Edition,
Bonus Tracks, B-Sides, bare years (2024), Official Music Single, etc.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- "recognize - Single" title → search uses "recognize" (strips "- Single/EP/Album/…")
- Adds Single/EP/Remaster/Deluxe/… to titleKws STOPWORDS so they never become
mandatory title-match filters (no YouTube video title contains "Single")
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Western tracks now use artist + title only (matches spotdl behaviour).
Album suffix was causing noise for short English titles like singles
where album name = title + "Single".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Album "recognize - Single" produced query "MINA - recognize recognize Single"
because "recognize" appeared in both title and album name. Now filters album
words already present in the title, plus generic words (Single, EP, Remaster…).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deletes backend/lib/squid.js; strips all references from downloader.js,
config.js, novasonic-server.js, docker-compose.yml, .env.example, README,
and CHANGELOG. Default DOWNLOAD_BACKEND is now ytdlp.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Colons in startDateTime were being percent-encoded which Ticketmaster
may reject. Also log errors/status so backend logs reveal the cause
when no events appear.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend: swap /api/events/:artist to use Ticketmaster Discovery API
(startDateTime=now, classificationName=music, size=20, sort by date).
Maps TM response to the same {datetime, venue, offers} shape the modal
expects. Returns [] if TICKETMASTER_API_KEY is not set.
Frontend: remove all debug console.logs from events.js; drop redundant
client-side date filter (TM already filters by startDateTime server-side).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- edit-metadata: use albumArtistSort (when provided) for artist folder naming
instead of auto-converted romajiAlbumArtist — ensures manual overrides sent
by the download pipeline also place files in the correct folder
- Add POST /api/fix-artist-sort endpoint: searches Navidrome for all songs
where artist or albumArtist matches the kanji, re-writes ARTISTSORT +
ALBUMARTISTSORT via ffmpeg (-map_metadata 0 preserves all other existing
tags), moves each file to the correct artist folder, cleans up empty old
dirs, triggers Navidrome rescan — returns a job ID for progress tracking
- addRomajiOverrideUI: after saving a new override, calls fix-artist-sort and
shows a job card with live progress
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Five separate root causes fixed:
1. buildTitleKeywords (ytdlp.js): For Japanese titles \w strips all
kana/kanji leaving no keywords. Now uses the first whitespace-
delimited segment as a literal regex keyword so --match-filter
title ~= requires the YTM video to contain the Japanese text.
2. Tokenizer wait (ytdlp.js): If kuromoji hasn't finished loading when
a Japanese download starts, romajiForStorage returns the original
Japanese text unchanged — producing Japanese folder/filenames and
skipping sort tags. Now waits up to 30 s for the tokenizer.
3. titleOnlyMatch guard (metadata.js): iTunes titleOnlyMatch (no artist
check) was used as a fallback for all searches. For Japanese
titles/artists this matched wrong US songs with identical titles
(common in anime OSTs). Now titleOnlyMatch is skipped when the
search title or artist contains Japanese characters.
4. squid.js rawArtist: rawArtist was not in squid's signature and
never forwarded to embedMetadata, so Qobuz-downloaded J-Pop tracks
stored the iTunes-resolved English name as artist instead of the
Japanese original. Fixed by accepting and forwarding rawArtist.
5. squid.js romaji paths: Folder and filename used raw artist/title
with no romaji conversion, creating Japanese-named paths
inconsistent with the yt-dlp path. Now uses romajiForStorage same
as ytdlp.js.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When Navidrome has a Japanese path in its DB but the file was saved with
a romaji filename by the download pipeline, the existing fallback chain
compared the Japanese title stem against romaji filenames and always
failed. Added _findFileByStemOrRomaji which converts the stem via
romajiForStorage before the final recursive search, covering all three
endpoints: edit-metadata, delete-song, song-sorttags.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- /api/delete-song endpoint: resolves path via same fallback chain as
edit-metadata, validates file is inside music root + is an audio file,
deletes it, then prunes the album folder if empty and the artist folder
if that too becomes empty — no recursive rm, depth strictly limited to
two levels above the file so no accidental mass-deletes
- Safety: path.resolve() guard ensures no path-traversal outside music root;
rmdirSync (non-recursive) fails if dir is non-empty as extra safety net
- Frontend: delete button (red outline, trash icon) at bottom of edit mode
with confirmation dialog showing title + folder-cleanup warning
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Handles "File not found" for songs with non-standard folder/artist names
(e.g. artist separator • or year-only album folders) by doing a deep
recursive title-stem search as a last resort after rebase and dir scan.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/api/itunes/lookup was returning 400 for any request without artist+title,
including id-based lookups (?id=...&entity=album) used by the artist page.
Use next() to fall through to the /:endpoint proxy for those requests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When iTunes enrichment returns a jingle/preview as the first result (e.g.
trackTimeMillis=9000), the yt-dlp duration filter becomes impossible
(duration > 30 & duration < 24), all filtered steps fail, and the final
unfiltered fallback downloads whatever YouTube returns first — often a
short clip.
Fix 1: spotify.js — only copy iTunes trackTimeMillis to durationMs when
it is > 30 seconds (genuine song, not a jingle or preview).
Fix 2: ytdlp.js — only enter the duration-filtered search branch when
durationMs > 30 000ms, so a corrupted short value silently falls back to
the unfiltered search path which still finds the correct full song.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a file is renamed or moved to a new artist/album folder during
metadata edit, Navidrome assigns a new song ID, breaking all playlist
entries that referenced the old ID.
Before the edit: collect which playlists contain the song.
After rename + rescan: poll Navidrome until the new song appears,
then update each affected playlist (add new ID, remove old index).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When Navidrome has a stale index (e.g. file was renamed from
'01 - Idea 10.mp3' to '01 - Gibran Alcocer - Idea 10.mp3'),
_findFileInDir now falls back to the original file's directory
and matches by track-number prefix ('01 - ') so the renamed file
is found even without a Navidrome rescan.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Spotify enrichment only copies album/trackNum/duration from iTunes.
Now also fetch totalTracks, discNum, discTotal from iTunes for:
- alreadyEnriched playlist tracks
- single track downloads where album came from Spotify
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. JP genre mapping table in metadata.js — when only JP iTunes has the song
(niche Japanese artists), map localized JP genre names (アニメ, ポップ, etc.)
to English equivalents so files never get Japanese characters in genre tag.
2. Spotify playlist import (alreadyEnriched path) — was skipping iTunes lookup
entirely, leaving genre empty. Now does a lightweight genre-only lookup.
3. Single track download with Spotify album — was skipping iTunes when album
already known from Spotify. Now also fetches genre from iTunes when missing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When Navidrome and the backend run in separate containers, Navidrome returns
absolute paths rooted at its own mount (e.g. /opt/navidrome/music) which
don't exist in the backend container (mounted at /music).
Add three-stage fallback:
1. _rebasePath: strip leading path components and try under CONFIG.musicPath
2. _findFileRecursive: exact filename search under CONFIG.musicPath
3. _findFileInDir: fuzzy search in album dir by track-number prefix or title stem
(handles files renamed to include artist in the filename)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JP store returns localized genres (e.g. イージーリスニング instead of Easy Listening).
Fetch JP and US in parallel, use JP match for title/album/artwork but always
take genre from US result so genre is always in English/Western characters.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevent false Japanese matches by never using JP title-only fallback.
Strategy: JP strict → US strict → US title-only so niche Western tracks
still resolve but same-English-title Japanese songs don't pollute results.
Also fix JS error in autofillItunes: ie-total → ie-totaltracks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Artist grid and detail header now show romaji names when toggle is on.
Builds a Map<artistId, artistRomaji> from the existing song romaji index
so no extra API call is needed.
- Apple Music autofill button added to edit modal next to MusicBrainz.
Reuses the existing lookupItunesTrackMeta backend function via a new
GET /api/itunes/lookup endpoint — fills album, year, genre, track#,
and cover art.
- Romaji index is invalidated after every successful download so newly
added songs appear with correct sort tags without requiring a page reload.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When kuromoji marks a katakana token as reading '*' (unknown word — common
for band names like ヨルシカ), the code fell back to the surface_form which
is still katakana. _hiraToRomaji can't handle katakana so it passed through
unchanged, causing sort tags to show ヨルシカ instead of Yorushika.
Fix: if the surface_form contains katakana and reading is *, convert it
mechanically via _kataToHira + _hiraToRomaji — katakana-to-romaji is always
unambiguous and needs no dictionary.
Also add a Japanese-char guard in the frontend when applying romaji index
values, so stale cache entries can never overwrite correct values with Japanese.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- backend: new GET /api/song-sorttags reads TITLESORT/ARTISTSORT/ALBUMARTISTSORT/
ALBUMSORT directly from the file via ffprobe
- ui.js: pre-fill sort fields from romaji index, then overwrite with real file
tags from the backend (shows actual values even if manually set)
- index.html: remove redundant hint texts in settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>