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:
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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in new issue
Block a user