1
0
mirror of https://github.com/taigrr/wtf synced 2025-01-18 04:03:14 -08:00

Update dependencies

This commit is contained in:
Chris Cummer
2018-10-21 12:48:22 -07:00
parent 8f3ae94b4e
commit 3a0bcd21e7
132 changed files with 18033 additions and 9138 deletions

5
vendor/github.com/zmb3/spotify/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,5 @@
language: go
go:
- 1.8
- 1.9

201
vendor/github.com/zmb3/spotify/LICENSE generated vendored Normal file
View 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
View File

@@ -0,0 +1,106 @@
Spotify
=======
[![GoDoc](https://godoc.org/github.com/zmb3/spotify?status.svg)](http://godoc.org/github.com/zmb3/spotify)
[![Build status](https://ci.appveyor.com/api/projects/status/1nr9vv0jqq438nj2?svg=true)](https://ci.appveyor.com/project/zmb3/spotify)
[![Build Status](https://travis-ci.org/zmb3/spotify.svg)](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
View 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 artists
// 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 cant 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 users 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
View 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
View 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
View 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
View 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
View 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
View 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 attributes 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 attributes 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
View 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)
}