spotify.go 7.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. package spotify
  2. import (
  3. "context"
  4. "crypto/tls"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "golang.org/x/oauth2"
  9. "golang.org/x/oauth2/clientcredentials"
  10. spotifyAuth "golang.org/x/oauth2/spotify"
  11. "io"
  12. "net/http"
  13. "net/url"
  14. "strings"
  15. )
  16. type SpotifyClient struct {
  17. clientID string
  18. clientSecret string
  19. bearerAuth string
  20. baseURL *url.URL
  21. httpClient *http.Client
  22. config *oauth2.Config
  23. context context.Context
  24. }
  25. type TrackInfo struct {
  26. Album AlbumInfo `json:"album"`
  27. Artists []ArtistInfo `json:"artists"`
  28. DiskNumber int `json:"disc_number"`
  29. DurationMS int `json:"duration_ms"`
  30. Explicit bool `json:"explicit"`
  31. ExternalIds ExternalIds `json:"external_ids"`
  32. ExternalUrls ExternalUrls `json:"external_urls"`
  33. Href string `json:"href"`
  34. ID string `json:"id"`
  35. IsPlayable bool `json:"is_playable"`
  36. Name string `json:"name"`
  37. Popularity int `json:"popularity"`
  38. PreviewUrl string `json:"preview_url"`
  39. TrackNumber int `json:"track_number"`
  40. Type string `json:"type"`
  41. Uri string `json:"uri"`
  42. }
  43. type AlbumInfo struct {
  44. AlbumType string `json:"album_type"`
  45. Artists []ArtistInfo `json:"artists"`
  46. ExternalUrls ExternalUrls `json:"external_urls"`
  47. Href string `json:"href"`
  48. ID string `json:"id"`
  49. Images []ImageInfo `json:"images"`
  50. Name string `json:"name"`
  51. Type string `json:"type"`
  52. Uri string `json:"uri"`
  53. }
  54. type ArtistInfo struct {
  55. ExternalUrls ExternalUrls `json:"external_urls"`
  56. Href string `json:"href"`
  57. ID string `json:"id"`
  58. Name string `json:"name"`
  59. Type string `json:"type"`
  60. Uri string `json:"uri"`
  61. }
  62. type ImageInfo struct {
  63. Height int `json:"height"`
  64. URL string `json:url`
  65. Width int `json:"width"`
  66. }
  67. type ExternalIds struct {
  68. ISRC string `json:"isrc"`
  69. }
  70. type ExternalUrls struct {
  71. Spotify string `json:"spotify"`
  72. }
  73. type TokenInfo struct {
  74. AccessToken string `json:"access_token"`
  75. TokenType string `json:"token_type"`
  76. ExpiresIn int `json:"expires_in"`
  77. }
  78. type AuthErrorResponse struct {
  79. Error string `json:"error"`
  80. ErrorDescription string `json:"error_description"`
  81. }
  82. type ErrorResponse struct {
  83. Error ErrorInfo `json:"error"`
  84. }
  85. type ErrorInfo struct {
  86. Status int `json:"status"`
  87. Message string `json:"message"`
  88. }
  89. type PlaylistInfo struct {
  90. Collaborative bool `json:"collaborative"`
  91. Description string `json:"description"`
  92. ExternalUrls ExternalUrls `json:"external_urls"`
  93. Followers Followers `json:"followers"`
  94. Href string `json:"href"`
  95. Id string `json:"id"`
  96. // TODO images
  97. Name string `json:"name"`
  98. // TODO owner
  99. Public bool `json:"public"`
  100. SnapshotId string `json:"snapshot_id"`
  101. Tracks TracksInfo `json:"tracks"`
  102. Type string `json:"type"`
  103. Uri string `json:"uri"`
  104. }
  105. type TracksInfo struct {
  106. Href string `json:"href"`
  107. Items []PlaylistTrack `json:"items"`
  108. Limit int `json:"limit"`
  109. Next string `json:"next"`
  110. Offset int `json:"offset"`
  111. Type string `json:"type"`
  112. Previous string `json:"string"`
  113. Total int `json:"total"`
  114. }
  115. type PlaylistTrack struct {
  116. AddedAt string `json:"added_at"`
  117. AddedBy string `json:"added_by"`
  118. IsLocal bool `json:"is_local"`
  119. Track TrackInfo `json":track"`
  120. }
  121. type Followers struct {
  122. Href string `json:"href"`
  123. Total int `json:"total"`
  124. }
  125. func NewClient(username, secret string) *SpotifyClient {
  126. baseURL, _ := url.Parse("https://api.spotify.com")
  127. httpClient := &http.Client{}
  128. return &SpotifyClient{
  129. clientID: username,
  130. clientSecret: secret,
  131. baseURL: baseURL,
  132. httpClient: httpClient,
  133. }
  134. }
  135. func (s *SpotifyClient) Authenticate() error {
  136. config := &clientcredentials.Config{
  137. ClientID: s.clientID,
  138. ClientSecret: s.clientSecret,
  139. TokenURL: spotifyAuth.Endpoint.TokenURL,
  140. }
  141. token, err := config.Token(context.Background())
  142. if err != nil {
  143. fmt.Println(err)
  144. return err
  145. }
  146. s.bearerAuth = token.AccessToken
  147. return nil
  148. }
  149. func (s *SpotifyClient) SetupUserAuthenticate() error {
  150. s.config = &oauth2.Config{
  151. ClientID: s.clientID,
  152. ClientSecret: s.clientSecret,
  153. RedirectURL: "http://localhost:8080/callback",
  154. Scopes: []string{"playlist-modify-public"},
  155. Endpoint: spotifyAuth.Endpoint,
  156. }
  157. tr := &http.Transport{
  158. TLSNextProto: map[string]func(authority string, c *tls.Conn) http.RoundTripper{},
  159. }
  160. s.context = context.WithValue(context.Background(), oauth2.HTTPClient, &http.Client{Transport: tr})
  161. return nil
  162. }
  163. func (s *SpotifyClient) GetTrack(trackId string) (*TrackInfo, error) {
  164. var trackInfo TrackInfo
  165. url := s.urlFromBase(fmt.Sprintf("/v1/tracks/%s", trackId))
  166. err := s.doRequest("GET", url, &trackInfo, nil)
  167. if err != nil {
  168. return nil, err
  169. } else {
  170. return &trackInfo, nil
  171. }
  172. }
  173. func (s *SpotifyClient) NewPlaylist(name, userId string) (*PlaylistInfo, error) {
  174. var playlistInfo PlaylistInfo
  175. values := &url.Values{}
  176. values.Add("name", name)
  177. url := s.urlFromBase(fmt.Sprintf("/v1/users/%s/playlists", userId))
  178. err := s.doRequest("POST", url, &playlistInfo, values)
  179. if err != nil {
  180. return nil, err
  181. } else {
  182. return &playlistInfo, nil
  183. }
  184. }
  185. func (s *SpotifyClient) UpdatePlaylist(userId, playlistId string, tracks []string) error {
  186. values := &url.Values{}
  187. uris := make([]string, 0)
  188. for _, track := range tracks {
  189. uris = append(uris, "spotify:track:"+track)
  190. }
  191. values.Add("uris", strings.Join(uris, ","))
  192. url := s.urlFromBase(fmt.Sprintf("/v1/users/%s/playlists/%s", userId, playlistId))
  193. err := s.doRequest("PUT", url, nil, values)
  194. return err
  195. }
  196. func (s *SpotifyClient) urlFromBase(path string) *url.URL {
  197. endpoint := &url.URL{Path: path}
  198. return s.baseURL.ResolveReference(endpoint)
  199. }
  200. func (s *SpotifyClient) doRequest(method string, url *url.URL, object interface{}, values *url.Values) error {
  201. var bodyReader io.Reader
  202. if values != nil {
  203. s := values.Encode()
  204. bodyReader = strings.NewReader(s)
  205. }
  206. req, err := http.NewRequest(method, url.String(), bodyReader)
  207. if err != nil {
  208. return err
  209. }
  210. if s.bearerAuth != "" {
  211. req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.bearerAuth))
  212. }
  213. if values != nil {
  214. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  215. }
  216. req.Header.Set("Accept", "application/json")
  217. req.Header.Set("User-Agent", "github.com/lamperi/e4bot/spotify")
  218. resp, err := s.httpClient.Do(req)
  219. if err != nil {
  220. return err
  221. }
  222. defer resp.Body.Close()
  223. if resp.StatusCode != 200 {
  224. var errorResp ErrorResponse
  225. err = json.NewDecoder(resp.Body).Decode(&errorResp)
  226. return errors.New(fmt.Sprintf("Spotify API returned: %d: %s", resp.StatusCode, errorResp.Error.Message))
  227. }
  228. err = json.NewDecoder(resp.Body).Decode(object)
  229. if err != nil {
  230. return err
  231. }
  232. return nil
  233. }
  234. func (s *SpotifyClient) GetAuthUrl(state string) string {
  235. return s.config.AuthCodeURL(state)
  236. }
  237. func (s *SpotifyClient) HandleToken(state string, r *http.Request) error {
  238. values := r.URL.Query()
  239. if e := values.Get("error"); e != "" {
  240. return errors.New("spotify: auth failed - " + e)
  241. }
  242. code := values.Get("code")
  243. if code == "" {
  244. return errors.New("spotify: didn't get access code")
  245. }
  246. actualState := values.Get("state")
  247. if actualState != state {
  248. return errors.New("spotify: redirect state parameter doesn't match")
  249. }
  250. token, err := s.config.Exchange(s.context, code)
  251. if err != nil {
  252. return err
  253. }
  254. fmt.Println("Got token from Spotify", token)
  255. client := s.config.Client(s.context, token)
  256. s.httpClient = client
  257. return nil
  258. }
  259. func (s *SpotifyClient) HasUserLogin() bool {
  260. return s.httpClient != nil && s.config != nil && s.context != nil
  261. }