Rewrites the internal/api comment blocks to rule 32: task, milestone and rule ids gone, the stories behind them restated as the constraint they left, stale references corrected (the stream secret is wired from config, the timezone handler already refreshes the scheduler, the Android app is the native client, the design docs cited are not in the repo). Also rewraps comment lines over 80 columns left by the earlier batches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
180 lines
7.3 KiB
Go
180 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 Android 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; the SPA ignores the body token, a native client may send
|
|
// it as a 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.
|
|
//
|
|
// 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.
|
|
// 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.
|
|
// 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.
|
|
// 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"`
|
|
}
|