fix: Japanese/CJK albums vanished from artist discography (web + android)

The album-name normaliser stripped everything but a-z0-9, so any JP/CJK
title collapsed to "". All of an artist's Japanese singles then normalised
identically, de-duplicated into one entry and "matched" each other — e.g.
inabakumori (稲葉曇) showed only 2 of 9 releases. Normalisers now keep
Unicode letters/numbers (\p{L}\p{N}) in:
- web _normAlbum (detail.js)
- android normAlbum (categorizeAlbums), normTrack (partial-album split),
  the enrichPartialCounts helpers, and the Deezer artist-name match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-08-22 18:49:59 +02:00
1 parent ed641e999c
commit 026b392657
6 files changed
+21 -6

No files matched your search

+8
View File
@@ -106,6 +106,14 @@ like Android), concert events become **reliable across web/Android**, several An
- **Android artist albums incomplete** — `lookupArtistAlbums` filtered to `record_type == "album"`, - **Android artist albums incomplete** — `lookupArtistAlbums` filtered to `record_type == "album"`,
dropping EPs and singles that Deezer (and the web app) show. Now returns all, sorted album → EP → dropping EPs and singles that Deezer (and the web app) show. Now returns all, sorted album → EP →
single. single.
- **Japanese/CJK albums vanished from the artist discography (Web + Android)** — the album-name
normaliser stripped everything except `a-z0-9`, so any Japanese/CJK title collapsed to an empty
string. All of an artist's JP singles then normalised identically (`""`), so they de-duplicated
into one entry and even "matched" each other — e.g. inabakumori (稲葉曇) showed only 2 of 9 releases,
the rest hidden behind one album. The normaliser now keeps Unicode letters/numbers (`\p{L}\p{N}`),
so JP titles stay distinct and the full discography (In Library / Unvollständig / Noch nicht
heruntergeladen) shows correctly. Same fix applied to the partial-album track match and the
Deezer-artist name match.
- **Android "Unvollständig" (partial albums) never showed** — Deezer's `/artist/{id}/albums` omits - **Android "Unvollständig" (partial albums) never showed** — Deezer's `/artist/{id}/albums` omits
`nb_tracks`, so an owned album's real total was unknown (0) and it never got flagged incomplete; `nb_tracks`, so an owned album's real total was unknown (0) and it never got flagged incomplete;
every partly-downloaded album looked complete. The artist VM now resolves the real track count for every partly-downloaded album looked complete. The artist VM now resolves the real track count for
@@ -40,7 +40,9 @@ import com.novasonic.app.ui.theme.*
import com.novasonic.app.viewmodel.ItunesArtistState import com.novasonic.app.viewmodel.ItunesArtistState
private fun normAlbum(s: String?): String = private fun normAlbum(s: String?): String =
(s ?: "").lowercase().replace(Regex("[^a-z0-9\\s]"), " ").replace(Regex("\\s+"), " ").trim() // Keep Unicode letters/numbers (\p{L}\p{N}) — stripping to a-z0-9 emptied every Japanese/CJK
// title, so all of an artist's JP singles collapsed into one and "matched" each other.
(s ?: "").lowercase().replace(Regex("[^\\p{L}\\p{N}\\s]"), " ").replace(Regex("\\s+"), " ").trim()
data class ArtistAlbumCategories( data class ArtistAlbumCategories(
val downloaded: List<Pair<Album, ItunesItem?>>, val downloaded: List<Pair<Album, ItunesItem?>>,
@@ -27,7 +27,9 @@ import com.novasonic.app.ui.theme.*
import com.novasonic.app.viewmodel.PartialAlbumState import com.novasonic.app.viewmodel.PartialAlbumState
private fun normTrack(s: String?) = private fun normTrack(s: String?) =
(s ?: "").lowercase().replace(Regex("[^a-z0-9]"), "").trim() // Keep Unicode letters/numbers so Japanese track titles aren't all emptied to "" (which made
// every downloaded JP track "match" every missing one in the partial-album split).
(s ?: "").lowercase().replace(Regex("[^\\p{L}\\p{N}]"), "").trim()
@Composable @Composable
fun PartialAlbumScreen( fun PartialAlbumScreen(
@@ -158,7 +158,7 @@ class ArtistViewModel(app: Application, savedState: SavedStateHandle) : AndroidV
// artists are fan-sorted (ItunesClient), so firstOrNull gives the most popular match. // artists are fan-sorted (ItunesClient), so firstOrNull gives the most popular match.
// Prefer an EXACT normalized-name match: "Flay!" must not resolve to the far more popular // Prefer an EXACT normalized-name match: "Flay!" must not resolve to the far more popular
// "K.FLAY" (its name merely contains "flay"). Only fall back to a loose contains-match. // "K.FLAY" (its name merely contains "flay"). Only fall back to a loose contains-match.
fun norm(s: String?) = (s ?: "").lowercase().replace(Regex("[^a-z0-9]"), "") fun norm(s: String?) = (s ?: "").lowercase().replace(Regex("[^\\p{L}\\p{N}]"), "")
val q = norm(artistName) val q = norm(artistName)
val iArtist = ( val iArtist = (
artists.firstOrNull { norm(it.artistName) == q } artists.firstOrNull { norm(it.artistName) == q }
@@ -198,7 +198,7 @@ class ArtistViewModel(app: Application, savedState: SavedStateHandle) : AndroidV
val ndAlbums = _state.value.artist?.album ?: return val ndAlbums = _state.value.artist?.album ?: return
val iAlbums = _state.value.iAlbums val iAlbums = _state.value.iAlbums
if (ndAlbums.isEmpty() || iAlbums.isEmpty()) return if (ndAlbums.isEmpty() || iAlbums.isEmpty()) return
fun norm(s: String?) = (s ?: "").lowercase().replace(Regex("[^a-z0-9]"), "") fun norm(s: String?) = (s ?: "").lowercase().replace(Regex("[^\\p{L}\\p{N}]"), "")
val enriched = coroutineScope { val enriched = coroutineScope {
iAlbums.map { ia -> iAlbums.map { ia ->
val cid = ia.collectionId val cid = ia.collectionId
@@ -220,7 +220,7 @@ class ExploreViewModel(app: Application) : AndroidViewModel(app) {
ndAlbums: List<Album>, iAlbums: List<ItunesItem>, ndAlbums: List<Album>, iAlbums: List<ItunesItem>,
): List<ItunesItem> { ): List<ItunesItem> {
if (ndAlbums.isEmpty() || iAlbums.isEmpty()) return iAlbums if (ndAlbums.isEmpty() || iAlbums.isEmpty()) return iAlbums
fun norm(s: String?) = (s ?: "").lowercase().replace(Regex("[^a-z0-9]"), "") fun norm(s: String?) = (s ?: "").lowercase().replace(Regex("[^\\p{L}\\p{N}]"), "")
var changed = false var changed = false
val out = coroutineScope { val out = coroutineScope {
iAlbums.map { ia -> iAlbums.map { ia ->
+4 -1
View File
@@ -337,7 +337,10 @@ export async function loadArtistDetail(artist) {
} }
function _normAlbum(s) { function _normAlbum(s) {
return (s || '').toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim(); // Keep Unicode letters/numbers (not just a-z0-9) — otherwise every Japanese/CJK title strips to an
// empty string, so all of an artist's JP singles collapse into one entry and even "match" each
// other (that hid inabakumori's whole singles discography behind one album).
return (s || '').toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, ' ').replace(/\s+/g, ' ').trim();
} }
async function _renderArtistAlbums(el, ndAlbums, iAlbums, artist) { async function _renderArtistAlbums(el, ndAlbums, iAlbums, artist) {