1package main
2
3import (
4	"crypto/md5"
5	"encoding/json"
6	"fmt"
7	"io/ioutil"
8	"math/rand"
9	"net/http"
10	"net/url"
11)
12
13// used for generating salt
14var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
15
16type SubsonicConnection struct {
17	Username       string
18	Password       string
19	Host           string
20	directoryCache map[string]SubsonicResponse
21}
22
23func randSeq(n int) string {
24	b := make([]rune, n)
25	for i := range b {
26		b[i] = letters[rand.Intn(len(letters))]
27	}
28	return string(b)
29}
30
31func authToken(password string) (string, string) {
32	salt := randSeq(8)
33	token := fmt.Sprintf("%x", md5.Sum([]byte(password+salt)))
34
35	return token, salt
36}
37
38func defaultQuery(connection *SubsonicConnection) url.Values {
39	token, salt := authToken(connection.Password)
40	query := url.Values{}
41	query.Set("u", connection.Username)
42	query.Set("t", token)
43	query.Set("s", salt)
44	query.Set("v", "1.15.1")
45	query.Set("c", "stmp")
46	query.Set("f", "json")
47
48	return query
49}
50
51// response structs
52type SubsonicError struct {
53	Code    int    `json:"code"`
54	Message string `json:"message"`
55}
56
57type SubsonicArtist struct {
58	Id         string
59	Name       string
60	AlbumCount int
61}
62
63type SubsonicDirectory struct {
64	Id       string           `json:"id"`
65	Parent   string           `json:"parent"`
66	Name     string           `json:"name"`
67	Entities []SubsonicEntity `json:"child"`
68}
69
70type SubsonicEntity struct {
71	Id          string `json:"id"`
72	IsDirectory bool   `json:"isDir"`
73	Parent      string `json:"parent"`
74	Title       string `json:"title"`
75	Artist      string `json:"artist"`
76	Duraction   int    `json:"duration"`
77	Track       int    `json:"track"`
78	DiskNumber  int    `json:"diskNumber"`
79	Path        string `json:"path"`
80}
81
82type SubsonicIndexes struct {
83	Index []SubsonicIndex
84}
85
86type SubsonicIndex struct {
87	Name    string           `json:"name"`
88	Artists []SubsonicArtist `json:"artist"`
89}
90
91type SubsonicResponse struct {
92	Status    string            `json:"status"`
93	Version   string            `json:"version"`
94	Indexes   SubsonicIndexes   `json:"indexes"`
95	Directory SubsonicDirectory `json:"directory"`
96	Error     SubsonicError     `json:"error"`
97}
98
99type responseWrapper struct {
100	Response SubsonicResponse `json:"subsonic-response"`
101}
102
103// requests
104func (connection *SubsonicConnection) GetServerInfo() (*SubsonicResponse, error) {
105	query := defaultQuery(connection)
106	requestUrl := connection.Host + "/rest/ping" + "?" + query.Encode()
107	res, err := http.Get(requestUrl)
108
109	if err != nil {
110		return nil, err
111	}
112
113	if res.Body != nil {
114		defer res.Body.Close()
115	}
116
117	responseBody, readErr := ioutil.ReadAll(res.Body)
118
119	if readErr != nil {
120		return nil, err
121	}
122
123	var decodedBody responseWrapper
124	err = json.Unmarshal(responseBody, &decodedBody)
125
126	if err != nil {
127		return nil, err
128	}
129
130	return &decodedBody.Response, nil
131}
132
133func (connection *SubsonicConnection) GetIndexes() (*SubsonicResponse, error) {
134	query := defaultQuery(connection)
135	requestUrl := connection.Host + "/rest/getIndexes" + "?" + query.Encode()
136	res, err := http.Get(requestUrl)
137
138	if err != nil {
139		return nil, err
140	}
141
142	if res.Body != nil {
143		defer res.Body.Close()
144	}
145
146	responseBody, readErr := ioutil.ReadAll(res.Body)
147
148	if readErr != nil {
149		return nil, err
150	}
151
152	var decodedBody responseWrapper
153	err = json.Unmarshal(responseBody, &decodedBody)
154
155	if err != nil {
156		return nil, err
157	}
158
159	return &decodedBody.Response, nil
160}
161
162func (connection *SubsonicConnection) GetMusicDirectory(id string) (*SubsonicResponse, error) {
163	if cachedResponse, present := connection.directoryCache[id]; present {
164		return &cachedResponse, nil
165	}
166
167	query := defaultQuery(connection)
168	query.Set("id", id)
169	requestUrl := connection.Host + "/rest/getMusicDirectory" + "?" + query.Encode()
170	res, err := http.Get(requestUrl)
171
172	if err != nil {
173		return nil, err
174	}
175
176	if res.Body != nil {
177		defer res.Body.Close()
178	}
179
180	responseBody, readErr := ioutil.ReadAll(res.Body)
181
182	if readErr != nil {
183		return nil, err
184	}
185
186	var decodedBody responseWrapper
187	err = json.Unmarshal(responseBody, &decodedBody)
188
189	if err != nil {
190		return nil, err
191	}
192
193	// on a sucessful request, cache the response
194	if decodedBody.Response.Status == "ok" {
195		connection.directoryCache[id] = decodedBody.Response
196	}
197
198	return &decodedBody.Response, nil
199}
200
201// note that this function does not make a request, it just formats the play url
202// to pass to mpv
203func (connection *SubsonicConnection) GetPlayUrl(entity *SubsonicEntity) string {
204	// we don't want to call stream on a directory
205	if entity.IsDirectory {
206		return ""
207	}
208
209	query := defaultQuery(connection)
210	query.Set("id", entity.Id)
211	return connection.Host + "/rest/stream" + "?" + query.Encode()
212}
213