// 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 . 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"` // OpenSubsonic core response additions (always populated): the // server's real product name and build version, and a flag // confirming OpenSubsonic support so clients can detect it from any // response, not just getOpenSubsonicExtensions. Type string `xml:"type,attr" json:"type"` ServerVersion string `xml:"serverVersion,attr" json:"serverVersion"` OpenSubsonic bool `xml:"openSubsonic,attr" json:"openSubsonic"` 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"` // OpenSubsonic extensions. OpenSubsonicExtensions []OpenSubsonicExtension `xml:"openSubsonicExtensions,omitempty" json:"openSubsonicExtensions,omitempty"` TokenInfo *TokenInfo `xml:"tokenInfo,omitempty" json:"tokenInfo,omitempty"` PodcastEpisode *PodcastEpisode `xml:"podcastEpisode,omitempty" json:"podcastEpisode,omitempty"` LyricsList *LyricsList `xml:"lyricsList,omitempty" json:"lyricsList,omitempty"` PlayQueueByIndex *PlayQueueByIndex `xml:"playQueueByIndex,omitempty" json:"playQueueByIndex,omitempty"` // SonicMatch is a top-level array (not wrapped in an object) on both // getSonicSimilarTracks and findSonicPath, per the OpenSubsonic // schema — the two endpoints are otherwise unrelated, so there is no // shared wrapper type to name it after. SonicMatch []SonicMatch `xml:"sonicMatch,omitempty" json:"sonicMatch,omitempty"` TranscodeDecision *TranscodeDecision `xml:"transcodeDecision,omitempty" json:"transcodeDecision,omitempty"` } // OpenSubsonicExtension describes one extension this server supports, // as returned by getOpenSubsonicExtensions. type OpenSubsonicExtension struct { Name string `xml:"name,attr" json:"name"` Versions []int `xml:"versions" json:"versions"` } type TokenInfo struct { Username string `xml:"username,attr" json:"username"` } // ServerName is the real product name advertised in every response's // "type" attribute, per the OpenSubsonic base response additions. const ServerName = "vibesonic" // AppVersion is VibeSonic's own build version, advertised in every // response's "serverVersion" attribute. Overridable at link time // (matching cmd/vibesonic's version variable); defaults to "dev". var AppVersion = "dev" func newResponse() *Response { return &Response{ Xmlns: "http://subsonic.org/restapi", Status: "ok", Version: ServerVersion, Type: ServerName, ServerVersion: AppVersion, OpenSubsonic: true, } } // 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"` HelpURL string `xml:"helpUrl,attr,omitempty" json:"helpUrl,omitempty"` } // --- 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"` }