feat: show English alias for non-Latin script artists
Extract primary English alias from MusicBrainz artist data when the canonical name uses non-Latin script (CJK, Cyrillic, etc.). Display it as the primary name in search results and artist detail header, with the native script name as a subtitle beneath. Example: 山下達郎 now shows 'Tatsuro Yamashita' prominently with '山下達郎' as a subtitle. Artists with Latin names are unchanged.
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"go.uploadedlobster.com/mbtypes"
|
"go.uploadedlobster.com/mbtypes"
|
||||||
"go.uploadedlobster.com/musicbrainzws2"
|
"go.uploadedlobster.com/musicbrainzws2"
|
||||||
@@ -336,7 +337,7 @@ func clampLimit(limit int) int {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func convertArtist(a musicbrainzws2.Artist) MBArtist {
|
func convertArtist(a musicbrainzws2.Artist) MBArtist {
|
||||||
return MBArtist{
|
out := MBArtist{
|
||||||
MBID: string(a.ID),
|
MBID: string(a.ID),
|
||||||
Name: a.Name,
|
Name: a.Name,
|
||||||
SortName: a.SortName,
|
SortName: a.SortName,
|
||||||
@@ -345,6 +346,15 @@ func convertArtist(a musicbrainzws2.Artist) MBArtist {
|
|||||||
Disambiguation: a.Disambiguation,
|
Disambiguation: a.Disambiguation,
|
||||||
Score: a.Score,
|
Score: a.Score,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract the primary English alias when the canonical name
|
||||||
|
// is non-Latin (CJK, Cyrillic, etc.). This lets the frontend
|
||||||
|
// show "Tatsuro Yamashita" alongside "山下達郎".
|
||||||
|
if !isLatinScript(a.Name) {
|
||||||
|
out.EnglishName = primaryEnglishAlias(a.Aliases)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertArtists(artists []musicbrainzws2.Artist) []MBArtist {
|
func convertArtists(artists []musicbrainzws2.Artist) []MBArtist {
|
||||||
@@ -356,6 +366,39 @@ func convertArtists(artists []musicbrainzws2.Artist) []MBArtist {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// primaryEnglishAlias returns the primary English alias name from
|
||||||
|
// a slice of aliases, or "" if none exists.
|
||||||
|
func primaryEnglishAlias(aliases []musicbrainzws2.Alias) string {
|
||||||
|
// Prefer primary English alias.
|
||||||
|
for _, a := range aliases {
|
||||||
|
if a.Locale == "en" && a.IsPrimary {
|
||||||
|
return a.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to any English alias.
|
||||||
|
for _, a := range aliases {
|
||||||
|
if a.Locale == "en" {
|
||||||
|
return a.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// isLatinScript returns true if the string consists primarily of
|
||||||
|
// Latin characters, digits, and common punctuation. Returns false
|
||||||
|
// for CJK, Cyrillic, Arabic, etc.
|
||||||
|
func isLatinScript(s string) bool {
|
||||||
|
for _, r := range s {
|
||||||
|
if unicode.IsLetter(r) && !unicode.In(r, unicode.Latin) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func convertReleaseGroup(rg musicbrainzws2.ReleaseGroup) MBReleaseGroup {
|
func convertReleaseGroup(rg musicbrainzws2.ReleaseGroup) MBReleaseGroup {
|
||||||
return MBReleaseGroup{
|
return MBReleaseGroup{
|
||||||
MBID: string(rg.ID),
|
MBID: string(rg.ID),
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type MBArtist struct {
|
|||||||
MBID string `json:"mbid"`
|
MBID string `json:"mbid"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
SortName string `json:"sortName"`
|
SortName string `json:"sortName"`
|
||||||
|
EnglishName string `json:"englishName,omitempty"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Country string `json:"country"`
|
Country string `json:"country"`
|
||||||
Disambiguation string `json:"disambiguation"`
|
Disambiguation string `json:"disambiguation"`
|
||||||
|
|||||||
@@ -169,6 +169,15 @@ export class ExploreArtistDetails extends LitElement {
|
|||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.artist-native-name {
|
||||||
|
font-size: var(--yj-text-md);
|
||||||
|
color: var(--yj-text-secondary, #b3b3b3);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.artist-meta {
|
.artist-meta {
|
||||||
font-size: var(--yj-text-md);
|
font-size: var(--yj-text-md);
|
||||||
color: var(--yj-text-secondary, #b3b3b3);
|
color: var(--yj-text-secondary, #b3b3b3);
|
||||||
@@ -611,6 +620,11 @@ export class ExploreArtistDetails extends LitElement {
|
|||||||
return name.charAt(0).toUpperCase();
|
return name.charAt(0).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** English name if available, otherwise the native name. */
|
||||||
|
private get displayName(): string {
|
||||||
|
return this.artist?.englishName || this.artistName;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Group release groups by type, returning entries in the
|
* Group release groups by type, returning entries in the
|
||||||
* canonical order: Albums → EP → Single → Other Albums → ...rest.
|
* canonical order: Albums → EP → Single → Other Albums → ...rest.
|
||||||
@@ -695,12 +709,15 @@ export class ExploreArtistDetails extends LitElement {
|
|||||||
class="artist-avatar"
|
class="artist-avatar"
|
||||||
style="background: hsl(${hue}, 45%, 35%)"
|
style="background: hsl(${hue}, 45%, 35%)"
|
||||||
>
|
>
|
||||||
${this.getInitial(this.artistName)}
|
${this.getInitial(this.displayName)}
|
||||||
</div>
|
</div>
|
||||||
<div class="artist-info">
|
<div class="artist-info">
|
||||||
<h1 class="artist-title" title="${this.artistName}">
|
<h1 class="artist-title" title="${this.displayName}">
|
||||||
${this.artistName}
|
${this.displayName}
|
||||||
</h1>
|
</h1>
|
||||||
|
${this.artist?.englishName
|
||||||
|
? html`<div class="artist-native-name">${this.artist.name}</div>`
|
||||||
|
: nothing}
|
||||||
${this.renderArtistMeta()}
|
${this.renderArtistMeta()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -303,6 +303,16 @@ export class ExploreView extends LitElement {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.artist-native-name {
|
||||||
|
color: var(--yj-text-secondary, #aaa);
|
||||||
|
font-size: var(--yj-text-xs);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
.artist-disambiguation {
|
.artist-disambiguation {
|
||||||
color: var(--yj-text-tertiary, #888);
|
color: var(--yj-text-tertiary, #888);
|
||||||
font-size: var(--yj-text-xs);
|
font-size: var(--yj-text-xs);
|
||||||
@@ -769,7 +779,7 @@ export class ExploreView extends LitElement {
|
|||||||
class="artist-avatar"
|
class="artist-avatar"
|
||||||
style="background: hsl(${hue}, 45%, 35%)"
|
style="background: hsl(${hue}, 45%, 35%)"
|
||||||
>
|
>
|
||||||
${a.name.charAt(0).toUpperCase()}
|
${(a.englishName || a.name).charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div class="top-card-info">
|
<div class="top-card-info">
|
||||||
<div class="top-card-name">${a.name}</div>
|
<div class="top-card-name">${a.name}</div>
|
||||||
@@ -826,11 +836,14 @@ export class ExploreView extends LitElement {
|
|||||||
class="artist-avatar"
|
class="artist-avatar"
|
||||||
style="background: hsl(${hue}, 45%, 35%)"
|
style="background: hsl(${hue}, 45%, 35%)"
|
||||||
>
|
>
|
||||||
${a.name.charAt(0).toUpperCase()}
|
${(a.englishName || a.name).charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div class="artist-name" title="${a.name}">
|
<div class="artist-name" title="${a.englishName || a.name}">
|
||||||
${a.name}
|
${a.englishName || a.name}
|
||||||
</div>
|
</div>
|
||||||
|
${a.englishName
|
||||||
|
? html`<div class="artist-native-name">${a.name}</div>`
|
||||||
|
: nothing}
|
||||||
${a.disambiguation
|
${a.disambiguation
|
||||||
? html`<div class="artist-disambiguation">
|
? html`<div class="artist-disambiguation">
|
||||||
${a.disambiguation}
|
${a.disambiguation}
|
||||||
|
|||||||
@@ -1,3 +1,204 @@
|
|||||||
|
export namespace explore {
|
||||||
|
|
||||||
|
export class LBSimilarArtist {
|
||||||
|
artistMbid: string;
|
||||||
|
name: string;
|
||||||
|
score: number;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new LBSimilarArtist(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.artistMbid = source["artistMbid"];
|
||||||
|
this.name = source["name"];
|
||||||
|
this.score = source["score"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class LBTopRecording {
|
||||||
|
recordingMbid: string;
|
||||||
|
artistName: string;
|
||||||
|
trackName: string;
|
||||||
|
totalListenCount: number;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new LBTopRecording(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.recordingMbid = source["recordingMbid"];
|
||||||
|
this.artistName = source["artistName"];
|
||||||
|
this.trackName = source["trackName"];
|
||||||
|
this.totalListenCount = source["totalListenCount"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class MBArtist {
|
||||||
|
mbid: string;
|
||||||
|
name: string;
|
||||||
|
sortName: string;
|
||||||
|
englishName?: string;
|
||||||
|
type: string;
|
||||||
|
country: string;
|
||||||
|
disambiguation: string;
|
||||||
|
score: number;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new MBArtist(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.mbid = source["mbid"];
|
||||||
|
this.name = source["name"];
|
||||||
|
this.sortName = source["sortName"];
|
||||||
|
this.englishName = source["englishName"];
|
||||||
|
this.type = source["type"];
|
||||||
|
this.country = source["country"];
|
||||||
|
this.disambiguation = source["disambiguation"];
|
||||||
|
this.score = source["score"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class MBRecording {
|
||||||
|
mbid: string;
|
||||||
|
title: string;
|
||||||
|
length: number;
|
||||||
|
artistCredit: string;
|
||||||
|
score: number;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new MBRecording(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.mbid = source["mbid"];
|
||||||
|
this.title = source["title"];
|
||||||
|
this.length = source["length"];
|
||||||
|
this.artistCredit = source["artistCredit"];
|
||||||
|
this.score = source["score"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class MBTrack {
|
||||||
|
position: number;
|
||||||
|
discNumber: number;
|
||||||
|
title: string;
|
||||||
|
length: number;
|
||||||
|
mbid: string;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new MBTrack(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.position = source["position"];
|
||||||
|
this.discNumber = source["discNumber"];
|
||||||
|
this.title = source["title"];
|
||||||
|
this.length = source["length"];
|
||||||
|
this.mbid = source["mbid"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class MBRelease {
|
||||||
|
mbid: string;
|
||||||
|
title: string;
|
||||||
|
date: string;
|
||||||
|
country: string;
|
||||||
|
status: string;
|
||||||
|
tracks?: MBTrack[];
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new MBRelease(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.mbid = source["mbid"];
|
||||||
|
this.title = source["title"];
|
||||||
|
this.date = source["date"];
|
||||||
|
this.country = source["country"];
|
||||||
|
this.status = source["status"];
|
||||||
|
this.tracks = this.convertValues(source["tracks"], MBTrack);
|
||||||
|
}
|
||||||
|
|
||||||
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
|
if (!a) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
if (a.slice && a.map) {
|
||||||
|
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||||
|
} else if ("object" === typeof a) {
|
||||||
|
if (asMap) {
|
||||||
|
for (const key of Object.keys(a)) {
|
||||||
|
a[key] = new classs(a[key]);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
return new classs(a);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class MBReleaseGroup {
|
||||||
|
mbid: string;
|
||||||
|
title: string;
|
||||||
|
primaryType: string;
|
||||||
|
secondaryTypes?: string[];
|
||||||
|
firstReleaseDate: string;
|
||||||
|
artistCredit: string;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new MBReleaseGroup(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.mbid = source["mbid"];
|
||||||
|
this.title = source["title"];
|
||||||
|
this.primaryType = source["primaryType"];
|
||||||
|
this.secondaryTypes = source["secondaryTypes"];
|
||||||
|
this.firstReleaseDate = source["firstReleaseDate"];
|
||||||
|
this.artistCredit = source["artistCredit"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class MBSearchResult {
|
||||||
|
artists?: MBArtist[];
|
||||||
|
releaseGroups?: MBReleaseGroup[];
|
||||||
|
recordings?: MBRecording[];
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new MBSearchResult(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.artists = this.convertValues(source["artists"], MBArtist);
|
||||||
|
this.releaseGroups = this.convertValues(source["releaseGroups"], MBReleaseGroup);
|
||||||
|
this.recordings = this.convertValues(source["recordings"], MBRecording);
|
||||||
|
}
|
||||||
|
|
||||||
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
|
if (!a) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
if (a.slice && a.map) {
|
||||||
|
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||||
|
} else if ("object" === typeof a) {
|
||||||
|
if (asMap) {
|
||||||
|
for (const key of Object.keys(a)) {
|
||||||
|
a[key] = new classs(a[key]);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
return new classs(a);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
export namespace library {
|
export namespace library {
|
||||||
|
|
||||||
export class Album {
|
export class Album {
|
||||||
|
|||||||
Reference in New Issue
Block a user