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