init
This commit is contained in:
212
main.go
Normal file
212
main.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"subsonic-api/subsonic"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("GET /rest/ping", func(w http.ResponseWriter, r *http.Request) {
|
||||
err := subsonic.VerifyUser(r, "daniil", "Daniil2000")
|
||||
if err != nil {
|
||||
subsonic.SendHTTPResponse(w, r, subsonic.ErrorResponse{
|
||||
Response: subsonic.Response{
|
||||
Status: "failed",
|
||||
Version: "1.16.1",
|
||||
},
|
||||
Error: subsonic.ErrorWrongAuth,
|
||||
}, 401)
|
||||
return
|
||||
}
|
||||
|
||||
subsonic.SendHTTPResponse(w, r, subsonic.PingResponse{
|
||||
Response: subsonic.Response{
|
||||
Status: "ok",
|
||||
Version: "1.16.1",
|
||||
},
|
||||
}, 200)
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /rest/getLicense", func(w http.ResponseWriter, r *http.Request) {
|
||||
subsonic.SendHTTPResponse(w, r, subsonic.LicenseResponse{
|
||||
Response: subsonic.Response{
|
||||
Status: "ok",
|
||||
Version: "1.16.1",
|
||||
},
|
||||
License: subsonic.License{
|
||||
Valid: true,
|
||||
Email: "daniil@tsivinsky.com",
|
||||
Expires: time.Now().Add(time.Hour * 24 * 365),
|
||||
},
|
||||
}, 200)
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /rest/getMusicFolders", func(w http.ResponseWriter, r *http.Request) {
|
||||
subsonic.SendHTTPResponse(w, r, subsonic.MusicFoldersResponse{
|
||||
Response: subsonic.Response{
|
||||
Status: "ok",
|
||||
Version: "1.16.1",
|
||||
},
|
||||
MusicFolders: []subsonic.MusicFolder{
|
||||
{
|
||||
ID: 1,
|
||||
Name: "Music",
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Name: "Movies",
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Name: "Incoming",
|
||||
},
|
||||
},
|
||||
}, 200)
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /rest/getPodcasts", func(w http.ResponseWriter, r *http.Request) {
|
||||
subsonic.SendHTTPResponse(w, r, subsonic.PodcastsResponse{
|
||||
Response: subsonic.Response{
|
||||
Status: "ok",
|
||||
Version: "1.16.1",
|
||||
},
|
||||
Podcasts: []subsonic.Podcast{
|
||||
{
|
||||
ID: 1,
|
||||
URL: "http://downloads.bbc.co.uk/podcasts/fivelive/drkarl/rss.xml",
|
||||
Title: "Dr Karl and the Naked Scientist",
|
||||
Description: "Dr Chris Smith aka The Naked Scientist with the latest news from the world of science and Dr Karl answers listeners' science questions.",
|
||||
CoverArt: "pod-1",
|
||||
OriginalImageURL: "http://downloads.bbc.co.uk/podcasts/fivelive/drkarl/drkarl.jpg",
|
||||
Status: "completed",
|
||||
Episodes: []subsonic.Episode{
|
||||
{
|
||||
ID: 34,
|
||||
StreamID: 523,
|
||||
ChannelID: 1,
|
||||
Title: "Scorpions have re-evolved eyes",
|
||||
Description: "This week Dr Chris fills us in on the UK's largest free science festival, plus all this week's big scientific discoveries.",
|
||||
PublishDate: time.Now(),
|
||||
Status: "completed",
|
||||
Parent: 11,
|
||||
IsDir: false,
|
||||
Year: 2011,
|
||||
Genre: "Podcast",
|
||||
CoverArt: 24,
|
||||
Size: 78421341,
|
||||
ContentType: "audio/mpeg",
|
||||
Suffix: "mp3",
|
||||
Duration: 3146,
|
||||
Bitrate: 128,
|
||||
Path: "Podcast/drkarl/20110203.mp3",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
URL: "http://foo.bar.com/xyz.rss",
|
||||
Status: "error",
|
||||
ErrorMessage: "Not found.",
|
||||
},
|
||||
},
|
||||
}, 200)
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /rest/getNewestPodcasts", func(w http.ResponseWriter, r *http.Request) {
|
||||
subsonic.SendHTTPResponse(w, r, subsonic.NewestPodcastsResponse{
|
||||
Response: subsonic.Response{
|
||||
Status: "ok",
|
||||
Version: "1.16.1",
|
||||
},
|
||||
NewestPodcasts: []subsonic.NewestEpisode{
|
||||
{
|
||||
ID: 7389,
|
||||
Parent: 7389,
|
||||
IsDir: false,
|
||||
Title: "Jonas Gahr Støre",
|
||||
Album: "NRK – Hallo P3",
|
||||
Artist: "Podcast",
|
||||
Year: 2015,
|
||||
CoverArt: 7389,
|
||||
Size: 41808585,
|
||||
ContentType: "audio/mpeg",
|
||||
Suffix: "mp3",
|
||||
Duration: 2619,
|
||||
Bitrate: 128,
|
||||
IsVideo: false,
|
||||
Created: time.Now(),
|
||||
ArtistID: 453,
|
||||
Type: "podcast",
|
||||
StreamID: 7410,
|
||||
ChannelID: 17,
|
||||
Description: "Jonas Gahr Støre fra Arbeiderpartiet er med i dagens partilederutspørring i Hallo P3! Han svarer på det som lytterne lurer på, alt fra bomring i Bodø til flyktningkrisen. I tillegg til dette rapporterer Reporter Silje Ese fra den greske øya Kos der det nå er flere tusen flyktninger. Og så skal vi ta farvel med et svært utrydningstruet dyr.",
|
||||
Status: "completed",
|
||||
PublishDate: time.Now(),
|
||||
},
|
||||
},
|
||||
}, 200)
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /rest/refreshPodcasts", func(w http.ResponseWriter, r *http.Request) {
|
||||
subsonic.SendHTTPResponse(w, r, subsonic.RefreshPodcastsResponse{
|
||||
Response: subsonic.Response{
|
||||
Status: "ok",
|
||||
Version: "1.16.1",
|
||||
},
|
||||
}, 200)
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /rest/createPodcastChannel", func(w http.ResponseWriter, r *http.Request) {
|
||||
subsonic.SendHTTPResponse(w, r, subsonic.CreatePodcastResponse{
|
||||
Response: subsonic.Response{
|
||||
Status: "ok",
|
||||
Version: "1.16.1",
|
||||
},
|
||||
}, 200)
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /rest/deletePodcastChannel", func(w http.ResponseWriter, r *http.Request) {
|
||||
subsonic.SendHTTPResponse(w, r, subsonic.DeletePodcastResponse{
|
||||
Response: subsonic.Response{
|
||||
Status: "ok",
|
||||
Version: "1.16.1",
|
||||
},
|
||||
}, 200)
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /rest/deletePodcastEpisode", func(w http.ResponseWriter, r *http.Request) {
|
||||
subsonic.SendHTTPResponse(w, r, subsonic.DeleteEpisodeResponse{
|
||||
Response: subsonic.Response{
|
||||
Status: "ok",
|
||||
Version: "1.16.1",
|
||||
},
|
||||
}, 200)
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /rest/downloadPodcastEpisode", func(w http.ResponseWriter, r *http.Request) {
|
||||
subsonic.SendHTTPResponse(w, r, subsonic.DownloadEpisodeResponse{
|
||||
Response: subsonic.Response{
|
||||
Status: "ok",
|
||||
Version: "1.16.1",
|
||||
},
|
||||
}, 200)
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /rest/error", func(w http.ResponseWriter, r *http.Request) {
|
||||
subsonic.SendHTTPResponse(w, r, subsonic.ErrorResponse{
|
||||
Response: subsonic.Response{
|
||||
Status: "failed",
|
||||
Version: "1.16.1",
|
||||
},
|
||||
Error: subsonic.ErrorWrongAuth,
|
||||
}, 500)
|
||||
})
|
||||
|
||||
if err := http.ListenAndServe(":5000", mux); err != nil {
|
||||
log.Fatalf("failed to start http server: %v", err)
|
||||
}
|
||||
}
|
||||
3
readme.md
Normal file
3
readme.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# subsonic api types
|
||||
|
||||
source - https://www.subsonic.org/pages/api.jsp
|
||||
51
subsonic/auth.go
Normal file
51
subsonic/auth.go
Normal 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
11
subsonic/browsing.go
Normal 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
23
subsonic/error.go
Normal 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
44
subsonic/http.go
Normal 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
134
subsonic/podcasts.go
Normal 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
11
subsonic/response.go
Normal 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
18
subsonic/system.go
Normal 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"`
|
||||
}
|
||||
Reference in New Issue
Block a user