The direct-URL (sourceUrl) path returned { success: true } even when no audio
file was produced (e.g. YouTube 403 / bot check), so the job was marked
"done/Fertig!" and web + Android both said the track was added. It now returns
{ failed: true } → the server sets status 'error' with a clear message, which
both frontends already render as a failure. Search-cascade path was already
correct.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
YouTube now returns "Sign in to confirm you're not a bot" for yt-dlp from
datacenter IPs, blocking downloads, search and link-resolve. When a cookies file
is present (YTDLP_COOKIES, default /data/youtube-cookies.txt) every yt-dlp call
passes --cookies so it authenticates. Checked at call time (drop the file in, no
code change); absent → unchanged behaviour. Applied to the download cascade, the
YTM search probe, and the importer resolve.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes same-name collisions where NovaSonic guessed the wrong Deezer artist (e.g.
'Nana' → a random DJ): fans, top songs, missing albums and the hero image were all
off. A '⚙ Falscher Künstler?' button on the artist page opens a picker (same-name
Deezer candidates with image + fan count); the chosen mapping is stored SERVER-SIDE
(shared across users, like the romaji overrides) in /data/deezer-artist-overrides.json.
Once pinned, the artist page short-circuits the name search to the pinned Deezer id
(drives albums/top-songs/fans) and overrides the hero image with the pinned artist's
Deezer picture (NovaSonic-side, so it works even for tag-merged same-name artists —
Navidrome can't disambiguate them without an MBID). New endpoints
GET/POST/DELETE /api/artist-overrides; api.js load/get/set/remove; loaded at startup.
SW cache -> ns-v74. Needs on-device runtime test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
findExistingTrack split the filename at the first ' - ' assuming 'Artist - Title',
so a title with internal dashes ('Kingdom Hearts 2 - Dearly Beloved - Main Theme')
or a garbage/absent artist prefix ('01-01 - …') was mis-parsed → the duplicate went
undetected and got re-downloaded (3-4x). Now match the query title against BOTH the
full stem AND the part after the first ' - ', so titles with dashes and prefix-only
filenames are recognized. Normal 'Artist - Title' files still match; no false
positives; remix-vs-base distinction preserved. Unit-tested on the exact KH case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stufe B routed /api/meta/lookup through the combined resolver, so the editor's
'Deezer' button could silently return MusicBrainz data while the status said
'von Deezer'. The editor already has a dedicated 'MusicBrainz' button, so keep the
two explicit and honest: Deezer button = Deezer only. The combined
Deezer→MusicBrainz→clean resolver stays on the DOWNLOAD paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause: deezer pickTrackMatch fell back to a blind title-only match, grabbing a
same-titled song by a completely different artist (e.g. 'Memories' by Rab Noakes for
'☆NoAki☆') and applying its album/cover/year. The download's clean fallback existed
but was never reached because the lookup almost always returned *something*.
Stufe A — deezer.js: the title-only fallback now REQUIRES an artist correspondence
(equal, or one name contained in the other after normalization). No match → {} →
the existing clean fallback finally runs (album_artist = song artist, album =
'Singles', cover = source thumbnail, year/genre untouched). Test that encoded the old
blind behaviour replaced; NoAki + loose-match regression tests added.
Stufe B — musicbrainz.js: a rate-limited (~1/s, User-Agent) MusicBrainz recording
lookup with the SAME artist discipline + Cover Art Archive cover. trackmeta.js is the
single resolver (Deezer → MusicBrainz → clean fallback); the three single-track call
sites use it (album batch stays Deezer-only to avoid MB rate-limit slowdowns).
Unit-tested (17/17) + live-verified: NoAki→clean fallback, real track→Deezer, MB finds
real albums when Deezer misses. Backend-only; no frontend/Android change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
findExistingTrack normalized titles by stripping ALL bracketed content, so
"Therapy (Remix)" collapsed to "Therapy" and was skipped because the base
track existed. Keep brackets that contain a version marker (remix/live/acoustic/
instrumental/vip/flip/edit/mix/…) so distinct recordings are recognized as new;
still strip noise brackets (feat. X, Official Video). Verified via Deezer that the
remix track carries the marker in its title. Unit-tested both directions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Apple's iTunes /search returns 403 from server/datacenter IPs, which had broken global
search, charts, album/artist/single downloads and the metadata-editor autofill in
production. Everything now sources metadata from Deezer (free, no key, more requests,
returns explicit versions + real artist images + durations). Web + Android share one
source, so an artist resolves to one consistent name everywhere. Deezer results are
mapped to the legacy "ItunesItem" shape, so the UI + download flow are unchanged.
Backend:
- New backend/lib/deezer.js: lookupTrackMeta, fetchAlbumTracks, searchAlbumTracks,
fetchArtistAlbums (+ pure pickTrackMatch/mapAlbumTrack for tests).
- metadata.js drops the iTunes lookups; novasonic-server download paths (track, album,
artist, playlist enrich, spotify artwork) + autofill now use Deezer.
- Artist download uses Deezer discography (real albums only), not the iTunes album search.
- Editor autofill endpoint /api/itunes/lookup -> /api/meta/lookup; removed the dead
iTunes /search proxy and the Apple charts RSS proxy.
- backend/test/deezer.test.js: unit tests for numbering, mixed-artist album-artist
stability, explicit flags, the Japanese-title match guard (npm test; RUN_LIVE=1 for live).
Web:
- api.js search/lookups already on Deezer; charts now use Deezer /chart. Artist cards
show the real Deezer photo. Visible "iTunes"/"Apple Music" labels -> "Deezer"
(search badge, charts badge, preview bar, editor button, artist heading). SW ns-v51.
Android:
- New DeezerModels.kt with Dz* models + mappers to ItunesItem/ItunesRssEntry.
- ItunesClient is Deezer-backed (search = tracks+albums+artists in parallel; album/artist
lookups; /chart charts), same method signatures. Visible strings -> Deezer.
Docs: README, CLAUDE.md and CHANGELOG (1.3) updated; Platform Parity Matrix reflects the
shared Deezer source + Android non-local artist page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`app.get('/api/deezer/*', …)` — a bare "*" — throws "Missing parameter name" in
Express 5's path-to-regexp at startup, so the server never came up and EVERY endpoint
(abs-proxy, romaji-overrides, deezer, …) failed with a CORS/ERR_FAILED in the browser.
Use a RegExp route instead, which registers cleanly on both Express 4 and 5. Verified
against express 5.2.1: bare "*" crashes, the regex is OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Apple's iTunes /search returns 403 from server IPs, killing the Erkunden search. Deezer
is free, needs no key, and is actually better: it returns explicit versions, artist
images, track durations and 30s previews.
- Backend: /api/deezer/* proxy (CORS-safe); album download accepts a `tracks` array in
the request body (Deezer albums send their tracks directly, pre-mapped).
- api.js: itunesSearch / itunesLookupAlbum / itunesLookupArtistSongs|Albums now hit
Deezer and map results to the existing ItunesItem shape, so the UI + download flow are
unchanged. Function names kept.
- downloads.js: album downloads fetch the Deezer album's tracks and send them in the body.
iTunes /lookup (by ID) and the charts RSS proxy stay. Known follow-ups: artist-type
download still calls iTunes /search server-side; the metadata-editor iTunes autofill;
and the Android client — all still on iTunes for now.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Confirmed via curl from a clean IP: Apple's iTunes /search returns 403 (blocked
server-side, likely datacenter-IP anti-scraping) while /lookup?id= still returns 200.
So the Erkunden Apple search fails through no fault of ours.
- Charts: the "most-played" RSS (rss.applemarketingtools.com) was fetched directly
from the browser and hit CORS; proxy it via /api/itunes-charts/:kind.
- Error visibility: itunesSearch records the last outcome (HTTP status / network);
the global-search view now shows "Apple-Suche gerade nicht verfügbar" with the
reason (403 = Apple blocks the API, not you) instead of a blank "no results".
- iTunes proxy now forwards the upstream status code instead of always 200.
Deezer (free, no key, ~50 req/5s) is the planned search fallback; MusicBrainz is too
rate-limited (1/s) for interactive search.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Search (web api.js) was JP-first, the download lookups were JP-first, and the Android
client was already US — so the same artist resolved inconsistently: sometimes kanji,
sometimes katakana, sometimes romaji, depending on which path and which store matched.
Switch everything to the US store:
- api.js itunesSearch → country=us (one fetch, matches the Android client)
- metadata.js lookupItunesTrackMeta → US only (drops the JP fetch: consistent names +
halves the download's iTunes calls, which also eases the rate limit)
- searchItunesAlbumTracks → country=us
- remove the JP-katakana guards in the album/artist download paths (redundant now, and
they wrongly romanized genuine Japanese artists like 羽岡佳 → "Kei Haneoka")
Titles still come from the source, so a Japanese title the user picked stays Japanese;
only iTunes-derived fields (album_artist, genre, year, artwork) follow the US store.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
embedMetadata already writes date/track/totaltracks/disc, but the single-track path's
"album already known" branch back-filled genre/total/disc/duration/track# from iTunes
and omitted the year — so a source track without a year got no `date` tag written.
Add !meta.year to the fetch trigger and merge genreMeta.year, so the release year is
set at download time like the other fields.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The iTunes lookup is JP-first (for correct Japanese titles/artwork), but the Japanese
store returns katakana-localized names for Western artists — so DJ Snake's single got
album_artist / folder "DJスネイク • アルーナジョージ" while the title/artist stayed English.
- lookupItunesTrackMeta: take the NAMES (album, album-artist, track, artist) from the US
store when the search isn't Japanese; keep JP names for actual Japanese songs.
- album & artist download paths: if iTunes' collectionArtistName is Japanese but the
request artist isn't, keep the request's English name for the tag and folder.
Already-downloaded files aren't rewritten — re-download or fix via the editor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The web Apple-Music search proxies through /api/itunes (browsers can't call
itunes.apple.com — no CORS). The proxy forwarded every request uncached, so heavy
download activity (each download does several album/track lookups) plus client
searches blew past Apple's ~20 req/min/IP limit and the search started returning
nothing. Add a 30-min in-memory cache keyed by the target URL; only genuine 200
responses (with the resultCount signature) are cached, never throttle/error pages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The strict cascade steps required the YouTube channel name to contain the artist, so
official uploads on label channels (BANGTANTV, HYBE LABELS, …) were rejected and the
no-channel fallback grabbed a fan reupload instead (observed: BTS "Pied Piper").
Add yt-dlp's music `artist` field as an OR alternative to the channel check in the
strict steps (two --match-filter args = OR). A metadata dump of "BTS Pied Piper"
confirmed only the official upload has artist=BTS (fan/live/lyric reuploads: NA), and
it's the #1 result — so it now passes strict step 2. channel_is_verified was rejected
as a signal: lyric videos and fan MVs also run on verified channels.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Apply the same collectionArtistName derivation to the per-album loop of the artist
download, so a collaboration/soundtrack in an artist's discography is not tagged with
the searched artist. Each track still gets its own iTunes artistName; album_artist
stays the iTunes album-level artist (consistent, no Navidrome splitting).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The album download tagged album_artist with the request's `artist`, which can be a
single track's performer — the lead single "TAKEDOWN" is by TWICE, so a whole
KPop-Demon-Hunters soundtrack got album_artist=TWICE on every track. Derive the album
artist from the iTunes tracks' collectionArtistName (the album-level artist, identical
across the album) and fall back to the request artist only if iTunes omits it. This is
the documented intent (mainArtist = collectionArtistName when available) and keeps
album_artist consistent so Navidrome doesn't split the album.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
downloadAlbumTracks passed each track's artistName as `artist` but no `rawArtist`,
so in ytdlp.js the YTM/YouTube search query and the channel filter used
`mainArtist = albumArtist || artist` — always the album-artist blob. On mixed-artist
soundtracks that matched nothing: e.g. TWICE's "Strategy" and MeloMance's "Love,
Maybe" were searched as "KPop Demon Hunters Cast, HUNTR/X & Saja Boys <title>" with
`channel ~= KPop`, and failed with "Nicht eindeutig gefunden".
Pass rawArtist = track.artistName so the search + channel filter use the actual
performer ("TWICE Strategy", channel ~= TWICE). The album_artist tag still comes from
`albumArtist`, so album grouping is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Artists like ".diedlonely" (leading dot, †, ✝, _ …) weren't surfaced by iTunes,
so the download's metadata lookup fell through to a titleOnly match — grabbing a
same-titled track by another artist (e.g. the "In the Bleak Midwinter" Christmas
carol: Holiday genre, 176s single) and steering the YouTube search to the wrong
(slowed) version.
- backend/metadata.js: strip leading non-alphanumeric from the search artist and
the first-word comparison so the real artist matches (and the right album/genre/
duration are used).
- android ExploreViewModel: strip leading symbols from the search term so the
Explore tab finds these artists.
Keeps the original name for display/storage — only the search/compare is trimmed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two-part fix for "[Unknown Album]" vs "Singles" mismatch that broke the
metadata editor (File not found):
- Download: always write an album tag, defaulting to "Singles" so it matches
the on-disk folder (album || 'Singles'). No-album tracks no longer show as
"[Unknown Album]" while living in a "Singles" folder. (Future downloads.)
- Editor resolve: Subsonic getSong returns a *virtual* tag-built path (e.g.
"[Unknown Album]/Title.mp3" with raw "|"), not the physical file ("Singles/
Artist - Title_YTPMV.mp3"). Add findExistingTrack as a final fallback, and
make its normalizer map the sanitized chars (/ \ : * ? " < > | and _) to a
space so a title tag matches the sanitized filename. (Fixes existing tracks.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Artist name from Spotify/source is stored directly in folder, filename,
and tags without being replaced by the iTunes artistName. iTunes is used
only for supplemental metadata (genre, year, cover, duration, trackNum,
albumArtist/collectionArtistName). Sort tags still receive romaji via
romajiForStorage() in ytdlp.js.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consistent with point 7: original (kanji) names in folder, filename,
and main tags — romaji only in ARTISTSORT/ALBUMARTISTSORT.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Japanese Art Tracks on YTM/YouTube are often romanized ("Souiunatsu"
for "そういう夏") — the Japanese keyword filter in --match-filter always
rejected the correct video. Channel + duration are sufficient quality
gates for JP tracks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Album words ("KAGUYA Music", "OST" etc.) appended to the search query
caused wrong or no results — "Kei Haneoka 確信 KAGUYA Music" would miss
tracks the plain "Kei Haneoka 確信" query finds immediately.
Artist + cleanTitle is sufficient for both JP and Western tracks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Japanese tracks stored with original names in folder/filename/tags; romaji only in sort tags
- findExistingTrack: case-insensitive, searches all album folders under artist, strips release suffix
- --max-filesize 30M + 2x duration cap on all yt-dlp calls
- iTunes collectionArtistName captured as album_artist; fixes compilation album splitting
- album_artist tag uses mainArtist (not rawArtist) so all tracks in one album are consistent
- No-duration cascade: steps 5-8 (title-only/no-filter) removed — fail cleanly like with-duration
- Autofill iTunes: fills title, artist, albumArtist, discNum (was album/genre/year/track/total only)
- Autofill MusicBrainz: fills albumArtist, discNum (was missing both)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a track's duration is known but nothing matches the duration window, the
cascade no longer falls back to blind title-only/no-filter grabs (which pulled
same-titled videos by other uploaders, e.g. a "recognize" fan clip). Instead it
relaxes only the duration constraint while still REQUIRING the artist channel +
title. If nothing matches, the download produces no file and the job now fails
cleanly with "Nicht eindeutig gefunden — bitte Direktlink im Importer nutzen"
(previously it falsely reported success with no file).
Single, album and playlist paths all surface the not-found state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-add the ¶ms= Songs-shelf filter (the documented way to song-search
YouTube Music) so results exclude videos/playlists/clips. Verified neutral
for the ambiguous-artist case (yt-dlp's search can't surface that track
regardless) but cleaner for normal tracks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The filename used the raw title, so "recognize - Single" leaked into the
filename ("01 - MINA - recognize - Single.mp3"). Now the cleaned title drives
both the filename and the embedded title tag, giving "01 - MINA - recognize.mp3"
(Track - Artist - Title). The album folder keeps the full album name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After the embedded-title change (recognize vs "recognize - Single"),
Navidrome's title-derived stem can drop the "- Single"/"- EP" suffix the
on-disk filename still carries, so _findFileByStem matched neither exact nor
suffix and every fallback failed -> "File not found on disk" on edit/delete.
Now also match on the structured prefix in both directions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>