This commit is contained in:
2026-03-05 13:22:00 +03:00
commit f276b28be7
10 changed files with 510 additions and 0 deletions

51
subsonic/auth.go Normal file
View File

@@ -0,0 +1,51 @@
package subsonic
import (
"crypto/md5"
"encoding/hex"
"fmt"
"net/http"
"strings"
)
func verifyAgainstPassword(userPassword, passwordParam string) bool {
p := passwordParam
if strings.HasPrefix(passwordParam, "enc:") {
b, err := hex.DecodeString(passwordParam)
if err != nil {
return false
}
p = string(b)
}
return userPassword == p
}
func verifyAgainstToken(password, token, salt string) bool {
hash := md5.Sum([]byte(password + salt))
return hex.EncodeToString(hash[:]) == token
}
func VerifyUser(r *http.Request, username, password string) error {
u := r.URL.Query().Get("u")
if u == "" {
return fmt.Errorf("username parameter missing")
}
p := r.URL.Query().Get("p")
if p != "" {
ok := verifyAgainstPassword(password, p)
if !ok {
return fmt.Errorf("passwords don't match")
}
return nil
}
t := r.URL.Query().Get("t")
s := r.URL.Query().Get("s")
if !verifyAgainstToken(password, t, s) {
return fmt.Errorf("passwords don't match")
}
return nil
}

11
subsonic/browsing.go Normal file
View File

@@ -0,0 +1,11 @@
package subsonic
type MusicFolder struct {
ID int `xml:"id,attr" json:"id"`
Name string `xml:"name,attr" json:"name"`
}
type MusicFoldersResponse struct {
Response
MusicFolders []MusicFolder `xml:"musicFolders>musicFolder" json:"musicFolders"`
}

23
subsonic/error.go Normal file
View File

@@ -0,0 +1,23 @@
package subsonic
type Error struct {
Code int `xml:"code,attr" json:"code"`
Message string `xml:"message,attr" json:"message"`
}
type ErrorResponse struct {
Response
Error Error `xml:"error" json:"error"`
}
var (
ErrorGeneric = Error{0, "A generic error."}
ErrorRequired = Error{10, "Required parameter is missing."}
ErrorIncompatibleClient = Error{20, "Incompatible Subsonic REST protocol version. Client must upgrade."}
ErrorIncompatibleServer = Error{30, "Incompatible Subsonic REST protocol version. Server must upgrade."}
ErrorWrongAuth = Error{40, "Wrong username or password."}
ErrorLDAPTokenAuth = Error{41, "Token authentication not supported for LDAP users."}
ErrorUserUnauthorized = Error{50, "User is not authorized for the given operation."}
ErrorTrialPeriodEnded = Error{60, "The trial period for the Subsonic server is over. Please upgrade to Subsonic Premium. Visit subsonic.org for details. "}
ErrorNotFound = Error{70, "The requested data was not found."}
)

44
subsonic/http.go Normal file
View File

@@ -0,0 +1,44 @@
package subsonic
import (
"encoding/json"
"encoding/xml"
"fmt"
"net/http"
)
func sendJSON(w http.ResponseWriter, data any, status int) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
return json.NewEncoder(w).Encode(data)
}
func sendXML(w http.ResponseWriter, data any, status int) error {
w.Header().Set("Content-Type", "application/xml")
w.WriteHeader(status)
return xml.NewEncoder(w).Encode(data)
}
func SendHTTPResponse(w http.ResponseWriter, r *http.Request, data any, status int) error {
format := r.URL.Query().Get("f")
if format == "" {
format = "json"
}
resp := struct {
SubsonicResponse any `xml:"subsonic-response" json:"subsonic-response"`
}{
SubsonicResponse: data,
}
switch format {
case "xml":
return sendXML(w, data, status)
case "json":
return sendJSON(w, resp, status)
case "jsonp":
return sendJSON(w, resp, status)
}
return fmt.Errorf("invalid format")
}

134
subsonic/podcasts.go Normal file
View File

@@ -0,0 +1,134 @@
package subsonic
import (
"encoding/json"
"encoding/xml"
"time"
)
type Episode struct {
XMLName xml.Name `xml:"episode" json:"-"`
ID int `xml:"id,attr" json:"id"`
StreamID int `xml:"streamId,attr" json:"streamId"`
ChannelID int `xml:"channelId,attr" json:"channelId"`
Title string `xml:"title,attr" json:"title"`
Description string `xml:"description,attr" json:"description"`
PublishDate time.Time `xml:"publishDate,attr" json:"publishDate"`
Status string `xml:"status,attr" json:"status"`
Parent int `xml:"parent,attr" json:"parent"`
IsDir bool `xml:"isDir,attr" json:"isDir"`
Year int `xml:"year,attr" json:"year"`
Genre string `xml:"genre,attr" json:"genre"`
CoverArt int `xml:"coverArt,attr" json:"coverArt"`
Size int `xml:"size,attr" json:"size"`
ContentType string `xml:"contentType,attr" json:"contentType"`
Suffix string `xml:"suffix,attr" json:"suffix"`
Duration int `xml:"duration,attr" json:"duration"`
Bitrate int `xml:"bitrate,attr" json:"bitrate"`
Path string `xml:"path,attr" json:"path"`
}
type Podcast struct {
XMLName xml.Name `xml:"channel" json:"-"`
ID int `xml:"id,attr" json:"id"`
URL string `xml:"url,attr" json:"url"`
Title string `xml:"title,attr" json:"title"`
Description string `xml:"description,attr" json:"description"`
CoverArt string `xml:"coverArt,attr" json:"coverArt"`
OriginalImageURL string `xml:"originalImageUrl,attr" json:"originalImageUrl"`
Status string `xml:"status,attr" json:"status"`
Episodes []Episode `xml:"episode" json:"episodes"`
ErrorMessage string `xml:"errorMessage,attr,omitempty" json:"errorMessage,omitempty"`
}
func (p Podcast) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if p.ErrorMessage != "" {
return e.EncodeElement(struct {
ID int `xml:"id,attr,omitempty"`
URL string `xml:"url,attr,omitempty"`
Status string `xml:"status,attr,omitempty"`
ErrorMessage string `xml:"errorMessage,attr,omitempty"`
}{
ID: p.ID,
URL: p.URL,
Status: p.Status,
ErrorMessage: p.ErrorMessage,
}, start)
}
type Alias Podcast
return e.EncodeElement(Alias(p), start)
}
func (p Podcast) MarshalJSON() ([]byte, error) {
if p.ErrorMessage != "" {
return json.Marshal(struct {
ID int `xml:"id,attr,omitempty" json:"id,omitempty"`
URL string `xml:"url,attr,omitempty" json:"url,omitempty"`
Status string `xml:"status,attr,omitempty" json:"status,omitempty"`
ErrorMessage string `xml:"errorMessage,attr,omitempty" json:"errorMessage,omitempty"`
}{
ID: p.ID,
URL: p.URL,
ErrorMessage: p.ErrorMessage,
})
}
type Alias Podcast
return json.Marshal(Alias(p))
}
type PodcastsResponse struct {
Response
Podcasts []Podcast `xml:"podcasts>channel" json:"podcasts"`
}
type NewestEpisode struct {
ID int `xml:"id,attr" json:"id"`
Parent int `xml:"parent,attr" json:"parent"`
IsDir bool `xml:"isDir,attr" json:"isDir"`
Title string `xml:"title,attr" json:"title"`
Album string `xml:"album,attr" json:"album"`
Artist string `xml:"artist,attr" json:"artist"`
Year int `xml:"year,attr" json:"year"`
CoverArt int `xml:"coverArt,attr" json:"coverArt"`
Size int `xml:"size,attr" json:"size"`
ContentType string `xml:"contentType,attr" json:"contentType"`
Suffix string `xml:"suffix,attr" json:"suffix"`
Duration int `xml:"duration,attr" json:"duration"`
Bitrate int `xml:"bitrate,attr" json:"bitrate"`
IsVideo bool `xml:"isVideo,attr" json:"isVideo"`
Created time.Time `xml:"created,attr" json:"created"`
ArtistID int `xml:"artistId,attr" json:"artistId"`
Type string `xml:"type,attr" json:"type"`
StreamID int `xml:"streamId,attr" json:"streamId"`
ChannelID int `xml:"channelId,attr" json:"channelId"`
Description string `xml:"description,attr" json:"description"`
Status string `xml:"status,attr" json:"status"`
PublishDate time.Time `xml:"publishDate,attr" json:"publishDate"`
}
type NewestPodcastsResponse struct {
Response
NewestPodcasts []NewestEpisode `xml:"newestPodcasts>episode" json:"newestPodcasts"`
}
type RefreshPodcastsResponse struct {
Response
}
type CreatePodcastResponse struct {
Response
}
type DeletePodcastResponse struct {
Response
}
type DeleteEpisodeResponse struct {
Response
}
type DownloadEpisodeResponse struct {
Response
}

11
subsonic/response.go Normal file
View File

@@ -0,0 +1,11 @@
package subsonic
import (
"encoding/xml"
)
type Response struct {
XMLName xml.Name `xml:"subsonic-response" json:"-"`
Status string `xml:"status,attr" json:"status"`
Version string `xml:"version,attr" json:"version"`
}

18
subsonic/system.go Normal file
View File

@@ -0,0 +1,18 @@
package subsonic
import "time"
type PingResponse struct {
Response
}
type License struct {
Valid bool `xml:"valid,attr" json:"valid"`
Email string `xml:"email,attr" json:"email"`
Expires time.Time `xml:"licenseExpires,attr" json:"licenseExpires"`
}
type LicenseResponse struct {
Response
License License `xml:"license" json:"license"`
}