Single object-position can never work well for portrait photos in a ~6:1 wide
banner — we'd only see ~11% of the image height no matter the value.
Desktop now uses two layers:
1. artist-hero-bg: same image, blurred + darkened, fills full banner width
2. artist-hero-img: sharp portrait anchored to the right at natural proportions
(height: 100%, width: auto — no cropping, full face visible)
Mobile unchanged (cover + center center was already working great).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Only override object-position on desktop (>=769px) to center 30%.
Mobile keeps center center which was already working great.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switching from background-image (inline JS, single position for all sizes)
to an <img> element lets CSS media queries apply different object-position:
- Mobile: center top (portrait images show face in upper portion)
- Desktop: center 30% (wide banner, lower anchor avoids showing only hair)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Theme → Bibliothek (+ Romaji) → Audiobookshelf → Discover APIs → Navidrome Server
Also merged the standalone Romaji Korrekturen row into the Bibliothek section.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
background-position: center crops to mid-body on wide desktop banners.
Shifting to center 20% anchors the crop in the upper-third of the image
where faces typically are, matching the phone experience.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
notify.js statically imported navigate from views/views.js, creating a new
circular chain: navigation.js → ui/ui.js → settings.js → downloads.js →
notify.js → views/views.js → navigation.js. This chain did not exist in the
old monolithic ui.js and caused module initialisation failures in some browsers.
Fix: make navigate a lazy dynamic import inside _notifNavigate() body, which
is the only call site. Also replace unnecessary dynamic import('../utils.js')
calls with the already-statically-imported toast function.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings dropdown: 15s (default), 30s, 1/3 of duration, 2/3 of duration, song end.
Duration-based modes use s.duration from Navidrome metadata (always available).
'end' mode skips the timer and fires the scrobble in onEnded (web) / onQueueEnded (native).
Skipping a song never scrobbles in any mode — _cancelScrobble() still runs on skip/pause.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
loadHeroCard() now pre-fetches the romaji index (same pattern as loadAlbumRow)
and uses dispAlbumName()/dispArtistById() instead of raw album.name/artist.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>