1
0
mirror of https://github.com/taigrr/wtf synced 2025-01-18 04:03:14 -08:00
2019-07-15 09:06:49 -07:00

529 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}