diff --git a/backend/explore/musicbrainz.go b/backend/explore/musicbrainz.go
index ebe0f38..dcf4831 100644
--- a/backend/explore/musicbrainz.go
+++ b/backend/explore/musicbrainz.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"log/slog"
"time"
+ "unicode"
"go.uploadedlobster.com/mbtypes"
"go.uploadedlobster.com/musicbrainzws2"
@@ -336,7 +337,7 @@ func clampLimit(limit int) int {
// ---------------------------------------------------------------------------
func convertArtist(a musicbrainzws2.Artist) MBArtist {
- return MBArtist{
+ out := MBArtist{
MBID: string(a.ID),
Name: a.Name,
SortName: a.SortName,
@@ -345,6 +346,15 @@ func convertArtist(a musicbrainzws2.Artist) MBArtist {
Disambiguation: a.Disambiguation,
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 {
@@ -356,6 +366,39 @@ func convertArtists(artists []musicbrainzws2.Artist) []MBArtist {
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 {
return MBReleaseGroup{
MBID: string(rg.ID),
diff --git a/backend/explore/types.go b/backend/explore/types.go
index 71d663a..51d7c8a 100644
--- a/backend/explore/types.go
+++ b/backend/explore/types.go
@@ -20,6 +20,7 @@ type MBArtist struct {
MBID string `json:"mbid"`
Name string `json:"name"`
SortName string `json:"sortName"`
+ EnglishName string `json:"englishName,omitempty"`
Type string `json:"type"`
Country string `json:"country"`
Disambiguation string `json:"disambiguation"`
diff --git a/frontend/src/components/explore-artist-details/explore-artist-details.ts b/frontend/src/components/explore-artist-details/explore-artist-details.ts
index 5965a78..e02dcf8 100644
--- a/frontend/src/components/explore-artist-details/explore-artist-details.ts
+++ b/frontend/src/components/explore-artist-details/explore-artist-details.ts
@@ -169,6 +169,15 @@ export class ExploreArtistDetails extends LitElement {
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 {
font-size: var(--yj-text-md);
color: var(--yj-text-secondary, #b3b3b3);
@@ -611,6 +620,11 @@ export class ExploreArtistDetails extends LitElement {
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
* canonical order: Albums → EP → Single → Other Albums → ...rest.
@@ -695,12 +709,15 @@ export class ExploreArtistDetails extends LitElement {
class="artist-avatar"
style="background: hsl(${hue}, 45%, 35%)"
>
- ${this.getInitial(this.artistName)}
+ ${this.getInitial(this.displayName)}
-
- ${this.artistName}
+
+ ${this.displayName}
+ ${this.artist?.englishName
+ ? html`
${this.artist.name}
`
+ : nothing}
${this.renderArtistMeta()}
diff --git a/frontend/src/components/explore-view/explore-view.ts b/frontend/src/components/explore-view/explore-view.ts
index 5ce4947..0b6a2d0 100644
--- a/frontend/src/components/explore-view/explore-view.ts
+++ b/frontend/src/components/explore-view/explore-view.ts
@@ -303,6 +303,16 @@ export class ExploreView extends LitElement {
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 {
color: var(--yj-text-tertiary, #888);
font-size: var(--yj-text-xs);
@@ -769,7 +779,7 @@ export class ExploreView extends LitElement {
class="artist-avatar"
style="background: hsl(${hue}, 45%, 35%)"
>
- ${a.name.charAt(0).toUpperCase()}
+ ${(a.englishName || a.name).charAt(0).toUpperCase()}
${a.name}
@@ -826,11 +836,14 @@ export class ExploreView extends LitElement {
class="artist-avatar"
style="background: hsl(${hue}, 45%, 35%)"
>
- ${a.name.charAt(0).toUpperCase()}
+ ${(a.englishName || a.name).charAt(0).toUpperCase()}
-
- ${a.name}
+
+ ${a.englishName || a.name}
+ ${a.englishName
+ ? html`
${a.name}
`
+ : nothing}
${a.disambiguation
? html`
${a.disambiguation}
diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts
index b35f67d..45e7717 100755
--- a/frontend/wailsjs/go/models.ts
+++ b/frontend/wailsjs/go/models.ts
@@ -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 class Album {