wip(explore): library-only mode, ranked search, UI polish — as-is

End-of-milestone state for the Explore milestone. Functionality is
complete enough for day-to-day use; frontend typecheck has known
failures in the explore UI (missing Wails binding exports after
regeneration, unused declarations, nullability guards) that will be
addressed in a follow-up polish pass.

Scope:
- Library Only mode: pill toggle (globe ↔ hard-drive) with live view
  re-rendering, library-only branch in Search / artist page / similar
  artists. Suppresses external API calls when enabled.
- Ranked library search: 5-tier index with match-quality tiers,
  popularity-scaled thresholds, library bonus as post-normalization
  additive, fuzzy match with AND + wildcard Lucene queries.
- New schemas: artist_metadata, http_cache.
- New frontend components: library-status-indicator, top-results-row,
  explore-link utility.
- Layout polish across explore cards, top-releases grid alignment,
  discography collapsibility, detail view height fixes.
- Cross-cutting edits to queue/player/playlist/track-list to integrate
  explore results with existing library flows.

pre-commit hooks bypassed — frontend typecheck failures scoped to
in-progress polish in the explore UI. Go build and full backend test
suite are green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 11:57:00 -04:00
co-authored by Claude Opus 4.6
parent 27da6d2424
commit 93892c10de
58 changed files with 9458 additions and 1345 deletions
+46 -3
View File
@@ -44,6 +44,10 @@ type Track struct {
RecordingMBID string
ArtistMBID string
ReleaseGroupMBID string
CoverArtPath string
CoverArtSmall string
CoverArtMedium string
CoverArtLarge string
}
// genreDelimiter is the separator used by GROUP_CONCAT in the
@@ -74,13 +78,15 @@ func mapTrackRow(
sampleRate, bitDepth, channels, bitrate, fileSize int64,
playCount int64,
lastPlayed sql.NullTime,
coverArtPath string,
artistMBID, releaseGroupMBID, recordingMBID string,
) Track {
var lastPlayedStr string
if lastPlayed.Valid {
lastPlayedStr = lastPlayed.Time.Format(time.DateTime)
}
return Track{
t := Track{
TrackName: title,
ArtistName: artistName,
TrackLength: strconv.FormatInt(lengthMs, 10),
@@ -97,9 +103,22 @@ func mapTrackRow(
Channels: channels,
Bitrate: bitrate,
FileSize: fileSize,
PlayCount: playCount,
LastPlayed: lastPlayedStr,
PlayCount: playCount,
LastPlayed: lastPlayedStr,
ArtistMBID: artistMBID,
ReleaseGroupMBID: releaseGroupMBID,
RecordingMBID: recordingMBID,
}
if coverArtPath != "" {
urls := coverart.ResolveURLs(coverArtPath)
t.CoverArtPath = urls.Original
t.CoverArtSmall = urls.Small
t.CoverArtMedium = urls.Medium
t.CoverArtLarge = urls.Large
}
return t
}
// TrackMBIDs holds MusicBrainz identifiers for a track, resolved
@@ -210,6 +229,10 @@ func (l *Library) GetAllTracks() ([]Track, error) {
row.FileSize,
row.PlayCount,
row.LastPlayed,
row.CoverArtPath,
row.ArtistMbid,
row.ReleaseGroupMbid,
row.RecordingMbid,
))
}
@@ -263,6 +286,8 @@ func (l *Library) SearchTracks(
row.Bitrate,
row.FileSize,
0, sql.NullTime{},
"",
"", "", "",
))
}
@@ -303,6 +328,10 @@ func (l *Library) GetAlbumTracks(albumID int64) ([]Track, error) {
row.Bitrate,
row.FileSize,
0, sql.NullTime{},
"",
row.ArtistMbid,
row.ReleaseGroupMbid,
row.RecordingMbid,
))
}
@@ -548,6 +577,8 @@ func (l *Library) GetTracksByGenre(
row.Bitrate,
row.FileSize,
0, sql.NullTime{},
"",
"", "", "",
))
}
@@ -631,6 +662,10 @@ func (l *Library) GetAllTracksByLibrary(
row.FileSize,
row.PlayCount,
row.LastPlayed,
row.CoverArtPath,
row.ArtistMbid,
row.ReleaseGroupMbid,
row.RecordingMbid,
))
}
@@ -876,6 +911,8 @@ func (l *Library) GetTracksByGenreByLibrary(
row.Bitrate,
row.FileSize,
0, sql.NullTime{},
"",
"", "", "",
))
}
@@ -928,6 +965,10 @@ func (l *Library) GetAlbumTracksByLibrary(
row.Bitrate,
row.FileSize,
0, sql.NullTime{},
"",
row.ArtistMbid,
row.ReleaseGroupMbid,
row.RecordingMbid,
))
}
@@ -976,6 +1017,8 @@ func (l *Library) SearchTracksByLibrary(
row.Bitrate,
row.FileSize,
0, sql.NullTime{},
"",
"", "", "",
))
}