After saving sort tags via the metadata editor, invalidate _artistSortMap
and the apiCached getArtists entry so the updated Navidrome sortName values
are picked up on the next page visit without a hard refresh.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ensureArtistSortMap() fetches getArtists once per session (cached) and
populates _artistSortMap automatically. Called at start of loadAlbumDetail
when showRomaji is on, so artist sort names are always available.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Song objects carry sortName for title only, no album/artist sort fields.
Album sort is only available on the album object itself (r.album.sortName).
Updated loadAlbumDetail to use the fresh API response album, not the nav album.
Simplified dispAlbum to not check non-existent song sort fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dispAlbumName now takes the full album object and reads album.sortName
directly — no kuromoji fallback, no backend translation.
dispAlbum (song context) also drops the kuromoji fallback, keeping only
the sortAlbum/albumSort file tag.
Override list → Navidrome sort tag → raw Japanese name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added _artistSortMap populated from getArtists (artist listing) and
getArtist (artist detail) responses. dispArtistById now checks the
Navidrome sort tag first, falls back to kuromoji only when missing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hero now only waits for getArtist (fast local Navidrome) before revealing.
getArtistImage (Last.fm lookup) was the 2-4s bottleneck when uncached —
it now resolves async and patches in the artist photo after the hero is
already visible.
Album card, album detail header, and home hero now display the raw album
name from Navidrome instead of converting to romaji when showRomaji is on.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend: swap /api/events/:artist to use Ticketmaster Discovery API
(startDateTime=now, classificationName=music, size=20, sort by date).
Maps TM response to the same {datetime, venue, offers} shape the modal
expects. Returns [] if TICKETMASTER_API_KEY is not set.
Frontend: remove all debug console.logs from events.js; drop redundant
client-side date filter (TM already filters by startDateTime server-side).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split the allSettled into two phases: await imgP+albumP first (both are
fast — getArtistImage hits S.artistImgCache when navigating from the
listing, getArtist is a local Navidrome call), then reveal the hero.
The slow calls (getArtistInfo2 / Last.fm bio, iTunes search, events)
keep running in parallel and are only awaited when rendering the content
sections below the hero — so the user sees the hero card immediately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
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>