- Change overlay from position:absolute to position:fixed so it isn't
clipped by main-content's overflow-y:auto
- Set overlay top dynamically from header.offsetHeight so it always
anchors exactly below the sticky header
- Responsive max-height: accounts for player bar + bottom nav on mobile
- Hide logo text on ≤359px screens to keep search pill wide enough
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the hidden toggle-based mobile search bar with a permanently
visible pill-shaped search input in the header row, next to the logo.
- Search is always visible — no button click required
- Typing shows a floating overlay with library results (artists, albums,
songs) without navigating away from the current page
- Clearing the input or navigating to any page closes the overlay silently
- The ✕ clear button appears only while text is present
- navigate() now calls closeHeaderSearch() so results disappear cleanly
when tapping any result or bottom nav tab
- Old toggleMobileSearch / closeMobileSearch kept as no-op stubs for safety
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>
Sort tag fields were not reset when opening a new song's edit modal,
so values from the previously opened song persisted until the async
romaji index fetch completed (or indefinitely if it found no entry).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
File sort tags containing Japanese characters are incorrect (sort tags
should be romaji/ASCII). Ignore them and keep the romaji index value
instead so the edit modal always shows the correct romaji pre-fill.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
romajiLookup returned null when the index wasn't loaded (only loaded when
showRomaji is ON). Now fetchRomajiIndex is called on-demand in the edit modal
so sort tags pre-fill from the index works even when the toggle is OFF.
Actual file tags from /api/song-sorttags still overwrite as the final step.
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>
On page reload the saved cfg restored showRomaji=true but _romajiMap was
never fetched, so all display helpers fell back to Japanese text. Await
fetchRomajiIndex before the first navigate when the setting is on.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
albumQuerySuffix and related consts (escRe, JP_CHARS, variantFilter, titleKws,
artistChannelPart) were declared after the ytmQuery/query lines that reference them,
causing ReferenceError on every download. Moves the block to before its first use.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Skip channel filter when artistFirstWord is non-ASCII (Japanese kanji/kana):
YouTube channels are named in romaji/English, not kanji — the filter was
rejecting all valid results and causing 6+ extra cascade steps
- Add albumQuerySuffix: when title is Japanese (no keyword match) OR has only
one romaji keyword, include the album's first two English words in the search
query so YTM ranks the official OST/album version above fan covers
Fixes: 照井順政 channel filter rejecting all YTM results
Fixes: "Souiunatsu" matching covers with romaji subtitle instead of official OST
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>
iTunes ranks by popularity, so searching "WRLD Tunes" returned Juice WRLD
first. Now fetches 5 results and picks the first where all query words
appear in the iTunes artist name, preventing false-positive matches.
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>
getSong often omits albumArtist; getAlbum always returns album.artist.
Enrich the song object before showing the edit form.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Album name no longer shown inline next to the artist — navigate via
right-click > Zum Album instead, available everywhere.
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>
When the user opens the edit modal before the getSong async call returns,
the form fields are populated from the initial song object (from the song
list) which may omit albumArtist and other fields that only getSong
returns. Now, when getSong resolves and edit mode is already visible, all
form fields are updated to the authoritative Navidrome data.
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>
JSON-LD could set track_name but return empty byArtist, making the OG
block (guarded by `if not track_name`) never run — leaving artist blank.
Now OG runs whenever either name or artist is missing, and only fills
fields that are still empty so JSON-LD name isn't overwritten.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The backdrop click handler was too easy to trigger accidentally,
especially when editing fields. Close button handles dismissal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes LRCLIB lookup, .lrc sidecar file writing, and lyrics/uslt
metadata embedding from all downloads.
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>
JSON-LD byArtist gives the correct artist list. As secondary fallback,
OG desc format is 'Artist · Song · Year' not 'Song · Artist · Album',
so use desc_parts[0] not desc_parts[1] for artist.
Also handle both attribute orderings in meta tags.
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>
Missing imports caused Object.assign(window,...) to throw before navigate
was ever assigned, breaking all onclick nav buttons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove top-level ui.js import from soundcloud.js (broke views.js→soundcloud.js→ui.js→views.js cycle)
- Use window.executeSpotifyDownload() instead to avoid the circular dep
- Merge Spotify + SoundCloud into one unified URL input that auto-detects
the service and colors the badge/button accordingly
- resolveImporterLink() routes to the correct handler based on URL
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>
navidromeArtist() now writes ; instead of / so Navidrome splits
correctly with the updated Tags.Artist.Split config.
Also handles & (e.g. "Artist A & Artist B" → "Artist A;Artist B").
Add fix-artist-separators.sh — one-time Debian script to update
existing library files from " / " to ";" in artist tags.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract parseArtists() helper — uses Navidrome's resolved artists array
when it has multiple entries, otherwise splits artist string on ';' only.
Removes old split on / that broke Au/Ra, HUNTR/X etc everywhere.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Manual split on / broke artist names like HUNTR/X and Au/Ra.
Now uses s.artists (OpenSubsonic array with correct IDs per artist)
and falls back to s.artist as a single unsplit link.
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>
Shuffle now shuffles the queue array once (current song first, rest
randomised behind it) instead of picking a random index on every skip.
Toggling shuffle off restores the original queue order. Removed the
'once' repeat state added in the previous commit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Scrobble timer: 12s → 15s
- Play count moved after action buttons, shown as "23×" (distinct from duration)
- Song star/menu buttons always visible (removed opacity:0 hover-only rule)
- Artist all-songs page: added star + 3-dots to every row (same as top songs)
- Hero card: picks from full recent+frequent pool (was capped at first 5 = all frequent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Scrobbling:
- Replace end-of-track scrobble with 12s timer approach
- Android: schedule on onTrackChanged, cancel on pause
- Web: schedule in playCurrent, cancel in togglePlay pause + onEnded
- Skip/new track resets the timer automatically (new song ID)
Artist top songs:
- Add star button and 3-dots context menu to every top song row
- Topsong grid expanded to 5 columns to fit action buttons
Settings — play count toggle:
- New "Bibliothek" section with toggle switch
- When on: shows play count in album/playlist song rows
- Artist page always shows play count regardless of toggle
- Persisted in ns_cfg localStorage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Scrobble to Navidrome on auto-advance (Android) and track end (web)
so type=recent and type=frequent are accurate for the hero card
- Genre section: pill chips → grid cards with colored left bars
- Top 15 genres shown by default (sorted by song count), expand toggle
- Genre card: click → genre detail page with full song list
- Genre card: hover play button → shuffle-play without navigating
- Genre detail page reuses album-detail layout (back btn, stats, song list)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Home view:
- Add hero card (random pick from recent+frequent combined pool)
- Add genre browser chips (sorted by song count, tap = shuffle-play)
Android sync fix:
- Add currentId to getState() snapshot so JS can verify song identity
- onReady: reset stale localStorage queue when native song ID mismatches
- onTrackChanged: reject stale old-queue callbacks by ID verification
- onPlaybackStateChanged safety net: ID-guard prevents wrong-queue index corruption
- connectToService: call updateStateSnapshot() before onReady so getState() is never empty
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dedup now checks only the last 50 queued songs instead of the entire
queue, so the radio doesn't stall on small libraries once every similar
song has been seen
- If every candidate is a recent duplicate (tiny library), allow up to 5
repeats rather than returning empty and letting the queue run dry
- Add second _queueGen guard after the getRandomSongs fallback await
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: addToQueue was sent to native twice for every radio top-up.
fillRadioQueue() already sends addToQueue internally, but onTrackChanged
and onQueueEnded also re-sent the same songs in their .then() callbacks,
causing native to accumulate duplicates. With native having double the
songs JS has, native indices no longer map to the correct JS queue entries.
Also fixed: playQueue called fillRadioQueue before setQueue was dispatched,
so addToQueue could land in the old native queue before setQueue wiped it.
Changes:
- fillRadioQueue: add _queueGen guard — bail if playQueue() replaced the
queue while the network request was in flight
- playQueue: increment _queueGen; move fillRadioQueue() inside the rAF
after setQueue so addToQueue always lands in the correct native queue
- onTrackChanged: remove .then() that re-sent addToQueue (double-send fix)
- onQueueEnded: remove addToQueue re-send, keep only _nsBridge('play')
- onPlaybackStateChanged: add safety-net sync when isPlaying transitions
to true — catches any remaining edge cases where onTrackChanged missed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 120ms debounce on JS-initiated transitions (seek/setQueue) delayed
updateStateSnapshot(), leaving a stale window where getState() returned
the old index. If an AUTO callback fired during that window and the JS
onTrackChanged handler called getState(), it would read the stale index
and overwrite the correct player bar state with the wrong song.
Fix: always fire onTrackChanged immediately so updateStateSnapshot() runs
synchronously. The JS-side getState() reconciliation (from the previous
commit) then reliably gets the settled native index every time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two-pronged fix for the state drift between the system notification
(ground truth) and the WebView player bar:
JS (player.js): onTrackChanged now reconciles the incoming index with a
synchronous NSBridge.getState() call before applying it. Stale callbacks
from earlier rapid skips immediately correct themselves to the live
settled index instead of overwriting the correct state.
Kotlin (MusicService.kt): debounce JS-initiated transitions (seek /
setQueue) by 120 ms so rapid skip sequences produce one onTrackChanged
notification at the final index rather than one per skip step. Auto-advance
transitions are still fired immediately since JS doesn't initiate those.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces getRandomSongs with getSimilarSongs2 seeded from the current track.
Falls back to random if Navidrome returns nothing (no Last.fm config or unknown song).
Deduplicates against songs already in the queue.
Compatible with AudioMuse-AI plugin if added later — same endpoint, better results.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MainActivity: emit onTrackChanged + openNowPlaying in one JS tick on notification tap
to prevent race where stale song appeared in NP overlay and bottom bar
- index.html: remove onclick/cursor from player-title so only album/artist are clickable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
.player-status-row had min-height:16px + margin-top:3px = 19px of permanent
invisible space, making player-info ~63px tall on desktop. Centered in 112px
this pushed the title to ~25px from top instead of ~34px.
Switched badges from opacity:0 (still in layout) to display:none, removed
min-height — row collapses to 0 when no badge is active so player-info
is only as tall as the visible title+album+artist rows (~44px), and
align-items:center on player-track places them at the true bar midpoint.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Desktop: player-track had align-self:center (from grid align-items:center),
collapsing it to 52px content height. align-items:center inside it then
centered against 52px, not the full 112px bar — text appeared at top.
align-self:stretch forces player-track to fill the bar height so internal
centering works correctly.
Mobile: padding:14px 12px was too symmetric — art filled the 46px content
area exactly and touched the progress strip. Changed to padding:6px 12px 14px
(6px top breathing room, 14px bottom for progress strip, ~54px content area).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
padding:0 12px 14px meant zero top padding, so align-items:center placed
content in the top 60px of the 74px bar — visually 7px above center.
Changed to padding:14px 12px (symmetric) so art+text center at exactly
37px, the visual midpoint of the bar. Progress strip stays absolute at bottom.
Artist color changed from hardcoded gold to var(--accent) in both the
bottom bar and Now Playing UI so it follows the active theme.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- line-height:1.2 on title and artist rows keeps the text block within
the 46px art circle so it no longer overflows above/below
- Artist name uses accent color (gold) in both the bottom bar and the
Now Playing UI, making it immediately distinguishable from the album
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Song restart: onPageFinished can fire multiple times mid-session (hash/state
changes in WebView). Added 3s debounce so onReady is not re-delivered
spuriously while a song is playing.
Notification artwork: warmArtworkCache was called from onTimelineChanged
where player.currentMediaItemIndex hadn't updated yet (lags until prepare()),
so it warmed the wrong tracks. WebBridge.setQueue now calls
MusicService.publicWarmArtwork(startIndex) immediately after setMediaItems
and before prepare()/play(), giving the bitmap loader a head start with
the correct index.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When onReady fires during MediaController reconnect (fragment navigation,
activity resume), calling setQueue restarts the current song from pos 0.
Guard: if native isPlaying=true, only re-sync JS UI — never touch the queue.
Only push a new queue when native is idle/empty (cold start).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>