Phase 2: Subsonic REST API framework (auth, response formatting, error handling, routing) plus user permission model

This commit is contained in:
2026-08-22 23:58:54 -05:00
parent f81c8704a5
commit b5e2bf01e2
26 changed files with 1772 additions and 75 deletions
+114
View File
@@ -0,0 +1,114 @@
// Package subsonic implements the original Subsonic REST API (through
// version 1.16.1) on top of VibeSonic's existing database, scanner,
// streaming, and cover art packages. It does not duplicate business
// logic: every handler here is a thin adapter from Subsonic's wire
// format onto internal/database, internal/library, internal/streaming,
// and internal/coverart.
//
// One set of Go structs (this file and its model_*.go siblings) is
// shared between XML and JSON output via struct tags, so response
// shape can never drift between formats — see response.go for how they
// are serialized.
package subsonic
import "encoding/xml"
// ServerVersion is the highest Subsonic REST API version VibeSonic
// implements. Requests from a client claiming a newer protocol version
// are rejected (error code 20); older clients are accepted, since the
// protocol is additive.
const ServerVersion = "1.16.1"
// Response is the envelope every /rest/* call returns: exactly one of
// the payload fields below is set, matching the single child element
// real Subsonic servers emit inside <subsonic-response>.
type Response struct {
XMLName xml.Name `xml:"subsonic-response" json:"-"`
Xmlns string `xml:"xmlns,attr" json:"-"`
Status string `xml:"status,attr" json:"status"`
Version string `xml:"version,attr" json:"version"`
Error *Error `xml:"error,omitempty" json:"error,omitempty"`
License *License `xml:"license,omitempty" json:"license,omitempty"`
MusicFolders *MusicFolders `xml:"musicFolders,omitempty" json:"musicFolders,omitempty"`
Indexes *Indexes `xml:"indexes,omitempty" json:"indexes,omitempty"`
Directory *Directory `xml:"directory,omitempty" json:"directory,omitempty"`
Genres *Genres `xml:"genres,omitempty" json:"genres,omitempty"`
Artists *ArtistsID3 `xml:"artists,omitempty" json:"artists,omitempty"`
Artist *ArtistWithAlbumsID3 `xml:"artist,omitempty" json:"artist,omitempty"`
Album *AlbumWithSongsID3 `xml:"album,omitempty" json:"album,omitempty"`
Song *Child `xml:"song,omitempty" json:"song,omitempty"`
Videos *Videos `xml:"videos,omitempty" json:"videos,omitempty"`
VideoInfo *VideoInfo `xml:"videoInfo,omitempty" json:"videoInfo,omitempty"`
ArtistInfo *ArtistInfo `xml:"artistInfo,omitempty" json:"artistInfo,omitempty"`
ArtistInfo2 *ArtistInfo2 `xml:"artistInfo2,omitempty" json:"artistInfo2,omitempty"`
AlbumInfo *AlbumInfo `xml:"albumInfo,omitempty" json:"albumInfo,omitempty"`
SimilarSongs *SimilarSongs `xml:"similarSongs,omitempty" json:"similarSongs,omitempty"`
SimilarSongs2 *SimilarSongs2 `xml:"similarSongs2,omitempty" json:"similarSongs2,omitempty"`
TopSongs *TopSongs `xml:"topSongs,omitempty" json:"topSongs,omitempty"`
AlbumList *AlbumList `xml:"albumList,omitempty" json:"albumList,omitempty"`
AlbumList2 *AlbumList2 `xml:"albumList2,omitempty" json:"albumList2,omitempty"`
RandomSongs *Songs `xml:"randomSongs,omitempty" json:"randomSongs,omitempty"`
SongsByGenre *Songs `xml:"songsByGenre,omitempty" json:"songsByGenre,omitempty"`
NowPlaying *NowPlaying `xml:"nowPlaying,omitempty" json:"nowPlaying,omitempty"`
Starred *Starred `xml:"starred,omitempty" json:"starred,omitempty"`
Starred2 *Starred2 `xml:"starred2,omitempty" json:"starred2,omitempty"`
SearchResult *SearchResult `xml:"searchResult,omitempty" json:"searchResult,omitempty"`
SearchResult2 *SearchResult2 `xml:"searchResult2,omitempty" json:"searchResult2,omitempty"`
SearchResult3 *SearchResult3 `xml:"searchResult3,omitempty" json:"searchResult3,omitempty"`
Playlists *Playlists `xml:"playlists,omitempty" json:"playlists,omitempty"`
Playlist *PlaylistWithSongs `xml:"playlist,omitempty" json:"playlist,omitempty"`
JukeboxStatus *JukeboxStatus `xml:"jukeboxStatus,omitempty" json:"jukeboxStatus,omitempty"`
JukeboxPlaylist *JukeboxPlaylist `xml:"jukeboxPlaylist,omitempty" json:"jukeboxPlaylist,omitempty"`
InternetRadioStations *InternetRadioStations `xml:"internetRadioStations,omitempty" json:"internetRadioStations,omitempty"`
ChatMessages *ChatMessages `xml:"chatMessages,omitempty" json:"chatMessages,omitempty"`
User *User `xml:"user,omitempty" json:"user,omitempty"`
Users *Users `xml:"users,omitempty" json:"users,omitempty"`
Bookmarks *Bookmarks `xml:"bookmarks,omitempty" json:"bookmarks,omitempty"`
PlayQueue *PlayQueue `xml:"playQueue,omitempty" json:"playQueue,omitempty"`
Shares *Shares `xml:"shares,omitempty" json:"shares,omitempty"`
Podcasts *Podcasts `xml:"podcasts,omitempty" json:"podcasts,omitempty"`
NewestPodcasts *NewestPodcasts `xml:"newestPodcasts,omitempty" json:"newestPodcasts,omitempty"`
ScanStatus *ScanStatus `xml:"scanStatus,omitempty" json:"scanStatus,omitempty"`
Lyrics *Lyrics `xml:"lyrics,omitempty" json:"lyrics,omitempty"`
}
func newResponse() *Response {
return &Response{Xmlns: "http://subsonic.org/restapi", Status: "ok", Version: ServerVersion}
}
// Error is the payload of a failed response.
type Error struct {
Code int `xml:"code,attr" json:"code"`
Message string `xml:"message,attr" json:"message"`
}
// --- System ---
type License struct {
Valid bool `xml:"valid,attr" json:"valid"`
Email string `xml:"email,attr,omitempty" json:"email,omitempty"`
LicenseExpires string `xml:"licenseExpires,attr,omitempty" json:"licenseExpires,omitempty"`
}
// --- Music folders / genres ---
type MusicFolders struct {
MusicFolder []MusicFolder `xml:"musicFolder" json:"musicFolder"`
}
type MusicFolder struct {
ID string `xml:"id,attr" json:"id"`
Name string `xml:"name,attr" json:"name"`
}
type Genres struct {
Genre []Genre `xml:"genre" json:"genre"`
}
type Genre struct {
Name string `xml:",chardata" json:"value"`
SongCount int `xml:"songCount,attr" json:"songCount"`
AlbumCount int `xml:"albumCount,attr" json:"albumCount"`
}