- Song context sheet now opens fully expanded (skipPartiallyExpanded) so the
"Herunterladen" option at the bottom is reachable without an extra drag-up.
- The "new playlist" field in the playlist picker now matches the app's search
bars: rounded 12dp, filled surface, leading icon, NsAccent/NsOnSurface3 borders.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The picker listed playlists with a generic list icon; now each row shows the
playlist's cover art (falling back to the icon when there's none), so you can
tell them apart at a glance when adding a song.
- web (modals.js): 34px cover thumbnail per row.
- android (PlaylistPickerSheet): 40dp AsyncImage cover, coverUrl passed from AppNavigation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Artists like ".diedlonely" (leading dot, †, ✝, _ …) weren't surfaced by iTunes,
so the download's metadata lookup fell through to a titleOnly match — grabbing a
same-titled track by another artist (e.g. the "In the Bleak Midwinter" Christmas
carol: Holiday genre, 176s single) and steering the YouTube search to the wrong
(slowed) version.
- backend/metadata.js: strip leading non-alphanumeric from the search artist and
the first-word comparison so the real artist matches (and the right album/genre/
duration are used).
- android ExploreViewModel: strip leading symbols from the search term so the
Explore tab finds these artists.
Keeps the original name for display/storage — only the search/compare is trimmed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Android: MusicRepository.coverArtUrl/streamUrl built a fresh SubsonicClient
per call — each spins up (and never closes) a Ktor HttpClient. That ran for
every cover on every recomposition, causing heavy GC/alloc churn (and a leak)
that showed up as scroll/tab lag. Reuse the cached ClientHolder client for URL
building instead.
Web: the home scroll-row arrows called getBoundingClientRect on every scroll
event (layout thrashing). Split it: only toggle arrow visibility on scroll
(rAF-throttled); do the getBoundingClientRect vertical-centering only on
resize/content change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every other favorite toggle uses Favorite/FavoriteBorder (heart); Now Playing
was the odd one out with Star/StarBorder. Make it a heart everywhere.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tapping the star only fired the API and never updated any UI state, so the
icon didn't change (no feedback). Add a global reactive StarState with
optimistic overrides: the star flips instantly across every screen showing
that song (SongRow lists, Now Playing, context sheet), the API runs in the
background, and the override reverts if the call fails.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The artist screen's "show all albums" overlay state was plain remember(), so
navigating into an album and pressing back reset it — dumping you on the
collapsed artist page instead of the list you came from. Persist which section
is open via rememberSaveable (a String? title) so it survives the round-trip,
and re-derive the overlay entries from the (surviving) categorized albums.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rapid skips + lyrics prefetch fired the full lrclib cascade many times over,
so lrclib throttled the IP and requests timed out (canceled) on both web and
Android. Reduce the request volume:
- Web: de-dupe concurrent fetches per song (prefetch + tab-open now share one
cascade); bump the per-request timeout 6s → 8s.
- Android: per-song Mutex so prefetch + on-open load don't both run the cascade;
send a proper User-Agent (lrclib asks for one and treats it better); debounce
the prefetch (1.5s, cancelled on the next skip) instead of firing immediately
on every track change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Warm the lrclib cache the moment a track starts (Android PlayerController
onMediaItemTransition; web onNpSongChange) so opening the Lyrics tab is
instant. Extract a DOM-free fetch helper on web for reuse.
Rewrite Android lyrics autoscroll to center on the active line's measured
position in a LazyColumn instead of a fixed line-height estimate — removes
stutter and the downward drift on wrapped lines; active line sits at a
~40%-from-top reading anchor.
CHANGELOG: fold into 1.0; drop the now-fixed lyrics known-issue note.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Download albums/playlists to private device storage (Room + WorkManager,
HttpURLConnection streaming). New Offline tab lists groups with progress;
tapping a group opens a detail screen (cover-art rows, play/shuffle, per-song
delete from device). Downloaded tracks play from disk on every screen.
Album/playlist cards show a downloaded badge; download button resumes missing
songs. Song storage is dedup'd by ID and reference-counted on delete.
Fixes: Android 15 download crash (dropped banned DATA_SYNC foreground service);
offline detail flash-back (initial empty state no longer mistaken for deleted).
Docs: CHANGELOG 0.10 + parity matrix; condensed README Features.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- icon.svg / icon-192/512.png: blue→purple equalizer gradient design
- nginx.conf: add /app/ alias so reverse-proxy prefix resolves assets
- vite.config.js: base /app/ + %BASE_URL% in favicon links so path
resolves correctly whether app is served at / or /app/
- ic_launcher.xml: same equalizer design for Android app icon
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Android: camera hero button removed; cover upload moved into edit dialog.
Web: trash icon removed from cover actions in edit modal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Playlist: create (FAB + dialog), rename, delete, remove songs, cover upload/remove
- Library Albums tab: sort chips (Neueste/Älteste/A-Z/Künstler) matching StarredScreen
- ABS notification: shows current chapter title; updates live without re-buffering
- Home hero: pool now matches web (frequent(5) ∪ recentlyPlayed(5)); label MEISTGESPIELT / ZULETZT GEHÖRT
- Home: added Zuletzt gehört section (type=recent) matching web layout
- Notifications: fixed delivery bug — polling moved to NotificationStore app-lifetime scope so notifications arrive even after user navigates away from ImporterScreen
- NotificationStore.startPoll() extended with navType/navAlbum/navArtist/navId params
- Removed defunct AbsPlayerService.kt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 'drift to next line' animation moved the text while the user was
still reading/singing the current line, making it feel out of sync.
Now matches the web behavior: smooth-scroll to center the active line
when it changes, nothing more.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Notification sheet: add 'Zum Album →' / 'Öffnen →' button; tapping searches
Navidrome and navigates to the matching album/playlist
- AppNotification: add navType/navAlbum/navArtist/navId fields (mirrors web notify.js)
- ImporterViewModel: store resolved album/artist/type in notification after download
- ABS Android: search bar with 400ms debounce, server-side search via /search endpoint
- AbsClient: add searchLibraryItems() for library-specific search
- AbsScreen/abs.js: strip HTML tags from book descriptions (<p> tags no longer visible)
- NowPlayingScreen: vibrant palette swatch + lighter scrim (0.15) + wider gradient
for more visible dominant color background
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ABS: new AbsBooksScreen loads books directly on tab open; library pills shown only when >1 library — no more two-step library→books navigation
- Nav: shorten "Hörbücher"→"Bücher", "Einstellungen"→"Einst.", reduce label font to 9sp with maxLines=1 to prevent label wrapping
- NowPlaying: increase top padding so the tab bar sits lower on screen (statusBarTop + 16dp, min 52dp)
- NowPlaying: replace blurred-cover background with dominant-color gradient extracted via Palette from cover art; animates on track change; falls back to dark color
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AbsClient: add session API (startSession, closeSession, getProgress, saveProgress, getItemChapters) and data classes (AbsChapter, AbsAudioTrack, AbsPlaySession, AbsProgress)
- AbsPlayerController (new): standalone ExoPlayer controller for ABS audio; tracks global time across multi-track audiobooks, syncs progress to ABS every 30 s, chapter navigation, speed control, sleep timer
- AbsScreen: book detail screen with cover/meta/progress/chapter list/play button; AbsNowPlayingScreen overlay (progress bar, prev/+30s/play/-30s/next controls, speed picker, sleep timer, chapter list with auto-scroll)
- AppNavigation: create AbsPlayerController, wire up book detail and player overlay, hardware back button handling for all ABS sub-screens
- PrefsStore + SettingsViewModel + SettingsScreen: absChapterTrackMode toggle — progress bar shows current chapter length instead of full book
- README: update ABS section to cover both web and Android features
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>
- 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>
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>
- 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>
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>