- Move player.js + nowplaying.js → src/player/
- Move views.js + spotify/soundcloud/youtube/abs → src/views/
- Move ui.js + components.js → src/ui/
- Update all import paths (static + lazy) across every file
- MusicService: wrap ExoPlayer in ForwardingPlayer that always advertises
next/prev commands so the notification skip buttons stay visible in radio
mode even when the JS queue has only one native track
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Await romaji overrides before restoring player bar so disp* functions
have the cache ready on first render after reload
- dispArtistParts now carries originalName so artist clicks pass the
Japanese name to Navidrome/iTunes search while displaying romaji
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause of all Android player bugs: two competing queue managers.
ExoPlayer held all tracks and auto-advanced with its own index while JS
tracked S.qIdx separately. seekToIndex() is async via runOnMain, so
skips would interleave with ExoPlayer's own advances — causing desync,
wrong-song display, and skips that silently did nothing.
Fix: ExoPlayer always holds exactly ONE MediaItem. JS is the single
source of truth for queue order, shuffle, and repeat.
JS changes (player.js):
- playQueue/playCurrent/nextSong/prevSong/toggleShuffle: replace
setQueue(allTracks, idx) + seekToIndex with setQueue([single], 0)
- _pushQueueIfBridge: send only current track
- nativeInsertAt: no-op (radio songs land in S.queue; JS picks them up)
- fillRadioQueue: remove addToQueue native call
- onTrackChanged: simplified to just schedule scrobble + radio top-up
- onPlaybackStateChanged: remove index-reconciliation block
- onPositionUpdate: remove reconciliation tick (no queue drift possible)
- onQueueEnded: call onEnded() — JS drives what plays next
- toggleRepeat: setRepeatMode('one') only for repeat=one, else 'off'
- Add onNotificationNext / onNotificationPrev to NSPlayer
- Remove _skipping/_skipTimer/_reconTick/_setSkipping (all dead code)
Android changes:
- MusicService: add onSkipToNext/onSkipToPrevious to StateListener;
add MediaSession.Callback to intercept notification skip buttons and
delegate them to JS instead of letting ExoPlayer handle them
- MainActivity: implement new StateListener methods → callJs()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reads ffprobe metadata from every audio file and places it into:
{dest}/{AlbumArtist}/{Album}/{NN - Artist - Title.ext}
Exactly mirrors the sanitize(), folder and filename rules in
backend/lib/ytdlp.js. Dry-run by default; --move to execute, --clean
to remove empty source dirs afterwards.
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>
- Fix missing closing brace for `if (ni >= 0...)` in nextSong — the removed
`else { _nsBridge('next') }` left the inner if-block unclosed, causing
vite/rollup to reject player.js as invalid JS
- Apply romaji override map to iTunes (executeDownload) and Spotify
(executeSpotifyDownload) download payloads — sends artistSort/albumArtistSort/
titleSort/albumSort fields so the backend writes correct sort tags to the
downloaded file and can organize into the right artist folder
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a maintained list in Settings where you can map kanji artist/album/title
strings to their correct romaji reading (e.g. 照井順政 → Terui Yoshimasa).
Overrides are applied last (highest priority) when the edit-metadata modal
opens, so saving the song writes the correct sort tags permanently to the file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When reaching the last shuffled song, the UI changed but audio kept playing
the same song. Root cause: ExoPlayer still had its own shuffle mode on (set
in onReady), so its auto-advance and _nsBridge('next') picked random indices
that conflicted with the pre-shuffled JS array.
- setShuffleMode always false: we own the playback order via pre-shuffled
JS array, ExoPlayer must advance sequentially through it
- Remove _nsBridge('next') fallback: never let ExoPlayer decide the next
song autonomously. End of queue with no repeat = let ExoPlayer stop.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The pseudo-element added for the flicker fix had position:absolute;inset:0
with no pointer-events:none, so it sat over the entire overlay and consumed
every click — queue items, lyrics lines, buttons. Add pointer-events:none.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getPlaylists returns stale/wrong songCount from Navidrome's index.
After rendering the grid, fire one getPlaylist request per playlist
in parallel and update each card's subtitle as responses arrive.
Counts are visually corrected within a second of the page loading.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NP overlay flicker (stronger fix):
- Remove contain:layout style paint (causes layer issues on some Android WebView)
- Add ::before pseudo-element with solid background so no frame can be transparent
- Add -webkit-transform prefixes for both open and closed states
Shuffle fully random:
- playQueue no longer pins startIdx when shuffle is on
- All songs shuffled into a completely random order, S.qIdx=0 always
- toggleShuffle mid-queue still pins current song first (Spotify toggle behavior)
Playlist count wrong in overview:
- Navidrome's getPlaylists returns stale/wrong songCount in its index
- Add data-pl-id to each playlist card in loadPlaylists
- After loadPlaylistDetail gets the accurate entry count, backpatch the
matching overview card's subtitle so the grid shows the real song count
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace will-change:transform with translate3d(0,100%,0) / translate3d(0,0,0).
will-change triggers a layer-creation/destruction cycle on WebView — when the
player bar DOM updates during a skip, the compositor can briefly expose the
layer's clear back buffer, flashing the content below through the overlay.
translate3d pins the element to a stable GPU layer from the start.
Also add contain:layout style paint to isolate the overlay's paint context
from external repaints, and -webkit-backface-visibility for older WebView.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- playQueue: unified shuffle for both web and Android — pre-shuffles the
songs array with the tapped song pinned first, saves _origQueue for restore.
Passes S.qIdx (0 when shuffled) to NSBridge.setQueue instead of startIdx.
- _computeNextIdx: removed the random-pick shuffle branch entirely.
Since the queue is already shuffled, sequential traversal IS the shuffle order.
- toggleShuffle: removed !HAS_BRIDGE guard so queue reorder runs on Android too.
Re-sends reordered queue to ExoPlayer so native order stays in sync with JS.
Removed setShuffleMode call (we manage order ourselves via pre-shuffled array).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove await on fetchRomajiIndex() at startup so navigation is not blocked
by the romaji index network call (was the main cause of 30s Android startup)
- Add window._imgRetry() with 3 retries (1.5s/3s/4.5s backoff) for all card/
song/cover images that fail to load; falls back to emoji only after exhausting retries
- playQueue with HAS_BRIDGE + shuffle: pick a random startIdx so album/playlist
playback starts at a random track, not always index 0
- Reduce mobile genre grid limit from 12 to 6
- Fix "Global Suche" badge text: was "iTunes Store + Local", now "iTunes Store"
- deleteSong: always refresh loadPlaylists() unconditionally (not just when
page is active) with a 3s delay so song counts are current when navigating back
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Guard both snapshot-reconciliation paths (_skipping window can return the
old index before ExoPlayer commits the new one):
- onPlaybackStateChanged: skip the safety-net snapshot read while _skipping
- onPositionUpdate: reset _reconTick (defer recon) while _skipping
- _setSkipping: always reset _reconTick on every new skip/seek
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>
- Call reconcileNativeState() on visibilitychange→visible so the player UI
updates immediately when the user opens the app after a background auto-advance,
before deferred evaluateJavascript callbacks have had a chance to fire
- After deleteSong() succeeds, refresh the active playlist view (grid or detail)
with a 2s delay to let Navidrome's background scan complete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>