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 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-06-15 22:50:11 +02:00
1 parent 0272b9e99f
commit f7012dab49
5 files changed
+34 -8

No files matched your search

@@ -68,7 +68,7 @@ object LrcLibClient {
val cacheKey = "$artist|$title" val cacheKey = "$artist|$title"
if (cache.containsKey(cacheKey)) return cache[cacheKey] 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 exact = runCatching {
val resp = client.get("https://lrclib.net/api/get") { val resp = client.get("https://lrclib.net/api/get") {
parameter("artist_name", artist) parameter("artist_name", artist)
@@ -78,9 +78,19 @@ object LrcLibClient {
} }
if (resp.status == HttpStatusCode.OK) resp.body<LrcLibResponse>() else null if (resp.status == HttpStatusCode.OK) resp.body<LrcLibResponse>() else null
}.getOrNull() }.getOrNull()
if (exact.hasContent()) return exact.also { cache[cacheKey] = it } 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<LrcLibResponse>() 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) // 2+3. lrclib search — artist+title and title-only in parallel (2 requests, not 3)
val lrclibResult = coroutineScope { val lrclibResult = coroutineScope {
val artistDef = async { val artistDef = async {
@@ -35,12 +35,15 @@ private fun NovaSonicRoot() {
val context = LocalContext.current val context = LocalContext.current
val prefs = remember { PrefsStore(context) } val prefs = remember { PrefsStore(context) }
val creds by prefs.credentials.collectAsStateWithLifecycle( val creds by prefs.credentials.collectAsStateWithLifecycle(
initialValue = PrefsStore.Credentials("", "", ""), initialValue = null,
) )
var forceLogin by remember { mutableStateOf(false) } 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 }) LoginScreen(onLoggedIn = { forceLogin = false })
} else { } else {
AppNavigation() AppNavigation()
@@ -118,7 +118,7 @@ fun NowPlayingScreen(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
.padding(horizontal = 20.dp) .padding(horizontal = 20.dp)
.padding(bottom = 16.dp), .padding(bottom = 28.dp),
) { ) {
NpCircleBtn(onClick = onDismiss) { NpCircleBtn(onClick = onDismiss) {
Icon(Icons.Default.KeyboardArrowDown, "Schließen", Icon(Icons.Default.KeyboardArrowDown, "Schließen",
@@ -386,7 +386,7 @@ fun NowPlayingScreen(
// Center of active line sits at 30% from top (clearly upper area) // Center of active line sits at 30% from top (clearly upper area)
val targetY = (lyricsPaddingPx + activeIdx * lineHeightPx + lineHeightPx / 2f val targetY = (lyricsPaddingPx + activeIdx * lineHeightPx + lineHeightPx / 2f
- lyricsViewH * 0.30f).coerceAtLeast(0f) - lyricsViewH * 0.30f).coerceAtLeast(0f)
lyricsScrollState.animateScrollTo(targetY.toInt(), tween<Int>(280, easing = FastOutSlowInEasing)) lyricsScrollState.animateScrollTo(targetY.toInt(), tween<Float>(280, easing = FastOutSlowInEasing))
} }
when { when {
+2 -1
View File
@@ -201,7 +201,8 @@ export function dispAlbumName(album) {
const ov = getRomajiOverrides(); const ov = getRomajiOverrides();
if (name && ov[name]) return ov[name]; if (name && ov[name]) return ov[name];
const sort = album?.sortName; 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; return name;
} }
+13 -1
View File
@@ -174,7 +174,7 @@ async function _loadNpLyrics(songId) {
return; 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 }); const exactP = new URLSearchParams({ artist_name: artist, track_name: title });
if(album) exactP.set('album_name', album); if(album) exactP.set('album_name', album);
if(dur) exactP.set('duration', dur); if(dur) exactP.set('duration', dur);
@@ -187,6 +187,18 @@ async function _loadNpLyrics(songId) {
} }
} catch {} } 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; if(token !== _lyricsLoadToken) return;
// 2+3. lrclib search — artist+title and title-only in parallel (2 requests, not 3) // 2+3. lrclib search — artist+title and title-only in parallel (2 requests, not 3)