release / govulncheck (push) Successful in 15s
release / web (push) Failing after 1m21s
release / go (push) Successful in 1m35s
release / integration (push) Failing after 4m57s
release / android (push) Successful in 5m11s
release / Build signed APK (releases and dev) (push) Successful in 5m28s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
Minstrel keeps its own list of what it is to get, keyed by MusicBrainz release group. A want comes from an approved request, a new release by a followed artist, or an album whose files went missing, and is held once the library has a full edition of it. - Migration 0081: release_type_profiles (Standard seeded: Album, EP, Single, studio only), follows, follow_seen_groups and wants. lidarr_requests becomes requests with plain *_mbid columns; approved album and track requests move onto the wanted list, approved artist requests become follows, and the library's album artists start out followed. missing_reacquisitions and the retry settings go. - internal/wanted: follows are checked against MusicBrainz discographies daily. The first check sets a baseline, and only releases after it are wanted unless the follow asked for what already exists. Profiles filter by primary and secondary type, so live recordings stay out unless a profile ticks Live. - internal/requests (was lidarrrequests): approving puts the request on the wanted list, and an artist request can be approved on a chosen profile. A request completes when its want is held. - The re-acquisition sweeper wants a missing album back instead of asking Lidarr; searching for it is the search schedule's (#5521). - Web: an admin Wanted page (Wanted, Missing files, Held, Following), a follow control on artist pages, a release-type profiles card on Integrations, "Approve on a profile" on Requests, and want state on Missing files. The artist detail API returns the artist's MBID. - Android: the request body sends artist_mbid, album_mbid and track_mbid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
179 lines
7.3 KiB
Go
179 lines
7.3 KiB
Go
package api
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
// LoginRequest is the POST /api/auth/login body. Kept boring on purpose —
|
|
// web and Flutter both send the same payload.
|
|
type LoginRequest struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
// LoginResponse carries the opaque session token AND the authenticated user
|
|
// so the SPA can hydrate its auth store in one round-trip. The token is also
|
|
// set as a cookie; SPAs ignore the body token, Flutter uses it as bearer.
|
|
type LoginResponse struct {
|
|
Token string `json:"token"`
|
|
User UserView `json:"user"`
|
|
}
|
|
|
|
// UserView is the /api/* view of a user. Narrower than dbq.User — no hash,
|
|
// no api_token, no subsonic_password.
|
|
type UserView struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Username string `json:"username"`
|
|
IsAdmin bool `json:"is_admin"`
|
|
}
|
|
|
|
// Page is the generic pagination envelope used by list and search endpoints.
|
|
// Items is always non-nil at JSON encode time — handlers must initialize
|
|
// empty slices explicitly so absent results render as [] rather than null.
|
|
type Page[T any] struct {
|
|
Items []T `json:"items"`
|
|
Total int `json:"total"`
|
|
Limit int `json:"limit"`
|
|
Offset int `json:"offset"`
|
|
}
|
|
|
|
// ArtistRef is the lightweight artist shape used in lists and search results.
|
|
// SortName is exposed so the SPA's alphabetical-grid divider can group rows
|
|
// by the catalogue sort order ("The Beatles" → "B"). CoverURL is derived from
|
|
// a representative album (most-recent with cover_art_path); empty when the
|
|
// artist's discography has no cover art.
|
|
type ArtistRef struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
SortName string `json:"sort_name"`
|
|
AlbumCount int `json:"album_count"`
|
|
CoverURL string `json:"cover_url"`
|
|
}
|
|
|
|
// AlbumRef is the lightweight album shape used in lists, artist details,
|
|
// and search results. SortTitle is exposed so the SPA's alphabetical-grid
|
|
// divider can group rows by sort order ("The Wall" → "W"). CoverURL points
|
|
// to /api/albums/{id}/cover.
|
|
type AlbumRef struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
SortTitle string `json:"sort_title"`
|
|
ArtistID string `json:"artist_id"`
|
|
ArtistName string `json:"artist_name"`
|
|
Year int `json:"year,omitempty"`
|
|
TrackCount int `json:"track_count"`
|
|
DurationSec int `json:"duration_sec"`
|
|
CoverURL string `json:"cover_url"`
|
|
CoverArtSource *string `json:"cover_art_source"`
|
|
}
|
|
|
|
// TrackRef is the lightweight track shape used in album details and search.
|
|
// StreamURL points to /api/tracks/{id}/stream which Plan 3 implements.
|
|
type TrackRef struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
AlbumID string `json:"album_id"`
|
|
AlbumTitle string `json:"album_title"`
|
|
ArtistID string `json:"artist_id"`
|
|
ArtistName string `json:"artist_name"`
|
|
TrackNumber int `json:"track_number,omitempty"`
|
|
DiscNumber int `json:"disc_number,omitempty"`
|
|
DurationSec int `json:"duration_sec"`
|
|
StreamURL string `json:"stream_url"`
|
|
// Unavailable reports that the file is missing from disk (#2704).
|
|
//
|
|
// Present on every TrackRef rather than only where it can be true: the
|
|
// selection surfaces (recommendation, discover, mixes, browse) filter
|
|
// missing tracks out entirely, so it is always false there. The surfaces
|
|
// that CAN return one are the direct lookups — album detail and search —
|
|
// where the user asked for that specific thing by name or container and
|
|
// hiding it would be the wrong answer. Marking it lets the client grey
|
|
// it and keep it out of a queue.
|
|
Unavailable bool `json:"unavailable,omitempty"`
|
|
}
|
|
|
|
// ArtistDetail is the response body of GET /api/artists/{id}. Embeds the ref
|
|
// so callers read id/name/album_count from the same path as list entries.
|
|
type ArtistDetail struct {
|
|
ArtistRef
|
|
Albums []AlbumRef `json:"albums"`
|
|
// Genres carried by this artist's tracks, for quick-jump chips (#367).
|
|
// Always non-nil at JSON so the client can iterate without a null check.
|
|
Genres []string `json:"genres"`
|
|
// MBID is the artist's MusicBrainz id, which following is keyed by
|
|
// (M502 #5520). Empty when the tags never named one.
|
|
MBID string `json:"mbid"`
|
|
}
|
|
|
|
// AlbumDetail is the response body of GET /api/albums/{id}.
|
|
type AlbumDetail struct {
|
|
AlbumRef
|
|
Tracks []TrackRef `json:"tracks"`
|
|
// Genres carried by this album's tracks, for quick-jump chips (#367).
|
|
// Derived from the tracks rather than stored on the album, because genre
|
|
// lives on tracks and an album's tracks can disagree. Non-nil at JSON.
|
|
Genres []string `json:"genres"`
|
|
}
|
|
|
|
// SearchResponse is the body of GET /api/search. Each facet carries its own
|
|
// total + limit + offset; the client sends one shared limit/offset and the
|
|
// server applies it uniformly to each facet's LIMIT/OFFSET query.
|
|
type SearchResponse struct {
|
|
Artists Page[ArtistRef] `json:"artists"`
|
|
Albums Page[AlbumRef] `json:"albums"`
|
|
Tracks Page[TrackRef] `json:"tracks"`
|
|
}
|
|
|
|
// HomePayload is the response body of GET /api/home. Field counts:
|
|
// 50 (recently_added) / 25 (rediscover_albums) / 25 (rediscover_artists)
|
|
// / 75 (most_played) / 25 (last_played). All slices are non-nil at JSON
|
|
// encode time so empty sections render as [] rather than null.
|
|
type HomePayload struct {
|
|
RecentlyAddedAlbums []AlbumRef `json:"recently_added_albums"`
|
|
RediscoverAlbums []AlbumRef `json:"rediscover_albums"`
|
|
RediscoverArtists []ArtistRef `json:"rediscover_artists"`
|
|
MostPlayedTracks []TrackRef `json:"most_played_tracks"`
|
|
LastPlayedArtists []ArtistRef `json:"last_played_artists"`
|
|
// You-might-like: in-library albums/artists predicted from the user's
|
|
// listening that they don't actively spin. Built daily; gated on a
|
|
// minimum listening history so a thin profile gets empty rows.
|
|
YouMightLikeAlbums []AlbumRef `json:"you_might_like_albums"`
|
|
YouMightLikeArtists []ArtistRef `json:"you_might_like_artists"`
|
|
}
|
|
|
|
// HomeIndexPayload is the response body of GET /api/home/index — the
|
|
// per-item rendering variant of /api/home. Same sections, same caps,
|
|
// but each section is a flat slice of entity IDs (UUID strings)
|
|
// instead of denormalized objects. Slices are non-nil at JSON encode
|
|
// time so empty sections render as `[]`.
|
|
//
|
|
// Client fetches this, then hydrates each tile against the per-entity
|
|
// endpoints (/api/albums/{id}, /api/artists/{id}, /api/tracks/{id})
|
|
// for genuine progressive rendering. The section name implies the
|
|
// entity type so no per-entry type tag is needed.
|
|
type HomeIndexPayload struct {
|
|
RecentlyAddedAlbums []string `json:"recently_added_albums"`
|
|
RediscoverAlbums []string `json:"rediscover_albums"`
|
|
RediscoverArtists []string `json:"rediscover_artists"`
|
|
MostPlayedTracks []string `json:"most_played_tracks"`
|
|
LastPlayedArtists []string `json:"last_played_artists"`
|
|
YouMightLikeAlbums []string `json:"you_might_like_albums"`
|
|
YouMightLikeArtists []string `json:"you_might_like_artists"`
|
|
}
|
|
|
|
// HistoryEvent is one play in a user's listening history. Used by
|
|
// /api/me/history; one event per row from play_events.
|
|
type HistoryEvent struct {
|
|
ID string `json:"id"` // play_events.id
|
|
PlayedAt time.Time `json:"played_at"` // play_events.started_at
|
|
Track TrackRef `json:"track"`
|
|
}
|
|
|
|
// HistoryResponse is the body of GET /api/me/history.
|
|
type HistoryResponse struct {
|
|
Events []HistoryEvent `json:"events"`
|
|
HasMore bool `json:"has_more"`
|
|
}
|