forked from dweymouth/go-subsonic
-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.go
209 lines (189 loc) · 6.12 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
// Package subsonic implements an API client library for Subsonic-compatible music streaming servers.
//
// This project handles communication with a remote *sonic server, but does not handle playback of media. The library user should be prepared to do something with the stream of audio in bytes, like decoding and playing that audio on a sound card.
// The list of API endpoints implemented is available on the project's github page.
//
// The API is divided between functions with no suffix, and functions that have a "2" suffix (or "3" in the case of Search3).
// Generally, things with "2" on the end are organized by file tags rather than folder structure. This is how you'd expect most music players to work and is recommended.
// The variants without a suffix organize the library by directory structure; artists are a directory, albums are children of that directory, songs (subsonic.Child) are children of albums.
// This has some disadvantages: possibly duplicating items with identical directory names, treating songs and albums in much the same fashion, and being more difficult to query consistently.
package subsonic
import (
"crypto/md5"
"encoding/xml"
"errors"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"net/url"
"path"
)
const (
defaultAPIVersion = "1.15.0"
libraryVersion = "0.0.5"
)
var (
ErrAuthenticationFailure = errors.New("authentication failure")
)
type Client struct {
Client *http.Client
BaseUrl string
User string
ClientName string
PasswordAuth bool
RequestedAPIVersion string
password string
salt string
token string
}
func generateSalt() string {
var corpus = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
// length is minimum 6, but let's use ten to start
b := make([]rune, 10)
for i := range b {
b[i] = corpus[rand.Intn(len(corpus))]
}
return string(b)
}
// Authenticate authenticates the current user with a provided password.
// If s.PasswordAuth is false, the password is salted before transmission and requires Subsonic > 1.13.0.
// Returns ErrAuthenticationFailure if the user/pass combo is incorrect,
// or another error type for any other failure reason.
func (s *Client) Authenticate(password string) error {
if s.PasswordAuth {
s.password = password
} else {
salt := generateSalt()
h := md5.New()
_, err := io.WriteString(h, password)
if err != nil {
return err
}
_, err = io.WriteString(h, salt)
if err != nil {
return err
}
s.salt = salt
s.token = fmt.Sprintf("%x", h.Sum(nil))
}
// Test authentication
// Don't use the s.Ping method because that always returns true as long as the servers is up.
resp, err := s.Request("GET", "ping", nil)
if err != nil {
return err
}
defer resp.Body.Close()
subsonicResp, err := unmarshalResponse(resp.Body)
if err != nil {
return err
}
if subsonicResp.Error != nil {
return ErrAuthenticationFailure
}
return nil
}
// Request performs a HTTP request against the Subsonic server as the current user.
// If a nil error is returned, the caller is responsible for closing the response body.
func (s *Client) Request(method string, endpoint string, params url.Values) (*http.Response, error) {
req, err := s.setupRequest(method, endpoint, params)
if err != nil {
return nil, err
}
resp, err := s.Client.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}
// Get is a convenience interface to issue a GET request and parse the response body (99% of Subsonic API calls)
func (s *Client) Get(endpoint string, params map[string]string) (*Response, error) {
parameters := url.Values{}
for k, v := range params {
parameters.Add(k, v)
}
return s.getValues(endpoint, parameters)
}
func (s *Client) setupRequest(method string, endpoint string, params url.Values) (*http.Request, error) {
baseUrl, err := url.Parse(s.BaseUrl)
if err != nil {
return nil, err
}
baseUrl.Path = path.Join(baseUrl.Path, "/rest/", endpoint)
req, err := http.NewRequest(method, baseUrl.String(), nil)
if err != nil {
return nil, err
}
q := req.URL.Query()
q.Add("f", "xml")
apiVersion := defaultAPIVersion
if s.RequestedAPIVersion != "" {
apiVersion = s.RequestedAPIVersion
}
q.Add("v", apiVersion)
q.Add("c", s.ClientName)
q.Add("u", s.User)
if s.PasswordAuth {
q.Add("p", s.password)
} else {
q.Add("t", s.token)
q.Add("s", s.salt)
}
for key, values := range params {
for _, val := range values {
q.Add(key, val)
}
}
req.URL.RawQuery = q.Encode()
//log.Printf("%s %s", method, req.URL.String())
return req, nil
}
// getValues is a convenience interface to issue a GET request and parse the response body. It supports multiple values by way of the url.Values argument.
func (s *Client) getValues(endpoint string, params url.Values) (*Response, error) {
response, err := s.Request("GET", endpoint, params)
if err != nil {
return nil, err
}
defer response.Body.Close()
parsed, err := unmarshalResponse(response.Body)
if err != nil {
return nil, err
}
if parsed.Error != nil {
return nil, fmt.Errorf("Error #%d: %s", parsed.Error.Code, parsed.Error.Message)
}
//log.Printf("%s: %s\n", endpoint, string(responseBody))
return parsed, nil
}
func unmarshalResponse(resp io.Reader) (*Response, error) {
responseBody, err := io.ReadAll(resp)
if err != nil {
return nil, err
}
parsed := &Response{}
if err = xml.Unmarshal(responseBody, parsed); err != nil {
return nil, err
}
return parsed, nil
}
// Ping is used to test connectivity with the server. It returns true if the server is up.
// Should generally NOT be called before authenticating as it will be considered an authentication
// by the Subsonic server. (Though this function will still return true)
func (s *Client) Ping() bool {
resp, err := s.Request("GET", "ping", nil)
if err != nil {
log.Println(err)
return false
}
resp.Body.Close()
return true
}
// GetLicense retrieves details about the software license. Subsonic requires a license after a 30-day trial, compatible applications have a perpetually valid license.
func (s *Client) GetLicense() (*License, error) {
resp, err := s.Get("getLicense", nil)
if err != nil {
return nil, err
}
return resp.License, nil
}