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

@@ -40,7 +40,9 @@ import com.novasonic.app.ui.theme.*
import com.novasonic.app.viewmodel.ItunesArtistState
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(
val downloaded: List<Pair<Album, ItunesItem?>>,
@@ -27,7 +27,9 @@ import com.novasonic.app.ui.theme.*
import com.novasonic.app.viewmodel.PartialAlbumState
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
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.
// 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.
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 iArtist = (
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 iAlbums = _state.value.iAlbums
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 {
iAlbums.map { ia ->
val cid = ia.collectionId
@@ -220,7 +220,7 @@ class ExploreViewModel(app: Application) : AndroidViewModel(app) {
ndAlbums: List<Album>, iAlbums: List<ItunesItem>,
): List<ItunesItem> {
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
val out = coroutineScope {
iAlbums.map { ia ->