feat: MusicBrainz verification badge + MBID links in track details

Track details dialog now shows:

1. Green checkmark badge next to the track title when the recording
   has a MusicBrainz ID (hover: 'Metadata verified by MusicBrainz')

2. MusicBrainz section at the bottom with clickable MBID links for:
   - Recording (track) → musicbrainz.org/recording/{mbid}
   - Release Group (album) → musicbrainz.org/release-group/{mbid}
   - Artist → musicbrainz.org/artist/{mbid}

   Links open in the system browser. Only shown for entities that
   have MBIDs from audio file tags.

Backend: GetTrackMBIDs(filePath) Wails binding queries recording,
release_group, and artist mbid columns via a single JOIN query.
Frontend: loaded async when the dialog opens, non-blocking.
This commit is contained in:
2026-03-28 10:11:52 -04:00
parent 54b074eae7
commit 9a841cd173
4 changed files with 190 additions and 18 deletions
@@ -18,6 +18,8 @@ import {
BatchWriteTrackTags,
CancelBatchWrite,
} from '@go/tagwriter/TagWriter';
import { GetTrackMBIDs } from '@go/library/Library';
import type { TrackMBIDs } from '@go/library/Library';
import { ImageFilePicker, ReadFile } from '@go/frontendutil/FrontendUtil';
import { libraryStore } from '../../store/library-store';
import { EventsOn, EventsOff } from '@runtime/runtime';
@@ -98,6 +100,7 @@ export class TrackDetails extends LitElement {
failures: Array<{ filePath: string; error: string }>;
} | null = null;
@state() private showConfirmation = false;
@state() private trackMBIDs: TrackMBIDs | null = null;
@query('wa-dialog')
private dialog!: HTMLElement & { open: boolean };
@@ -117,9 +120,13 @@ export class TrackDetails extends LitElement {
this.editing = false;
this.editValues = {};
this.errorMessage = '';
this.trackMBIDs = null;
this.cleanupPendingCoverArt();
this.resetBatchState();
// Load MBIDs asynchronously.
this.loadMBIDs(track.FilePath);
this.updateComplete.then(() => {
if (this.dialog) this.dialog.open = true;
});
@@ -316,6 +323,39 @@ export class TrackDetails extends LitElement {
font-style: italic;
}
/* MusicBrainz badge + links */
.mb-verified-badge {
color: #1db954;
font-size: 14px;
margin-left: 6px;
vertical-align: middle;
cursor: help;
}
.mb-section-header {
display: flex;
align-items: center;
gap: 6px;
}
.mb-icon {
color: #1db954;
font-size: 14px;
}
.mb-link {
font-size: var(--yj-text-xs);
color: var(--yj-text-secondary, #b3b3b3);
text-decoration: none;
word-break: break-all;
font-family: monospace;
}
.mb-link:hover {
color: #1db954;
text-decoration: underline;
}
/* Edit mode inputs */
.meta-input {
width: 100%;
@@ -725,6 +765,7 @@ export class TrackDetails extends LitElement {
<div class="metadata-grid">
${this.renderAudioProperties(t)}
</div>
${this.renderMusicBrainzSection()}
<div class="action-bar">
${this.renderActions()}
</div>
@@ -1297,6 +1338,14 @@ export class TrackDetails extends LitElement {
<label class="main-field-label">Title</label>
<span class="title">
${t.TrackName || this.fileNameFromPath(t.FilePath)}
${this.trackMBIDs?.recordingMbid
? html`<span
class="mb-verified-badge"
title="Metadata verified by MusicBrainz"
>
<wa-icon name="circle-check"></wa-icon>
</span>`
: nothing}
</span>
</div>
<div class="main-field-group">
@@ -1420,6 +1469,61 @@ export class TrackDetails extends LitElement {
);
}
private renderMusicBrainzSection() {
if (!this.trackMBIDs) return nothing;
const mbids = this.trackMBIDs;
const links: Array<{ label: string; mbid: string; type: string }> = [];
if (mbids.recordingMbid) {
links.push({
label: 'Recording',
mbid: mbids.recordingMbid,
type: 'recording',
});
}
if (mbids.releaseGroupMbid) {
links.push({
label: 'Release Group',
mbid: mbids.releaseGroupMbid,
type: 'release-group',
});
}
if (mbids.artistMbid) {
links.push({
label: 'Artist',
mbid: mbids.artistMbid,
type: 'artist',
});
}
if (links.length === 0) return nothing;
return html`
<div class="section-header mb-section-header">
<wa-icon name="circle-check" class="mb-icon"></wa-icon>
MusicBrainz
</div>
<div class="metadata-grid">
${links.map(
(l) => html`
<span class="meta-label">${l.label}</span>
<a
class="mb-link"
href="https://musicbrainz.org/${l.type}/${l.mbid}"
target="_blank"
title="View on MusicBrainz"
>
${l.mbid}
</a>
`,
)}
</div>
`;
}
private renderField(f: MetadataField) {
const display =
this.getEditValue(f.key, f.value) || f.value;
@@ -1885,6 +1989,18 @@ export class TrackDetails extends LitElement {
this.cleanupPendingCoverArt();
}
private async loadMBIDs(filePath: string): Promise<void> {
try {
const mbids = await GetTrackMBIDs(filePath);
if (mbids.recordingMbid || mbids.releaseGroupMbid || mbids.artistMbid) {
this.trackMBIDs = mbids;
}
} catch {
// Non-critical — MBIDs just won't show.
}
}
private cleanupPendingCoverArt(): void {
if (this.pendingCoverArt?.previewUrl) {
URL.revokeObjectURL(