diff --git a/backend/library/query.go b/backend/library/query.go index f0c6e5b..0ca40a6 100644 --- a/backend/library/query.go +++ b/backend/library/query.go @@ -20,24 +20,27 @@ var ( // Track represents a playable audio file in the library. type Track struct { - TrackName string - ArtistName string - TrackLength string - FilePath string - TrackNumber int64 - DiscNumber int64 - Album string - Genre []string - Year int64 - Composer string - FileType string - SampleRate int64 - BitDepth int64 - Channels int64 - Bitrate int64 - FileSize int64 - PlayCount int64 - LastPlayed string + TrackName string + ArtistName string + TrackLength string + FilePath string + TrackNumber int64 + DiscNumber int64 + Album string + Genre []string + Year int64 + Composer string + FileType string + SampleRate int64 + BitDepth int64 + Channels int64 + Bitrate int64 + FileSize int64 + PlayCount int64 + LastPlayed string + RecordingMBID string + ArtistMBID string + ReleaseGroupMBID string } // genreDelimiter is the separator used by GROUP_CONCAT in the @@ -96,6 +99,47 @@ func mapTrackRow( } } +// TrackMBIDs holds MusicBrainz identifiers for a track, resolved +// from the recording, release group, and artist tables. +type TrackMBIDs struct { + RecordingMBID string `json:"recordingMbid"` + ReleaseGroupMBID string `json:"releaseGroupMbid"` + ArtistMBID string `json:"artistMbid"` +} + +// GetTrackMBIDs returns the MusicBrainz IDs for the track at the +// given file path. Returns empty strings for entities without MBIDs. +func (l *Library) GetTrackMBIDs(filePath string) TrackMBIDs { + rows, err := l.db.QueryContext(` + SELECT + COALESCE(r.mbid, '') AS recording_mbid, + COALESCE(rg.mbid, '') AS release_group_mbid, + COALESCE(a.mbid, '') AS artist_mbid + FROM audio_files af + JOIN recordings r ON af.recording_id = r.id + JOIN artist_credit ac ON r.artist_credit_id = ac.id + JOIN artist_credit_artist aca ON aca.credit_id = ac.id + JOIN artists a ON a.id = aca.artist_id + LEFT JOIN release_group_recordings rgr ON r.id = rgr.recording_id + LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id + WHERE af.file_path = ? + LIMIT 1 + `, filePath) + if err != nil { + return TrackMBIDs{} + } + + defer func() { _ = rows.Close() }() + + var result TrackMBIDs + + if rows.Next() { + _ = rows.Scan(&result.RecordingMBID, &result.ReleaseGroupMBID, &result.ArtistMBID) + } + + return result +} + // Artist represents an artist in the library. type Artist struct { ID int64 diff --git a/frontend/src/components/track-details/track-details.ts b/frontend/src/components/track-details/track-details.ts index fcba31e..bc056cc 100644 --- a/frontend/src/components/track-details/track-details.ts +++ b/frontend/src/components/track-details/track-details.ts @@ -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 {
+ ${this.renderMusicBrainzSection()} @@ -1297,6 +1338,14 @@ export class TrackDetails extends LitElement { ${t.TrackName || this.fileNameFromPath(t.FilePath)} + ${this.trackMBIDs?.recordingMbid + ? html` +