From f7012dab49ea03a0a749f8ff29bb4226b7998b02 Mon Sep 17 00:00:00 2001 From: Honey Date: Mon, 15 Jun 2026 22:50:11 +0200 Subject: [PATCH] fix: login flash, album name casing, lrclib duration mismatch - 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 --- .../kotlin/com/novasonic/app/api/LrcLibClient.kt | 14 ++++++++++++-- .../kotlin/com/novasonic/app/ui/MainActivity.kt | 7 +++++-- .../novasonic/app/ui/screen/NowPlayingScreen.kt | 4 ++-- src/api.js | 3 ++- src/player/nowplaying.js | 14 +++++++++++++- 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/android/app/src/main/kotlin/com/novasonic/app/api/LrcLibClient.kt b/android/app/src/main/kotlin/com/novasonic/app/api/LrcLibClient.kt index 60d8457..71c315a 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/api/LrcLibClient.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/api/LrcLibClient.kt @@ -68,7 +68,7 @@ object LrcLibClient { val cacheKey = "$artist|$title" if (cache.containsKey(cacheKey)) return cache[cacheKey] - // 1. lrclib exact match — single request, only trust HTTP 200 + // 1a. lrclib exact match with duration val exact = runCatching { val resp = client.get("https://lrclib.net/api/get") { parameter("artist_name", artist) @@ -78,9 +78,19 @@ object LrcLibClient { } if (resp.status == HttpStatusCode.OK) resp.body() else null }.getOrNull() - if (exact.hasContent()) return exact.also { cache[cacheKey] = it } + // 1b. lrclib exact match without duration — handles version mismatches (e.g. 5:58 file vs 4:24 in lrclib) + val exactNoDur = if (durationSecs != null && !album.isNullOrBlank()) runCatching { + val resp = client.get("https://lrclib.net/api/get") { + parameter("artist_name", artist) + parameter("track_name", title) + parameter("album_name", album) + } + if (resp.status == HttpStatusCode.OK) resp.body() else null + }.getOrNull() else null + if (exactNoDur.hasContent()) return exactNoDur.also { cache[cacheKey] = it } + // 2+3. lrclib search — artist+title and title-only in parallel (2 requests, not 3) val lrclibResult = coroutineScope { val artistDef = async { diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/MainActivity.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/MainActivity.kt index 926afa2..93efa55 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/ui/MainActivity.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/MainActivity.kt @@ -35,12 +35,15 @@ private fun NovaSonicRoot() { val context = LocalContext.current val prefs = remember { PrefsStore(context) } val creds by prefs.credentials.collectAsStateWithLifecycle( - initialValue = PrefsStore.Credentials("", "", ""), + initialValue = null, ) var forceLogin by remember { mutableStateOf(false) } - if (!creds.isValid || forceLogin) { + // null = still loading from DataStore; don't show anything yet to avoid login screen flash + if (creds == null) return + + if (!creds!!.isValid || forceLogin) { LoginScreen(onLoggedIn = { forceLogin = false }) } else { AppNavigation() diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/NowPlayingScreen.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/NowPlayingScreen.kt index 72a5cdd..c3cc147 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/NowPlayingScreen.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/NowPlayingScreen.kt @@ -118,7 +118,7 @@ fun NowPlayingScreen( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth() .padding(horizontal = 20.dp) - .padding(bottom = 16.dp), + .padding(bottom = 28.dp), ) { NpCircleBtn(onClick = onDismiss) { Icon(Icons.Default.KeyboardArrowDown, "Schließen", @@ -386,7 +386,7 @@ fun NowPlayingScreen( // Center of active line sits at 30% from top (clearly upper area) val targetY = (lyricsPaddingPx + activeIdx * lineHeightPx + lineHeightPx / 2f - lyricsViewH * 0.30f).coerceAtLeast(0f) - lyricsScrollState.animateScrollTo(targetY.toInt(), tween(280, easing = FastOutSlowInEasing)) + lyricsScrollState.animateScrollTo(targetY.toInt(), tween(280, easing = FastOutSlowInEasing)) } when { diff --git a/src/api.js b/src/api.js index 3d9c165..10be670 100644 --- a/src/api.js +++ b/src/api.js @@ -201,7 +201,8 @@ export function dispAlbumName(album) { const ov = getRomajiOverrides(); if (name && ov[name]) return ov[name]; const sort = album?.sortName; - if (sort && !_isJp(sort)) return sort; + // Only use sortName as romaji when the album title itself is Japanese — otherwise sortName is just a sort key (e.g. lowercase "ultra blue") + if (sort && _isJp(name) && !_isJp(sort)) return sort; return name; } diff --git a/src/player/nowplaying.js b/src/player/nowplaying.js index 0b10044..79226f3 100644 --- a/src/player/nowplaying.js +++ b/src/player/nowplaying.js @@ -174,7 +174,7 @@ async function _loadNpLyrics(songId) { return; } - // 1. lrclib exact match — single request, only trust HTTP 200 + // 1a. lrclib exact match with duration const exactP = new URLSearchParams({ artist_name: artist, track_name: title }); if(album) exactP.set('album_name', album); if(dur) exactP.set('duration', dur); @@ -187,6 +187,18 @@ async function _loadNpLyrics(songId) { } } catch {} + // 1b. exact match without duration — handles version mismatches (e.g. file at 5:58, lrclib has 4:24) + if(!result && dur && album) { + try { + const exactNoDurP = new URLSearchParams({ artist_name: artist, track_name: title, album_name: album }); + const r = await fetch(`https://lrclib.net/api/get?${exactNoDurP}`, { signal: SIG() }); + if(r.status === 200) { + const d = await r.json(); + if(d?.syncedLyrics || d?.plainLyrics || d?.instrumental) result = d; + } + } catch {} + } + if(token !== _lyricsLoadToken) return; // 2+3. lrclib search — artist+title and title-only in parallel (2 requests, not 3)