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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user