feat(explore): give the album page a primary action that tells the truth

H-13: no Play, no Shuffle, no Add to queue on the album header. The
reason it is not just three buttons is that explore-album-details is a
catalog page — there is no library-side album detail page at all — so
the album shown may be wholly the user's, partly theirs, or not theirs.
A Play button that plays 7 of a 40-track release under a label saying
'Play' is the page lying about what is owned, so the button says which:
'Play' when all of it is owned, 'Play 7 of 12' when some is, and no
play button at all when none is.

albumLibraryStatus() stays as it was — four claims of decreasing
confidence OR'd into one tick, the weakest firing when a single
recording matches. That is a fine answer to 'is any of this mine' and a
useless basis for a button, so ownership() counts the displayed
tracklist instead.

GetFilePathsByRecordingMBIDs is the catalog-side sibling of
GetFilePathsByAlbums: one query, paths only, grouped so the caller
keeps the tracklist's order. It is keyed on recording MBID because that
is how the backend decides a track is inLibrary, and because
MBTrack.LocalID is declared and never written by anything. The local
album id is preferred where there is one — a library-only album has no
MBIDs at all, and keying on them alone queued nothing.

The ticks also get the legend H-13 asks for. They were never unlabelled
— the indicator has carried a title and aria-label all along — but a
sighted user got a column of green circles and no key.
This commit is contained in:
2026-08-12 15:28:22 -04:00
parent 71324b561a
commit f854076d95
9 changed files with 928 additions and 2 deletions
@@ -907,6 +907,115 @@ func (q *Queries) GetAudioFilesNeedingMetadata(ctx context.Context) ([]AudioFile
return items, nil
}
const getFilePathsByRecordingMBIDs = `-- name: GetFilePathsByRecordingMBIDs :many
SELECT r.mbid AS recording_mbid, af.file_path
FROM recordings r
JOIN audio_files af ON af.recording_id = r.id
WHERE r.mbid IN (/*SLICE:mbids*/?)
ORDER BY af.file_path
`
type GetFilePathsByRecordingMBIDsRow struct {
RecordingMbid sql.NullString
FilePath string
}
// Same shape again, keyed on recording MBID, for the catalog side.
// An Explore album page knows which of its tracks the user owns only
// as a set of recording MBIDs -- that is exactly how the backend
// decides `inLibrary` (markReleasesInLibrary -> CheckMBIDs) -- and
// MBTrack.LocalID is declared but never written by anything, so there
// is no id to ask by. Grouped by MBID because a recording can have
// more than one file (the duplicate fixtures are precisely that) and
// because the caller owns the order: the tracklist's, not the
// database's.
func (q *Queries) GetFilePathsByRecordingMBIDs(ctx context.Context, mbids []sql.NullString) ([]GetFilePathsByRecordingMBIDsRow, error) {
query := getFilePathsByRecordingMBIDs
var queryParams []interface{}
if len(mbids) > 0 {
for _, v := range mbids {
queryParams = append(queryParams, v)
}
query = strings.Replace(query, "/*SLICE:mbids*/?", strings.Repeat(",?", len(mbids))[1:], 1)
} else {
query = strings.Replace(query, "/*SLICE:mbids*/?", "NULL", 1)
}
rows, err := q.db.QueryContext(ctx, query, queryParams...)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetFilePathsByRecordingMBIDsRow
for rows.Next() {
var i GetFilePathsByRecordingMBIDsRow
if err := rows.Scan(&i.RecordingMbid, &i.FilePath); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getFilePathsByRecordingMBIDsByLibrary = `-- name: GetFilePathsByRecordingMBIDsByLibrary :many
SELECT r.mbid AS recording_mbid, af.file_path
FROM recordings r
JOIN audio_files af ON af.recording_id = r.id
WHERE r.mbid IN (/*SLICE:mbids*/?)
AND af.library_id = ?
ORDER BY af.file_path
`
type GetFilePathsByRecordingMBIDsByLibraryParams struct {
Mbids []sql.NullString
LibraryID int64
}
type GetFilePathsByRecordingMBIDsByLibraryRow struct {
RecordingMbid sql.NullString
FilePath string
}
func (q *Queries) GetFilePathsByRecordingMBIDsByLibrary(ctx context.Context, arg GetFilePathsByRecordingMBIDsByLibraryParams) ([]GetFilePathsByRecordingMBIDsByLibraryRow, error) {
query := getFilePathsByRecordingMBIDsByLibrary
var queryParams []interface{}
if len(arg.Mbids) > 0 {
for _, v := range arg.Mbids {
queryParams = append(queryParams, v)
}
query = strings.Replace(query, "/*SLICE:mbids*/?", strings.Repeat(",?", len(arg.Mbids))[1:], 1)
} else {
query = strings.Replace(query, "/*SLICE:mbids*/?", "NULL", 1)
}
queryParams = append(queryParams, arg.LibraryID)
rows, err := q.db.QueryContext(ctx, query, queryParams...)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetFilePathsByRecordingMBIDsByLibraryRow
for rows.Next() {
var i GetFilePathsByRecordingMBIDsByLibraryRow
if err := rows.Scan(&i.RecordingMbid, &i.FilePath); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getFilePathsByReleaseGroups = `-- name: GetFilePathsByReleaseGroups :many
SELECT rgr.release_group_id, af.file_path