- /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 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>
- 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>
- 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>
- views.js: skip ASCII word-match when artist name is non-ASCII so Japanese
artists (e.g. ヨルシカ) resolve correctly to an iTunes artist ID
- index.html + styles.css: collapsible Sort-Tags section in edit modal
- ui.js: pre-fill sort fields from romaji index; include in save body
- novasonic-server.js: accept manual titleSort/artistSort/albumArtistSort/albumSort
overrides; fall back to auto-computed romaji when not provided
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Downloads now store original Japanese in TITLE/ARTIST/ALBUM tags
so Apple Music, iTunes, YTM etc. all see the authentic original text
- Sort tags (TITLESORT/ARTISTSORT/ALBUMSORT) written with romaji for
apps that use them for sorting/display
- iTunes-resolved English artist name used for ARTISTSORT and folder names
when available (more accurate than kuromoji for known artists)
- rawArtist parameter added to downloadTrack so YTM search query uses
the original Japanese channel name (fixes wrong song downloads)
- spotify.js: artistName preserved as original; itunesArtistName stored
separately so the Spotify importer shows Japanese titles
- romaji index now includes titleRomaji/artistRomaji/albumRomaji fields
- Frontend: showRomaji setting (Settings > Bibliothek) — when on, player
bar, now-playing, queue, and song lists all show romaji via index lookup
- refreshDisplayMode() updates all live UI when the toggle is saved
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Clean Japanese punctuation (、。・) in romajiForStorage so it doesn't pass through
- Add POST /api/romaji-convert endpoint for batch title conversion
- Add romajiConvert() to api.js
- Add third fallback in _createNdPlaylist: convert Japanese title to romaji and
search Navidrome with it (for songs already downloaded with romaji tags)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- spotify.js: stop overwriting trackName with iTunes English title —
yt-dlp must search with original Japanese title to find the right song
(was downloading English covers e.g. Ghost In A Flower instead of 幽霊花)
- metadata.js: add title-only fallback in lookupItunesTrackMeta so tracks
are found even when kuromoji gives wrong artist romanization (e.g.
Terui Jun Sei vs iTunes Yoshimasa Terui); also return itunesArtistName
- novasonic-server.js: use itunesArtistName from iTunes lookup as the
stored artist when available (single-track and playlist download paths)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ytdlp.js + novasonic-server.js: apply romajiForStorage to album tag
and folder/rename paths (was only applied to artist/title before)
- ui.js: _createNdPlaylist falls back to romaji index for Japanese titles
since Navidrome search3 doesn't index Japanese text properly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Search iTunes with original Japanese names (accurate) rather than
pre-converting with kuromoji (inaccurate for uncommon names/readings).
iTunes returns the correct English artist name and title directly.
Also run iTunes enrichment for single-track Spotify imports.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add romajiForStorage() to romaji.js — smart word spacing + Title Case
- ytdlp.js: apply to folder names, filenames, and embedded tags
- novasonic-server.js: apply in edit-metadata endpoint and Spotify resolve
- spotify.js: update track artist/name from iTunes canonical English names
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Space as fallback separator on save when no explicit separator present
(Artist1 Artist2 → Artist1;Artist2)
- & and / added to explicit separator set everywhere
- navidromeArtist (download path) also handles /
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_fmtArtists and _normalizeArtists now split on & and / in addition to
• · ; , — so "Arriez & King Dif" typed in the field normalizes correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
"Arriez & King Dif" was used as-is for the folder; now splits on , ; &
so only the first artist is used: "Arriez".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Normalize •, ·, ;, , separators on save (both edit and download paths)
- Fix ndSong scope bug (was inaccessible outside try block in edit-metadata)
- Apply _normalizeArtists to artist/albumArtist before ffmpeg tags and file rename
- Storage format: Artist A;Artist B (no space, Navidrome/MusicBrainz style)
- Display format: Artist A; Artist B (space, via _fmtArtists in frontend)
- Show separator hint below label in metadata editor fields
- Remove hover effect on player bar song title
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Naming convention (download + edit-metadata):
Before: {01 - }Title.ext
After: {01 - }Artist1, Artist2 - Title.ext
- ytdlp.js: builds fileBase with artist prefix; findExistingTrack checks
both new and old format so existing files aren't re-downloaded
- metadata.js: findExistingTrack accepts optional safeArtist param
- novasonic-server.js: file rename on title edit now includes artist
(falls back to Navidrome's stored artist if not provided in body)
Song list subtitle: hide album name when album === title (singles where
the album is named after the track appear to duplicate the title)
Player bar: removed .player-title:hover underline/colour effect
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After saving metadata with ffmpeg:
1. Title change → rename the mp3 in place to "{01 - }?{sanitized title}.ext"
(same convention as the downloader uses)
2. Album-artist or album change → compute new {artistFirstPart}/{album}
directory, move the file there (create dirs as needed)
3. Old album dir is deleted if empty after the move; old artist dir
is deleted too if it also becomes empty
When only the album name changes (no artist change) the old in-place
folder-rename behaviour is preserved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
iTunes: split artist by [\s,;/] not just space so "VXLLAIN, iGRES"
yields first word "vxllain" (no trailing comma) and matches correctly.
MusicBrainz: extract first artist from the full credit string (split on
,;&•) and use `artistname` field — MB stores credits like "VXLLAIN feat.
iGRES & ENXK" which never matched the comma-joined full string.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Info modal view now shows Album-Künstler row from Navidrome albumArtist
- Song Importer detects youtube.com / youtu.be URLs (red badge)
- New /api/youtube/resolve endpoint: yt-dlp --dump-json for single
videos and playlists (up to 50 entries), enriched with iTunes lookup
for proper album/year/genre/cover on single tracks
- New src/youtube.js renders preview card and delegates download to
executeSpotifyDownload (which already handles sourceUrl → direct yt-dlp)
- Importer hint updated to list YouTube as a supported source
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds an editable Album-Künstler input to the info modal. Populated from
Navidrome's albumArtist field on open; saved as album_artist tag via
ffmpeg. Explicit value takes precedence; falls back to deriving from
artist (first semicolon segment) when artist is changed without an
explicit albumArtist.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SC resolve: strip yt-dlp placeholder albums (starting with '['), use track title as album for singles
- edit-metadata: if Navidrome path stale, search music root recursively for file by name
- Both single and playlist SC track handling updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SC tracks: use artworkUrl/album/genre fields (not iTunes artworkUrl100/collectionName/primaryGenreName)
- Playlist handler: same SC field fallbacks for artwork, album, genre, year
- ytdlp.js: convert & to , in album_artist tag so Navidrome albumartist.Split works
- Importer: clear both previews on any URL input change, not just on clear
- edit-metadata: rename album folder on disk when album name changes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New src/soundcloud.js: resolves SoundCloud URLs via yt-dlp --dump-single-json,
renders track/playlist preview with per-track download buttons
- Backend POST /api/soundcloud/resolve: extracts metadata (title, artist, genre,
artwork, duration) directly from SoundCloud — no iTunes lookup needed
- ytdlp.js: sourceUrl param — if set, downloads directly from URL instead of
ytsearch cascade (used for SoundCloud tracks)
- Spotify importer page renamed to Song Importer, now contains both
Spotify and SoundCloud sections in one view
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-map 0:v? copies the embedded cover stream from the original file when
no new artwork is supplied, preventing it from being silently stripped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Info modal gets an edit pencil button that switches to edit form
- Editable fields: title, artist, album, genre, year, track#, disc#, total tracks
- Cover art: upload your own image or auto-fetch from Cover Art Archive via MusicBrainz
- MusicBrainz button queries /api/musicbrainz/lookup and autofills all fields
- Save POSTs to /api/edit-metadata which rewrites tags via ffmpeg and triggers Navidrome rescan
- Backend: GET /api/musicbrainz/lookup + POST /api/edit-metadata endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Page loads instantly with song-count order, then silently re-sorts
once the backend aggregates play counts across all songs. Result cached
20 min server-side to avoid repeated full-library fetches.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- app.get/delete for /api/romaji-index were placed before const app = express()
causing ReferenceError on startup — moved into the ROUTES section
- kuromoji moved from backend/package.json to root package.json so Docker
picks it up during the existing npm install step
- Fixed dicPath resolution to use path.dirname(require.resolve(...))
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- Add /api/romaji-index endpoint using kuromoji morphological analyzer
- Fetches all Navidrome songs, converts kanji → hiragana readings → romaji
- Only indexes songs with Japanese characters; caches for 20 min
- /api/romaji-index DELETE clears cache (force rebuild)
Frontend:
- fetchRomajiIndex() in api.js: lazy-fetches and caches the index
- doLocalSearch: when query is romaji (ASCII), queries romaji index in
parallel with Navidrome search3 and merges+deduplicates results
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Frontend was calling itunes.apple.com directly; browsers block it (no CORS headers).
Added /api/itunes/:endpoint proxy on the backend and updated all itunesSearch/Lookup
calls in api.js to route through it when backendUrl is configured.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Server tracks _spRateLimitUntil and rejects retries instantly without
hitting Spotify, returning the remaining wait time. Retrying before the
window expired was resetting Spotify's timer (19s → 7s → 48s).
Frontend now shows a live countdown with a note not to retry early.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SpotipyFree crashes with KeyError: 'ownerV2' — Spotify changed their
internal API response format and the library is out of date.
curl_cffi presents a real Chrome TLS fingerprint so open.spotify.com/api/
server-time responds (plain Node.js TLS is filtered by Spotify's WAF).
Node.js TOTP fallback always failed on this for the same reason — removed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Log each Python stderr line separately (no 500-char truncation) so
we can see exactly what's happening in the bundle scan
- Try embed page (open.spotify.com/embed/...) first for GQL hashes —
focused app, fewer bundles, GQL chunks directly referenced
- Suppress noisy bundle-fetch errors to keep logs readable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
spotdl save hangs for 60s initializing its full pipeline. Rewrite
spotify_resolve.py to use curl_cffi (already installed) which
impersonates Chrome's TLS fingerprint directly. TOTP token fetched
in Python too. RATE_LIMITED propagates without TOTP fallback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
spotdl (SpotAPI) works without developer credentials. Remove the
health-check warning, credential error UI, and Client Credentials
fallback that were blocking the importer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>