mirror of
https://github.com/taigrr/wtf
synced 2025-01-18 04:03:14 -08:00
Update dependencies
This commit is contained in:
5
vendor/github.com/zmb3/spotify/.travis.yml
generated
vendored
Normal file
5
vendor/github.com/zmb3/spotify/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.8
|
||||
- 1.9
|
||||
201
vendor/github.com/zmb3/spotify/LICENSE
generated
vendored
Normal file
201
vendor/github.com/zmb3/spotify/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
106
vendor/github.com/zmb3/spotify/README.md
generated
vendored
Normal file
106
vendor/github.com/zmb3/spotify/README.md
generated
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
|
||||
Spotify
|
||||
=======
|
||||
|
||||
[](http://godoc.org/github.com/zmb3/spotify)
|
||||
[](https://ci.appveyor.com/project/zmb3/spotify)
|
||||
[](https://travis-ci.org/zmb3/spotify)
|
||||
|
||||
This is a Go wrapper for working with Spotify's
|
||||
[Web API](https://developer.spotify.com/web-api/).
|
||||
|
||||
It aims to support every task listed in the Web API Endpoint Reference,
|
||||
located [here](https://developer.spotify.com/web-api/endpoint-reference/).
|
||||
|
||||
By using this library you agree to Spotify's
|
||||
[Developer Terms of Use](https://developer.spotify.com/developer-terms-of-use/).
|
||||
|
||||
## Installation
|
||||
|
||||
To install the library, simply
|
||||
|
||||
`go get github.com/zmb3/spotify`
|
||||
|
||||
## Authentication
|
||||
|
||||
Spotify uses OAuth2 for authentication and authorization.
|
||||
As of May 29, 2017 _all_ Web API endpoints require an access token.
|
||||
|
||||
You can authenticate using a client credentials flow, but this does not provide
|
||||
any authorization to access a user's private data. For most use cases, you'll
|
||||
want to use the authorization code flow. This package includes an `Authenticator`
|
||||
type to handle the details for you.
|
||||
|
||||
Start by registering your application at the following page:
|
||||
|
||||
https://developer.spotify.com/my-applications/.
|
||||
|
||||
You'll get a __client ID__ and __secret key__ for your application. An easy way to
|
||||
provide this data to your application is to set the SPOTIFY_ID and SPOTIFY_SECRET
|
||||
environment variables. If you choose not to use environment variables, you can
|
||||
provide this data manually.
|
||||
|
||||
|
||||
````Go
|
||||
// the redirect URL must be an exact match of a URL you've registered for your application
|
||||
// scopes determine which permissions the user is prompted to authorize
|
||||
auth := spotify.NewAuthenticator(redirectURL, spotify.ScopeUserReadPrivate)
|
||||
|
||||
// if you didn't store your ID and secret key in the specified environment variables,
|
||||
// you can set them manually here
|
||||
auth.SetAuthInfo(clientID, secretKey)
|
||||
|
||||
// get the user to this URL - how you do that is up to you
|
||||
// you should specify a unique state string to identify the session
|
||||
url := auth.AuthURL(state)
|
||||
|
||||
// the user will eventually be redirected back to your redirect URL
|
||||
// typically you'll have a handler set up like the following:
|
||||
func redirectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// use the same state string here that you used to generate the URL
|
||||
token, err := auth.Token(state, r)
|
||||
if err != nil {
|
||||
http.Error(w, "Couldn't get token", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
// create a client using the specified token
|
||||
client := auth.NewClient(token)
|
||||
|
||||
// the client can now be used to make authenticated requests
|
||||
}
|
||||
````
|
||||
|
||||
You may find the following resources useful:
|
||||
|
||||
1. Spotify's Web API Authorization Guide:
|
||||
https://developer.spotify.com/web-api/authorization-guide/
|
||||
|
||||
2. Go's OAuth2 package:
|
||||
https://godoc.org/golang.org/x/oauth2/google
|
||||
|
||||
|
||||
## Helpful Hints
|
||||
|
||||
|
||||
### Optional Parameters
|
||||
|
||||
Many of the functions in this package come in two forms - a simple version that
|
||||
omits optional parameters and uses reasonable defaults, and a more sophisticated
|
||||
version that accepts additional parameters. The latter is suffixed with `Opt`
|
||||
to indicate that it accepts some optional parameters.
|
||||
|
||||
### Automatic Retries
|
||||
|
||||
The API will throttle your requests if you are sending them too rapidly.
|
||||
The client can be configured to wait and re-attempt the request.
|
||||
To enable this, set the `AutoRetry` field on the `Client` struct to `true`.
|
||||
|
||||
For more information, see Spotify [rate-limits](https://developer.spotify.com/web-api/user-guide/#rate-limiting).
|
||||
|
||||
## API Examples
|
||||
|
||||
Examples of the API can be found in the [examples](examples) directory.
|
||||
|
||||
You may find tools such as [Spotify's Web API Console](https://developer.spotify.com/web-api/console/)
|
||||
or [Rapid API](https://rapidapi.com/package/SpotifyPublicAPI/functions?utm_source=SpotifyGitHub&utm_medium=button&utm_content=Vendor_GitHub)
|
||||
valuable for experimenting with the API.
|
||||
220
vendor/github.com/zmb3/spotify/album.go
generated
vendored
Normal file
220
vendor/github.com/zmb3/spotify/album.go
generated
vendored
Normal file
@@ -0,0 +1,220 @@
|
||||
package spotify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SimpleAlbum contains basic data about an album.
|
||||
type SimpleAlbum struct {
|
||||
// The name of the album.
|
||||
Name string `json:"name"`
|
||||
// A slice of SimpleArtists
|
||||
Artists []SimpleArtist `json:"artists"`
|
||||
// The field is present when getting an artist’s
|
||||
// albums. Possible values are “album”, “single”,
|
||||
// “compilation”, “appears_on”. Compare to album_type
|
||||
// this field represents relationship between the artist
|
||||
// and the album.
|
||||
AlbumGroup string `json:"album_group"`
|
||||
// The type of the album: one of "album",
|
||||
// "single", or "compilation".
|
||||
AlbumType string `json:"album_type"`
|
||||
// The SpotifyID for the album.
|
||||
ID ID `json:"id"`
|
||||
// The SpotifyURI for the album.
|
||||
URI URI `json:"uri"`
|
||||
// The markets in which the album is available,
|
||||
// identified using ISO 3166-1 alpha-2 country
|
||||
// codes. Note that al album is considered
|
||||
// available in a market when at least 1 of its
|
||||
// tracks is available in that market.
|
||||
AvailableMarkets []string `json:"available_markets"`
|
||||
// A link to the Web API enpoint providing full
|
||||
// details of the album.
|
||||
Endpoint string `json:"href"`
|
||||
// The cover art for the album in various sizes,
|
||||
// widest first.
|
||||
Images []Image `json:"images"`
|
||||
// Known external URLs for this album.
|
||||
ExternalURLs map[string]string `json:"external_urls"`
|
||||
// The date the album was first released. For example, "1981-12-15".
|
||||
// Depending on the ReleaseDatePrecision, it might be shown as
|
||||
// "1981" or "1981-12". You can use ReleaseDateTime to convert this
|
||||
// to a time.Time value.
|
||||
ReleaseDate string `json:"release_date"`
|
||||
// The precision with which ReleaseDate value is known: "year", "month", or "day"
|
||||
ReleaseDatePrecision string `json:"release_date_precision"`
|
||||
}
|
||||
|
||||
// Copyright contains the copyright statement associated with an album.
|
||||
type Copyright struct {
|
||||
// The copyright text for the album.
|
||||
Text string `json:"text"`
|
||||
// The type of copyright.
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// FullAlbum provides extra album data in addition to the data provided by SimpleAlbum.
|
||||
type FullAlbum struct {
|
||||
SimpleAlbum
|
||||
Artists []SimpleArtist `json:"artists"`
|
||||
Copyrights []Copyright `json:"copyrights"`
|
||||
Genres []string `json:"genres"`
|
||||
// The popularity of the album, represented as an integer between 0 and 100,
|
||||
// with 100 being the most popular. Popularity of an album is calculated
|
||||
// from the popularify of the album's individual tracks.
|
||||
Popularity int `json:"popularity"`
|
||||
// The date the album was first released. For example, "1981-12-15".
|
||||
// Depending on the ReleaseDatePrecision, it might be shown as
|
||||
// "1981" or "1981-12". You can use ReleaseDateTime to convert this
|
||||
// to a time.Time value.
|
||||
ReleaseDate string `json:"release_date"`
|
||||
// The precision with which ReleaseDate value is known: "year", "month", or "day"
|
||||
ReleaseDatePrecision string `json:"release_date_precision"`
|
||||
Tracks SimpleTrackPage `json:"tracks"`
|
||||
ExternalIDs map[string]string `json:"external_ids"`
|
||||
}
|
||||
|
||||
// SavedAlbum provides info about an album saved to an user's account.
|
||||
type SavedAlbum struct {
|
||||
// The date and time the track was saved, represented as an ISO
|
||||
// 8601 UTC timestamp with a zero offset (YYYY-MM-DDTHH:MM:SSZ).
|
||||
// You can use the TimestampLayout constant to convert this to
|
||||
// a time.Time value.
|
||||
AddedAt string `json:"added_at"`
|
||||
FullAlbum `json:"album"`
|
||||
}
|
||||
|
||||
// ReleaseDateTime converts the album's ReleaseDate to a time.TimeValue.
|
||||
// All of the fields in the result may not be valid. For example, if
|
||||
// f.ReleaseDatePrecision is "month", then only the month and year
|
||||
// (but not the day) of the result are valid.
|
||||
func (f *FullAlbum) ReleaseDateTime() time.Time {
|
||||
if f.ReleaseDatePrecision == "day" {
|
||||
result, _ := time.Parse(DateLayout, f.ReleaseDate)
|
||||
return result
|
||||
}
|
||||
if f.ReleaseDatePrecision == "month" {
|
||||
ym := strings.Split(f.ReleaseDate, "-")
|
||||
year, _ := strconv.Atoi(ym[0])
|
||||
month, _ := strconv.Atoi(ym[1])
|
||||
return time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
|
||||
}
|
||||
year, _ := strconv.Atoi(f.ReleaseDate)
|
||||
return time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
// GetAlbum gets Spotify catalog information for a single album, given its Spotify ID.
|
||||
func (c *Client) GetAlbum(id ID) (*FullAlbum, error) {
|
||||
spotifyURL := fmt.Sprintf("%salbums/%s", c.baseURL, id)
|
||||
|
||||
var a FullAlbum
|
||||
|
||||
err := c.get(spotifyURL, &a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
func toStringSlice(ids []ID) []string {
|
||||
result := make([]string, len(ids))
|
||||
for i, str := range ids {
|
||||
result[i] = str.String()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetAlbums gets Spotify Catalog information for multiple albums, given their
|
||||
// Spotify IDs. It supports up to 20 IDs in a single call. Albums are returned
|
||||
// in the order requested. If an album is not found, that position in the
|
||||
// result slice will be nil.
|
||||
func (c *Client) GetAlbums(ids ...ID) ([]*FullAlbum, error) {
|
||||
if len(ids) > 20 {
|
||||
return nil, errors.New("spotify: exceeded maximum number of albums")
|
||||
}
|
||||
spotifyURL := fmt.Sprintf("%salbums?ids=%s", c.baseURL, strings.Join(toStringSlice(ids), ","))
|
||||
|
||||
var a struct {
|
||||
Albums []*FullAlbum `json:"albums"`
|
||||
}
|
||||
|
||||
err := c.get(spotifyURL, &a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return a.Albums, nil
|
||||
}
|
||||
|
||||
// AlbumType represents the type of an album. It can be used to filter
|
||||
// results when searching for albums.
|
||||
type AlbumType int
|
||||
|
||||
// AlbumType values that can be used to filter which types of albums are
|
||||
// searched for. These are flags that can be bitwise OR'd together
|
||||
// to search for multiple types of albums simultaneously.
|
||||
const (
|
||||
AlbumTypeAlbum AlbumType = 1 << iota
|
||||
AlbumTypeSingle = 1 << iota
|
||||
AlbummTypeAppearsOn = 1 << iota
|
||||
AlbumTypeCompilation = 1 << iota
|
||||
)
|
||||
|
||||
func (at AlbumType) encode() string {
|
||||
types := []string{}
|
||||
if at&AlbumTypeAlbum != 0 {
|
||||
types = append(types, "album")
|
||||
}
|
||||
if at&AlbumTypeSingle != 0 {
|
||||
types = append(types, "single")
|
||||
}
|
||||
if at&AlbummTypeAppearsOn != 0 {
|
||||
types = append(types, "appears_on")
|
||||
}
|
||||
if at&AlbumTypeCompilation != 0 {
|
||||
types = append(types, "compilation")
|
||||
}
|
||||
return strings.Join(types, ",")
|
||||
}
|
||||
|
||||
// GetAlbumTracks gets the tracks for a particular album.
|
||||
// If you only care about the tracks, this call is more efficient
|
||||
// than GetAlbum.
|
||||
func (c *Client) GetAlbumTracks(id ID) (*SimpleTrackPage, error) {
|
||||
return c.GetAlbumTracksOpt(id, -1, -1)
|
||||
}
|
||||
|
||||
// GetAlbumTracksOpt behaves like GetAlbumTracks, with the exception that it
|
||||
// allows you to specify extra parameters that limit the number of results returned.
|
||||
// The maximum number of results to return is specified by limit.
|
||||
// The offset argument can be used to specify the index of the first track to return.
|
||||
// It can be used along with limit to reqeust the next set of results.
|
||||
func (c *Client) GetAlbumTracksOpt(id ID, limit, offset int) (*SimpleTrackPage, error) {
|
||||
spotifyURL := fmt.Sprintf("%salbums/%s/tracks", c.baseURL, id)
|
||||
v := url.Values{}
|
||||
if limit != -1 {
|
||||
v.Set("limit", strconv.Itoa(limit))
|
||||
}
|
||||
if offset != -1 {
|
||||
v.Set("offset", strconv.Itoa(offset))
|
||||
}
|
||||
optional := v.Encode()
|
||||
if optional != "" {
|
||||
spotifyURL = spotifyURL + "?" + optional
|
||||
}
|
||||
|
||||
var result SimpleTrackPage
|
||||
err := c.get(spotifyURL, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
152
vendor/github.com/zmb3/spotify/artist.go
generated
vendored
Normal file
152
vendor/github.com/zmb3/spotify/artist.go
generated
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
package spotify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SimpleArtist contains basic info about an artist.
|
||||
type SimpleArtist struct {
|
||||
Name string `json:"name"`
|
||||
ID ID `json:"id"`
|
||||
// The Spotify URI for the artist.
|
||||
URI URI `json:"uri"`
|
||||
// A link to the Web API enpoint providing full details of the artist.
|
||||
Endpoint string `json:"href"`
|
||||
ExternalURLs map[string]string `json:"external_urls"`
|
||||
}
|
||||
|
||||
// FullArtist provides extra artist data in addition to what is provided by SimpleArtist.
|
||||
type FullArtist struct {
|
||||
SimpleArtist
|
||||
// The popularity of the artist, expressed as an integer between 0 and 100.
|
||||
// The artist's popularity is calculated from the popularity of the artist's tracks.
|
||||
Popularity int `json:"popularity"`
|
||||
// A list of genres the artist is associated with. For example, "Prog Rock"
|
||||
// or "Post-Grunge". If not yet classified, the slice is empty.
|
||||
Genres []string `json:"genres"`
|
||||
Followers Followers
|
||||
// Images of the artist in various sizes, widest first.
|
||||
Images []Image `json:"images"`
|
||||
}
|
||||
|
||||
// GetArtist gets Spotify catalog information for a single artist, given its Spotify ID.
|
||||
func (c *Client) GetArtist(id ID) (*FullArtist, error) {
|
||||
spotifyURL := fmt.Sprintf("%sartists/%s", c.baseURL, id)
|
||||
|
||||
var a FullArtist
|
||||
err := c.get(spotifyURL, &a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
// GetArtists gets spotify catalog information for several artists based on their
|
||||
// Spotify IDs. It supports up to 50 artists in a single call. Artists are
|
||||
// returned in the order requested. If an artist is not found, that position
|
||||
// in the result will be nil. Duplicate IDs will result in duplicate artists
|
||||
// in the result.
|
||||
func (c *Client) GetArtists(ids ...ID) ([]*FullArtist, error) {
|
||||
spotifyURL := fmt.Sprintf("%sartists?ids=%s", c.baseURL, strings.Join(toStringSlice(ids), ","))
|
||||
|
||||
var a struct {
|
||||
Artists []*FullArtist
|
||||
}
|
||||
|
||||
err := c.get(spotifyURL, &a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return a.Artists, nil
|
||||
}
|
||||
|
||||
// GetArtistsTopTracks gets Spotify catalog information about an artist's top
|
||||
// tracks in a particular country. It returns a maximum of 10 tracks. The
|
||||
// country is specified as an ISO 3166-1 alpha-2 country code.
|
||||
func (c *Client) GetArtistsTopTracks(artistID ID, country string) ([]FullTrack, error) {
|
||||
spotifyURL := fmt.Sprintf("%sartists/%s/top-tracks?country=%s", c.baseURL, artistID, country)
|
||||
|
||||
var t struct {
|
||||
Tracks []FullTrack `json:"tracks"`
|
||||
}
|
||||
|
||||
err := c.get(spotifyURL, &t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t.Tracks, nil
|
||||
}
|
||||
|
||||
// GetRelatedArtists gets Spotify catalog information about artists similar to a
|
||||
// given artist. Similarity is based on analysis of the Spotify community's
|
||||
// listening history. This function returns up to 20 artists that are considered
|
||||
// related to the specified artist.
|
||||
func (c *Client) GetRelatedArtists(id ID) ([]FullArtist, error) {
|
||||
spotifyURL := fmt.Sprintf("%sartists/%s/related-artists", c.baseURL, id)
|
||||
|
||||
var a struct {
|
||||
Artists []FullArtist `json:"artists"`
|
||||
}
|
||||
|
||||
err := c.get(spotifyURL, &a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return a.Artists, nil
|
||||
}
|
||||
|
||||
// GetArtistAlbums gets Spotify catalog information about an artist's albums.
|
||||
// It is equivalent to GetArtistAlbumsOpt(artistID, nil).
|
||||
func (c *Client) GetArtistAlbums(artistID ID) (*SimpleAlbumPage, error) {
|
||||
return c.GetArtistAlbumsOpt(artistID, nil, nil)
|
||||
}
|
||||
|
||||
// GetArtistAlbumsOpt is just like GetArtistAlbums, but it accepts optional
|
||||
// parameters used to filter and sort the result.
|
||||
//
|
||||
// The AlbumType argument can be used to find a particular type of album. Search
|
||||
// for multiple types by OR-ing the types together.
|
||||
func (c *Client) GetArtistAlbumsOpt(artistID ID, options *Options, t *AlbumType) (*SimpleAlbumPage, error) {
|
||||
spotifyURL := fmt.Sprintf("%sartists/%s/albums", c.baseURL, artistID)
|
||||
// add optional query string if options were specified
|
||||
values := url.Values{}
|
||||
if t != nil {
|
||||
values.Set("album_type", t.encode())
|
||||
}
|
||||
if options != nil {
|
||||
if options.Country != nil {
|
||||
values.Set("market", *options.Country)
|
||||
} else {
|
||||
// if the market is not specified, Spotify will likely return a lot
|
||||
// of duplicates (one for each market in which the album is available)
|
||||
// - prevent this behavior by falling back to the US by default
|
||||
// TODO: would this ever be the desired behavior?
|
||||
values.Set("market", CountryUSA)
|
||||
}
|
||||
if options.Limit != nil {
|
||||
values.Set("limit", strconv.Itoa(*options.Limit))
|
||||
}
|
||||
if options.Offset != nil {
|
||||
values.Set("offset", strconv.Itoa(*options.Offset))
|
||||
}
|
||||
}
|
||||
if query := values.Encode(); query != "" {
|
||||
spotifyURL += "?" + query
|
||||
}
|
||||
|
||||
var p SimpleAlbumPage
|
||||
|
||||
err := c.get(spotifyURL, &p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &p, nil
|
||||
}
|
||||
110
vendor/github.com/zmb3/spotify/audio_analysis.go
generated
vendored
Normal file
110
vendor/github.com/zmb3/spotify/audio_analysis.go
generated
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
package spotify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// AudioAnalysis contains a detailed audio analysis for a single track
|
||||
// identified by its unique Spotify ID. See:
|
||||
// https://developer.spotify.com/web-api/get-audio-analysis/
|
||||
type AudioAnalysis struct {
|
||||
Bars []Marker `json:"bars"`
|
||||
Beats []Marker `json:"beats"`
|
||||
Meta AnalysisMeta `json:"meta"`
|
||||
Sections []Section `json:"sections"`
|
||||
Segments []Segment `json:"segments"`
|
||||
Tatums []Marker `json:"tatums"`
|
||||
Track AnalysisTrack `json:"track"`
|
||||
}
|
||||
|
||||
// Marker represents beats, bars, tatums and are used in segment and section
|
||||
// descriptions.
|
||||
type Marker struct {
|
||||
Start float64 `json:"start"`
|
||||
Duration float64 `json:"duration"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
}
|
||||
|
||||
// AnalysisMeta describes details about Spotify's audio analysis of the track
|
||||
type AnalysisMeta struct {
|
||||
AnalyzerVersion string `json:"analyzer_version"`
|
||||
Platform string `json:"platform"`
|
||||
DetailedStatus string `json:"detailed_status"`
|
||||
StatusCode int `json:"status"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
AnalysisTime float64 `json:"analysis_time"`
|
||||
InputProcess string `json:"input_process"`
|
||||
}
|
||||
|
||||
// Section represents a large variation in rhythm or timbre, e.g. chorus, verse,
|
||||
// bridge, guitar solo, etc. Each section contains its own descriptions of
|
||||
// tempo, key, mode, time_signature, and loudness.
|
||||
type Section struct {
|
||||
Marker
|
||||
Loudness float64 `json:"loudness"`
|
||||
Tempo float64 `json:"tempo"`
|
||||
TempoConfidence float64 `json:"tempo_confidence"`
|
||||
Key Key `json:"key"`
|
||||
KeyConfidence float64 `json:"key_confidence"`
|
||||
Mode Mode `json:"mode"`
|
||||
ModeConfidence float64 `json:"mode_confidence"`
|
||||
TimeSignature int `json:"time_signature"`
|
||||
TimeSignatureConfidence float64 `json:"time_signature_confidence"`
|
||||
}
|
||||
|
||||
// Segment is characterized by it's perceptual onset and duration in seconds,
|
||||
// loudness (dB), pitch and timbral content.
|
||||
type Segment struct {
|
||||
Marker
|
||||
LoudnessStart float64 `json:"loudness_start"`
|
||||
LoudnessMaxTime float64 `json:"loudness_max_time"`
|
||||
LoudnessMax float64 `json:"loudness_max"`
|
||||
LoudnessEnd float64 `json:"loudness_end"`
|
||||
Pitches []float64 `json:"pitches"`
|
||||
Timbre []float64 `json:"timbre"`
|
||||
}
|
||||
|
||||
// AnalysisTrack contains audio analysis data about the track as a whole
|
||||
type AnalysisTrack struct {
|
||||
NumSamples int64 `json:"num_samples"`
|
||||
Duration float64 `json:"duration"`
|
||||
SampleMD5 string `json:"sample_md5"`
|
||||
OffsetSeconds int `json:"offset_seconds"`
|
||||
WindowSeconds int `json:"window_seconds"`
|
||||
AnalysisSampleRate int64 `json:"analysis_sample_rate"`
|
||||
AnalysisChannels int `json:"analysis_channels"`
|
||||
EndOfFadeIn float64 `json:"end_of_fade_in"`
|
||||
StartOfFadeOut float64 `json:"start_of_fade_out"`
|
||||
Loudness float64 `json:"loudness"`
|
||||
Tempo float64 `json:"tempo"`
|
||||
TempoConfidence float64 `json:"tempo_confidence"`
|
||||
TimeSignature int `json:"time_signature"`
|
||||
TimeSignatureConfidence float64 `json:"time_signature_confidence"`
|
||||
Key Key `json:"key"`
|
||||
KeyConfidence float64 `json:"key_confidence"`
|
||||
Mode Mode `json:"mode"`
|
||||
ModeConfidence float64 `json:"mode_confidence"`
|
||||
CodeString string `json:"codestring"`
|
||||
CodeVersion float64 `json:"code_version"`
|
||||
EchoprintString string `json:"echoprintstring"`
|
||||
EchoprintVersion float64 `json:"echoprint_version"`
|
||||
SynchString string `json:"synchstring"`
|
||||
SynchVersion float64 `json:"synch_version"`
|
||||
RhythmString string `json:"rhythmstring"`
|
||||
RhythmVersion float64 `json:"rhythm_version"`
|
||||
}
|
||||
|
||||
// GetAudioAnalysis queries the Spotify web API for an audio analysis of a
|
||||
// single track.
|
||||
func (c *Client) GetAudioAnalysis(id ID) (*AudioAnalysis, error) {
|
||||
url := fmt.Sprintf("%saudio-analysis/%s", c.baseURL, id)
|
||||
|
||||
temp := AudioAnalysis{}
|
||||
|
||||
err := c.get(url, &temp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &temp, nil
|
||||
}
|
||||
123
vendor/github.com/zmb3/spotify/audio_features.go
generated
vendored
Normal file
123
vendor/github.com/zmb3/spotify/audio_features.go
generated
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
package spotify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AudioFeatures contains various high-level acoustic attributes
|
||||
// for a particular track.
|
||||
type AudioFeatures struct {
|
||||
//Acousticness is a confidence measure from 0.0 to 1.0 of whether
|
||||
// the track is acoustic. A value of 1.0 represents high confidence
|
||||
// that the track is acoustic.
|
||||
Acousticness float32 `json:"acousticness"`
|
||||
// An HTTP URL to access the full audio analysis of the track.
|
||||
// The URL is cryptographically signed and configured to expire
|
||||
// after roughly 10 minutes. Do not store these URLs for later use.
|
||||
AnalysisURL string `json:"analysis_url"`
|
||||
// Danceability describes how suitable a track is for dancing based
|
||||
// on a combination of musical elements including tempo, rhythm stability,
|
||||
// beat strength, and overall regularity. A value of 0.0 is least danceable
|
||||
// and 1.0 is most danceable.
|
||||
Danceability float32 `json:"danceability"`
|
||||
// The length of the track in milliseconds.
|
||||
Duration int `json:"duration_ms"`
|
||||
// Energy is a measure from 0.0 to 1.0 and represents a perceptual mesaure
|
||||
// of intensity and activity. Typically, energetic tracks feel fast, loud,
|
||||
// and noisy.
|
||||
Energy float32 `json:"energy"`
|
||||
// The Spotify ID for the track.
|
||||
ID ID `json:"id"`
|
||||
// Predicts whether a track contains no vocals. "Ooh" and "aah" sounds are
|
||||
// treated as instrumental in this context. Rap or spoken words are clearly
|
||||
// "vocal". The closer the Instrumentalness value is to 1.0, the greater
|
||||
// likelihood the track contains no vocal content. Values above 0.5 are
|
||||
// intended to represent instrumental tracks, but confidence is higher as the
|
||||
// value approaches 1.0.
|
||||
Instrumentalness float32 `json:"instrumentalness"`
|
||||
// The key the track is in. Integers map to pitches using standard Pitch Class notation
|
||||
// (https://en.wikipedia.org/wiki/Pitch_class).
|
||||
Key int `json:"key"`
|
||||
// Detects the presence of an audience in the recording. Higher liveness
|
||||
// values represent an increased probability that the track was performed live.
|
||||
// A value above 0.8 provides strong likelihook that the track is live.
|
||||
Liveness float32 `json:"liveness"`
|
||||
// The overall loudness of a track in decibels (dB). Loudness values are
|
||||
// averaged across the entire track and are useful for comparing the relative
|
||||
// loudness of tracks. Typical values range between -60 and 0 dB.
|
||||
Loudness float32 `json:"loudness"`
|
||||
// Mode indicates the modality (major or minor) of a track.
|
||||
Mode int `json:"mode"`
|
||||
// Detects the presence of spoken words in a track. The more exclusively
|
||||
// speech-like the recording, the closer to 1.0 the speechiness will be.
|
||||
// Values above 0.66 describe tracks that are probably made entirely of
|
||||
// spoken words. Values between 0.33 and 0.66 describe tracks that may
|
||||
// contain both music and speech, including such cases as rap music.
|
||||
// Values below 0.33 most likely represent music and other non-speech-like tracks.
|
||||
Speechiness float32 `json:"speechiness"`
|
||||
// The overall estimated tempo of the track in beats per minute (BPM).
|
||||
Tempo float32 `json:"tempo"`
|
||||
// An estimated overall time signature of a track. The time signature (meter)
|
||||
// is a notational convention to specify how many beats are in each bar (or measure).
|
||||
TimeSignature int `json:"time_signature"`
|
||||
// A link to the Web API endpoint providing full details of the track.
|
||||
TrackURL string `json:"track_href"`
|
||||
// The Spotify URI for the track.
|
||||
URI URI `json:"uri"`
|
||||
// A measure from 0.0 to 1.0 describing the musical positiveness conveyed
|
||||
// by a track. Tracks with high valence sound more positive (e.g. happy,
|
||||
// cheerful, euphoric), while tracks with low valence sound more negative
|
||||
// (e.g. sad, depressed, angry).
|
||||
Valence float32 `json:"valence"`
|
||||
}
|
||||
|
||||
// Key represents a pitch using Pitch Class notation.
|
||||
type Key int
|
||||
|
||||
const (
|
||||
C Key = iota
|
||||
CSharp
|
||||
D
|
||||
DSharp
|
||||
E
|
||||
F
|
||||
FSharp
|
||||
G
|
||||
GSharp
|
||||
A
|
||||
ASharp
|
||||
B
|
||||
DFlat = CSharp
|
||||
EFlat = DSharp
|
||||
GFlat = FSharp
|
||||
AFlat = GSharp
|
||||
BFlat = ASharp
|
||||
)
|
||||
|
||||
// Mode indicates the modality (major or minor) of a track.
|
||||
type Mode int
|
||||
|
||||
const (
|
||||
Minor Mode = iota
|
||||
Major
|
||||
)
|
||||
|
||||
// GetAudioFeatures queries the Spotify Web API for various
|
||||
// high-level acoustic attributes of audio tracks.
|
||||
// Objects are returned in the order requested. If an object
|
||||
// is not found, a nil value is returned in the appropriate position.
|
||||
func (c *Client) GetAudioFeatures(ids ...ID) ([]*AudioFeatures, error) {
|
||||
url := fmt.Sprintf("%saudio-features?ids=%s", c.baseURL, strings.Join(toStringSlice(ids), ","))
|
||||
|
||||
temp := struct {
|
||||
F []*AudioFeatures `json:"audio_features"`
|
||||
}{}
|
||||
|
||||
err := c.get(url, &temp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return temp.F, nil
|
||||
}
|
||||
180
vendor/github.com/zmb3/spotify/auth.go
generated
vendored
Normal file
180
vendor/github.com/zmb3/spotify/auth.go
generated
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
package spotify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
// AuthURL is the URL to Spotify Accounts Service's OAuth2 endpoint.
|
||||
AuthURL = "https://accounts.spotify.com/authorize"
|
||||
// TokenURL is the URL to the Spotify Accounts Service's OAuth2
|
||||
// token endpoint.
|
||||
TokenURL = "https://accounts.spotify.com/api/token"
|
||||
)
|
||||
|
||||
// Scopes let you specify exactly which types of data your application wants to access.
|
||||
// The set of scopes you pass in your authentication request determines what access the
|
||||
// permissions the user is asked to grant.
|
||||
const (
|
||||
// ScopeImageUpload seeks permission to upload images to Spotify on your behalf.
|
||||
ScopeImageUpload = "ugc-image-upload"
|
||||
// ScopePlaylistReadPrivate seeks permission to read
|
||||
// a user's private playlists.
|
||||
ScopePlaylistReadPrivate = "playlist-read-private"
|
||||
// ScopePlaylistModifyPublic seeks write access
|
||||
// to a user's public playlists.
|
||||
ScopePlaylistModifyPublic = "playlist-modify-public"
|
||||
// ScopePlaylistModifyPrivate seeks write access to
|
||||
// a user's private playlists.
|
||||
ScopePlaylistModifyPrivate = "playlist-modify-private"
|
||||
// ScopePlaylistReadCollaborative seeks permission to
|
||||
// access a user's collaborative playlists.
|
||||
ScopePlaylistReadCollaborative = "playlist-read-collaborative"
|
||||
// ScopeUserFollowModify seeks write/delete access to
|
||||
// the list of artists and other users that a user follows.
|
||||
ScopeUserFollowModify = "user-follow-modify"
|
||||
// ScopeUserFollowRead seeks read access to the list of
|
||||
// artists and other users that a user follows.
|
||||
ScopeUserFollowRead = "user-follow-read"
|
||||
// ScopeUserLibraryModify seeks write/delete access to a
|
||||
// user's "Your Music" library.
|
||||
ScopeUserLibraryModify = "user-library-modify"
|
||||
// ScopeUserLibraryRead seeks read access to a user's "Your Music" library.
|
||||
ScopeUserLibraryRead = "user-library-read"
|
||||
// ScopeUserReadPrivate seeks read access to a user's
|
||||
// subsription details (type of user account).
|
||||
ScopeUserReadPrivate = "user-read-private"
|
||||
// ScopeUserReadEmail seeks read access to a user's email address.
|
||||
ScopeUserReadEmail = "user-read-email"
|
||||
// ScopeUserReadBirthdate seeks read access to a user's birthdate.
|
||||
ScopeUserReadBirthdate = "user-read-birthdate"
|
||||
// ScopeUserReadCurrentlyPlaying seeks read access to a user's currently playing track
|
||||
ScopeUserReadCurrentlyPlaying = "user-read-currently-playing"
|
||||
// ScopeUserReadPlaybackState seeks read access to the user's current playback state
|
||||
ScopeUserReadPlaybackState = "user-read-playback-state"
|
||||
// ScopeUserModifyPlaybackState seeks write access to the user's current playback state
|
||||
ScopeUserModifyPlaybackState = "user-modify-playback-state"
|
||||
// ScopeUserReadRecentlyPlayed allows access to a user's recently-played songs
|
||||
ScopeUserReadRecentlyPlayed = "user-read-recently-played"
|
||||
// ScopeUserTopRead seeks read access to a user's top tracks and artists
|
||||
ScopeUserTopRead = "user-top-read"
|
||||
)
|
||||
|
||||
// Authenticator provides convenience functions for implementing the OAuth2 flow.
|
||||
// You should always use `NewAuthenticator` to make them.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// a := spotify.NewAuthenticator(redirectURL, spotify.ScopeUserLibaryRead, spotify.ScopeUserFollowRead)
|
||||
// // direct user to Spotify to log in
|
||||
// http.Redirect(w, r, a.AuthURL("state-string"), http.StatusFound)
|
||||
//
|
||||
// // then, in redirect handler:
|
||||
// token, err := a.Token(state, r)
|
||||
// client := a.NewClient(token)
|
||||
//
|
||||
type Authenticator struct {
|
||||
config *oauth2.Config
|
||||
context context.Context
|
||||
}
|
||||
|
||||
// NewAuthenticator creates an authenticator which is used to implement the
|
||||
// OAuth2 authorization flow. The redirectURL must exactly match one of the
|
||||
// URLs specified in your Spotify developer account.
|
||||
//
|
||||
// By default, NewAuthenticator pulls your client ID and secret key from the
|
||||
// SPOTIFY_ID and SPOTIFY_SECRET environment variables. If you'd like to provide
|
||||
// them from some other source, you can call `SetAuthInfo(id, key)` on the
|
||||
// returned authenticator.
|
||||
func NewAuthenticator(redirectURL string, scopes ...string) Authenticator {
|
||||
cfg := &oauth2.Config{
|
||||
ClientID: os.Getenv("SPOTIFY_ID"),
|
||||
ClientSecret: os.Getenv("SPOTIFY_SECRET"),
|
||||
RedirectURL: redirectURL,
|
||||
Scopes: scopes,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: AuthURL,
|
||||
TokenURL: TokenURL,
|
||||
},
|
||||
}
|
||||
|
||||
// disable HTTP/2 for DefaultClient, see: https://github.com/zmb3/spotify/issues/20
|
||||
tr := &http.Transport{
|
||||
TLSNextProto: map[string]func(authority string, c *tls.Conn) http.RoundTripper{},
|
||||
}
|
||||
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, &http.Client{Transport: tr})
|
||||
return Authenticator{
|
||||
config: cfg,
|
||||
context: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// SetAuthInfo overwrites the client ID and secret key used by the authenticator.
|
||||
// You can use this if you don't want to store this information in environment variables.
|
||||
func (a *Authenticator) SetAuthInfo(clientID, secretKey string) {
|
||||
a.config.ClientID = clientID
|
||||
a.config.ClientSecret = secretKey
|
||||
}
|
||||
|
||||
// AuthURL returns a URL to the the Spotify Accounts Service's OAuth2 endpoint.
|
||||
//
|
||||
// State is a token to protect the user from CSRF attacks. You should pass the
|
||||
// same state to `Token`, where it will be validated. For more info, refer to
|
||||
// http://tools.ietf.org/html/rfc6749#section-10.12.
|
||||
func (a Authenticator) AuthURL(state string) string {
|
||||
return a.config.AuthCodeURL(state)
|
||||
}
|
||||
|
||||
// Token pulls an authorization code from an HTTP request and attempts to exchange
|
||||
// it for an access token. The standard use case is to call Token from the handler
|
||||
// that handles requests to your application's redirect URL.
|
||||
func (a Authenticator) Token(state string, r *http.Request) (*oauth2.Token, error) {
|
||||
values := r.URL.Query()
|
||||
if e := values.Get("error"); e != "" {
|
||||
return nil, errors.New("spotify: auth failed - " + e)
|
||||
}
|
||||
code := values.Get("code")
|
||||
if code == "" {
|
||||
return nil, errors.New("spotify: didn't get access code")
|
||||
}
|
||||
actualState := values.Get("state")
|
||||
if actualState != state {
|
||||
return nil, errors.New("spotify: redirect state parameter doesn't match")
|
||||
}
|
||||
return a.config.Exchange(a.context, code)
|
||||
}
|
||||
|
||||
// Exchange is like Token, except it allows you to manually specify the access
|
||||
// code instead of pulling it out of an HTTP request.
|
||||
func (a Authenticator) Exchange(code string) (*oauth2.Token, error) {
|
||||
return a.config.Exchange(a.context, code)
|
||||
}
|
||||
|
||||
// NewClient creates a Client that will use the specified access token for its API requests.
|
||||
func (a Authenticator) NewClient(token *oauth2.Token) Client {
|
||||
client := a.config.Client(a.context, token)
|
||||
return Client{
|
||||
http: client,
|
||||
baseURL: baseAddress,
|
||||
}
|
||||
}
|
||||
|
||||
// Token gets the client's current token.
|
||||
func (c *Client) Token() (*oauth2.Token, error) {
|
||||
transport, ok := c.http.Transport.(*oauth2.Transport)
|
||||
if !ok {
|
||||
return nil, errors.New("spotify: oauth2 transport type not correct")
|
||||
}
|
||||
t, err := transport.Source.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
140
vendor/github.com/zmb3/spotify/category.go
generated
vendored
Normal file
140
vendor/github.com/zmb3/spotify/category.go
generated
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
package spotify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Category is used by Spotify to tag items in. For example, on the Spotify
|
||||
// player's "Browse" tab.
|
||||
type Category struct {
|
||||
// A link to the Web API endpoint returning full details of the category
|
||||
Endpoint string `json:"href"`
|
||||
// The category icon, in various sizes
|
||||
Icons []Image `json:"icons"`
|
||||
// The Spotify category ID. This isn't a base-62 Spotify ID, its just
|
||||
// a short string that describes and identifies the category (ie "party").
|
||||
ID string `json:"id"`
|
||||
// The name of the category
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// GetCategoryOpt is like GetCategory, but it accepts optional arguments.
|
||||
// The country parameter is an ISO 3166-1 alpha-2 country code. It can be
|
||||
// used to ensure that the category exists for a particular country. The
|
||||
// locale argument is an ISO 639 language code and an ISO 3166-1 alpha-2
|
||||
// country code, separated by an underscore. It can be used to get the
|
||||
// category strings in a particular language (for example: "es_MX" means
|
||||
// get categories in Mexico, returned in Spanish).
|
||||
//
|
||||
// This call requries authorization.
|
||||
func (c *Client) GetCategoryOpt(id, country, locale string) (Category, error) {
|
||||
cat := Category{}
|
||||
spotifyURL := fmt.Sprintf("%sbrowse/categories/%s", c.baseURL, id)
|
||||
values := url.Values{}
|
||||
if country != "" {
|
||||
values.Set("country", country)
|
||||
}
|
||||
if locale != "" {
|
||||
values.Set("locale", locale)
|
||||
}
|
||||
if query := values.Encode(); query != "" {
|
||||
spotifyURL += "?" + query
|
||||
}
|
||||
|
||||
err := c.get(spotifyURL, &cat)
|
||||
if err != nil {
|
||||
return cat, err
|
||||
}
|
||||
|
||||
return cat, err
|
||||
}
|
||||
|
||||
// GetCategory gets a single category used to tag items in Spotify
|
||||
// (on, for example, the Spotify player's Browse tab).
|
||||
func (c *Client) GetCategory(id string) (Category, error) {
|
||||
return c.GetCategoryOpt(id, "", "")
|
||||
}
|
||||
|
||||
// GetCategoryPlaylists gets a list of Spotify playlists tagged with a paricular category.
|
||||
func (c *Client) GetCategoryPlaylists(catID string) (*SimplePlaylistPage, error) {
|
||||
return c.GetCategoryPlaylistsOpt(catID, nil)
|
||||
}
|
||||
|
||||
// GetCategoryPlaylistsOpt is like GetCategoryPlaylists, but it accepts optional
|
||||
// arguments.
|
||||
func (c *Client) GetCategoryPlaylistsOpt(catID string, opt *Options) (*SimplePlaylistPage, error) {
|
||||
spotifyURL := fmt.Sprintf("%sbrowse/categories/%s/playlists", c.baseURL, catID)
|
||||
if opt != nil {
|
||||
values := url.Values{}
|
||||
if opt.Country != nil {
|
||||
values.Set("country", *opt.Country)
|
||||
}
|
||||
if opt.Limit != nil {
|
||||
values.Set("limit", strconv.Itoa(*opt.Limit))
|
||||
}
|
||||
if opt.Offset != nil {
|
||||
values.Set("offset", strconv.Itoa(*opt.Offset))
|
||||
}
|
||||
if query := values.Encode(); query != "" {
|
||||
spotifyURL += "?" + query
|
||||
}
|
||||
}
|
||||
|
||||
wrapper := struct {
|
||||
Playlists SimplePlaylistPage `json:"playlists"`
|
||||
}{}
|
||||
|
||||
err := c.get(spotifyURL, &wrapper)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &wrapper.Playlists, nil
|
||||
}
|
||||
|
||||
// GetCategories gets a list of categories used to tag items in Spotify
|
||||
// (on, for example, the Spotify player's "Browse" tab).
|
||||
func (c *Client) GetCategories() (*CategoryPage, error) {
|
||||
return c.GetCategoriesOpt(nil, "")
|
||||
}
|
||||
|
||||
// GetCategoriesOpt is like GetCategories, but it accepts optional parameters.
|
||||
//
|
||||
// The locale option can be used to get the results in a particular language.
|
||||
// It consists of an ISO 639 language code and an ISO 3166-1 alpha-2 country
|
||||
// code, separated by an underscore. Specify the empty string to have results
|
||||
// returned in the Spotify default language (American English).
|
||||
func (c *Client) GetCategoriesOpt(opt *Options, locale string) (*CategoryPage, error) {
|
||||
spotifyURL := c.baseURL + "browse/categories"
|
||||
values := url.Values{}
|
||||
if locale != "" {
|
||||
values.Set("locale", locale)
|
||||
}
|
||||
if opt != nil {
|
||||
if opt.Country != nil {
|
||||
values.Set("country", *opt.Country)
|
||||
}
|
||||
if opt.Limit != nil {
|
||||
values.Set("limit", strconv.Itoa(*opt.Limit))
|
||||
}
|
||||
if opt.Offset != nil {
|
||||
values.Set("offset", strconv.Itoa(*opt.Offset))
|
||||
}
|
||||
}
|
||||
if query := values.Encode(); query != "" {
|
||||
spotifyURL += "?" + query
|
||||
}
|
||||
|
||||
wrapper := struct {
|
||||
Categories CategoryPage `json:"categories"`
|
||||
}{}
|
||||
|
||||
err := c.get(spotifyURL, &wrapper)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &wrapper.Categories, nil
|
||||
}
|
||||
32
vendor/github.com/zmb3/spotify/countries.go
generated
vendored
Normal file
32
vendor/github.com/zmb3/spotify/countries.go
generated
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
package spotify
|
||||
|
||||
// ISO 3166-1 alpha 2 country codes.
|
||||
//
|
||||
// see: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
||||
const (
|
||||
CountryArgentina = "AR"
|
||||
CountryAustralia = "AU"
|
||||
CountryAustria = "AT"
|
||||
CountryBelarus = "BY"
|
||||
CountryBelgium = "BE"
|
||||
CountryBrazil = "BR"
|
||||
CountryCanada = "CA"
|
||||
CountryChile = "CL"
|
||||
CountryChina = "CN"
|
||||
CountryGermany = "DE"
|
||||
CountryHongKong = "HK"
|
||||
CountryIreland = "IE"
|
||||
CountryIndia = "IN"
|
||||
CountryItaly = "IT"
|
||||
CountryJapan = "JP"
|
||||
CountrySpain = "ES"
|
||||
CountryFinland = "FI"
|
||||
CountryFrance = "FR"
|
||||
CountryMexico = "MX"
|
||||
CountryNewZealand = "NZ"
|
||||
CountryRussia = "RU"
|
||||
CountrySwitzerland = "CH"
|
||||
CountryUnitedArabEmirates = "AE"
|
||||
CountryUnitedKingdom = "GB"
|
||||
CountryUSA = "US"
|
||||
)
|
||||
37
vendor/github.com/zmb3/spotify/cursor.go
generated
vendored
Normal file
37
vendor/github.com/zmb3/spotify/cursor.go
generated
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
package spotify
|
||||
|
||||
// This file contains the types that implement Spotify's cursor-based
|
||||
// paging object. Like the standard paging object, this object is a
|
||||
// container for a set of items. Unlike the standard paging object, a
|
||||
// cursor-based paging object does not provide random access to the results.
|
||||
|
||||
// Cursor contains a key that can be used to find the next set
|
||||
// of items.
|
||||
type Cursor struct {
|
||||
After string `json:"after"`
|
||||
}
|
||||
|
||||
// cursorPage contains all of the fields in a Spotify cursor-based
|
||||
// paging object, except for the actual items. This type is meant
|
||||
// to be embedded in other types that add the Items field.
|
||||
type cursorPage struct {
|
||||
// A link to the Web API endpoint returning the full
|
||||
// result of this request.
|
||||
Endpoint string `json:"href"`
|
||||
// The maximum number of items returned, as set in the query
|
||||
// (or default value if unset).
|
||||
Limit int `json:"limit"`
|
||||
// The URL to the next set of items.
|
||||
Next string `json:"next"`
|
||||
// The total number of items available to return.
|
||||
Total int `json:"total"`
|
||||
// The cursor used to find the next set of items.
|
||||
Cursor Cursor `json:"cursors"`
|
||||
}
|
||||
|
||||
// FullArtistCursorPage is a cursor-based paging object containing
|
||||
// a set of FullArtist objects.
|
||||
type FullArtistCursorPage struct {
|
||||
cursorPage
|
||||
Artists []FullArtist `json:"items"`
|
||||
}
|
||||
5
vendor/github.com/zmb3/spotify/full_tests.bat
generated
vendored
Normal file
5
vendor/github.com/zmb3/spotify/full_tests.bat
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
REM - The tests that actually hit the Spotify Web API don't run by default.
|
||||
REM - Use this script to run them in addition to the standard unit tests.
|
||||
|
||||
cmd /C "set FULLTEST=y && go test %*"
|
||||
61
vendor/github.com/zmb3/spotify/library.go
generated
vendored
Normal file
61
vendor/github.com/zmb3/spotify/library.go
generated
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
package spotify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UserHasTracks checks if one or more tracks are saved to the current user's
|
||||
// "Your Music" library.
|
||||
func (c *Client) UserHasTracks(ids ...ID) ([]bool, error) {
|
||||
if l := len(ids); l == 0 || l > 50 {
|
||||
return nil, errors.New("spotify: UserHasTracks supports 1 to 50 IDs per call")
|
||||
}
|
||||
spotifyURL := fmt.Sprintf("%sme/tracks/contains?ids=%s", c.baseURL, strings.Join(toStringSlice(ids), ","))
|
||||
|
||||
var result []bool
|
||||
|
||||
err := c.get(spotifyURL, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
// AddTracksToLibrary saves one or more tracks to the current user's
|
||||
// "Your Music" library. This call requires the ScopeUserLibraryModify scope.
|
||||
// A track can only be saved once; duplicate IDs are ignored.
|
||||
func (c *Client) AddTracksToLibrary(ids ...ID) error {
|
||||
return c.modifyLibraryTracks(true, ids...)
|
||||
}
|
||||
|
||||
// RemoveTracksFromLibrary removes one or more tracks from the current user's
|
||||
// "Your Music" library. This call requires the ScopeUserModifyLibrary scope.
|
||||
// Trying to remove a track when you do not have the user's authorization
|
||||
// results in a `spotify.Error` with the status code set to http.StatusUnauthorized.
|
||||
func (c *Client) RemoveTracksFromLibrary(ids ...ID) error {
|
||||
return c.modifyLibraryTracks(false, ids...)
|
||||
}
|
||||
|
||||
func (c *Client) modifyLibraryTracks(add bool, ids ...ID) error {
|
||||
if l := len(ids); l == 0 || l > 50 {
|
||||
return errors.New("spotify: this call supports 1 to 50 IDs per call")
|
||||
}
|
||||
spotifyURL := fmt.Sprintf("%sme/tracks?ids=%s", c.baseURL, strings.Join(toStringSlice(ids), ","))
|
||||
method := "DELETE"
|
||||
if add {
|
||||
method = "PUT"
|
||||
}
|
||||
req, err := http.NewRequest(method, spotifyURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.execute(req, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
87
vendor/github.com/zmb3/spotify/page.go
generated
vendored
Normal file
87
vendor/github.com/zmb3/spotify/page.go
generated
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
package spotify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// ErrNoMorePages is the error returned when you attempt to get the next
|
||||
// (or previous) set of data but you've reached the end of the data set.
|
||||
var ErrNoMorePages = errors.New("spotify: no more pages")
|
||||
|
||||
// This file contains the types that implement Spotify's paging object.
|
||||
// See: https://developer.spotify.com/web-api/object-model/#paging-object
|
||||
|
||||
// basePage contains all of the fields in a Spotify paging object, except
|
||||
// for the actual items. This type is meant to be embedded in other types
|
||||
// that add the Items field.
|
||||
type basePage struct {
|
||||
// A link to the Web API Endpoint returning the full
|
||||
// result of this request.
|
||||
Endpoint string `json:"href"`
|
||||
// The maximum number of items in the response, as set
|
||||
// in the query (or default value if unset).
|
||||
Limit int `json:"limit"`
|
||||
// The offset of the items returned, as set in the query
|
||||
// (or default value if unset).
|
||||
Offset int `json:"offset"`
|
||||
// The total number of items available to return.
|
||||
Total int `json:"total"`
|
||||
// The URL to the next page of items (if available).
|
||||
Next string `json:"next"`
|
||||
// The URL to the previous page of items (if available).
|
||||
Previous string `json:"previous"`
|
||||
}
|
||||
|
||||
// FullArtistPage contains FullArtists returned by the Web API.
|
||||
type FullArtistPage struct {
|
||||
basePage
|
||||
Artists []FullArtist `json:"items"`
|
||||
}
|
||||
|
||||
// SimpleAlbumPage contains SimpleAlbums returned by the Web API.
|
||||
type SimpleAlbumPage struct {
|
||||
basePage
|
||||
Albums []SimpleAlbum `json:"items"`
|
||||
}
|
||||
|
||||
// SavedAlbumPage contains SavedAlbums returned by the Web API.
|
||||
type SavedAlbumPage struct {
|
||||
basePage
|
||||
Albums []SavedAlbum `json:"items"`
|
||||
}
|
||||
|
||||
// SimplePlaylistPage contains SimplePlaylists returned by the Web API.
|
||||
type SimplePlaylistPage struct {
|
||||
basePage
|
||||
Playlists []SimplePlaylist `json:"items"`
|
||||
}
|
||||
|
||||
// SimpleTrackPage contains SimpleTracks returned by the Web API.
|
||||
type SimpleTrackPage struct {
|
||||
basePage
|
||||
Tracks []SimpleTrack `json:"items"`
|
||||
}
|
||||
|
||||
// FullTrackPage contains FullTracks returned by the Web API.
|
||||
type FullTrackPage struct {
|
||||
basePage
|
||||
Tracks []FullTrack `json:"items"`
|
||||
}
|
||||
|
||||
// SavedTrackPage contains SavedTracks return by the Web API.
|
||||
type SavedTrackPage struct {
|
||||
basePage
|
||||
Tracks []SavedTrack `json:"items"`
|
||||
}
|
||||
|
||||
// PlaylistTrackPage contains information about tracks in a playlist.
|
||||
type PlaylistTrackPage struct {
|
||||
basePage
|
||||
Tracks []PlaylistTrack `json:"items"`
|
||||
}
|
||||
|
||||
// CategoryPage contains Category objects returned by the Web API.
|
||||
type CategoryPage struct {
|
||||
basePage
|
||||
Categories []Category `json:"items"`
|
||||
}
|
||||
528
vendor/github.com/zmb3/spotify/player.go
generated
vendored
Executable file
528
vendor/github.com/zmb3/spotify/player.go
generated
vendored
Executable file
@@ -0,0 +1,528 @@
|
||||
package spotify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PlayerDevice contains information about a device that a user can play music on
|
||||
type PlayerDevice struct {
|
||||
// ID of the device. This may be empty.
|
||||
ID ID `json:"id"`
|
||||
// Active If this device is the currently active device.
|
||||
Active bool `json:"is_active"`
|
||||
// Restricted Whether controlling this device is restricted. At present if
|
||||
// this is "true" then no Web API commands will be accepted by this device.
|
||||
Restricted bool `json:"is_restricted"`
|
||||
// Name The name of the device.
|
||||
Name string `json:"name"`
|
||||
// Type of device, such as "Computer", "Smartphone" or "Speaker".
|
||||
Type string `json:"type"`
|
||||
// Volume The current volume in percent.
|
||||
Volume int `json:"volume_percent"`
|
||||
}
|
||||
|
||||
// PlayerState contains information about the current playback.
|
||||
type PlayerState struct {
|
||||
CurrentlyPlaying
|
||||
// Device The device that is currently active
|
||||
Device PlayerDevice `json:"device"`
|
||||
// ShuffleState Shuffle is on or off
|
||||
ShuffleState bool `json:"shuffle_state"`
|
||||
// RepeatState off, track, context
|
||||
RepeatState string `json:"repeat_state"`
|
||||
}
|
||||
|
||||
// PlaybackContext is the playback context
|
||||
type PlaybackContext struct {
|
||||
// ExternalURLs of the context, or null if not available.
|
||||
ExternalURLs map[string]string `json:"external_urls"`
|
||||
// Endpoint of the context, or null if not available.
|
||||
Endpoint string `json:"href"`
|
||||
// Type of the item's context. Can be one of album, artist or playlist.
|
||||
Type string `json:"type"`
|
||||
// URI is the Spotify URI for the context.
|
||||
URI URI `json:"uri"`
|
||||
}
|
||||
|
||||
// CurrentlyPlaying contains the information about currently playing items
|
||||
type CurrentlyPlaying struct {
|
||||
// Timestamp when data was fetched
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
// PlaybackContext current context
|
||||
PlaybackContext PlaybackContext `json:"context"`
|
||||
// Progress into the currently playing track.
|
||||
Progress int `json:"progress_ms"`
|
||||
// Playing If something is currently playing.
|
||||
Playing bool `json:"is_playing"`
|
||||
// The currently playing track. Can be null.
|
||||
Item *FullTrack `json:"Item"`
|
||||
}
|
||||
|
||||
type RecentlyPlayedItem struct {
|
||||
// Track is the track information
|
||||
Track SimpleTrack `json:"track"`
|
||||
|
||||
// PlayedAt is the time that this song was played
|
||||
PlayedAt time.Time `json:"played_at"`
|
||||
|
||||
// PlaybackContext is the current playback context
|
||||
PlaybackContext PlaybackContext `json:"context"`
|
||||
}
|
||||
|
||||
type RecentlyPlayedResult struct {
|
||||
Items []RecentlyPlayedItem `json:"items"`
|
||||
}
|
||||
|
||||
// PlaybackOffset can be specified either by track URI OR Position. If both are present the
|
||||
// request will return 400 BAD REQUEST. If incorrect values are provided for position or uri,
|
||||
// the request may be accepted but with an unpredictable resulting action on playback.
|
||||
type PlaybackOffset struct {
|
||||
// Position is zero based and can’t be negative.
|
||||
Position int `json:"position,omitempty"`
|
||||
// URI is a string representing the uri of the item to start at.
|
||||
URI URI `json:"uri,omitempty"`
|
||||
}
|
||||
|
||||
type PlayOptions struct {
|
||||
// DeviceID The id of the device this command is targeting. If not
|
||||
// supplied, the user's currently active device is the target.
|
||||
DeviceID *ID `json:"-"`
|
||||
// PlaybackContext Spotify URI of the context to play.
|
||||
// Valid contexts are albums, artists & playlists.
|
||||
PlaybackContext *URI `json:"context_uri,omitempty"`
|
||||
// URIs Array of the Spotify track URIs to play
|
||||
URIs []URI `json:"uris,omitempty"`
|
||||
// PlaybackOffset Indicates from where in the context playback should start.
|
||||
// Only available when context corresponds to an album or playlist
|
||||
// object, or when the URIs parameter is used.
|
||||
PlaybackOffset *PlaybackOffset `json:"offset,omitempty"`
|
||||
// PositionMs Indicates from what position to start playback.
|
||||
// Must be a positive number. Passing in a position that is greater
|
||||
// than the length of the track will cause the player to start playing the next song.
|
||||
// Defaults to 0, starting a track from the beginning.
|
||||
PositionMs int `json:"position_ms,omitempty"`
|
||||
}
|
||||
|
||||
// RecentlyPlayedOptions describes options for the recently-played request. All
|
||||
// fields are optional. Only one of `AfterEpochMs` and `BeforeEpochMs` may be
|
||||
// given. Note that it seems as if Spotify only remembers the fifty most-recent
|
||||
// tracks as of right now.
|
||||
type RecentlyPlayedOptions struct {
|
||||
// Limit is the maximum number of items to return. Must be no greater than
|
||||
// fifty.
|
||||
Limit int
|
||||
|
||||
// AfterEpochMs is a Unix epoch in milliseconds that describes a time after
|
||||
// which to return songs.
|
||||
AfterEpochMs int64
|
||||
|
||||
// BeforeEpochMs is a Unix epoch in milliseconds that describes a time
|
||||
// before which to return songs.
|
||||
BeforeEpochMs int64
|
||||
}
|
||||
|
||||
// PlayerDevices information about available devices for the current user.
|
||||
//
|
||||
// Requires the ScopeUserReadPlaybackState scope in order to read information
|
||||
func (c *Client) PlayerDevices() ([]PlayerDevice, error) {
|
||||
var result struct {
|
||||
PlayerDevices []PlayerDevice `json:"devices"`
|
||||
}
|
||||
|
||||
err := c.get(c.baseURL+"me/player/devices", &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result.PlayerDevices, nil
|
||||
}
|
||||
|
||||
// PlayerState gets information about the playing state for the current user
|
||||
//
|
||||
// Requires the ScopeUserReadPlaybackState scope in order to read information
|
||||
func (c *Client) PlayerState() (*PlayerState, error) {
|
||||
return c.PlayerStateOpt(nil)
|
||||
}
|
||||
|
||||
// PlayerStateOpt is like PlayerState, but it accepts additional
|
||||
// options for sorting and filtering the results.
|
||||
func (c *Client) PlayerStateOpt(opt *Options) (*PlayerState, error) {
|
||||
spotifyURL := c.baseURL + "me/player"
|
||||
if opt != nil {
|
||||
v := url.Values{}
|
||||
if opt.Country != nil {
|
||||
v.Set("market", *opt.Country)
|
||||
}
|
||||
if params := v.Encode(); params != "" {
|
||||
spotifyURL += "?" + params
|
||||
}
|
||||
}
|
||||
|
||||
var result PlayerState
|
||||
|
||||
err := c.get(spotifyURL, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// PlayerCurrentlyPlaying gets information about the currently playing status
|
||||
// for the current user.
|
||||
//
|
||||
// Requires the ScopeUserReadCurrentlyPlaying scope or the ScopeUserReadPlaybackState
|
||||
// scope in order to read information
|
||||
func (c *Client) PlayerCurrentlyPlaying() (*CurrentlyPlaying, error) {
|
||||
return c.PlayerCurrentlyPlayingOpt(nil)
|
||||
}
|
||||
|
||||
// PlayerCurrentlyPlayingOpt is like PlayerCurrentlyPlaying, but it accepts
|
||||
// additional options for sorting and filtering the results.
|
||||
func (c *Client) PlayerCurrentlyPlayingOpt(opt *Options) (*CurrentlyPlaying, error) {
|
||||
spotifyURL := c.baseURL + "me/player/currently-playing"
|
||||
if opt != nil {
|
||||
v := url.Values{}
|
||||
if opt.Country != nil {
|
||||
v.Set("market", *opt.Country)
|
||||
}
|
||||
if params := v.Encode(); params != "" {
|
||||
spotifyURL += "?" + params
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", spotifyURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result CurrentlyPlaying
|
||||
err = c.execute(req, &result, http.StatusNoContent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// PlayerRecentlyPlayed gets a list of recently-played tracks for the current
|
||||
// user. This call requires ScopeUserReadRecentlyPlayed.
|
||||
func (c *Client) PlayerRecentlyPlayed() ([]RecentlyPlayedItem, error) {
|
||||
return c.PlayerRecentlyPlayedOpt(nil)
|
||||
}
|
||||
|
||||
// PlayerRecentlyPlayedOpt is like PlayerRecentlyPlayed, but it accepts
|
||||
// additional options for sorting and filtering the results.
|
||||
func (c *Client) PlayerRecentlyPlayedOpt(opt *RecentlyPlayedOptions) ([]RecentlyPlayedItem, error) {
|
||||
spotifyURL := c.baseURL + "me/player/recently-played"
|
||||
if opt != nil {
|
||||
v := url.Values{}
|
||||
if opt.Limit != 0 {
|
||||
v.Set("limit", strconv.FormatInt(int64(opt.Limit), 10))
|
||||
}
|
||||
if opt.BeforeEpochMs != 0 {
|
||||
v.Set("before", strconv.FormatInt(int64(opt.BeforeEpochMs), 10))
|
||||
}
|
||||
if opt.AfterEpochMs != 0 {
|
||||
v.Set("after", strconv.FormatInt(int64(opt.AfterEpochMs), 10))
|
||||
}
|
||||
if params := v.Encode(); params != "" {
|
||||
spotifyURL += "?" + params
|
||||
}
|
||||
}
|
||||
|
||||
result := RecentlyPlayedResult{}
|
||||
err := c.get(spotifyURL, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result.Items, nil
|
||||
}
|
||||
|
||||
// TransferPlayback transfers playback to a new device and determine if
|
||||
// it should start playing.
|
||||
//
|
||||
// Note that a value of false for the play parameter when also transferring
|
||||
// to another device_id will not pause playback. To ensure that playback is
|
||||
// paused on the new device you should send a pause command to the currently
|
||||
// active device before transferring to the new device_id.
|
||||
//
|
||||
// Requires the ScopeUserModifyPlaybackState in order to modify the player state
|
||||
func (c *Client) TransferPlayback(deviceID ID, play bool) error {
|
||||
reqData := struct {
|
||||
DeviceID []ID `json:"device_ids"`
|
||||
Play bool `json:"play"`
|
||||
}{
|
||||
DeviceID: []ID{deviceID},
|
||||
Play: play,
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
err := json.NewEncoder(buf).Encode(reqData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPut, c.baseURL+"me/player", buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.execute(req, nil, http.StatusNoContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Play Start a new context or resume current playback on the user's active
|
||||
// device. This call requires ScopeUserModifyPlaybackState in order to modify the player state.
|
||||
func (c *Client) Play() error {
|
||||
return c.PlayOpt(nil)
|
||||
}
|
||||
|
||||
// PlayOpt is like Play but with more options
|
||||
func (c *Client) PlayOpt(opt *PlayOptions) error {
|
||||
spotifyURL := c.baseURL + "me/player/play"
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
if opt != nil {
|
||||
v := url.Values{}
|
||||
if opt.DeviceID != nil {
|
||||
v.Set("device_id", opt.DeviceID.String())
|
||||
}
|
||||
if params := v.Encode(); params != "" {
|
||||
spotifyURL += "?" + params
|
||||
}
|
||||
|
||||
err := json.NewEncoder(buf).Encode(opt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPut, spotifyURL, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.execute(req, nil, http.StatusNoContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pause Playback on the user's currently active device.
|
||||
//
|
||||
// Requires the ScopeUserModifyPlaybackState in order to modify the player state
|
||||
func (c *Client) Pause() error {
|
||||
return c.PauseOpt(nil)
|
||||
}
|
||||
|
||||
// PauseOpt is like Pause but with more options
|
||||
//
|
||||
// Only expects PlayOptions.DeviceID, all other options will be ignored
|
||||
func (c *Client) PauseOpt(opt *PlayOptions) error {
|
||||
spotifyURL := c.baseURL + "me/player/pause"
|
||||
|
||||
if opt != nil {
|
||||
v := url.Values{}
|
||||
if opt.DeviceID != nil {
|
||||
v.Set("device_id", opt.DeviceID.String())
|
||||
}
|
||||
if params := v.Encode(); params != "" {
|
||||
spotifyURL += "?" + params
|
||||
}
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPut, spotifyURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.execute(req, nil, http.StatusNoContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Next skips to the next track in the user's queue in the user's
|
||||
// currently active device. This call requires ScopeUserModifyPlaybackState
|
||||
// in order to modify the player state
|
||||
func (c *Client) Next() error {
|
||||
return c.NextOpt(nil)
|
||||
}
|
||||
|
||||
// NextOpt is like Next but with more options
|
||||
//
|
||||
// Only expects PlayOptions.DeviceID, all other options will be ignored
|
||||
func (c *Client) NextOpt(opt *PlayOptions) error {
|
||||
spotifyURL := c.baseURL + "me/player/next"
|
||||
|
||||
if opt != nil {
|
||||
v := url.Values{}
|
||||
if opt.DeviceID != nil {
|
||||
v.Set("device_id", opt.DeviceID.String())
|
||||
}
|
||||
if params := v.Encode(); params != "" {
|
||||
spotifyURL += "?" + params
|
||||
}
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, spotifyURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.execute(req, nil, http.StatusNoContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Previous skips to the Previous track in the user's queue in the user's
|
||||
// currently active device. This call requires ScopeUserModifyPlaybackState
|
||||
// in order to modify the player state
|
||||
func (c *Client) Previous() error {
|
||||
return c.PreviousOpt(nil)
|
||||
}
|
||||
|
||||
// PreviousOpt is like Previous but with more options
|
||||
//
|
||||
// Only expects PlayOptions.DeviceID, all other options will be ignored
|
||||
func (c *Client) PreviousOpt(opt *PlayOptions) error {
|
||||
spotifyURL := c.baseURL + "me/player/previous"
|
||||
|
||||
if opt != nil {
|
||||
v := url.Values{}
|
||||
if opt.DeviceID != nil {
|
||||
v.Set("device_id", opt.DeviceID.String())
|
||||
}
|
||||
if params := v.Encode(); params != "" {
|
||||
spotifyURL += "?" + params
|
||||
}
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, spotifyURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.execute(req, nil, http.StatusNoContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Seek to the given position in the user’s currently playing track.
|
||||
//
|
||||
// The position in milliseconds to seek to. Must be a positive number.
|
||||
// Passing in a position that is greater than the length of the track
|
||||
// will cause the player to start playing the next song.
|
||||
//
|
||||
// Requires the ScopeUserModifyPlaybackState in order to modify the player state
|
||||
func (c *Client) Seek(position int) error {
|
||||
return c.SeekOpt(position, nil)
|
||||
}
|
||||
|
||||
// SeekOpt is like Seek but with more options
|
||||
//
|
||||
// Only expects PlayOptions.DeviceID, all other options will be ignored
|
||||
func (c *Client) SeekOpt(position int, opt *PlayOptions) error {
|
||||
return c.playerFuncWithOpt(
|
||||
"me/player/seek",
|
||||
url.Values{
|
||||
"position_ms": []string{strconv.FormatInt(int64(position), 10)},
|
||||
},
|
||||
opt,
|
||||
)
|
||||
}
|
||||
|
||||
// Repeat Set the repeat mode for the user's playback.
|
||||
//
|
||||
// Options are repeat-track, repeat-context, and off.
|
||||
//
|
||||
// Requires the ScopeUserModifyPlaybackState in order to modify the player state.
|
||||
func (c *Client) Repeat(state string) error {
|
||||
return c.RepeatOpt(state, nil)
|
||||
}
|
||||
|
||||
// RepeatOpt is like Repeat but with more options
|
||||
//
|
||||
// Only expects PlayOptions.DeviceID, all other options will be ignored.
|
||||
func (c *Client) RepeatOpt(state string, opt *PlayOptions) error {
|
||||
return c.playerFuncWithOpt(
|
||||
"me/player/repeat",
|
||||
url.Values{
|
||||
"state": []string{state},
|
||||
},
|
||||
opt,
|
||||
)
|
||||
}
|
||||
|
||||
// Volume set the volume for the user's current playback device.
|
||||
//
|
||||
// Percent is must be a value from 0 to 100 inclusive.
|
||||
//
|
||||
// Requires the ScopeUserModifyPlaybackState in order to modify the player state
|
||||
func (c *Client) Volume(percent int) error {
|
||||
return c.VolumeOpt(percent, nil)
|
||||
}
|
||||
|
||||
// VolumeOpt is like Volume but with more options
|
||||
//
|
||||
// Only expects PlayOptions.DeviceID, all other options will be ignored
|
||||
func (c *Client) VolumeOpt(percent int, opt *PlayOptions) error {
|
||||
return c.playerFuncWithOpt(
|
||||
"me/player/volume",
|
||||
url.Values{
|
||||
"volume_percent": []string{strconv.FormatInt(int64(percent), 10)},
|
||||
},
|
||||
opt,
|
||||
)
|
||||
}
|
||||
|
||||
// Shuffle switches shuffle on or off for user's playback.
|
||||
//
|
||||
// Requires the ScopeUserModifyPlaybackState in order to modify the player state
|
||||
func (c *Client) Shuffle(shuffle bool) error {
|
||||
return c.ShuffleOpt(shuffle, nil)
|
||||
}
|
||||
|
||||
// ShuffleOpt is like Shuffle but with more options
|
||||
//
|
||||
// Only expects PlayOptions.DeviceID, all other options will be ignored
|
||||
func (c *Client) ShuffleOpt(shuffle bool, opt *PlayOptions) error {
|
||||
return c.playerFuncWithOpt(
|
||||
"me/player/shuffle",
|
||||
url.Values{
|
||||
"state": []string{strconv.FormatBool(shuffle)},
|
||||
},
|
||||
opt,
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) playerFuncWithOpt(urlSuffix string, values url.Values, opt *PlayOptions) error {
|
||||
spotifyURL := c.baseURL + urlSuffix
|
||||
|
||||
if opt != nil {
|
||||
if opt.DeviceID != nil {
|
||||
values.Set("device_id", opt.DeviceID.String())
|
||||
}
|
||||
}
|
||||
|
||||
if params := values.Encode(); params != "" {
|
||||
spotifyURL += "?" + params
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPut, spotifyURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.execute(req, nil, http.StatusNoContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
662
vendor/github.com/zmb3/spotify/playlist.go
generated
vendored
Normal file
662
vendor/github.com/zmb3/spotify/playlist.go
generated
vendored
Normal file
@@ -0,0 +1,662 @@
|
||||
package spotify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PlaylistTracks contains details about the tracks in a playlist.
|
||||
type PlaylistTracks struct {
|
||||
// A link to the Web API endpoint where full details of
|
||||
// the playlist's tracks can be retrieved.
|
||||
Endpoint string `json:"href"`
|
||||
// The total number of tracks in the playlist.
|
||||
Total uint `json:"total"`
|
||||
}
|
||||
|
||||
// SimplePlaylist contains basic info about a Spotify playlist.
|
||||
type SimplePlaylist struct {
|
||||
// Indicates whether the playlist owner allows others to modify the playlist.
|
||||
// Note: only non-collaborative playlists are currently returned by Spotify's Web API.
|
||||
Collaborative bool `json:"collaborative"`
|
||||
ExternalURLs map[string]string `json:"external_urls"`
|
||||
// A link to the Web API endpoint providing full details of the playlist.
|
||||
Endpoint string `json:"href"`
|
||||
ID ID `json:"id"`
|
||||
// The playlist image. Note: this field is only returned for modified,
|
||||
// verified playlists. Otherwise the slice is empty. If returned, the source
|
||||
// URL for the image is temporary and will expire in less than a day.
|
||||
Images []Image `json:"images"`
|
||||
Name string `json:"name"`
|
||||
Owner User `json:"owner"`
|
||||
IsPublic bool `json:"public"`
|
||||
// The version identifier for the current playlist. Can be supplied in other
|
||||
// requests to target a specific playlist version.
|
||||
SnapshotID string `json:"snapshot_id"`
|
||||
// A collection to the Web API endpoint where full details of the playlist's
|
||||
// tracks can be retrieved, along with the total number of tracks in the playlist.
|
||||
Tracks PlaylistTracks `json:"tracks"`
|
||||
URI URI `json:"uri"`
|
||||
}
|
||||
|
||||
// FullPlaylist provides extra playlist data in addition to the data provided by SimplePlaylist.
|
||||
type FullPlaylist struct {
|
||||
SimplePlaylist
|
||||
// The playlist description. Only returned for modified, verified playlists.
|
||||
Description string `json:"description"`
|
||||
// Information about the followers of this playlist.
|
||||
Followers Followers `json:"followers"`
|
||||
Tracks PlaylistTrackPage `json:"tracks"`
|
||||
}
|
||||
|
||||
// PlaylistOptions contains optional parameters that can be used when querying
|
||||
// for featured playlists. Only the non-nil fields are used in the request.
|
||||
type PlaylistOptions struct {
|
||||
Options
|
||||
// The desired language, consisting of a lowercase IO 639
|
||||
// language code and an uppercase ISO 3166-1 alpha-2
|
||||
// country code, joined by an underscore. Provide this
|
||||
// parameter if you want the results returned in a particular
|
||||
// language. If not specified, the result will be returned
|
||||
// in the Spotify default language (American English).
|
||||
Locale *string
|
||||
// A timestamp in ISO 8601 format (yyyy-MM-ddTHH:mm:ss).
|
||||
// use this paramter to specify the user's local time to
|
||||
// get results tailored for that specific date and time
|
||||
// in the day. If not provided, the response defaults to
|
||||
// the current UTC time.
|
||||
Timestamp *string
|
||||
}
|
||||
|
||||
// FeaturedPlaylistsOpt gets a list of playlists featured by Spotify.
|
||||
// It accepts a number of optional parameters via the opt argument.
|
||||
func (c *Client) FeaturedPlaylistsOpt(opt *PlaylistOptions) (message string, playlists *SimplePlaylistPage, e error) {
|
||||
spotifyURL := c.baseURL + "browse/featured-playlists"
|
||||
if opt != nil {
|
||||
v := url.Values{}
|
||||
if opt.Locale != nil {
|
||||
v.Set("locale", *opt.Locale)
|
||||
}
|
||||
if opt.Country != nil {
|
||||
v.Set("country", *opt.Country)
|
||||
}
|
||||
if opt.Timestamp != nil {
|
||||
v.Set("timestamp", *opt.Timestamp)
|
||||
}
|
||||
if opt.Limit != nil {
|
||||
v.Set("limit", strconv.Itoa(*opt.Limit))
|
||||
}
|
||||
if opt.Offset != nil {
|
||||
v.Set("offset", strconv.Itoa(*opt.Offset))
|
||||
}
|
||||
if params := v.Encode(); params != "" {
|
||||
spotifyURL += "?" + params
|
||||
}
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Playlists SimplePlaylistPage `json:"playlists"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
err := c.get(spotifyURL, &result)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return result.Message, &result.Playlists, nil
|
||||
}
|
||||
|
||||
// FeaturedPlaylists gets a list of playlists featured by Spotify.
|
||||
// It is equivalent to c.FeaturedPlaylistsOpt(nil).
|
||||
func (c *Client) FeaturedPlaylists() (message string, playlists *SimplePlaylistPage, e error) {
|
||||
return c.FeaturedPlaylistsOpt(nil)
|
||||
}
|
||||
|
||||
// FollowPlaylist adds the current user as a follower of the specified
|
||||
// playlist. Any playlist can be followed, regardless of its private/public
|
||||
// status, as long as you know the owner and playlist ID.
|
||||
//
|
||||
// If the public argument is true, then the playlist will be included in the
|
||||
// user's public playlists. To be able to follow playlists privately, the user
|
||||
// must have granted the ScopePlaylistModifyPrivate scope. The
|
||||
// ScopePlaylistModifyPublic scope is required to follow playlists publicly.
|
||||
func (c *Client) FollowPlaylist(owner ID, playlist ID, public bool) error {
|
||||
spotifyURL := buildFollowURI(c.baseURL, owner, playlist)
|
||||
body := strings.NewReader(strconv.FormatBool(public))
|
||||
req, err := http.NewRequest("PUT", spotifyURL, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
err = c.execute(req, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnfollowPlaylist removes the current user as a follower of a playlist.
|
||||
// Unfollowing a publicly followed playlist requires ScopePlaylistModifyPublic.
|
||||
// Unfolowing a privately followed playlist requies ScopePlaylistModifyPrivate.
|
||||
func (c *Client) UnfollowPlaylist(owner, playlist ID) error {
|
||||
spotifyURL := buildFollowURI(c.baseURL, owner, playlist)
|
||||
req, err := http.NewRequest("DELETE", spotifyURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.execute(req, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildFollowURI(url string, owner, playlist ID) string {
|
||||
return fmt.Sprintf("%susers/%s/playlists/%s/followers",
|
||||
url, string(owner), string(playlist))
|
||||
}
|
||||
|
||||
// GetPlaylistsForUser gets a list of the playlists owned or followed by a
|
||||
// particular Spotify user.
|
||||
//
|
||||
// Private playlists and collaborative playlists are only retrievable for the
|
||||
// current user. In order to read private playlists, the user must have granted
|
||||
// the ScopePlaylistReadPrivate scope. Note that this scope alone will not
|
||||
// return collaborative playlists, even though they are always private. In
|
||||
// order to read collaborative playlists, the user must have granted the
|
||||
// ScopePlaylistReadCollaborative scope.
|
||||
func (c *Client) GetPlaylistsForUser(userID string) (*SimplePlaylistPage, error) {
|
||||
return c.GetPlaylistsForUserOpt(userID, nil)
|
||||
}
|
||||
|
||||
// GetPlaylistsForUserOpt is like PlaylistsForUser, but it accepts optional paramters
|
||||
// for filtering the results.
|
||||
func (c *Client) GetPlaylistsForUserOpt(userID string, opt *Options) (*SimplePlaylistPage, error) {
|
||||
spotifyURL := c.baseURL + "users/" + userID + "/playlists"
|
||||
if opt != nil {
|
||||
v := url.Values{}
|
||||
if opt.Limit != nil {
|
||||
v.Set("limit", strconv.Itoa(*opt.Limit))
|
||||
}
|
||||
if opt.Offset != nil {
|
||||
v.Set("offset", strconv.Itoa(*opt.Offset))
|
||||
}
|
||||
if params := v.Encode(); params != "" {
|
||||
spotifyURL += "?" + params
|
||||
}
|
||||
}
|
||||
|
||||
var result SimplePlaylistPage
|
||||
|
||||
err := c.get(spotifyURL, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, err
|
||||
}
|
||||
|
||||
// GetPlaylist gets a playlist
|
||||
func (c *Client) GetPlaylist(playlistID ID) (*FullPlaylist, error) {
|
||||
return c.GetPlaylistOpt(playlistID, "")
|
||||
}
|
||||
|
||||
// GetPlaylistOpt is like GetPlaylist, but it accepts an optional fields parameter
|
||||
// that can be used to filter the query.
|
||||
//
|
||||
// fields is a comma-separated list of the fields to return.
|
||||
// See the JSON tags on the FullPlaylist struct for valid field options.
|
||||
// For example, to get just the playlist's description and URI:
|
||||
// fields = "description,uri"
|
||||
//
|
||||
// A dot separator can be used to specify non-reoccurring fields, while
|
||||
// parentheses can be used to specify reoccurring fields within objects.
|
||||
// For example, to get just the added date and the user ID of the adder:
|
||||
// fields = "tracks.items(added_at,added_by.id)"
|
||||
//
|
||||
// Use multiple parentheses to drill down into nested objects, for example:
|
||||
// fields = "tracks.items(track(name,href,album(name,href)))"
|
||||
//
|
||||
// Fields can be excluded by prefixing them with an exclamation mark, for example;
|
||||
// fields = "tracks.items(track(name,href,album(!name,href)))"
|
||||
func (c *Client) GetPlaylistOpt(playlistID ID, fields string) (*FullPlaylist, error) {
|
||||
spotifyURL := fmt.Sprintf("%splaylists/%s", c.baseURL, playlistID)
|
||||
if fields != "" {
|
||||
spotifyURL += "?fields=" + url.QueryEscape(fields)
|
||||
}
|
||||
|
||||
var playlist FullPlaylist
|
||||
|
||||
err := c.get(spotifyURL, &playlist)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &playlist, err
|
||||
}
|
||||
|
||||
// GetPlaylistTracks gets full details of the tracks in a playlist, given the
|
||||
// playlist's Spotify ID.
|
||||
func (c *Client) GetPlaylistTracks(playlistID ID) (*PlaylistTrackPage, error) {
|
||||
return c.GetPlaylistTracksOpt(playlistID, nil, "")
|
||||
}
|
||||
|
||||
// GetPlaylistTracksOpt is like GetPlaylistTracks, but it accepts optional parameters
|
||||
// for sorting and filtering the results.
|
||||
//
|
||||
// The field parameter is a comma-separated list of the fields to return. See the
|
||||
// JSON struct tags for the PlaylistTrackPage type for valid field names.
|
||||
// For example, to get just the total number of tracks and the request limit:
|
||||
// fields = "total,limit"
|
||||
//
|
||||
// A dot separator can be used to specify non-reoccurring fields, while parentheses
|
||||
// can be used to specify reoccurring fields within objects. For example, to get
|
||||
// just the added date and user ID of the adder:
|
||||
// fields = "items(added_at,added_by.id
|
||||
//
|
||||
// Use multiple parentheses to drill down into nested objects. For example:
|
||||
// fields = "items(track(name,href,album(name,href)))"
|
||||
//
|
||||
// Fields can be excluded by prefixing them with an exclamation mark. For example:
|
||||
// fields = "items.track.album(!external_urls,images)"
|
||||
func (c *Client) GetPlaylistTracksOpt(playlistID ID,
|
||||
opt *Options, fields string) (*PlaylistTrackPage, error) {
|
||||
|
||||
spotifyURL := fmt.Sprintf("%splaylists/%s/tracks", c.baseURL, playlistID)
|
||||
v := url.Values{}
|
||||
if fields != "" {
|
||||
v.Set("fields", fields)
|
||||
}
|
||||
if opt != nil {
|
||||
if opt.Limit != nil {
|
||||
v.Set("limit", strconv.Itoa(*opt.Limit))
|
||||
}
|
||||
if opt.Offset != nil {
|
||||
v.Set("offset", strconv.Itoa(*opt.Offset))
|
||||
}
|
||||
}
|
||||
if params := v.Encode(); params != "" {
|
||||
spotifyURL += "?" + params
|
||||
}
|
||||
|
||||
var result PlaylistTrackPage
|
||||
|
||||
err := c.get(spotifyURL, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, err
|
||||
}
|
||||
|
||||
// CreatePlaylistForUser creates a playlist for a Spotify user.
|
||||
// The playlist will be empty until you add tracks to it.
|
||||
// The playlistName does not need to be unique - a user can have
|
||||
// several playlists with the same name.
|
||||
//
|
||||
// Creating a public playlist for a user requires ScopePlaylistModifyPublic;
|
||||
// creating a private playlist requires ScopePlaylistModifyPrivate.
|
||||
//
|
||||
// On success, the newly created playlist is returned.
|
||||
func (c *Client) CreatePlaylistForUser(userID, playlistName, description string, public bool) (*FullPlaylist, error) {
|
||||
spotifyURL := fmt.Sprintf("%susers/%s/playlists", c.baseURL, userID)
|
||||
body := struct {
|
||||
Name string `json:"name"`
|
||||
Public bool `json:"public"`
|
||||
Description string `json:"description"`
|
||||
}{
|
||||
playlistName,
|
||||
public,
|
||||
description,
|
||||
}
|
||||
bodyJSON, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequest("POST", spotifyURL, bytes.NewReader(bodyJSON))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
var p FullPlaylist
|
||||
err = c.execute(req, &p, http.StatusCreated)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &p, err
|
||||
}
|
||||
|
||||
// ChangePlaylistName changes the name of a playlist. This call requires that the
|
||||
// user has authorized the ScopePlaylistModifyPublic or ScopePlaylistModifyPrivate
|
||||
// scopes (depending on whether the playlist is public or private).
|
||||
// The current user must own the playlist in order to modify it.
|
||||
func (c *Client) ChangePlaylistName(playlistID ID, newName string) error {
|
||||
return c.modifyPlaylist(playlistID, newName, "", nil)
|
||||
}
|
||||
|
||||
// ChangePlaylistAccess modifies the public/private status of a playlist. This call
|
||||
// requires that the user has authorized the ScopePlaylistModifyPublic or
|
||||
// ScopePlaylistModifyPrivate scopes (depending on whether the playlist is
|
||||
// currently public or private). The current user must own the playlist in order to modify it.
|
||||
func (c *Client) ChangePlaylistAccess(playlistID ID, public bool) error {
|
||||
return c.modifyPlaylist(playlistID, "", "", &public)
|
||||
}
|
||||
|
||||
// ChangePlaylistDescription modifies the description of a playlist. This call
|
||||
// requires that the user has authorized the ScopePlaylistModifyPublic or
|
||||
// ScopePlaylistModifyPrivate scopes (depending on whether the playlist is
|
||||
// currently public or private). The current user must own the playlist in order to modify it.
|
||||
func (c *Client) ChangePlaylistDescription(playlistID ID, newDescription string) error {
|
||||
return c.modifyPlaylist(playlistID, "", newDescription, nil)
|
||||
}
|
||||
|
||||
// ChangePlaylistNameAndAccess combines ChangePlaylistName and ChangePlaylistAccess into
|
||||
// a single Web API call. It requires that the user has authorized the ScopePlaylistModifyPublic
|
||||
// or ScopePlaylistModifyPrivate scopes (depending on whether the playlist is currently
|
||||
// public or private). The current user must own the playlist in order to modify it.
|
||||
func (c *Client) ChangePlaylistNameAndAccess(playlistID ID, newName string, public bool) error {
|
||||
return c.modifyPlaylist(playlistID, newName, "", &public)
|
||||
}
|
||||
|
||||
// ChangePlaylistNameAccessAndDescription combines ChangePlaylistName, ChangePlaylistAccess, and
|
||||
// ChangePlaylistDescription into a single Web API call. It requires that the user has authorized
|
||||
// the ScopePlaylistModifyPublic or ScopePlaylistModifyPrivate scopes (depending on whether the
|
||||
// playlist is currently public or private). The current user must own the playlist in order to modify it.
|
||||
func (c *Client) ChangePlaylistNameAccessAndDescription(playlistID ID, newName, newDescription string, public bool) error {
|
||||
return c.modifyPlaylist(playlistID, newName, newDescription, &public)
|
||||
}
|
||||
|
||||
func (c *Client) modifyPlaylist(playlistID ID, newName, newDescription string, public *bool) error {
|
||||
body := struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Public *bool `json:"public,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}{
|
||||
newName,
|
||||
public,
|
||||
newDescription,
|
||||
}
|
||||
bodyJSON, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
spotifyURL := fmt.Sprintf("%splaylists/%s", c.baseURL, string(playlistID))
|
||||
req, err := http.NewRequest("PUT", spotifyURL, bytes.NewReader(bodyJSON))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
err = c.execute(req, nil, http.StatusCreated)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddTracksToPlaylist adds one or more tracks to a user's playlist.
|
||||
// This call requires ScopePlaylistModifyPublic or ScopePlaylistModifyPrivate.
|
||||
// A maximum of 100 tracks can be added per call. It returns a snapshot ID that
|
||||
// can be used to identify this version (the new version) of the playlist in
|
||||
// future requests.
|
||||
func (c *Client) AddTracksToPlaylist(playlistID ID, trackIDs ...ID) (snapshotID string, err error) {
|
||||
|
||||
uris := make([]string, len(trackIDs))
|
||||
for i, id := range trackIDs {
|
||||
uris[i] = fmt.Sprintf("spotify:track:%s", id)
|
||||
}
|
||||
m := make(map[string]interface{})
|
||||
m["uris"] = uris
|
||||
|
||||
spotifyURL := fmt.Sprintf("%splaylists/%s/tracks",
|
||||
c.baseURL, string(playlistID))
|
||||
body, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req, err := http.NewRequest("POST", spotifyURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
result := struct {
|
||||
SnapshotID string `json:"snapshot_id"`
|
||||
}{}
|
||||
|
||||
err = c.execute(req, &result, http.StatusCreated)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return result.SnapshotID, nil
|
||||
}
|
||||
|
||||
// RemoveTracksFromPlaylist removes one or more tracks from a user's playlist.
|
||||
// This call requrles that the user has authorized the ScopePlaylistModifyPublic
|
||||
// or ScopePlaylistModifyPrivate scopes.
|
||||
//
|
||||
// If the track(s) occur multiple times in the specified playlist, then all occurrences
|
||||
// of the track will be removed. If successful, the snapshot ID returned can be used to
|
||||
// identify the playlist version in future requests.
|
||||
func (c *Client) RemoveTracksFromPlaylist(playlistID ID, trackIDs ...ID) (newSnapshotID string, err error) {
|
||||
|
||||
tracks := make([]struct {
|
||||
URI string `json:"uri"`
|
||||
}, len(trackIDs))
|
||||
|
||||
for i, u := range trackIDs {
|
||||
tracks[i].URI = fmt.Sprintf("spotify:track:%s", u)
|
||||
}
|
||||
return c.removeTracksFromPlaylist(playlistID, tracks, "")
|
||||
}
|
||||
|
||||
// TrackToRemove specifies a track to be removed from a playlist.
|
||||
// Positions is a slice of 0-based track indices.
|
||||
// TrackToRemove is used with RemoveTracksFromPlaylistOpt.
|
||||
type TrackToRemove struct {
|
||||
URI string `json:"uri"`
|
||||
Positions []int `json:"positions"`
|
||||
}
|
||||
|
||||
// NewTrackToRemove creates a new TrackToRemove object with the specified
|
||||
// track ID and playlist locations.
|
||||
func NewTrackToRemove(trackID string, positions []int) TrackToRemove {
|
||||
return TrackToRemove{
|
||||
URI: fmt.Sprintf("spotify:track:%s", trackID),
|
||||
Positions: positions,
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveTracksFromPlaylistOpt is like RemoveTracksFromPlaylist, but it supports
|
||||
// optional parameters that offer more fine-grained control. Instead of deleting
|
||||
// all occurrences of a track, this function takes an index with each track URI
|
||||
// that indicates the position of the track in the playlist.
|
||||
//
|
||||
// In addition, the snapshotID parameter allows you to specify the snapshot ID
|
||||
// against which you want to make the changes. Spotify will validate that the
|
||||
// specified tracks exist in the specified positions and make the changes, even
|
||||
// if more recent changes have been made to the playlist. If a track in the
|
||||
// specified position is not found, the entire request will fail and no edits
|
||||
// will take place. (Note: the snapshot is optional, pass the empty string if
|
||||
// you don't care about it.)
|
||||
func (c *Client) RemoveTracksFromPlaylistOpt(playlistID ID,
|
||||
tracks []TrackToRemove, snapshotID string) (newSnapshotID string, err error) {
|
||||
|
||||
return c.removeTracksFromPlaylist(playlistID, tracks, snapshotID)
|
||||
}
|
||||
|
||||
func (c *Client) removeTracksFromPlaylist(playlistID ID,
|
||||
tracks interface{}, snapshotID string) (newSnapshotID string, err error) {
|
||||
|
||||
m := make(map[string]interface{})
|
||||
m["tracks"] = tracks
|
||||
if snapshotID != "" {
|
||||
m["snapshot_id"] = snapshotID
|
||||
}
|
||||
|
||||
spotifyURL := fmt.Sprintf("%splaylists/%s/tracks",
|
||||
c.baseURL, string(playlistID))
|
||||
body, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req, err := http.NewRequest("DELETE", spotifyURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
result := struct {
|
||||
SnapshotID string `json:"snapshot_id"`
|
||||
}{}
|
||||
|
||||
err = c.execute(req, &result)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return result.SnapshotID, err
|
||||
}
|
||||
|
||||
// ReplacePlaylistTracks replaces all of the tracks in a playlist, overwriting its
|
||||
// exising tracks This can be useful for replacing or reordering tracks, or for
|
||||
// clearing a playlist.
|
||||
//
|
||||
// Modifying a public playlist requires that the user has authorized the
|
||||
// ScopePlaylistModifyPublic scope. Modifying a private playlist requires the
|
||||
// ScopePlaylistModifyPrivate scope.
|
||||
//
|
||||
// A maximum of 100 tracks is permited in this call. Additional tracks must be
|
||||
// added via AddTracksToPlaylist.
|
||||
func (c *Client) ReplacePlaylistTracks(playlistID ID, trackIDs ...ID) error {
|
||||
trackURIs := make([]string, len(trackIDs))
|
||||
for i, u := range trackIDs {
|
||||
trackURIs[i] = fmt.Sprintf("spotify:track:%s", u)
|
||||
}
|
||||
spotifyURL := fmt.Sprintf("%splaylists/%s/tracks?uris=%s",
|
||||
c.baseURL, playlistID, strings.Join(trackURIs, ","))
|
||||
req, err := http.NewRequest("PUT", spotifyURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.execute(req, nil, http.StatusCreated)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserFollowsPlaylist checks if one or more (up to 5) Spotify users are following
|
||||
// a Spotify playlist, given the playlist's owner and ID.
|
||||
//
|
||||
// Checking if a user follows a playlist publicly doesn't require any scopes.
|
||||
// Checking if the user is privately following a playlist is only possible for the
|
||||
// current user when that user has granted access to the ScopePlaylistReadPrivate scope.
|
||||
func (c *Client) UserFollowsPlaylist(playlistID ID, userIDs ...string) ([]bool, error) {
|
||||
spotifyURL := fmt.Sprintf("%splaylists/%s/followers/contains?ids=%s",
|
||||
c.baseURL, playlistID, strings.Join(userIDs, ","))
|
||||
|
||||
follows := make([]bool, len(userIDs))
|
||||
|
||||
err := c.get(spotifyURL, &follows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return follows, err
|
||||
}
|
||||
|
||||
// PlaylistReorderOptions is used with ReorderPlaylistTracks to reorder
|
||||
// a track or group of tracks in a playlist.
|
||||
//
|
||||
// For example, in a playlist with 10 tracks, you can:
|
||||
//
|
||||
// - move the first track to the end of the playlist by setting
|
||||
// RangeStart to 0 and InsertBefore to 10
|
||||
// - move the last track to the beginning of the playlist by setting
|
||||
// RangeStart to 9 and InsertBefore to 0
|
||||
// - Move the last 2 tracks to the beginning of the playlist by setting
|
||||
// RangeStart to 8 and RangeLength to 2.
|
||||
type PlaylistReorderOptions struct {
|
||||
// The position of the first track to be reordered.
|
||||
// This field is required.
|
||||
RangeStart int `json:"range_start"`
|
||||
// The amount of tracks to be reordered. This field is optional. If
|
||||
// you don't set it, the value 1 will be used.
|
||||
RangeLength int `json:"range_length,omitempty"`
|
||||
// The position where the tracks should be inserted. To reorder the
|
||||
// tracks to the end of the playlist, simply set this to the position
|
||||
// after the last track. This field is required.
|
||||
InsertBefore int `json:"insert_before"`
|
||||
// The playlist's snapshot ID against which you wish to make the changes.
|
||||
// This field is optional.
|
||||
SnapshotID string `json:"snapshot_id,omitempty"`
|
||||
}
|
||||
|
||||
// ReorderPlaylistTracks reorders a track or group of tracks in a playlist. It
|
||||
// returns a snapshot ID that can be used to identify the [newly modified] playlist
|
||||
// version in future requests.
|
||||
//
|
||||
// See the docs for PlaylistReorderOptions for information on how the reordering
|
||||
// works.
|
||||
//
|
||||
// Reordering tracks in the current user's public playlist requires ScopePlaylistModifyPublic.
|
||||
// Reordering tracks in the user's private playlists (including collaborative playlists) requires
|
||||
// ScopePlaylistModifyPrivate.
|
||||
func (c *Client) ReorderPlaylistTracks(playlistID ID, opt PlaylistReorderOptions) (snapshotID string, err error) {
|
||||
spotifyURL := fmt.Sprintf("%splaylists/%s/tracks", c.baseURL, playlistID)
|
||||
j, err := json.Marshal(opt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req, err := http.NewRequest("PUT", spotifyURL, bytes.NewReader(j))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
result := struct {
|
||||
SnapshotID string `json:"snapshot_id"`
|
||||
}{}
|
||||
err = c.execute(req, &result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return result.SnapshotID, err
|
||||
}
|
||||
|
||||
// SetPlaylistImage replaces the image used to represent a playlist.
|
||||
// This action can only be performed by the owner of the playlist,
|
||||
// and requires ScopeImageUpload as well as ScopeModifyPlaylist{Public|Private}..
|
||||
func (c *Client) SetPlaylistImage(playlistID ID, img io.Reader) error {
|
||||
spotifyURL := fmt.Sprintf("%splaylists/%s/images", c.baseURL, playlistID)
|
||||
// data flow:
|
||||
// img (reader) -> copy into base64 encoder (writer) -> pipe (write end)
|
||||
// pipe (read end) -> request body
|
||||
r, w := io.Pipe()
|
||||
go func() {
|
||||
enc := base64.NewEncoder(base64.StdEncoding, w)
|
||||
_, err := io.Copy(enc, img)
|
||||
enc.Close()
|
||||
w.CloseWithError(err)
|
||||
}()
|
||||
|
||||
req, err := http.NewRequest("PUT", spotifyURL, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "image/jpeg")
|
||||
return c.execute(req, nil, http.StatusAccepted)
|
||||
}
|
||||
121
vendor/github.com/zmb3/spotify/recommendation.go
generated
vendored
Normal file
121
vendor/github.com/zmb3/spotify/recommendation.go
generated
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
package spotify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Seeds contains IDs of artists, genres and/or tracks
|
||||
// to be used as seeds for recommendations
|
||||
type Seeds struct {
|
||||
Artists []ID
|
||||
Tracks []ID
|
||||
Genres []string
|
||||
}
|
||||
|
||||
// count returns the total number of seeds contained in s
|
||||
func (s Seeds) count() int {
|
||||
return len(s.Artists) + len(s.Tracks) + len(s.Genres)
|
||||
}
|
||||
|
||||
// Recommendations contains a list of recommended tracks based on seeds
|
||||
type Recommendations struct {
|
||||
Seeds []RecommendationSeed `json:"seeds"`
|
||||
Tracks []SimpleTrack `json:"tracks"`
|
||||
}
|
||||
|
||||
// RecommendationSeed represents a recommendation seed after
|
||||
// being processed by the Spotify API
|
||||
type RecommendationSeed struct {
|
||||
AfterFilteringSize int `json:"afterFilteringSize"`
|
||||
AfterRelinkingSize int `json:"afterRelinkingSize"`
|
||||
Endpoint string `json:"href"`
|
||||
ID ID `json:"id"`
|
||||
InitialPoolSize int `json:"initialPoolSize"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// MaxNumberOfSeeds allowed by Spotify for a recommendation request
|
||||
const MaxNumberOfSeeds = 5
|
||||
|
||||
// setSeedValues sets url values into v for each seed in seeds
|
||||
func setSeedValues(seeds Seeds, v url.Values) {
|
||||
if len(seeds.Artists) != 0 {
|
||||
v.Set("seed_artists", strings.Join(toStringSlice(seeds.Artists), ","))
|
||||
}
|
||||
if len(seeds.Tracks) != 0 {
|
||||
v.Set("seed_tracks", strings.Join(toStringSlice(seeds.Tracks), ","))
|
||||
}
|
||||
if len(seeds.Genres) != 0 {
|
||||
v.Set("seed_genres", strings.Join(seeds.Genres, ","))
|
||||
}
|
||||
}
|
||||
|
||||
// setTrackAttributesValues sets track attributes values to the given url values
|
||||
func setTrackAttributesValues(trackAttributes *TrackAttributes, values url.Values) {
|
||||
if trackAttributes == nil {
|
||||
return
|
||||
}
|
||||
for attr, val := range trackAttributes.intAttributes {
|
||||
values.Set(attr, strconv.Itoa(val))
|
||||
}
|
||||
for attr, val := range trackAttributes.floatAttributes {
|
||||
values.Set(attr, strconv.FormatFloat(val, 'f', -1, 64))
|
||||
}
|
||||
}
|
||||
|
||||
// GetRecommendations returns a list of recommended tracks based on the given seeds.
|
||||
// Recommendations are generated based on the available information for a given seed entity
|
||||
// and matched against similar artists and tracks. If there is sufficient information
|
||||
// about the provided seeds, a list of tracks will be returned together with pool size details.
|
||||
// For artists and tracks that are very new or obscure
|
||||
// there might not be enough data to generate a list of tracks.
|
||||
func (c *Client) GetRecommendations(seeds Seeds, trackAttributes *TrackAttributes, opt *Options) (*Recommendations, error) {
|
||||
v := url.Values{}
|
||||
|
||||
if seeds.count() == 0 {
|
||||
return nil, fmt.Errorf("spotify: at least one seed is required")
|
||||
}
|
||||
if seeds.count() > MaxNumberOfSeeds {
|
||||
return nil, fmt.Errorf("spotify: exceeded maximum of %d seeds", MaxNumberOfSeeds)
|
||||
}
|
||||
|
||||
setSeedValues(seeds, v)
|
||||
setTrackAttributesValues(trackAttributes, v)
|
||||
|
||||
if opt != nil {
|
||||
if opt.Limit != nil {
|
||||
v.Set("limit", strconv.Itoa(*opt.Limit))
|
||||
}
|
||||
if opt.Country != nil {
|
||||
v.Set("market", *opt.Country)
|
||||
}
|
||||
}
|
||||
|
||||
spotifyURL := c.baseURL + "recommendations?" + v.Encode()
|
||||
|
||||
var recommendations Recommendations
|
||||
err := c.get(spotifyURL, &recommendations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &recommendations, err
|
||||
}
|
||||
|
||||
// GetAvailableGenreSeeds retrieves a list of available genres seed parameter values for
|
||||
// recommendations.
|
||||
func (c *Client) GetAvailableGenreSeeds() ([]string, error) {
|
||||
spotifyURL := c.baseURL + "recommendations/available-genre-seeds"
|
||||
|
||||
genreSeeds := make(map[string][]string)
|
||||
|
||||
err := c.get(spotifyURL, &genreSeeds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return genreSeeds["genres"], nil
|
||||
}
|
||||
208
vendor/github.com/zmb3/spotify/search.go
generated
vendored
Normal file
208
vendor/github.com/zmb3/spotify/search.go
generated
vendored
Normal file
@@ -0,0 +1,208 @@
|
||||
package spotify
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// MarketFromToken can be used in place of the Market parameter
|
||||
// if the Client has a valid access token. In this case, the
|
||||
// results will be limited to content that is playable in the
|
||||
// country associated with the user's account. The user must have
|
||||
// granted access to the user-read-private scope when the access
|
||||
// token was issued.
|
||||
MarketFromToken = "from_token"
|
||||
)
|
||||
|
||||
// SearchType represents the type of a query used in the Search function.
|
||||
type SearchType int
|
||||
|
||||
// Search type values that can be passed to the Search function. These are flags
|
||||
// that can be bitwise OR'd together to search for multiple types of content simultaneously.
|
||||
const (
|
||||
SearchTypeAlbum SearchType = 1 << iota
|
||||
SearchTypeArtist = 1 << iota
|
||||
SearchTypePlaylist = 1 << iota
|
||||
SearchTypeTrack = 1 << iota
|
||||
)
|
||||
|
||||
func (st SearchType) encode() string {
|
||||
types := []string{}
|
||||
if st&SearchTypeAlbum != 0 {
|
||||
types = append(types, "album")
|
||||
}
|
||||
if st&SearchTypeArtist != 0 {
|
||||
types = append(types, "artist")
|
||||
}
|
||||
if st&SearchTypePlaylist != 0 {
|
||||
types = append(types, "playlist")
|
||||
}
|
||||
if st&SearchTypeTrack != 0 {
|
||||
types = append(types, "track")
|
||||
}
|
||||
return strings.Join(types, ",")
|
||||
}
|
||||
|
||||
// SearchResult contains the results of a call to Search.
|
||||
// Fields that weren't searched for will be nil pointers.
|
||||
type SearchResult struct {
|
||||
Artists *FullArtistPage `json:"artists"`
|
||||
Albums *SimpleAlbumPage `json:"albums"`
|
||||
Playlists *SimplePlaylistPage `json:"playlists"`
|
||||
Tracks *FullTrackPage `json:"tracks"`
|
||||
}
|
||||
|
||||
// Search gets Spotify catalog information about artists, albums, tracks,
|
||||
// or playlists that match a keyword string. t is a mask containing one or more
|
||||
// search types. For example, `Search(query, SearchTypeArtist|SearchTypeAlbum)`
|
||||
// will search for artists or albums matching the specified keywords.
|
||||
//
|
||||
// Matching
|
||||
//
|
||||
// Matching of search keywords is NOT case sensitive. Keywords are matched in
|
||||
// any order unless surrounded by double quotes. Searching for playlists will
|
||||
// return results where the query keyword(s) match any part of the playlist's
|
||||
// name or description. Only popular public playlists are returned.
|
||||
//
|
||||
// Operators
|
||||
//
|
||||
// The operator NOT can be used to exclude results. For example,
|
||||
// query = "roadhouse NOT blues" returns items that match "roadhouse" but exludes
|
||||
// those that also contain the keyword "blues". Similarly, the OR operator can
|
||||
// be used to broaden the search. query = "roadhouse OR blues" returns all results
|
||||
// that include either of the terms. Only one OR operator can be used in a query.
|
||||
//
|
||||
// Operators should be specified in uppercase.
|
||||
//
|
||||
// Wildcards
|
||||
//
|
||||
// The asterisk (*) character can, with some limitations, be used as a wildcard
|
||||
// (maximum of 2 per query). It will match a variable number of non-white-space
|
||||
// characters. It cannot be used in a quoted phrase, in a field filter, or as
|
||||
// the first character of a keyword string.
|
||||
//
|
||||
// Field filters
|
||||
//
|
||||
// By default, results are returned when a match is found in any field of the
|
||||
// target object type. Searches can be made more specific by specifying an album,
|
||||
// artist, or track field filter. For example, "album:gold artist:abba type:album"
|
||||
// will only return results with the text "gold" in the album name and the text
|
||||
// "abba" in the artist's name.
|
||||
//
|
||||
// The field filter "year" can be used with album, artist, and track searches to
|
||||
// limit the results to a particular year. For example "bob year:2014" or
|
||||
// "bob year:1980-2020".
|
||||
//
|
||||
// The field filter "tag:new" can be used in album searches to retrieve only
|
||||
// albums released in the last two weeks. The field filter "tag:hipster" can be
|
||||
// used in album searches to retrieve only albums with the lowest 10% popularity.
|
||||
//
|
||||
// Other possible field filters, depending on object types being searched,
|
||||
// include "genre", "upc", and "isrc". For example "damian genre:reggae-pop".
|
||||
func (c *Client) Search(query string, t SearchType) (*SearchResult, error) {
|
||||
return c.SearchOpt(query, t, nil)
|
||||
}
|
||||
|
||||
// SearchOpt works just like Search, but it accepts additional
|
||||
// parameters for filtering the output. See the documentation for Search more
|
||||
// more information.
|
||||
//
|
||||
// If the Country field is specified in the options, then the results will only
|
||||
// contain artists, albums, and tracks playable in the specified country
|
||||
// (playlist results are not affected by the Country option). Additionally,
|
||||
// the constant MarketFromToken can be used with authenticated clients.
|
||||
// If the client has a valid access token, then the results will only include
|
||||
// content playable in the user's country.
|
||||
func (c *Client) SearchOpt(query string, t SearchType, opt *Options) (*SearchResult, error) {
|
||||
v := url.Values{}
|
||||
v.Set("q", query)
|
||||
v.Set("type", t.encode())
|
||||
if opt != nil {
|
||||
if opt.Limit != nil {
|
||||
v.Set("limit", strconv.Itoa(*opt.Limit))
|
||||
}
|
||||
if opt.Country != nil {
|
||||
v.Set("market", *opt.Country)
|
||||
}
|
||||
if opt.Offset != nil {
|
||||
v.Set("offset", strconv.Itoa(*opt.Offset))
|
||||
}
|
||||
}
|
||||
|
||||
spotifyURL := c.baseURL + "search?" + v.Encode()
|
||||
|
||||
var result SearchResult
|
||||
|
||||
err := c.get(spotifyURL, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, err
|
||||
}
|
||||
|
||||
// NextArtistResults loads the next page of artists into the specified search result.
|
||||
func (c *Client) NextArtistResults(s *SearchResult) error {
|
||||
if s.Artists == nil || s.Artists.Next == "" {
|
||||
return ErrNoMorePages
|
||||
}
|
||||
return c.get(s.Artists.Next, s)
|
||||
}
|
||||
|
||||
// PreviousArtistResults loads the previous page of artists into the specified search result.
|
||||
func (c *Client) PreviousArtistResults(s *SearchResult) error {
|
||||
if s.Artists == nil || s.Artists.Previous == "" {
|
||||
return ErrNoMorePages
|
||||
}
|
||||
return c.get(s.Artists.Previous, s)
|
||||
}
|
||||
|
||||
// NextAlbumResults loads the next page of albums into the specified search result.
|
||||
func (c *Client) NextAlbumResults(s *SearchResult) error {
|
||||
if s.Albums == nil || s.Albums.Next == "" {
|
||||
return ErrNoMorePages
|
||||
}
|
||||
return c.get(s.Albums.Next, s)
|
||||
}
|
||||
|
||||
// PreviousAlbumResults loads the previous page of albums into the specified search result.
|
||||
func (c *Client) PreviousAlbumResults(s *SearchResult) error {
|
||||
if s.Albums == nil || s.Albums.Previous == "" {
|
||||
return ErrNoMorePages
|
||||
}
|
||||
return c.get(s.Albums.Previous, s)
|
||||
}
|
||||
|
||||
// NextPlaylistResults loads the next page of playlists into the specified search result.
|
||||
func (c *Client) NextPlaylistResults(s *SearchResult) error {
|
||||
if s.Playlists == nil || s.Playlists.Next == "" {
|
||||
return ErrNoMorePages
|
||||
}
|
||||
return c.get(s.Playlists.Next, s)
|
||||
}
|
||||
|
||||
// PreviousPlaylistResults loads the previous page of playlists into the specified search result.
|
||||
func (c *Client) PreviousPlaylistResults(s *SearchResult) error {
|
||||
if s.Playlists == nil || s.Playlists.Previous == "" {
|
||||
return ErrNoMorePages
|
||||
}
|
||||
return c.get(s.Playlists.Previous, s)
|
||||
}
|
||||
|
||||
// PreviousTrackResults loads the previous page of tracks into the specified search result.
|
||||
func (c *Client) PreviousTrackResults(s *SearchResult) error {
|
||||
if s.Tracks == nil || s.Tracks.Previous == "" {
|
||||
return ErrNoMorePages
|
||||
}
|
||||
return c.get(s.Tracks.Previous, s)
|
||||
}
|
||||
|
||||
// NextTrackResults loads the next page of tracks into the specified search result.
|
||||
func (c *Client) NextTrackResults(s *SearchResult) error {
|
||||
if s.Tracks == nil || s.Tracks.Next == "" {
|
||||
return ErrNoMorePages
|
||||
}
|
||||
return c.get(s.Tracks.Next, s)
|
||||
}
|
||||
291
vendor/github.com/zmb3/spotify/spotify.go
generated
vendored
Normal file
291
vendor/github.com/zmb3/spotify/spotify.go
generated
vendored
Normal file
@@ -0,0 +1,291 @@
|
||||
// Package spotify provides utilties for interfacing
|
||||
// with Spotify's Web API.
|
||||
package spotify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Version is the version of this library.
|
||||
const Version = "1.0.0"
|
||||
|
||||
const (
|
||||
// DateLayout can be used with time.Parse to create time.Time values
|
||||
// from Spotify date strings. For example, PrivateUser.Birthdate
|
||||
// uses this format.
|
||||
DateLayout = "2006-01-02"
|
||||
// TimestampLayout can be used with time.Parse to create time.Time
|
||||
// values from SpotifyTimestamp strings. It is an ISO 8601 UTC timestamp
|
||||
// with a zero offset. For example, PlaylistTrack's AddedAt field uses
|
||||
// this format.
|
||||
TimestampLayout = "2006-01-02T15:04:05Z"
|
||||
|
||||
// defaultRetryDurationS helps us fix an apparent server bug whereby we will
|
||||
// be told to retry but not be given a wait-interval.
|
||||
defaultRetryDuration = time.Second * 5
|
||||
|
||||
// rateLimitExceededStatusCode is the code that the server returns when our
|
||||
// request frequency is too high.
|
||||
rateLimitExceededStatusCode = 429
|
||||
)
|
||||
|
||||
const baseAddress = "https://api.spotify.com/v1/"
|
||||
|
||||
// Client is a client for working with the Spotify Web API.
|
||||
// To create an authenticated client, use the `Authenticator.NewClient` method.
|
||||
type Client struct {
|
||||
http *http.Client
|
||||
baseURL string
|
||||
|
||||
AutoRetry bool
|
||||
}
|
||||
|
||||
// URI identifies an artist, album, track, or category. For example,
|
||||
// spotify:track:6rqhFgbbKwnb9MLmUQDhG6
|
||||
type URI string
|
||||
|
||||
// ID is a base-62 identifier for an artist, track, album, etc.
|
||||
// It can be found at the end of a spotify.URI.
|
||||
type ID string
|
||||
|
||||
func (id *ID) String() string {
|
||||
return string(*id)
|
||||
}
|
||||
|
||||
// Followers contains information about the number of people following a
|
||||
// particular artist or playlist.
|
||||
type Followers struct {
|
||||
// The total number of followers.
|
||||
Count uint `json:"total"`
|
||||
// A link to the Web API endpoint providing full details of the followers,
|
||||
// or the empty string if this data is not available.
|
||||
Endpoint string `json:"href"`
|
||||
}
|
||||
|
||||
// Image identifies an image associated with an item.
|
||||
type Image struct {
|
||||
// The image height, in pixels.
|
||||
Height int `json:"height"`
|
||||
// The image width, in pixels.
|
||||
Width int `json:"width"`
|
||||
// The source URL of the image.
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// Download downloads the image and writes its data to the specified io.Writer.
|
||||
func (i Image) Download(dst io.Writer) error {
|
||||
resp, err := http.Get(i.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
// TODO: get Content-Type from header?
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New("Couldn't download image - HTTP" + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
_, err = io.Copy(dst, resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
// Error represents an error returned by the Spotify Web API.
|
||||
type Error struct {
|
||||
// A short description of the error.
|
||||
Message string `json:"message"`
|
||||
// The HTTP status code.
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// decodeError decodes an Error from an io.Reader.
|
||||
func (c *Client) decodeError(resp *http.Response) error {
|
||||
responseBody, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(responseBody) == 0 {
|
||||
return fmt.Errorf("spotify: HTTP %d: %s (body empty)", resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(responseBody)
|
||||
|
||||
var e struct {
|
||||
E Error `json:"error"`
|
||||
}
|
||||
err = json.NewDecoder(buf).Decode(&e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("spotify: couldn't decode error: (%d) [%s]", len(responseBody), responseBody)
|
||||
}
|
||||
|
||||
if e.E.Message == "" {
|
||||
// Some errors will result in there being a useful status-code but an
|
||||
// empty message, which will confuse the user (who only has access to
|
||||
// the message and not the code). An example of this is when we send
|
||||
// some of the arguments directly in the HTTP query and the URL ends-up
|
||||
// being too long.
|
||||
|
||||
e.E.Message = fmt.Sprintf("spotify: unexpected HTTP %d: %s (empty error)",
|
||||
resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
return e.E
|
||||
}
|
||||
|
||||
// shouldRetry determines whether the status code indicates that the
|
||||
// previous operation should be retried at a later time
|
||||
func shouldRetry(status int) bool {
|
||||
return status == http.StatusAccepted || status == http.StatusTooManyRequests
|
||||
}
|
||||
|
||||
// isFailure determines whether the code indicates failure
|
||||
func isFailure(code int, validCodes []int) bool {
|
||||
for _, item := range validCodes {
|
||||
if item == code {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// execute executes a non-GET request. `needsStatus` describes other HTTP status codes
|
||||
// that can represent success. Note that in all current usages of this function,
|
||||
// we need to still allow a 200 even if we'd also like to check for additional
|
||||
// success codes.
|
||||
func (c *Client) execute(req *http.Request, result interface{}, needsStatus ...int) error {
|
||||
for {
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if c.AutoRetry && shouldRetry(resp.StatusCode) {
|
||||
time.Sleep(retryDuration(resp))
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK && isFailure(resp.StatusCode, needsStatus) {
|
||||
return c.decodeError(resp)
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func retryDuration(resp *http.Response) time.Duration {
|
||||
raw := resp.Header.Get("Retry-After")
|
||||
if raw == "" {
|
||||
return defaultRetryDuration
|
||||
}
|
||||
seconds, err := strconv.ParseInt(raw, 10, 32)
|
||||
if err != nil {
|
||||
return defaultRetryDuration
|
||||
}
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
|
||||
func (c *Client) get(url string, result interface{}) error {
|
||||
for {
|
||||
resp, err := c.http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == rateLimitExceededStatusCode && c.AutoRetry {
|
||||
time.Sleep(retryDuration(resp))
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return c.decodeError(resp)
|
||||
}
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Options contains optional parameters that can be provided
|
||||
// to various API calls. Only the non-nil fields are used
|
||||
// in queries.
|
||||
type Options struct {
|
||||
// Country is an ISO 3166-1 alpha-2 country code. Provide
|
||||
// this parameter if you want the list of returned items to
|
||||
// be relevant to a particular country. If omitted, the
|
||||
// results will be relevant to all countries.
|
||||
Country *string
|
||||
// Limit is the maximum number of items to return.
|
||||
Limit *int
|
||||
// Offset is the index of the first item to return. Use it
|
||||
// with Limit to get the next set of items.
|
||||
Offset *int
|
||||
// Timerange is the period of time from which to return results
|
||||
// in certain API calls. The three options are the following string
|
||||
// literals: "short", "medium", and "long"
|
||||
Timerange *string
|
||||
}
|
||||
|
||||
// NewReleasesOpt is like NewReleases, but it accepts optional parameters
|
||||
// for filtering the results.
|
||||
func (c *Client) NewReleasesOpt(opt *Options) (albums *SimpleAlbumPage, err error) {
|
||||
spotifyURL := c.baseURL + "browse/new-releases"
|
||||
if opt != nil {
|
||||
v := url.Values{}
|
||||
if opt.Country != nil {
|
||||
v.Set("country", *opt.Country)
|
||||
}
|
||||
if opt.Limit != nil {
|
||||
v.Set("limit", strconv.Itoa(*opt.Limit))
|
||||
}
|
||||
if opt.Offset != nil {
|
||||
v.Set("offset", strconv.Itoa(*opt.Offset))
|
||||
}
|
||||
if params := v.Encode(); params != "" {
|
||||
spotifyURL += "?" + params
|
||||
}
|
||||
}
|
||||
|
||||
var objmap map[string]*json.RawMessage
|
||||
err = c.get(spotifyURL, &objmap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result SimpleAlbumPage
|
||||
err = json.Unmarshal(*objmap["albums"], &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// NewReleases gets a list of new album releases featured in Spotify.
|
||||
// This call requires bearer authorization.
|
||||
func (c *Client) NewReleases() (albums *SimpleAlbumPage, err error) {
|
||||
return c.NewReleasesOpt(nil)
|
||||
}
|
||||
120
vendor/github.com/zmb3/spotify/track.go
generated
vendored
Normal file
120
vendor/github.com/zmb3/spotify/track.go
generated
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
package spotify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SimpleTrack contains basic info about a track.
|
||||
type SimpleTrack struct {
|
||||
Artists []SimpleArtist `json:"artists"`
|
||||
// A list of the countries in which the track can be played,
|
||||
// identified by their ISO 3166-1 alpha-2 codes.
|
||||
AvailableMarkets []string `json:"available_markets"`
|
||||
// The disc number (usually 1 unless the album consists of more than one disc).
|
||||
DiscNumber int `json:"disc_number"`
|
||||
// The length of the track, in milliseconds.
|
||||
Duration int `json:"duration_ms"`
|
||||
// Whether or not the track has explicit lyrics.
|
||||
// true => yes, it does; false => no, it does not.
|
||||
Explicit bool `json:"explicit"`
|
||||
// External URLs for this track.
|
||||
ExternalURLs map[string]string `json:"external_urls"`
|
||||
// A link to the Web API endpoint providing full details for this track.
|
||||
Endpoint string `json:"href"`
|
||||
ID ID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
// A URL to a 30 second preview (MP3) of the track.
|
||||
PreviewURL string `json:"preview_url"`
|
||||
// The number of the track. If an album has several
|
||||
// discs, the track number is the number on the specified
|
||||
// DiscNumber.
|
||||
TrackNumber int `json:"track_number"`
|
||||
URI URI `json:"uri"`
|
||||
}
|
||||
|
||||
func (st SimpleTrack) String() string {
|
||||
return fmt.Sprintf("TRACK<[%s] [%s]>", st.ID, st.Name)
|
||||
}
|
||||
|
||||
// FullTrack provides extra track data in addition to what is provided by SimpleTrack.
|
||||
type FullTrack struct {
|
||||
SimpleTrack
|
||||
// The album on which the track appears. The album object includes a link in href to full information about the album.
|
||||
Album SimpleAlbum `json:"album"`
|
||||
// Known external IDs for the track.
|
||||
ExternalIDs map[string]string `json:"external_ids"`
|
||||
// Popularity of the track. The value will be between 0 and 100,
|
||||
// with 100 being the most popular. The popularity is calculated from
|
||||
// both total plays and most recent plays.
|
||||
Popularity int `json:"popularity"`
|
||||
}
|
||||
|
||||
// PlaylistTrack contains info about a track in a playlist.
|
||||
type PlaylistTrack struct {
|
||||
// The date and time the track was added to the playlist.
|
||||
// You can use the TimestampLayout constant to convert
|
||||
// this field to a time.Time value.
|
||||
// Warning: very old playlists may not populate this value.
|
||||
AddedAt string `json:"added_at"`
|
||||
// The Spotify user who added the track to the playlist.
|
||||
// Warning: vary old playlists may not populate this value.
|
||||
AddedBy User `json:"added_by"`
|
||||
// Information about the track.
|
||||
Track FullTrack `json:"track"`
|
||||
}
|
||||
|
||||
// SavedTrack provides info about a track saved to a user's account.
|
||||
type SavedTrack struct {
|
||||
// The date and time the track was saved, represented as an ISO
|
||||
// 8601 UTC timestamp with a zero offset (YYYY-MM-DDTHH:MM:SSZ).
|
||||
// You can use the TimestampLayout constant to convert this to
|
||||
// a time.Time value.
|
||||
AddedAt string `json:"added_at"`
|
||||
FullTrack `json:"track"`
|
||||
}
|
||||
|
||||
// TimeDuration returns the track's duration as a time.Duration value.
|
||||
func (t *SimpleTrack) TimeDuration() time.Duration {
|
||||
return time.Duration(t.Duration) * time.Millisecond
|
||||
}
|
||||
|
||||
// GetTrack gets Spotify catalog information for
|
||||
// a single track identified by its unique Spotify ID.
|
||||
func (c *Client) GetTrack(id ID) (*FullTrack, error) {
|
||||
spotifyURL := c.baseURL + "tracks/" + string(id)
|
||||
|
||||
var t FullTrack
|
||||
|
||||
err := c.get(spotifyURL, &t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
// GetTracks gets Spotify catalog information for multiple tracks based on their
|
||||
// Spotify IDs. It supports up to 50 tracks in a single call. Tracks are
|
||||
// returned in the order requested. If a track is not found, that position in the
|
||||
// result will be nil. Duplicate ids in the query will result in duplicate
|
||||
// tracks in the result.
|
||||
func (c *Client) GetTracks(ids ...ID) ([]*FullTrack, error) {
|
||||
if len(ids) > 50 {
|
||||
return nil, errors.New("spotify: FindTracks supports up to 50 tracks")
|
||||
}
|
||||
spotifyURL := c.baseURL + "tracks?ids=" + strings.Join(toStringSlice(ids), ",")
|
||||
|
||||
var t struct {
|
||||
Tracks []*FullTrack `json:"tracks"`
|
||||
}
|
||||
|
||||
err := c.get(spotifyURL, &t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t.Tracks, nil
|
||||
}
|
||||
431
vendor/github.com/zmb3/spotify/track_attributes.go
generated
vendored
Normal file
431
vendor/github.com/zmb3/spotify/track_attributes.go
generated
vendored
Normal file
@@ -0,0 +1,431 @@
|
||||
package spotify
|
||||
|
||||
// TrackAttributes contains various tuneable parameters that can be used for recommendations.
|
||||
// For each of the tuneable track attributes, target, min and max values may be provided.
|
||||
// Target:
|
||||
// Tracks with the attribute values nearest to the target values will be preferred.
|
||||
// For example, you might request TargetEnergy=0.6 and TargetDanceability=0.8.
|
||||
// All target values will be weighed equally in ranking results.
|
||||
// Max:
|
||||
// A hard ceiling on the selected track attribute’s value can be provided.
|
||||
// For example, MaxInstrumentalness=0.35 would filter out most tracks
|
||||
// that are likely to be instrumental.
|
||||
// Min:
|
||||
// A hard floor on the selected track attribute’s value can be provided.
|
||||
// For example, min_tempo=140 would restrict results to only those tracks
|
||||
// with a tempo of greater than 140 beats per minute.
|
||||
type TrackAttributes struct {
|
||||
intAttributes map[string]int
|
||||
floatAttributes map[string]float64
|
||||
}
|
||||
|
||||
// NewTrackAttributes returns a new TrackAttributes instance with no attributes set.
|
||||
// Attributes can then be chained following a builder pattern:
|
||||
// ta := NewTrackAttributes().
|
||||
// MaxAcousticness(0.15).
|
||||
// TargetPopularity(90)
|
||||
func NewTrackAttributes() *TrackAttributes {
|
||||
return &TrackAttributes{
|
||||
intAttributes: map[string]int{},
|
||||
floatAttributes: map[string]float64{},
|
||||
}
|
||||
}
|
||||
|
||||
// MaxAcousticness sets the maximum acousticness
|
||||
// Acousticness is a confidence measure from 0.0 to 1.0 of whether
|
||||
// the track is acoustic. A value of 1.0 represents high confidence
|
||||
// that the track is acoustic.
|
||||
func (ta *TrackAttributes) MaxAcousticness(acousticness float64) *TrackAttributes {
|
||||
ta.floatAttributes["max_acousticness"] = acousticness
|
||||
return ta
|
||||
}
|
||||
|
||||
// MinAcousticness sets the minimum acousticness
|
||||
// Acousticness is a confidence measure from 0.0 to 1.0 of whether
|
||||
// the track is acoustic. A value of 1.0 represents high confidence
|
||||
// that the track is acoustic.
|
||||
func (ta *TrackAttributes) MinAcousticness(acousticness float64) *TrackAttributes {
|
||||
ta.floatAttributes["min_acousticness"] = acousticness
|
||||
return ta
|
||||
}
|
||||
|
||||
// TargetAcousticness sets the target acousticness
|
||||
// Acousticness is a confidence measure from 0.0 to 1.0 of whether
|
||||
// the track is acoustic. A value of 1.0 represents high confidence
|
||||
// that the track is acoustic.
|
||||
func (ta *TrackAttributes) TargetAcousticness(acousticness float64) *TrackAttributes {
|
||||
ta.floatAttributes["target_acousticness"] = acousticness
|
||||
return ta
|
||||
}
|
||||
|
||||
// MaxDanceability sets the maximum danceability
|
||||
// Danceability describes how suitable a track is for dancing based on
|
||||
// a combination of musical elements including tempo, rhythm stability,
|
||||
// beat strength, and overall regularity.
|
||||
// A value of 0.0 is least danceable and 1.0 is most danceable.
|
||||
func (ta *TrackAttributes) MaxDanceability(danceability float64) *TrackAttributes {
|
||||
ta.floatAttributes["max_danceability"] = danceability
|
||||
return ta
|
||||
}
|
||||
|
||||
// MinDanceability sets the minimum danceability
|
||||
// Danceability describes how suitable a track is for dancing based on
|
||||
// a combination of musical elements including tempo, rhythm stability,
|
||||
// beat strength, and overall regularity.
|
||||
// A value of 0.0 is least danceable and 1.0 is most danceable.
|
||||
func (ta *TrackAttributes) MinDanceability(danceability float64) *TrackAttributes {
|
||||
ta.floatAttributes["min_danceability"] = danceability
|
||||
return ta
|
||||
}
|
||||
|
||||
// TargetDanceability sets the target danceability
|
||||
// Danceability describes how suitable a track is for dancing based on
|
||||
// a combination of musical elements including tempo, rhythm stability,
|
||||
// beat strength, and overall regularity.
|
||||
// A value of 0.0 is least danceable and 1.0 is most danceable.
|
||||
func (ta *TrackAttributes) TargetDanceability(danceability float64) *TrackAttributes {
|
||||
ta.floatAttributes["target_danceability"] = danceability
|
||||
return ta
|
||||
}
|
||||
|
||||
// MaxDuration sets the maximum length of the track in milliseconds
|
||||
func (ta *TrackAttributes) MaxDuration(duration int) *TrackAttributes {
|
||||
ta.intAttributes["max_duration_ms"] = duration
|
||||
return ta
|
||||
}
|
||||
|
||||
// MinDuration sets the minimum length of the track in milliseconds
|
||||
func (ta *TrackAttributes) MinDuration(duration int) *TrackAttributes {
|
||||
ta.intAttributes["min_duration_ms"] = duration
|
||||
return ta
|
||||
}
|
||||
|
||||
// TargetDuration sets the target length of the track in milliseconds
|
||||
func (ta *TrackAttributes) TargetDuration(duration int) *TrackAttributes {
|
||||
ta.intAttributes["target_duration_ms"] = duration
|
||||
return ta
|
||||
}
|
||||
|
||||
// MaxEnergy sets the maximum energy
|
||||
// Energy is a measure from 0.0 to 1.0 and represents a perceptual mesaure
|
||||
// of intensity and activity. Typically, energetic tracks feel fast, loud,
|
||||
// and noisy.
|
||||
func (ta *TrackAttributes) MaxEnergy(energy float64) *TrackAttributes {
|
||||
ta.floatAttributes["max_energy"] = energy
|
||||
return ta
|
||||
}
|
||||
|
||||
// MinEnergy sets the minimum energy
|
||||
// Energy is a measure from 0.0 to 1.0 and represents a perceptual mesaure
|
||||
// of intensity and activity. Typically, energetic tracks feel fast, loud,
|
||||
// and noisy.
|
||||
func (ta *TrackAttributes) MinEnergy(energy float64) *TrackAttributes {
|
||||
ta.floatAttributes["min_energy"] = energy
|
||||
return ta
|
||||
}
|
||||
|
||||
// TargetEnergy sets the target energy
|
||||
// Energy is a measure from 0.0 to 1.0 and represents a perceptual mesaure
|
||||
// of intensity and activity. Typically, energetic tracks feel fast, loud,
|
||||
// and noisy.
|
||||
func (ta *TrackAttributes) TargetEnergy(energy float64) *TrackAttributes {
|
||||
ta.floatAttributes["target_energy"] = energy
|
||||
return ta
|
||||
}
|
||||
|
||||
// MaxInstrumentalness sets the maximum instrumentalness
|
||||
// Instrumentalness predicts whether a track contains no vocals.
|
||||
// "Ooh" and "aah" sounds are treated as instrumental in this context.
|
||||
// Rap or spoken word tracks are clearly "vocal".
|
||||
// The closer the instrumentalness value is to 1.0,
|
||||
// the greater likelihood the track contains no vocal content.
|
||||
// Values above 0.5 are intended to represent instrumental tracks,
|
||||
// but confidence is higher as the value approaches 1.0.
|
||||
func (ta *TrackAttributes) MaxInstrumentalness(instrumentalness float64) *TrackAttributes {
|
||||
ta.floatAttributes["max_instrumentalness"] = instrumentalness
|
||||
return ta
|
||||
|
||||
}
|
||||
|
||||
// MinInstrumentalness sets the minimum instrumentalness
|
||||
// Instrumentalness predicts whether a track contains no vocals.
|
||||
// "Ooh" and "aah" sounds are treated as instrumental in this context.
|
||||
// Rap or spoken word tracks are clearly "vocal".
|
||||
// The closer the instrumentalness value is to 1.0,
|
||||
// the greater likelihood the track contains no vocal content.
|
||||
// Values above 0.5 are intended to represent instrumental tracks,
|
||||
// but confidence is higher as the value approaches 1.0.
|
||||
func (ta *TrackAttributes) MinInstrumentalness(instrumentalness float64) *TrackAttributes {
|
||||
ta.floatAttributes["min_instrumentalness"] = instrumentalness
|
||||
return ta
|
||||
|
||||
}
|
||||
|
||||
// TargetInstrumentalness sets the target instrumentalness
|
||||
// Instrumentalness predicts whether a track contains no vocals.
|
||||
// "Ooh" and "aah" sounds are treated as instrumental in this context.
|
||||
// Rap or spoken word tracks are clearly "vocal".
|
||||
// The closer the instrumentalness value is to 1.0,
|
||||
// the greater likelihood the track contains no vocal content.
|
||||
// Values above 0.5 are intended to represent instrumental tracks,
|
||||
// but confidence is higher as the value approaches 1.0.
|
||||
func (ta *TrackAttributes) TargetInstrumentalness(instrumentalness float64) *TrackAttributes {
|
||||
ta.floatAttributes["target_instrumentalness"] = instrumentalness
|
||||
return ta
|
||||
|
||||
}
|
||||
|
||||
// MaxKey sets the maximum key
|
||||
// Integers map to pitches using standard Pitch Class notation
|
||||
// (https://en.wikipedia.org/wiki/Pitch_class).
|
||||
func (ta *TrackAttributes) MaxKey(key int) *TrackAttributes {
|
||||
ta.intAttributes["max_key"] = key
|
||||
return ta
|
||||
}
|
||||
|
||||
// MinKey sets the minimum key
|
||||
// Integers map to pitches using standard Pitch Class notation
|
||||
// (https://en.wikipedia.org/wiki/Pitch_class).
|
||||
func (ta *TrackAttributes) MinKey(key int) *TrackAttributes {
|
||||
ta.intAttributes["min_key"] = key
|
||||
return ta
|
||||
}
|
||||
|
||||
// TargetKey sets the target key
|
||||
// Integers map to pitches using standard Pitch Class notation
|
||||
// (https://en.wikipedia.org/wiki/Pitch_class).
|
||||
func (ta *TrackAttributes) TargetKey(key int) *TrackAttributes {
|
||||
ta.intAttributes["target_key"] = key
|
||||
return ta
|
||||
}
|
||||
|
||||
// MaxLiveness sets the maximum liveness
|
||||
// Detects the presence of an audience in the recording. Higher liveness
|
||||
// values represent an increased probability that the track was performed live.
|
||||
// A value above 0.8 provides strong likelihook that the track is live.
|
||||
func (ta *TrackAttributes) MaxLiveness(liveness float64) *TrackAttributes {
|
||||
ta.floatAttributes["max_liveness"] = liveness
|
||||
return ta
|
||||
}
|
||||
|
||||
// MinLiveness sets the minimum liveness
|
||||
// Detects the presence of an audience in the recording. Higher liveness
|
||||
// values represent an increased probability that the track was performed live.
|
||||
// A value above 0.8 provides strong likelihook that the track is live.
|
||||
func (ta *TrackAttributes) MinLiveness(liveness float64) *TrackAttributes {
|
||||
ta.floatAttributes["min_liveness"] = liveness
|
||||
return ta
|
||||
}
|
||||
|
||||
// TargetLiveness sets the target liveness
|
||||
// Detects the presence of an audience in the recording. Higher liveness
|
||||
// values represent an increased probability that the track was performed live.
|
||||
// A value above 0.8 provides strong likelihook that the track is live.
|
||||
func (ta *TrackAttributes) TargetLiveness(liveness float64) *TrackAttributes {
|
||||
ta.floatAttributes["target_liveness"] = liveness
|
||||
return ta
|
||||
}
|
||||
|
||||
// MaxLoudness sets the maximum loudness in decibels (dB)
|
||||
// Loudness values are averaged across the entire track and are
|
||||
// useful for comparing the relative loudness of tracks.
|
||||
// Typical values range between -60 and 0 dB.
|
||||
func (ta *TrackAttributes) MaxLoudness(loudness float64) *TrackAttributes {
|
||||
ta.floatAttributes["max_loudness"] = loudness
|
||||
return ta
|
||||
}
|
||||
|
||||
// MinLoudness sets the minimum loudness in decibels (dB)
|
||||
// Loudness values are averaged across the entire track and are
|
||||
// useful for comparing the relative loudness of tracks.
|
||||
// Typical values range between -60 and 0 dB.
|
||||
func (ta *TrackAttributes) MinLoudness(loudness float64) *TrackAttributes {
|
||||
ta.floatAttributes["min_loudness"] = loudness
|
||||
return ta
|
||||
}
|
||||
|
||||
// TargetLoudness sets the target loudness in decibels (dB)
|
||||
// Loudness values are averaged across the entire track and are
|
||||
// useful for comparing the relative loudness of tracks.
|
||||
// Typical values range between -60 and 0 dB.
|
||||
func (ta *TrackAttributes) TargetLoudness(loudness float64) *TrackAttributes {
|
||||
ta.floatAttributes["target_loudness"] = loudness
|
||||
return ta
|
||||
}
|
||||
|
||||
// MaxMode sets the maximum mode
|
||||
// Mode indicates the modality (major or minor) of a track.
|
||||
func (ta *TrackAttributes) MaxMode(mode int) *TrackAttributes {
|
||||
ta.intAttributes["max_mode"] = mode
|
||||
return ta
|
||||
}
|
||||
|
||||
// MinMode sets the minimum mode
|
||||
// Mode indicates the modality (major or minor) of a track.
|
||||
func (ta *TrackAttributes) MinMode(mode int) *TrackAttributes {
|
||||
ta.intAttributes["min_mode"] = mode
|
||||
return ta
|
||||
}
|
||||
|
||||
// TargetMode sets the target mode
|
||||
// Mode indicates the modality (major or minor) of a track.
|
||||
func (ta *TrackAttributes) TargetMode(mode int) *TrackAttributes {
|
||||
ta.intAttributes["target_mode"] = mode
|
||||
return ta
|
||||
}
|
||||
|
||||
// MaxPopularity sets the maximum popularity.
|
||||
// The value will be between 0 and 100, with 100 being the most popular.
|
||||
// The popularity is calculated by algorithm and is based, in the most part,
|
||||
// on the total number of plays the track has had and how recent those plays are.
|
||||
// Note: When applying track relinking via the market parameter, it is expected to find
|
||||
// relinked tracks with popularities that do not match min_*, max_* and target_* popularities.
|
||||
// These relinked tracks are accurate replacements for unplayable tracks
|
||||
// with the expected popularity scores. Original, non-relinked tracks are
|
||||
// available via the linked_from attribute of the relinked track response.
|
||||
func (ta *TrackAttributes) MaxPopularity(popularity int) *TrackAttributes {
|
||||
ta.intAttributes["max_popularity"] = popularity
|
||||
return ta
|
||||
}
|
||||
|
||||
// MinPopularity sets the minimum popularity.
|
||||
// The value will be between 0 and 100, with 100 being the most popular.
|
||||
// The popularity is calculated by algorithm and is based, in the most part,
|
||||
// on the total number of plays the track has had and how recent those plays are.
|
||||
// Note: When applying track relinking via the market parameter, it is expected to find
|
||||
// relinked tracks with popularities that do not match min_*, max_* and target_* popularities.
|
||||
// These relinked tracks are accurate replacements for unplayable tracks
|
||||
// with the expected popularity scores. Original, non-relinked tracks are
|
||||
// available via the linked_from attribute of the relinked track response.
|
||||
func (ta *TrackAttributes) MinPopularity(popularity int) *TrackAttributes {
|
||||
ta.intAttributes["min_popularity"] = popularity
|
||||
return ta
|
||||
}
|
||||
|
||||
// TargetPopularity sets the target popularity.
|
||||
// The value will be between 0 and 100, with 100 being the most popular.
|
||||
// The popularity is calculated by algorithm and is based, in the most part,
|
||||
// on the total number of plays the track has had and how recent those plays are.
|
||||
// Note: When applying track relinking via the market parameter, it is expected to find
|
||||
// relinked tracks with popularities that do not match min_*, max_* and target_* popularities.
|
||||
// These relinked tracks are accurate replacements for unplayable tracks
|
||||
// with the expected popularity scores. Original, non-relinked tracks are
|
||||
// available via the linked_from attribute of the relinked track response.
|
||||
func (ta *TrackAttributes) TargetPopularity(popularity int) *TrackAttributes {
|
||||
ta.intAttributes["target_popularity"] = popularity
|
||||
return ta
|
||||
}
|
||||
|
||||
// MaxSpeechiness sets the maximum speechiness.
|
||||
// Speechiness detects the presence of spoken words in a track.
|
||||
// The more exclusively speech-like the recording, the closer to 1.0
|
||||
// the speechiness will be.
|
||||
// Values above 0.66 describe tracks that are probably made entirely of
|
||||
// spoken words. Values between 0.33 and 0.66 describe tracks that may
|
||||
// contain both music and speech, including such cases as rap music.
|
||||
// Values below 0.33 most likely represent music and other non-speech-like tracks.
|
||||
func (ta *TrackAttributes) MaxSpeechiness(speechiness float64) *TrackAttributes {
|
||||
ta.floatAttributes["max_speechiness"] = speechiness
|
||||
return ta
|
||||
|
||||
}
|
||||
|
||||
// MinSpeechiness sets the minimum speechiness.
|
||||
// Speechiness detects the presence of spoken words in a track.
|
||||
// The more exclusively speech-like the recording, the closer to 1.0
|
||||
// the speechiness will be.
|
||||
// Values above 0.66 describe tracks that are probably made entirely of
|
||||
// spoken words. Values between 0.33 and 0.66 describe tracks that may
|
||||
// contain both music and speech, including such cases as rap music.
|
||||
// Values below 0.33 most likely represent music and other non-speech-like tracks.
|
||||
func (ta *TrackAttributes) MinSpeechiness(speechiness float64) *TrackAttributes {
|
||||
ta.floatAttributes["min_speechiness"] = speechiness
|
||||
return ta
|
||||
|
||||
}
|
||||
|
||||
// TargetSpeechiness sets the target speechiness.
|
||||
// Speechiness detects the presence of spoken words in a track.
|
||||
// The more exclusively speech-like the recording, the closer to 1.0
|
||||
// the speechiness will be.
|
||||
// Values above 0.66 describe tracks that are probably made entirely of
|
||||
// spoken words. Values between 0.33 and 0.66 describe tracks that may
|
||||
// contain both music and speech, including such cases as rap music.
|
||||
// Values below 0.33 most likely represent music and other non-speech-like tracks.
|
||||
func (ta *TrackAttributes) TargetSpeechiness(speechiness float64) *TrackAttributes {
|
||||
ta.floatAttributes["target_speechiness"] = speechiness
|
||||
return ta
|
||||
}
|
||||
|
||||
// MaxTempo sets the maximum tempo in beats per minute (BPM).
|
||||
func (ta *TrackAttributes) MaxTempo(tempo float64) *TrackAttributes {
|
||||
ta.floatAttributes["max_tempo"] = tempo
|
||||
return ta
|
||||
}
|
||||
|
||||
// MinTempo sets the minimum tempo in beats per minute (BPM).
|
||||
func (ta *TrackAttributes) MinTempo(tempo float64) *TrackAttributes {
|
||||
ta.floatAttributes["min_tempo"] = tempo
|
||||
return ta
|
||||
}
|
||||
|
||||
// TargetTempo sets the target tempo in beats per minute (BPM).
|
||||
func (ta *TrackAttributes) TargetTempo(tempo float64) *TrackAttributes {
|
||||
ta.floatAttributes["target_tempo"] = tempo
|
||||
return ta
|
||||
|
||||
}
|
||||
|
||||
// MaxTimeSignature sets the maximum time signature
|
||||
// The time signature (meter) is a notational convention to
|
||||
// specify how many beats are in each bar (or measure).
|
||||
func (ta *TrackAttributes) MaxTimeSignature(timeSignature int) *TrackAttributes {
|
||||
ta.intAttributes["max_time_signature"] = timeSignature
|
||||
return ta
|
||||
}
|
||||
|
||||
// MinTimeSignature sets the minimum time signature
|
||||
// The time signature (meter) is a notational convention to
|
||||
// specify how many beats are in each bar (or measure).
|
||||
func (ta *TrackAttributes) MinTimeSignature(timeSignature int) *TrackAttributes {
|
||||
ta.intAttributes["min_time_signature"] = timeSignature
|
||||
return ta
|
||||
}
|
||||
|
||||
// TargetTimeSignature sets the target time signature
|
||||
// The time signature (meter) is a notational convention to
|
||||
// specify how many beats are in each bar (or measure).
|
||||
func (ta *TrackAttributes) TargetTimeSignature(timeSignature int) *TrackAttributes {
|
||||
ta.intAttributes["target_time_signature"] = timeSignature
|
||||
return ta
|
||||
}
|
||||
|
||||
// MaxValence sets the maximum valence.
|
||||
// Valence is a measure from 0.0 to 1.0 describing the musical positiveness
|
||||
/// conveyed by a track.
|
||||
// Tracks with high valence sound more positive (e.g. happy, cheerful, euphoric),
|
||||
// while tracks with low valence sound more negative (e.g. sad, depressed, angry).
|
||||
func (ta *TrackAttributes) MaxValence(valence float64) *TrackAttributes {
|
||||
ta.floatAttributes["max_valence"] = valence
|
||||
return ta
|
||||
}
|
||||
|
||||
// MinValence sets the minimum valence.
|
||||
// Valence is a measure from 0.0 to 1.0 describing the musical positiveness
|
||||
/// conveyed by a track.
|
||||
// Tracks with high valence sound more positive (e.g. happy, cheerful, euphoric),
|
||||
// while tracks with low valence sound more negative (e.g. sad, depressed, angry).
|
||||
func (ta *TrackAttributes) MinValence(valence float64) *TrackAttributes {
|
||||
ta.floatAttributes["min_valence"] = valence
|
||||
return ta
|
||||
}
|
||||
|
||||
// TargetValence sets the target valence.
|
||||
// Valence is a measure from 0.0 to 1.0 describing the musical positiveness
|
||||
/// conveyed by a track.
|
||||
// Tracks with high valence sound more positive (e.g. happy, cheerful, euphoric),
|
||||
// while tracks with low valence sound more negative (e.g. sad, depressed, angry).
|
||||
func (ta *TrackAttributes) TargetValence(valence float64) *TrackAttributes {
|
||||
ta.floatAttributes["target_valence"] = valence
|
||||
return ta
|
||||
}
|
||||
402
vendor/github.com/zmb3/spotify/user.go
generated
vendored
Normal file
402
vendor/github.com/zmb3/spotify/user.go
generated
vendored
Normal file
@@ -0,0 +1,402 @@
|
||||
package spotify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// User contains the basic, publicly available information about a Spotify user.
|
||||
type User struct {
|
||||
// The name displayed on the user's profile.
|
||||
// Note: Spotify currently fails to populate
|
||||
// this field when querying for a playlist.
|
||||
DisplayName string `json:"display_name"`
|
||||
// Known public external URLs for the user.
|
||||
ExternalURLs map[string]string `json:"external_urls"`
|
||||
// Information about followers of the user.
|
||||
Followers Followers `json:"followers"`
|
||||
// A link to the Web API endpoint for this user.
|
||||
Endpoint string `json:"href"`
|
||||
// The Spotify user ID for the user.
|
||||
ID string `json:"id"`
|
||||
// The user's profile image.
|
||||
Images []Image `json:"images"`
|
||||
// The Spotify URI for the user.
|
||||
URI URI `json:"uri"`
|
||||
}
|
||||
|
||||
// PrivateUser contains additional information about a user.
|
||||
// This data is private and requires user authentication.
|
||||
type PrivateUser struct {
|
||||
User
|
||||
// The country of the user, as set in the user's account profile.
|
||||
// An ISO 3166-1 alpha-2 country code. This field is only available when the
|
||||
// current user has granted acess to the ScopeUserReadPrivate scope.
|
||||
Country string `json:"country"`
|
||||
// The user's email address, as entered by the user when creating their account.
|
||||
// Note: this email is UNVERIFIED - there is no proof that it actually
|
||||
// belongs to the user. This field is only available when the current user
|
||||
// has granted access to the ScopeUserReadEmail scope.
|
||||
Email string `json:"email"`
|
||||
// The user's Spotify subscription level: "premium", "free", etc.
|
||||
// The subscription level "open" can be considered the same as "free".
|
||||
// This field is only available when the current user has granted access to
|
||||
// the ScopeUserReadPrivate scope.
|
||||
Product string `json:"product"`
|
||||
// The user's date of birth, in the format 'YYYY-MM-DD'. You can use
|
||||
// the DateLayout constant to convert this to a time.Time value.
|
||||
// This field is only available when the current user has granted
|
||||
// access to the ScopeUserReadBirthdate scope.
|
||||
Birthdate string `json:"birthdate"`
|
||||
}
|
||||
|
||||
// GetUsersPublicProfile gets public profile information about a
|
||||
// Spotify User. It does not require authentication.
|
||||
func (c *Client) GetUsersPublicProfile(userID ID) (*User, error) {
|
||||
spotifyURL := c.baseURL + "users/" + string(userID)
|
||||
|
||||
var user User
|
||||
|
||||
err := c.get(spotifyURL, &user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// CurrentUser gets detailed profile information about the
|
||||
// current user.
|
||||
//
|
||||
// Reading the user's email address requires that the application
|
||||
// has the ScopeUserReadEmail scope. Reading the country, display
|
||||
// name, profile images, and product subscription level requires
|
||||
// that the application has the ScopeUserReadPrivate scope.
|
||||
//
|
||||
// Warning: The email address in the response will be the address
|
||||
// that was entered when the user created their spotify account.
|
||||
// This email address is unverified - do not assume that Spotify has
|
||||
// checked that the email address actually belongs to the user.
|
||||
func (c *Client) CurrentUser() (*PrivateUser, error) {
|
||||
var result PrivateUser
|
||||
|
||||
err := c.get(c.baseURL+"me", &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// CurrentUsersTracks gets a list of songs saved in the current
|
||||
// Spotify user's "Your Music" library.
|
||||
func (c *Client) CurrentUsersTracks() (*SavedTrackPage, error) {
|
||||
return c.CurrentUsersTracksOpt(nil)
|
||||
}
|
||||
|
||||
// CurrentUsersTracksOpt is like CurrentUsersTracks, but it accepts additional
|
||||
// options for sorting and filtering the results.
|
||||
func (c *Client) CurrentUsersTracksOpt(opt *Options) (*SavedTrackPage, error) {
|
||||
spotifyURL := c.baseURL + "me/tracks"
|
||||
if opt != nil {
|
||||
v := url.Values{}
|
||||
if opt.Country != nil {
|
||||
v.Set("country", *opt.Country)
|
||||
}
|
||||
if opt.Limit != nil {
|
||||
v.Set("limit", strconv.Itoa(*opt.Limit))
|
||||
}
|
||||
if opt.Offset != nil {
|
||||
v.Set("offset", strconv.Itoa(*opt.Offset))
|
||||
}
|
||||
if params := v.Encode(); params != "" {
|
||||
spotifyURL += "?" + params
|
||||
}
|
||||
}
|
||||
|
||||
var result SavedTrackPage
|
||||
|
||||
err := c.get(spotifyURL, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// FollowUser adds the current user as a follower of one or more
|
||||
// spotify users, identified by their Spotify IDs.
|
||||
//
|
||||
// Modifying the lists of artists or users the current user follows
|
||||
// requires that the application has the ScopeUserFollowModify scope.
|
||||
func (c *Client) FollowUser(ids ...ID) error {
|
||||
return c.modifyFollowers("user", true, ids...)
|
||||
}
|
||||
|
||||
// FollowArtist adds the current user as a follower of one or more
|
||||
// spotify artists, identified by their Spotify IDs.
|
||||
//
|
||||
// Modifying the lists of artists or users the current user follows
|
||||
// requires that the application has the ScopeUserFollowModify scope.
|
||||
func (c *Client) FollowArtist(ids ...ID) error {
|
||||
return c.modifyFollowers("artist", true, ids...)
|
||||
}
|
||||
|
||||
// UnfollowUser removes the current user as a follower of one or more
|
||||
// Spotify users.
|
||||
//
|
||||
// Modifying the lists of artists or users the current user follows
|
||||
// requires that the application has the ScopeUserFollowModify scope.
|
||||
func (c *Client) UnfollowUser(ids ...ID) error {
|
||||
return c.modifyFollowers("user", false, ids...)
|
||||
}
|
||||
|
||||
// UnfollowArtist removes the current user as a follower of one or more
|
||||
// Spotify artists.
|
||||
//
|
||||
// Modifying the lists of artists or users the current user follows
|
||||
// requires that the application has the ScopeUserFollowModify scope.
|
||||
func (c *Client) UnfollowArtist(ids ...ID) error {
|
||||
return c.modifyFollowers("artist", false, ids...)
|
||||
}
|
||||
|
||||
// CurrentUserFollows checks to see if the current user is following
|
||||
// one or more artists or other Spotify Users. This call requires
|
||||
// ScopeUserFollowRead.
|
||||
//
|
||||
// The t argument indicates the type of the IDs, and must be either
|
||||
// "user" or "artist".
|
||||
//
|
||||
// The result is returned as a slice of bool values in the same order
|
||||
// in which the IDs were specified.
|
||||
func (c *Client) CurrentUserFollows(t string, ids ...ID) ([]bool, error) {
|
||||
if l := len(ids); l == 0 || l > 50 {
|
||||
return nil, errors.New("spotify: UserFollows supports 1 to 50 IDs")
|
||||
}
|
||||
if t != "artist" && t != "user" {
|
||||
return nil, errors.New("spotify: t must be 'artist' or 'user'")
|
||||
}
|
||||
spotifyURL := fmt.Sprintf("%sme/following/contains?type=%s&ids=%s",
|
||||
c.baseURL, t, strings.Join(toStringSlice(ids), ","))
|
||||
|
||||
var result []bool
|
||||
|
||||
err := c.get(spotifyURL, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) modifyFollowers(usertype string, follow bool, ids ...ID) error {
|
||||
if l := len(ids); l == 0 || l > 50 {
|
||||
return errors.New("spotify: Follow/Unfollow supports 1 to 50 IDs")
|
||||
}
|
||||
v := url.Values{}
|
||||
v.Add("type", usertype)
|
||||
v.Add("ids", strings.Join(toStringSlice(ids), ","))
|
||||
spotifyURL := c.baseURL + "me/following?" + v.Encode()
|
||||
method := "PUT"
|
||||
if !follow {
|
||||
method = "DELETE"
|
||||
}
|
||||
req, err := http.NewRequest(method, spotifyURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.execute(req, nil, http.StatusNoContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CurrentUsersFollowedArtists gets the current user's followed artists.
|
||||
// This call requires that the user has granted the ScopeUserFollowRead scope.
|
||||
func (c *Client) CurrentUsersFollowedArtists() (*FullArtistCursorPage, error) {
|
||||
return c.CurrentUsersFollowedArtistsOpt(-1, "")
|
||||
}
|
||||
|
||||
// CurrentUsersFollowedArtistsOpt is like CurrentUsersFollowedArtists,
|
||||
// but it accept the optional arguments limit and after. Limit is the
|
||||
// maximum number of items to return (1 <= limit <= 50), and after is
|
||||
// the last artist ID retrieved from the previous request. If you don't
|
||||
// wish to specify either of the parameters, use -1 for limit and the empty
|
||||
// string for after.
|
||||
func (c *Client) CurrentUsersFollowedArtistsOpt(limit int, after string) (*FullArtistCursorPage, error) {
|
||||
spotifyURL := c.baseURL + "me/following"
|
||||
v := url.Values{}
|
||||
v.Set("type", "artist")
|
||||
if limit != -1 {
|
||||
v.Set("limit", strconv.Itoa(limit))
|
||||
}
|
||||
if after != "" {
|
||||
v.Set("after", after)
|
||||
}
|
||||
if params := v.Encode(); params != "" {
|
||||
spotifyURL += "?" + params
|
||||
}
|
||||
|
||||
var result struct {
|
||||
A FullArtistCursorPage `json:"artists"`
|
||||
}
|
||||
|
||||
err := c.get(spotifyURL, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result.A, nil
|
||||
}
|
||||
|
||||
// CurrentUsersAlbums gets a list of albums saved in the current
|
||||
// Spotify user's "Your Music" library.
|
||||
func (c *Client) CurrentUsersAlbums() (*SavedAlbumPage, error) {
|
||||
return c.CurrentUsersAlbumsOpt(nil)
|
||||
}
|
||||
|
||||
// CurrentUsersAlbumsOpt is like CurrentUsersAlbums, but it accepts additional
|
||||
// options for sorting and filtering the results.
|
||||
func (c *Client) CurrentUsersAlbumsOpt(opt *Options) (*SavedAlbumPage, error) {
|
||||
spotifyURL := c.baseURL + "me/albums"
|
||||
if opt != nil {
|
||||
v := url.Values{}
|
||||
if opt.Country != nil {
|
||||
v.Set("market", *opt.Country)
|
||||
}
|
||||
if opt.Limit != nil {
|
||||
v.Set("limit", strconv.Itoa(*opt.Limit))
|
||||
}
|
||||
if opt.Offset != nil {
|
||||
v.Set("offset", strconv.Itoa(*opt.Offset))
|
||||
}
|
||||
if params := v.Encode(); params != "" {
|
||||
spotifyURL += "?" + params
|
||||
}
|
||||
}
|
||||
|
||||
var result SavedAlbumPage
|
||||
|
||||
err := c.get(spotifyURL, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// CurrentUsersPlaylists gets a list of the playlists owned or followed by
|
||||
// the current spotify user.
|
||||
//
|
||||
// Private playlists require the ScopePlaylistReadPrivate scope. Note that
|
||||
// this scope alone will not return collaborative playlists, even though
|
||||
// they are always private. In order to retrieve collaborative playlists
|
||||
// the user must authorize the ScopePlaylistReadCollaborative scope.
|
||||
func (c *Client) CurrentUsersPlaylists() (*SimplePlaylistPage, error) {
|
||||
return c.CurrentUsersPlaylistsOpt(nil)
|
||||
}
|
||||
|
||||
// CurrentUsersPlaylistsOpt is like CurrentUsersPlaylists, but it accepts
|
||||
// additional options for sorting and filtering the results.
|
||||
func (c *Client) CurrentUsersPlaylistsOpt(opt *Options) (*SimplePlaylistPage, error) {
|
||||
spotifyURL := c.baseURL + "me/playlists"
|
||||
if opt != nil {
|
||||
v := url.Values{}
|
||||
if opt.Limit != nil {
|
||||
v.Set("limit", strconv.Itoa(*opt.Limit))
|
||||
}
|
||||
if opt.Offset != nil {
|
||||
v.Set("offset", strconv.Itoa(*opt.Offset))
|
||||
}
|
||||
if params := v.Encode(); params != "" {
|
||||
spotifyURL += "?" + params
|
||||
}
|
||||
}
|
||||
|
||||
var result SimplePlaylistPage
|
||||
|
||||
err := c.get(spotifyURL, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// CurrentUsersTopArtistsOpt gets a list of the top played artists in a given time
|
||||
// range of the current Spotify user. It supports up to 50 artists in a single
|
||||
// call. This call requires ScopeUserTopRead.
|
||||
func (c *Client) CurrentUsersTopArtistsOpt(opt *Options) (*FullArtistPage, error) {
|
||||
spotifyURL := c.baseURL + "me/top/artists"
|
||||
if opt != nil {
|
||||
v := url.Values{}
|
||||
if opt.Limit != nil {
|
||||
v.Set("limit", strconv.Itoa(*opt.Limit))
|
||||
}
|
||||
if opt.Timerange != nil {
|
||||
v.Set("time_range", *opt.Timerange+"_term")
|
||||
}
|
||||
if params := v.Encode(); params != "" {
|
||||
spotifyURL += "?" + params
|
||||
}
|
||||
}
|
||||
|
||||
var result FullArtistPage
|
||||
|
||||
err := c.get(spotifyURL, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// CurrentUsersTopArtists is like CurrentUsersTopArtistsOpt but with
|
||||
// sensible defaults. The default limit is 20 and the default timerange
|
||||
// is medium_term.
|
||||
func (c *Client) CurrentUsersTopArtists() (*FullArtistPage, error) {
|
||||
return c.CurrentUsersTopArtistsOpt(nil)
|
||||
}
|
||||
|
||||
// CurrentUsersTopTracksOpt gets a list of the top played tracks in a given time
|
||||
// range of the current Spotify user. It supports up to 50 tracks in a single
|
||||
// call. This call requires ScopeUserTopRead.
|
||||
func (c *Client) CurrentUsersTopTracksOpt(opt *Options) (*FullTrackPage, error) {
|
||||
spotifyURL := c.baseURL + "me/top/tracks"
|
||||
if opt != nil {
|
||||
v := url.Values{}
|
||||
if opt.Limit != nil {
|
||||
v.Set("limit", strconv.Itoa(*opt.Limit))
|
||||
}
|
||||
if opt.Timerange != nil {
|
||||
v.Set("time_range", *opt.Timerange+"_term")
|
||||
}
|
||||
if opt.Offset != nil {
|
||||
v.Set("offset", strconv.Itoa(*opt.Offset))
|
||||
}
|
||||
if params := v.Encode(); params != "" {
|
||||
spotifyURL += "?" + params
|
||||
}
|
||||
}
|
||||
|
||||
var result FullTrackPage
|
||||
|
||||
err := c.get(spotifyURL, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// CurrentUsersTopTracks is like CurrentUsersTopTracksOpt but with
|
||||
// sensible defaults. The default limit is 20 and the default timerange
|
||||
// is medium_term.
|
||||
func (c *Client) CurrentUsersTopTracks() (*FullTrackPage, error) {
|
||||
return c.CurrentUsersTopTracksOpt(nil)
|
||||
}
|
||||
Reference in New Issue
Block a user