spotify_web 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rspec +2 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +309 -0
- data/Rakefile +13 -0
- data/examples/playlists.rb +14 -0
- data/lib/spotify_web.rb +105 -0
- data/lib/spotify_web/album.rb +58 -0
- data/lib/spotify_web/artist.rb +54 -0
- data/lib/spotify_web/assertions.rb +36 -0
- data/lib/spotify_web/authorized_user.rb +119 -0
- data/lib/spotify_web/client.rb +369 -0
- data/lib/spotify_web/connection.rb +162 -0
- data/lib/spotify_web/error.rb +13 -0
- data/lib/spotify_web/event.rb +95 -0
- data/lib/spotify_web/handler.rb +74 -0
- data/lib/spotify_web/loggable.rb +13 -0
- data/lib/spotify_web/playlist.rb +48 -0
- data/lib/spotify_web/resource.rb +309 -0
- data/lib/spotify_web/resource_collection.rb +99 -0
- data/lib/spotify_web/restriction.rb +32 -0
- data/lib/spotify_web/schema.rb +120 -0
- data/lib/spotify_web/schema/core.pb.rb +31 -0
- data/lib/spotify_web/schema/mercury.pb.rb +66 -0
- data/lib/spotify_web/schema/metadata.pb.rb +257 -0
- data/lib/spotify_web/schema/playlist4.pb.rb +461 -0
- data/lib/spotify_web/schema/radio.pb.rb +110 -0
- data/lib/spotify_web/schema/socialgraph.pb.rb +106 -0
- data/lib/spotify_web/song.rb +58 -0
- data/lib/spotify_web/user.rb +7 -0
- data/lib/spotify_web/version.rb +9 -0
- data/lib/tasks/spotify_web.rake +1 -0
- data/lib/tasks/spotify_web.rb +9 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/spotify_web_spec.rb +4 -0
- data/spotify_web.gemspec +27 -0
- metadata +216 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'spotify_web/resource'
|
2
|
+
require 'spotify_web/artist'
|
3
|
+
require 'spotify_web/restriction'
|
4
|
+
require 'spotify_web/schema/metadata.pb'
|
5
|
+
|
6
|
+
module SpotifyWeb
|
7
|
+
# Represents an album on Spotify
|
8
|
+
class Album < Resource
|
9
|
+
self.metadata_schema = Schema::Metadata::Album
|
10
|
+
|
11
|
+
# The title of the album
|
12
|
+
# @return [String]
|
13
|
+
attribute :title, :name
|
14
|
+
|
15
|
+
# Info about the artist
|
16
|
+
# @return [String]
|
17
|
+
attribute :artist do |artist|
|
18
|
+
Artist.new(client, artist[0].to_hash)
|
19
|
+
end
|
20
|
+
|
21
|
+
# The label that released the album
|
22
|
+
# @return [String]
|
23
|
+
attribute :label
|
24
|
+
|
25
|
+
# The date the album was published on
|
26
|
+
# @return [Date]
|
27
|
+
attribute :published_on, :date do |date|
|
28
|
+
Date.new(date.year, date.month || 1, date.day || 1)
|
29
|
+
end
|
30
|
+
|
31
|
+
# The relative popularity of this artist on Spotify
|
32
|
+
# @return [Fixnum]
|
33
|
+
attribute :popularity
|
34
|
+
|
35
|
+
# The songs recorded on this album
|
36
|
+
# @return [Array<SpotifyWeb::Song>]
|
37
|
+
attribute :songs, :disc do |discs|
|
38
|
+
songs = []
|
39
|
+
discs.each do |disc|
|
40
|
+
disc_songs = disc.track.map {|track| Song.new(client, track.to_hash)}
|
41
|
+
songs.concat(disc_songs)
|
42
|
+
end
|
43
|
+
ResourceCollection.new(client, songs)
|
44
|
+
end
|
45
|
+
|
46
|
+
# The countries for which this album is permitted to be played
|
47
|
+
# @return [Array<SpotifyWeb::Restriction>]
|
48
|
+
attribute :restrictions, :restriction do |restrictions|
|
49
|
+
restrictions.map {|restriction| Restriction.new(client, restriction.to_hash)}
|
50
|
+
end
|
51
|
+
|
52
|
+
# Whether this album is available to the user
|
53
|
+
# @return [Boolean]
|
54
|
+
def available?
|
55
|
+
restrictions.all? {|restriction| restriction.permitted?}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'spotify_web/resource'
|
2
|
+
require 'spotify_web/schema/metadata.pb'
|
3
|
+
|
4
|
+
module SpotifyWeb
|
5
|
+
# Represents an artist on Spotify
|
6
|
+
class Artist < Resource
|
7
|
+
self.metadata_schema = Schema::Metadata::Artist
|
8
|
+
|
9
|
+
# The title of the artist
|
10
|
+
# @return [String]
|
11
|
+
attribute :name
|
12
|
+
|
13
|
+
# The relative popularity of this artist on Spotify
|
14
|
+
# @return [Fixnum]
|
15
|
+
attribute :popularity
|
16
|
+
|
17
|
+
# The top songs for this artist
|
18
|
+
# @return [Array<SpotifyWeb::Song>]
|
19
|
+
attribute :top_songs, :top_track do |groups|
|
20
|
+
group = groups.detect {|group| group.country == 'US'}
|
21
|
+
songs = group.track.map {|song| Song.new(client, song.to_hash)}
|
22
|
+
ResourceCollection.new(client, songs)
|
23
|
+
end
|
24
|
+
|
25
|
+
# The albums this artist has recorded
|
26
|
+
# @return [Array<SpotifyWeb::Album>]
|
27
|
+
attribute :albums, :album_group do |groups|
|
28
|
+
# Track all available albums
|
29
|
+
albums = []
|
30
|
+
groups.each do |group|
|
31
|
+
group_albums = []
|
32
|
+
group.album.each do |album|
|
33
|
+
album = Album.new(client, album.to_hash)
|
34
|
+
group_albums << album if album.available?
|
35
|
+
end
|
36
|
+
|
37
|
+
albums.concat(group_albums)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Load the data to completely determine what albums to give back
|
41
|
+
albums = ResourceCollection.new(client, albums)
|
42
|
+
albums.load
|
43
|
+
|
44
|
+
# Reject duplicate titles
|
45
|
+
albums_by_title = albums.inject({}) do |result, album|
|
46
|
+
result[album.title] = album
|
47
|
+
result
|
48
|
+
end
|
49
|
+
albums.reject! {|album| albums_by_title[album.title] != album}
|
50
|
+
|
51
|
+
albums
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module SpotifyWeb
|
2
|
+
# Provides a set of helper methods for making assertions about the content
|
3
|
+
# of various objects
|
4
|
+
# @api private
|
5
|
+
module Assertions
|
6
|
+
# Validates that the given hash *only* includes the specified valid keys.
|
7
|
+
#
|
8
|
+
# @return [nil]
|
9
|
+
# @raise [ArgumentError] if any invalid keys are found
|
10
|
+
# @example
|
11
|
+
# options = {:name => 'John Smith', :age => 30}
|
12
|
+
#
|
13
|
+
# assert_valid_keys(options, :name) # => ArgumentError: Invalid key(s): age
|
14
|
+
# assert_valid_keys(options, 'name', 'age') # => ArgumentError: Invalid key(s): age, name
|
15
|
+
# assert_valid_keys(options, :name, :age) # => nil
|
16
|
+
def assert_valid_keys(hash, *valid_keys)
|
17
|
+
invalid_keys = hash.keys - valid_keys
|
18
|
+
raise ArgumentError, "Invalid key(s): #{invalid_keys.join(', ')}" unless invalid_keys.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
# Validates that the given value *only* matches one of the specified valid
|
22
|
+
# values.
|
23
|
+
#
|
24
|
+
# @return [nil]
|
25
|
+
# @raise [ArgumentError] if the value is not found
|
26
|
+
# @example
|
27
|
+
# value = :age
|
28
|
+
#
|
29
|
+
# assert_valid_values(value, :name) # => ArgumentError: :age is an invalid value; must be one of: :name
|
30
|
+
# assert_valid_values(value, 'name', 'age') # => ArgumentError: :age is an invalid value; must be one of: :name, "age"
|
31
|
+
# assert_valid_values(value, :name, :age) # => nil
|
32
|
+
def assert_valid_values(value, *valid_values)
|
33
|
+
raise ArgumentError, "#{value} is an invalid value; must be one of: #{valid_values.join(', ')}" unless valid_values.include?(value)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'spotify_web/playlist'
|
2
|
+
require 'spotify_web/resource_collection'
|
3
|
+
require 'spotify_web/user'
|
4
|
+
require 'spotify_web/schema/playlist4.pb'
|
5
|
+
|
6
|
+
module SpotifyWeb
|
7
|
+
# Represents a user who has authorized with the Spotify service
|
8
|
+
class AuthorizedUser < User
|
9
|
+
# The username the user registered with on Spotify.
|
10
|
+
# @return [String]
|
11
|
+
attribute :username
|
12
|
+
|
13
|
+
# The password associated with the username registered with on Spotify.
|
14
|
+
# @return [String]
|
15
|
+
attribute :password
|
16
|
+
|
17
|
+
# Gets the authentication settings associated with this user for use with API
|
18
|
+
# services. This will log the user in via username / password if it's not already
|
19
|
+
# set.
|
20
|
+
#
|
21
|
+
# @return [String]
|
22
|
+
# @raise [SpotifyWeb::Error] if the command fails
|
23
|
+
def settings
|
24
|
+
login unless @settings
|
25
|
+
@settings
|
26
|
+
end
|
27
|
+
|
28
|
+
# Logs the user in using the associated e-mail address / password. This will
|
29
|
+
# generate a user id / auth token for authentication with the API services.
|
30
|
+
#
|
31
|
+
# @api private
|
32
|
+
# @return [true]
|
33
|
+
# @raise [SpotifyWeb::Error] if the command fails
|
34
|
+
def login
|
35
|
+
# Look up the init options
|
36
|
+
request = EventMachine::HttpRequest.new('https://play.spotify.com/')
|
37
|
+
response = request.get(:head => {'User-Agent' => USER_AGENT})
|
38
|
+
|
39
|
+
if response.response_header.successful?
|
40
|
+
json = response.response.match(/Spotify\.Web\.Login\(document, (\{.+\}),[^\}]+\);/)[1]
|
41
|
+
options = JSON.parse(json)
|
42
|
+
|
43
|
+
# Authenticate the user
|
44
|
+
request = EventMachine::HttpRequest.new('https://play.spotify.com/xhr/json/auth.php')
|
45
|
+
response = request.post(
|
46
|
+
:body => {
|
47
|
+
:username => username,
|
48
|
+
:password => password,
|
49
|
+
:type => 'sp',
|
50
|
+
:secret => options['csrftoken'],
|
51
|
+
:trackingId => options['trackingId'],
|
52
|
+
:landingURL => options['landingURL'],
|
53
|
+
:referrer => options['referrer'],
|
54
|
+
:cf => nil
|
55
|
+
},
|
56
|
+
:head => {'User-Agent' => USER_AGENT}
|
57
|
+
)
|
58
|
+
|
59
|
+
if response.response_header.successful?
|
60
|
+
data = JSON.parse(response.response)
|
61
|
+
|
62
|
+
if data['status'] == 'OK'
|
63
|
+
@settings = data['config']
|
64
|
+
else
|
65
|
+
error = "Unable to authenticate (#{data['message']})"
|
66
|
+
end
|
67
|
+
else
|
68
|
+
error = "Unable to authenticate (#{response.response_header.status})"
|
69
|
+
end
|
70
|
+
else
|
71
|
+
error = "Landing page unavailable (#{response.response_header.status})"
|
72
|
+
end
|
73
|
+
|
74
|
+
raise(ConnectionError, error) if error
|
75
|
+
|
76
|
+
true
|
77
|
+
end
|
78
|
+
|
79
|
+
# Gets the playlists managed by the user.
|
80
|
+
#
|
81
|
+
# @param [Hash] options The search options
|
82
|
+
# @option options [Fixnum] :limit (100) The total number of playlists to get
|
83
|
+
# @option options [Fixnum] :skip (0) The number of playlists to skip when loading the list
|
84
|
+
# @option options [Boolean] :include_starred (false) Whether to include the playlist for songs the user starred
|
85
|
+
# @return [Array<SpotifyWeb::Playlist>]
|
86
|
+
# @example
|
87
|
+
# user.playlists # => [#<SpotifyWeb::Playlist ...>, ...]
|
88
|
+
def playlists(options = {})
|
89
|
+
options = {:limit => 100, :skip => 0, :include_starred => false}.merge(options)
|
90
|
+
|
91
|
+
response = api('request',
|
92
|
+
:uri => "hm://playlist/user/#{username}/rootlist?from=#{options[:skip]}&length=#{options[:limit]}",
|
93
|
+
:response_schema => Schema::Playlist4::SelectedListContent
|
94
|
+
)
|
95
|
+
|
96
|
+
playlists = response['result'].contents.items.map do |item|
|
97
|
+
playlist(:uri => item.uri)
|
98
|
+
end
|
99
|
+
playlists << playlist(:starred) if options[:include_starred]
|
100
|
+
|
101
|
+
ResourceCollection.new(client, playlists)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Builds a playlist with the given attributes.
|
105
|
+
#
|
106
|
+
# @param [Hash] attributes The attributes identifying the playlist
|
107
|
+
# @return [SpotifyWeb::Playlist]
|
108
|
+
# @example
|
109
|
+
# user.playlist(:starred) # => #<SpotifyWeb::Playlist ...>
|
110
|
+
# user.playlist(:uri => "spotify:user:benzelano:playlist:starred") # => #<SpotifyWeb::Playlist ...>
|
111
|
+
def playlist(attributes = {})
|
112
|
+
if attributes == :starred
|
113
|
+
attributes = {:uri_id => 'starred'}
|
114
|
+
end
|
115
|
+
|
116
|
+
Playlist.new(client, attributes.merge(:user => self))
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,369 @@
|
|
1
|
+
require 'fiber'
|
2
|
+
require 'execjs'
|
3
|
+
|
4
|
+
require 'spotify_web/album'
|
5
|
+
require 'spotify_web/artist'
|
6
|
+
require 'spotify_web/authorized_user'
|
7
|
+
require 'spotify_web/connection'
|
8
|
+
require 'spotify_web/error'
|
9
|
+
require 'spotify_web/event'
|
10
|
+
require 'spotify_web/handler'
|
11
|
+
require 'spotify_web/loggable'
|
12
|
+
require 'spotify_web/song'
|
13
|
+
require 'spotify_web/schema/mercury.pb'
|
14
|
+
|
15
|
+
module SpotifyWeb
|
16
|
+
# Provides access to the Spotify Web API
|
17
|
+
class Client
|
18
|
+
include Assertions
|
19
|
+
include Loggable
|
20
|
+
|
21
|
+
# The interval with which to send keepalives
|
22
|
+
KEEPALIVE_INTERVAL = 180
|
23
|
+
|
24
|
+
# The current authorized user
|
25
|
+
# @return [SpotifyWeb::User]
|
26
|
+
attr_reader :user
|
27
|
+
|
28
|
+
# The response timeout configured for the connection
|
29
|
+
# @return [Fixnum]
|
30
|
+
attr_reader :timeout
|
31
|
+
|
32
|
+
# Creates a new client for communicating with Spotify with the given
|
33
|
+
# username / password.
|
34
|
+
#
|
35
|
+
# @param [String] username The username to authenticate with
|
36
|
+
# @param [String] password The Spotify password associated with the username
|
37
|
+
# @param [Hash] options The configuration options for the client
|
38
|
+
# @option options [Fixnum] :timeout (10) The amount of seconds to allow to elapse for requests before timing out
|
39
|
+
# @option options [Boolean] :reconnect (false) Whether to allow the client to automatically reconnect when disconnected either by Spotify or by the network
|
40
|
+
# @option options [Fixnum] :reconnect_wait (5) The amount of seconds to wait before reconnecting
|
41
|
+
# @raise [SpotifyWeb::Error] if an invalid option is specified
|
42
|
+
# @yield Runs the given block within the context if the client (for DSL-type usage)
|
43
|
+
def initialize(username, password, options = {}, &block)
|
44
|
+
options = {
|
45
|
+
:timeout => 10,
|
46
|
+
:reconnect => false,
|
47
|
+
:reconnect_wait => 5
|
48
|
+
}.merge(options)
|
49
|
+
assert_valid_keys(options, :timeout, :reconnect, :reconnect_wait)
|
50
|
+
|
51
|
+
@user = AuthorizedUser.new(self, :username => username, :password => password)
|
52
|
+
@event_handlers = {}
|
53
|
+
@timeout = options[:timeout]
|
54
|
+
@reconnect = options[:reconnect]
|
55
|
+
@reconnect_wait = options[:reconnect_wait]
|
56
|
+
|
57
|
+
# Setup default event handlers
|
58
|
+
on(:work_requested) {|work| on_work_requested(work) }
|
59
|
+
on(:session_ended) { on_session_ended }
|
60
|
+
|
61
|
+
reconnect_from(ConnectionError, APIError) do
|
62
|
+
connect
|
63
|
+
end
|
64
|
+
|
65
|
+
instance_eval(&block) if block_given?
|
66
|
+
end
|
67
|
+
|
68
|
+
# Initiates a connection with the given url. Once a connection is started,
|
69
|
+
# this will also attempt to authenticate the user.
|
70
|
+
#
|
71
|
+
# @api private
|
72
|
+
# @note This will only open a new connection if the client isn't already connected to the given url
|
73
|
+
# @param [String] url The url to open a connection to
|
74
|
+
# @return [true]
|
75
|
+
# @raise [SpotifyWeb::Error] if the connection cannot be opened
|
76
|
+
def connect
|
77
|
+
if !@connection || !@connection.connected?
|
78
|
+
# Close any existing connection
|
79
|
+
close
|
80
|
+
|
81
|
+
# Create a new connection to the given url
|
82
|
+
@connection = Connection.new(access_point_url, :timeout => timeout)
|
83
|
+
@connection.handler = lambda {|data| trigger(data.delete('command'), data)}
|
84
|
+
@connection.start
|
85
|
+
|
86
|
+
# Wait for connection to open
|
87
|
+
wait do |&resume|
|
88
|
+
on(:session_started, :once => true) { resume.call }
|
89
|
+
end
|
90
|
+
|
91
|
+
# Send the user's credentials
|
92
|
+
creds = user.settings['credentials'][0].split(':')
|
93
|
+
message = [creds[0], creds[1], creds[2..-1] * ':']
|
94
|
+
api('connect', message)
|
95
|
+
|
96
|
+
wait do |&resume|
|
97
|
+
on(:session_authenticated, :once => true) { resume.call }
|
98
|
+
end
|
99
|
+
|
100
|
+
start_keepalives
|
101
|
+
end
|
102
|
+
|
103
|
+
true
|
104
|
+
end
|
105
|
+
|
106
|
+
# Closes the current connection to Spotify if one was previously opened.
|
107
|
+
#
|
108
|
+
# @return [true]
|
109
|
+
def close(allow_reconnect = false)
|
110
|
+
if @connection
|
111
|
+
# Disable reconnects if specified
|
112
|
+
reconnect = @reconnect
|
113
|
+
@reconnect = reconnect && allow_reconnect
|
114
|
+
|
115
|
+
# Clean up timers / connections
|
116
|
+
@keepalive_timer.cancel if @keepalive_timer
|
117
|
+
@keepalive_timer = nil
|
118
|
+
@connection.close
|
119
|
+
|
120
|
+
# Revert change to reconnect config once the final signal is received
|
121
|
+
wait do |&resume|
|
122
|
+
on(:session_ended, :once => true) { resume.call }
|
123
|
+
end
|
124
|
+
@reconnect = reconnect
|
125
|
+
end
|
126
|
+
|
127
|
+
true
|
128
|
+
end
|
129
|
+
|
130
|
+
# Runs the given API command.
|
131
|
+
#
|
132
|
+
# @api private
|
133
|
+
# @param [String] command The name of the command to execute
|
134
|
+
# @param [Object] args The arguments to pass into the command
|
135
|
+
# @return [Hash] The data returned from the Spotify service
|
136
|
+
# @raise [SpotifyWeb::Error] if the connection is not open or the command fails to execute
|
137
|
+
def api(command, args = nil)
|
138
|
+
raise(ConnectionError, 'Connection is not open') unless @connection && @connection.connected?
|
139
|
+
|
140
|
+
if command == 'request' && args.delete(:batch)
|
141
|
+
batch(command, args) do |batch_command, batch_args|
|
142
|
+
api(batch_command, batch_args)
|
143
|
+
end
|
144
|
+
else
|
145
|
+
# Process this as a mercury request
|
146
|
+
if command == 'request'
|
147
|
+
response_schema = args.delete(:response_schema)
|
148
|
+
end
|
149
|
+
|
150
|
+
message_id = @connection.publish(command, args)
|
151
|
+
|
152
|
+
# Wait until we get a response for the given message
|
153
|
+
data = wait do |&resume|
|
154
|
+
on(:response_received, :once => true, :if => {'id' => message_id}) {|data| resume.call(data)}
|
155
|
+
end
|
156
|
+
|
157
|
+
if command == 'request' && !data['error']
|
158
|
+
# Parse the response bsed on the schema provided
|
159
|
+
header, body = data['result']
|
160
|
+
request = Schema::Mercury::MercuryRequest.decode(Base64.decode64(header))
|
161
|
+
|
162
|
+
if (400..599).include?(request.status_code)
|
163
|
+
data['error'] = "Failed response: #{request.status_code}"
|
164
|
+
else
|
165
|
+
data['result'] = response_schema.decode(Base64.decode64(body))
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
if error = data['error']
|
170
|
+
raise APIError, "Command \"#{command}\" failed with message: \"#{error}\""
|
171
|
+
else
|
172
|
+
data
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Starts the keepalive timer for ensure the connection remains open.
|
178
|
+
# @api private
|
179
|
+
def start_keepalives
|
180
|
+
@keepalive_timer.cancel if @keepalive_timer
|
181
|
+
@keepalive_timer = EM::Synchrony.add_periodic_timer(KEEPALIVE_INTERVAL) do
|
182
|
+
SpotifyWeb.run { api('sp/echo', 'h') }
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Builds a new song bound to the given id.
|
187
|
+
#
|
188
|
+
# @param [String, Hash] attributes The id of the song to build or a hash of attributes
|
189
|
+
# @return [SpotifyWeb::Song]
|
190
|
+
# @example
|
191
|
+
# client.song("\x92\xA9...") # => #<SpotifyWeb::Song id="\x92\xA9..." ...>
|
192
|
+
def song(attributes)
|
193
|
+
attributes = {:gid => attributes} unless attributes.is_a?(Hash)
|
194
|
+
Song.new(self, attributes)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Builds a new artist bound to the given id.
|
198
|
+
#
|
199
|
+
# @param [String, Hash] attributes The id of the artist to build or a hash of attributes
|
200
|
+
# @return [SpotifyWeb::Artist]
|
201
|
+
# @example
|
202
|
+
# client.artist("\xC1\x8Fr...") # => #<SpotifyWeb::Artist gid="\xC1\x8Fr..." ...>
|
203
|
+
def artist(attributes)
|
204
|
+
attributes = {:gid => attributes} unless attributes.is_a?(Hash)
|
205
|
+
Artist.new(self, attributes)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Builds a new album bound to the given id / attributes.
|
209
|
+
#
|
210
|
+
# @param [String, Hash] attributes The id of the album to build or a hash of attributes
|
211
|
+
# @return [SpotifyWeb::Album]
|
212
|
+
# @example
|
213
|
+
# client.album("\x03\xC0...") # => #<SpotifyWeb::Album id="\x03\xC0..." ...>
|
214
|
+
def album(attributes)
|
215
|
+
attributes = {:gid => attributes} unless attributes.is_a?(Hash)
|
216
|
+
Album.new(self, attributes)
|
217
|
+
end
|
218
|
+
|
219
|
+
private
|
220
|
+
# Determines the web socket url to connect to
|
221
|
+
def access_point_url
|
222
|
+
resolver = user.settings['aps']['resolver']
|
223
|
+
query = {:client => "24:0:0:#{user.settings['version']}"}
|
224
|
+
query[:site] = resolver['site'] if resolver['site']
|
225
|
+
|
226
|
+
request = EventMachine::HttpRequest.new("http://#{resolver['hostname']}")
|
227
|
+
response = request.get(:query => query, :head => {'User-Agent' => USER_AGENT})
|
228
|
+
|
229
|
+
if response.response_header.successful?
|
230
|
+
data = JSON.parse(response.response)
|
231
|
+
data['ap_list'][0]
|
232
|
+
else
|
233
|
+
raise(ConnectionError, data['message'])
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Runs a batch command within an API call
|
238
|
+
def batch(command, options)
|
239
|
+
# Update payload to be a batch
|
240
|
+
requests = options[:payload].map do |attrs|
|
241
|
+
attrs[:method] ||= 'GET'
|
242
|
+
Schema::Mercury::MercuryRequest.new(attrs)
|
243
|
+
end
|
244
|
+
options[:payload] = Schema::Mercury::MercuryMultiGetRequest.new(:request => requests)
|
245
|
+
|
246
|
+
# Track the schema
|
247
|
+
response_schema = options[:response_schema]
|
248
|
+
options[:response_schema] = Schema::Mercury::MercuryMultiGetReply
|
249
|
+
|
250
|
+
response = yield(command, options)
|
251
|
+
|
252
|
+
# Process each reply
|
253
|
+
results = []
|
254
|
+
response['result'].reply.each_with_index do |reply, index|
|
255
|
+
if (400..599).include?(reply.status_code)
|
256
|
+
request = requests[index]
|
257
|
+
raise APIError, "Command \"#{command}\" for URI \"#{request.uri}\" failed with message: \"#{reply.status_code}\""
|
258
|
+
else
|
259
|
+
results << response_schema.decode(reply.body)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
response['result'] = results
|
264
|
+
response
|
265
|
+
end
|
266
|
+
|
267
|
+
# Registers a handler to invoke when an event occurs in Spotify.
|
268
|
+
#
|
269
|
+
# @param [Symbol] event The event to register a handler for
|
270
|
+
# @param [Hash] options The configuration options for the handler
|
271
|
+
# @option options [Hash] :if Specifies a set of key-value pairs that must be matched in the event data in order to run the handler
|
272
|
+
# @option options [Boolean] :once (false) Whether to only run the handler once
|
273
|
+
# @return [true]
|
274
|
+
def on(event, options = {}, &block)
|
275
|
+
event = event.to_sym
|
276
|
+
@event_handlers[event] ||= []
|
277
|
+
@event_handlers[event] << Handler.new(event, options, &block)
|
278
|
+
true
|
279
|
+
end
|
280
|
+
|
281
|
+
# Triggers callback handlers for the given Spotify command. This should
|
282
|
+
# be invoked when responses are received for Spotify.
|
283
|
+
#
|
284
|
+
# @note If the command is unknown, it will simply get skipped and not raise an exception
|
285
|
+
# @param [Symbol] command The name of the command triggered. This is typically the same name as the event.
|
286
|
+
# @param [Array] args The arguments to be processed by the event
|
287
|
+
# @return [true]
|
288
|
+
def trigger(command, *args)
|
289
|
+
command = command.to_sym if command
|
290
|
+
|
291
|
+
if Event.command?(command)
|
292
|
+
event = Event.new(self, command, args)
|
293
|
+
handlers = @event_handlers[event.name] || []
|
294
|
+
handlers.each do |handler|
|
295
|
+
success = handler.run(event)
|
296
|
+
handlers.delete(handler) if success && handler.once
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
true
|
301
|
+
end
|
302
|
+
|
303
|
+
# Callback when Spotify has requested to evaluate javascript
|
304
|
+
def on_work_requested(work)
|
305
|
+
script = <<-eos
|
306
|
+
this.reply = function() {
|
307
|
+
this.result = Array.prototype.slice.call(arguments);
|
308
|
+
};
|
309
|
+
#{work['args'][0]}
|
310
|
+
eos
|
311
|
+
context = ExecJS.compile(script)
|
312
|
+
result = context.eval('this.result')
|
313
|
+
|
314
|
+
api('sp/work_done', result)
|
315
|
+
end
|
316
|
+
|
317
|
+
# Callback when the session has ended. This will automatically reconnect if
|
318
|
+
# allowed to do so.
|
319
|
+
def on_session_ended
|
320
|
+
@connection = nil
|
321
|
+
|
322
|
+
# Automatically reconnect to the server if allowed
|
323
|
+
if @reconnect
|
324
|
+
reconnect_from(Exception) do
|
325
|
+
connect
|
326
|
+
trigger(:reconnected)
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
# Runs a given block and retries that block after a certain period of time
|
332
|
+
# if any of the specified exceptions are raised. Note that there is no
|
333
|
+
# limit on the number of attempts to retry.
|
334
|
+
def reconnect_from(*exceptions)
|
335
|
+
begin
|
336
|
+
yield
|
337
|
+
rescue *exceptions => ex
|
338
|
+
if @reconnect
|
339
|
+
logger.debug "Connection failed: #{ex.message}"
|
340
|
+
EM::Synchrony.sleep(@reconnect_wait)
|
341
|
+
logger.debug 'Attempting to reconnect'
|
342
|
+
retry
|
343
|
+
else
|
344
|
+
raise
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
# Pauses the current fiber until it is resumed with response data. This
|
350
|
+
# can only get resumed explicitly by the provided block.
|
351
|
+
def wait(&block)
|
352
|
+
fiber = Fiber.current
|
353
|
+
|
354
|
+
# Resume the fiber when a response is received
|
355
|
+
allow_resume = true
|
356
|
+
block.call do |*args|
|
357
|
+
fiber.resume(*args) if allow_resume
|
358
|
+
end
|
359
|
+
|
360
|
+
# Attempt to pause the fiber until a response is received
|
361
|
+
begin
|
362
|
+
Fiber.yield
|
363
|
+
rescue FiberError => ex
|
364
|
+
allow_resume = false
|
365
|
+
raise Error, 'Spotify Web APIs cannot be called from root fiber; use SpotifyWeb.run { ... } instead'
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|