Removed:
- "WebView wrapper" — Android is native Jetpack Compose, not a WebView
- "Artwork pre-warming / pre-buffer on cold start" — not implemented this way
- ABS chapters/speed/sleep/progress on Android — web only for now
Added:
- Lyrics section: lrclib.net, fallback chain, in-memory cache, synced scroll
- Android: last played song in MiniPlayer across restarts
- Android: ABS library browser tab (hidden until configured)
- Android: no login flash on startup
- Settings: connection status dots, save button top+bottom, matched web order
- Multi-artist support in NowPlaying
- Artist cards with images in search
- Genre browsing
- ABS web vs Android split into separate entries
- Accurate Android project structure
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings:
- Reordered to match web: Navidrome → Bibliothek → Theme → Audiobookshelf → Discover APIs
- Save button now appears at top AND bottom for easy access
- Audiobookshelf section: Server URL + API Key (masked) + green/red status dot with Test button
- NovaSonic Backend section: same green/red conn dot + Test button
- Connection status uses ConnStatus sealed class (Idle/Checking/Ok/Err)
Audiobookshelf:
- New AbsClient.kt: Ktor HTTP client, proxies through NovaSonic backend or direct with Bearer token
- New AbsScreen.kt: library list → book list with covers, author, duration
- Hörbücher tab appears in bottom nav only when ABS URL + API key are both set
- absUrl + absApiKey persisted in DataStore
Last played song:
- PlayerController saves song to DataStore on every media item transition
- MiniPlayer shows last song (dimmed, single play button) when no active queue
- Tapping play fetches the song from Navidrome and resumes
- LastPlayedSong stored in DataStore (id, title, artist, coverArt)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Android: show nothing while DataStore loads credentials (null initial
value) to prevent login screen flash on startup when already logged in
- Web: dispAlbumName now only uses sortName as romaji for Japanese titles;
English albums with lowercase ALBUMSORT tags now display correct casing
- lrclib: add no-duration fallback exact match to find songs where local
file duration differs from lrclib entry (e.g. Sanctuary Opening 5:58 vs 4:24)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NetEase returned wrong/unsynced results. lrclib with exact + fuzzy
search is sufficient; no third-party Chinese platform needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 simultaneous requests were hitting lrclib rate limiter.
Now: exact match alone → if no content, 2 parallel searches.
Also check response.status === 200 before parsing to avoid treating
404/429 error bodies as valid empty lyrics responses.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Android + web: fire exact/artist-search/title-search in parallel so
total wait = slowest single request instead of sum of all three
- Android: HashMap cache keyed by artist|title, cleared on app restart
- Web: Map cache (session lifetime), cache hit is instant re-open
- Web: extract _showLyricsResult helper, safeJson wrapper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Android: add 6s HttpTimeout to LrcLibClient, wrap each step in withTimeoutOrNull
- Both: add title-only lrclib search (step 3) so "Sanctuary (Opening)" is found
even when artist tag differs ("Utada" vs "Hikaru Utada")
- Web: reduce all timeouts 8s → 5s, extract bestMatch helper
Search order: lrclib exact → lrclib artist+title → lrclib title-only → NetEase
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Search order: lrclib exact → lrclib fuzzy search → NetEase.
NetEase has strong coverage for Asian/Japanese music that lrclib misses.
Duration-matched candidate selection on all three steps.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Navidrome lyrics plugin does not implement getLyricsBySongId for synced
lyrics — removed all plugin calls. Both Android and web now query
lrclib.net/api/get (exact match) with /api/search fallback for metadata
mismatches (e.g. artist tagged differently in Navidrome vs lrclib).
Also fixes Android lyrics scroll tween<Int> type inference error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace AnnotatedString/ClickableText (offset-based click was unreliable)
with a horizontally-scrollable Row of individual Text composables. Each
artist has its own explicit clickable modifier bound directly to its ID.
ID de-duplication: if Navidrome copies the primary artistId to all entries
in the artists array, only the first keeps the ID; others render as
non-navigable text (dimmed NsAccent) instead of silently going to wrong page.
Fallback: split song.artist by ';' when artists array is absent/single,
matching the web app's parseArtists() logic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Same pool of music-themed phrases as Android — picks randomly each load.
With username: "Ready for some beats, Marc" — without: "Pick something great"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Song model: add SongArtist{id,name} + artists: List<SongArtist>
Navidrome sends this via OpenSubsonic extension — previously ignored
- NowPlaying: if song.artists has >1 entry, render each name as an
inline AnnotatedString with ClickableText, separated by ' · ';
every artist navigates to their own page with their real ID
Falls back to single artist/artistId entry when array is absent
- AppNavigation: onArtistClick now receives artistId: String instead
of a fixed closure, enabling per-artist navigation from the sheet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace static time-of-day greetings (Guten Morgen/Tag/Abend) with a random
pick from a pool of 10-12 music-themed phrases each time HomeScreen is composed.
With username: "Ready for some beats, Marc" — without: "What's on the playlist today?"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Search: replace SmallArtistChip (text pill) with ArtistCard showing
circular profile image (72dp) + name below; works for both Navidrome
artists (uses coverArt) and iTunes artists (uses artworkUrl100 if present)
ArtistDetailScreen: convert action bar Row → LazyRow so Play / Shuffle /
Konzerte pills sit comfortably with horizontal scroll instead of cramming
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GenreViewModel: GenreUiState(genres = it) left isLoading=true (default),
so genres loaded but spinner never cleared. Added isLoading=false explicitly.
AppNavigation: replace BackHandler + navController.enableOnBackPressed with a
DisposableEffect that re-adds an OnBackPressedCallback each time NowPlaying
opens. Since callbacks are LIFO, a freshly-added callback always beats the
BackHandler NavHost registers internally, regardless of composition order.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ExploreScreen: add BackHandlers for state-based overlays (partialAlbum →
albumTracks → artistDetail hierarchy) so system back navigates correctly
- AppNavigation: disable NavController's OnBackPressedCallback while NowPlaying
is open so our BackHandler always intercepts first (fixes tab-switch-then-back bug)
- Home tab: tapping Home while already on Home triggers vm.load() refresh via refreshKey
- HomeViewModel: load all genres sorted by songCount desc (removed take(12) cap)
- HomeScreen: show only top-6 genres + 'Alle anzeigen' button with total count
- New GenreListScreen + GenreViewModel: full scrollable genre list at 'genres' route
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Partial album detail: tapping incomplete albums now shows downloaded songs
(normal playback) above and missing iTunes tracks (30s preview + download)
below — on both Android and web PWA
- Android: ItunesArtistScreen now renders full-screen above the tab/search bar
- Android: ArtistDetailScreen (Bibliothek) now loads iTunes data in parallel —
adds Weltweit populär section and In Library/Unvollständig/Nicht heruntergeladen
album sections identical to the Erkunden artist page
- Fix: artist no longer appears in its own Similar Artists row
- Fix: duplicate album entries deduplicated by collectionId in categorizeAlbums
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Enable dark navigation headers in Tab navigator (fixes status bar overlap
on all 5 tab screens automatically)
- Use useSafeAreaInsets for AlbumDetail/ArtistDetail back buttons
- Use useSafeAreaInsets for NowPlaying header and bottom padding
- Remove redundant paddingTop from Home heading and Login content
- Cleaner tab bar (height 60, better label spacing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without this, Metro fails to bundle because the preset is not found.
Verified: npx expo export --platform android now bundles 1093 modules clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- RNTP 4.1.2 used ExoPlayer 2.x which is incompatible with RN 0.85.3
- RNTP 5.0.0-alpha0 uses androidx.media3 and supports New Architecture
- Remove newArchEnabled:false from app.json (New Arch is default)
- Remove Kotlin null safety patch (no longer needed)
- Remove patch-package (no more patches required)
- Fix getState() -> getPlaybackState() API rename in player.js
- Add babel.config.js with babel-preset-expo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These packages require New Architecture (Fabric), which conflicts with
react-native-track-player requiring Old Architecture (newArchEnabled: false).
Neither of these packages was imported anywhere in our code.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full React Native app using react-native-track-player for native
audio — fixes background freeze, autoplay stopping, stuck play
button, and enables Android Auto in future. Includes:
- Navidrome API layer (ported from src/api.js)
- Zustand global store with SecureStore config persistence
- react-native-track-player with background playback service
- Bottom tab navigation: Home, Artists, Albums, Search, Settings
- Screens: Home, Library (Artists/Albums), ArtistDetail, AlbumDetail,
Search, NowPlaying, Login/Settings
- Persistent PlayerBar with progress indicator
- Romaji/sort-tag display support carried over from web app
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Was missing from docker-compose environment list so the key was never
visible to the Node.js process even when set in .env.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Colons in startDateTime were being percent-encoded which Ticketmaster
may reject. Also log errors/status so backend logs reveal the cause
when no events appear.
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>
The Spotify importer resolves links without OAuth credentials — these
variables were never read by any code.
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>
The queue serialization was saving artist as a plain string but not the
artists array. On cold start the app restores from localStorage, so
parseArtists had no array to work with and fell back to splitting the
artist string by semicolons — "Maduk Veela" has none, so it returned
one combined entry. PWA always re-fetches fresh API data so the array
was always present. Adding artists:[] to both song and queue entries
fixes artist navigation after cold start on Android.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WebBridge: skip setMediaItems/prepare when the track is already buffered
from the cold-start pre-warm, so resuming the last song is instant (~0s)
instead of re-buffering from scratch (~10s).
Player: show pause icon during buffering (isPlaying=false, isBuffering=true)
so the button doesn't flash back to "play" between tracks. Also optimistically
set _nativePlaying=true and call setPlayIcon immediately on playCurrent so the
icon updates before the native callback round-trip.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Returning RESULT_ERROR_NOT_SUPPORTED from onPlayerCommandRequest caused
Media3 to mark the skip commands as failed and hide the buttons. Now
returns RESULT_SUCCESS so the notification keeps them enabled, while
ForwardingPlayer no-ops seekToNext/Previous so ExoPlayer never tries to
advance its single-item queue — JS still owns all queue navigation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Call updateMediaSession immediately on track change so skip buttons and
cover art in the OS notification bar update without waiting for audio load
- Replace link[rel=prefetch] (no-op for audio streams) with a shadow Audio
element (preload=auto) that actually buffers the next track
- Start preloading 60s before track end instead of 30s for more buffer margin
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>
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>