spotify_web 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,99 @@
1
+ module SpotifyWeb
2
+ # Represents a collection of related resources (of the same type) on
3
+ # Spotify
4
+ class ResourceCollection < Array
5
+ # Initializes this collection with the given resources. This will continue
6
+ # to call the superclass's constructor with any additional arguments that
7
+ # get specified.
8
+ #
9
+ # @api private
10
+ def initialize(client, *args)
11
+ @client = client
12
+ @loaded = false
13
+ super(*args)
14
+
15
+ # Load all resources if attempted for a single one
16
+ each do |resource|
17
+ resource.metadata = lambda { load unless loaded? }
18
+ end
19
+ end
20
+
21
+ # Loads the attributes for these resources from Spotify. By default this is
22
+ # a no-op and just marks the resource as loaded.
23
+ #
24
+ # @return [true]
25
+ def load
26
+ if count == 1
27
+ # Remove the metadata loader / load directly from the resource
28
+ first.metadata = nil
29
+ first.load
30
+ else
31
+ # Load each resource's metadata
32
+ metadata.each_with_index do |result, index|
33
+ self[index].metadata = result
34
+ end
35
+
36
+ true
37
+ end
38
+
39
+ @loaded = true
40
+ end
41
+ alias :reload :load
42
+
43
+ # Determines whether the current collection has been loaded from Spotify.
44
+ #
45
+ # @return [Boolean] +true+ if the collection has been loaded, otherwise +false+
46
+ def loaded?
47
+ @loaded
48
+ end
49
+
50
+ # Looks up the metadata associated with all of the resources in this
51
+ # collection.
52
+ #
53
+ # @api private
54
+ # @return [Array] The resulting metadata for each resource
55
+ def metadata
56
+ if any? && metadata_schema
57
+ response = api('request',
58
+ :uri => "hm://metadata/#{resource_name}s",
59
+ :batch => true,
60
+ :payload => map {|resource| {:uri => resource.metadata_uri}},
61
+ :response_schema => metadata_schema
62
+ )
63
+ response['result']
64
+ else
65
+ []
66
+ end
67
+ end
68
+
69
+ private
70
+ # The types of resources being stored in this collection. This should only
71
+ # be called when there are actually resources available.
72
+ def resource_class
73
+ if any?
74
+ first.class
75
+ else
76
+ raise ArgumentError, 'Cannot determine resource class on empty collection'
77
+ end
78
+ end
79
+
80
+ # The Spotify name for the resource type
81
+ def resource_name
82
+ resource_class.resource_name
83
+ end
84
+
85
+ # The response schema used for looking up metadata associated with the
86
+ # resources
87
+ def metadata_schema
88
+ resource_class.metadata_schema
89
+ end
90
+
91
+ # The client that all APIs filter through
92
+ attr_reader :client
93
+
94
+ # Runs the given API command on the client.
95
+ def api(command, options = {})
96
+ client.api(command, options)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,32 @@
1
+ require 'spotify_web/resource'
2
+
3
+ module SpotifyWeb
4
+ # Represents a country-based restriction on Spotify data
5
+ class Restriction < Resource
6
+ # The countries allowed to access the data
7
+ # @return [String]
8
+ attribute :countries_allowed do |countries|
9
+ countries.scan(/.{2}/)
10
+ end
11
+
12
+ # The countries forbidden to access the data
13
+ # @return [String]
14
+ attribute :countries_forbidden do |countries|
15
+ countries.scan(/.{2}/)
16
+ end
17
+
18
+ # Whether the user is permitted to access data based on this restriction
19
+ # @return [Boolean]
20
+ def permitted?
21
+ country = client.user.settings['country']
22
+
23
+ if countries_allowed
24
+ countries_allowed.include?(country)
25
+ elsif countries_forbidden
26
+ !countries_forbidden.include?(country)
27
+ else
28
+ true
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,120 @@
1
+ require 'net/http'
2
+ require 'json'
3
+ require 'rexml/document'
4
+
5
+ module SpotifyWeb
6
+ module Schema
7
+ # The services that are used
8
+ SERVICES = %w(
9
+ mercury
10
+ metadata
11
+ playlist4changes
12
+ playlist4content
13
+ playlist4issues
14
+ playlist4meta
15
+ playlist4ops
16
+ radio
17
+ social
18
+ socialgraph
19
+ toplist
20
+ )
21
+
22
+ class << self
23
+ # Rebuilds all of the Beecake::Message schema definitions in Spotify.
24
+ # Note that this schema is not always kept up-to-date in Spotify --
25
+ # and can sometimes include parser errors. As a result, there may be
26
+ # some manual changes that need to be made once the build is complete.
27
+ def build_all
28
+ # Prepare target directories
29
+ proto_dir = File.join(File.dirname(__FILE__), '../../proto')
30
+ schema_dir = File.join(File.dirname(__FILE__), 'schema')
31
+ Dir.mkdir(proto_dir) unless Dir.exists?(proto_dir)
32
+
33
+ # Build the proto files
34
+ packages.each do |name, package|
35
+ File.open("#{proto_dir}/#{name}.proto", 'w') {|f| f << package[:content]}
36
+ end
37
+
38
+ # Convert each proto file to a Beefcake message
39
+ packages.each do |name, package|
40
+ system(
41
+ {'BEEFCAKE_NAMESPACE' => package[:namespace]},
42
+ "protoc --beefcake_out #{schema_dir} -I #{proto_dir} #{proto_dir}/#{name}.proto"
43
+ )
44
+ end
45
+ end
46
+
47
+ # Generates the Protocol Buffer packages based on the current list of
48
+ # Spotify services. This will merge certain services together under
49
+ # the same package if they have the same namespace.
50
+ def packages
51
+ packages = {}
52
+
53
+ services.values.each do |service|
54
+ namespace = 'SpotifyWeb::Schema'
55
+ if match = service[:content].match(/package spotify\.(.+)\.proto;/)
56
+ name = match[1]
57
+ namespace << '::' + name.split('.').map {|part| part.capitalize} * '::'
58
+ else
59
+ name = 'core'
60
+ end
61
+
62
+ if package = packages[name]
63
+ # Package already exists: just append the message definitions
64
+ content = service[:content]
65
+ content = content[content.index('message')..-1]
66
+ package[:content] += "\n#{content}"
67
+ else
68
+ # Create a new package with the entire definition
69
+ packages[name] = {:name => name, :namespace => namespace, :content => service[:content]}
70
+ end
71
+ end
72
+
73
+ packages
74
+ end
75
+
76
+ # Gets the collection of services defined in Spotify and the resource
77
+ # definitions associated with them
78
+ def services
79
+ services = {}
80
+
81
+ # Get the current schema
82
+ request = Net::HTTP::Get.new(data_url)
83
+ request['User-Agent'] = SpotifyWeb::USER_AGENT
84
+ uri = URI(data_url)
85
+ response = Net::HTTP.start(uri.host, uri.port, :use_ssl => true) do |http|
86
+ http.request(request)
87
+ end
88
+
89
+ # Parse each service definition
90
+ doc = REXML::Document.new(response.body)
91
+ doc.elements.each('services') do |root|
92
+ root.elements.each do |service|
93
+ name = service.name
94
+ if SERVICES.include?(name)
95
+ content = service.text.strip
96
+ services[name] = {:name => name, :content => content}
97
+ end
98
+ end
99
+ end
100
+
101
+ services
102
+ end
103
+
104
+ # Looks up the url representing the current schema for all Spotify services
105
+ def data_url
106
+ # Grab the login init options
107
+ request = Net::HTTP::Get.new('https://play.spotify.com')
108
+ request['User-Agent'] = SpotifyWeb::USER_AGENT
109
+ response = Net::HTTP.start('play.spotify.com', 443, :use_ssl => true) do |http|
110
+ http.request(request)
111
+ end
112
+
113
+ json = response.body.match(/Spotify\.Web\.Login\(document, (\{.+\}),[^\}]+\);/)[1]
114
+ options = JSON.parse(json)
115
+
116
+ "#{options['corejs']['protoSchemasLocation']}data.xml"
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,31 @@
1
+ ## Generated from core.proto for
2
+ require "beefcake"
3
+
4
+ module SpotifyWeb
5
+ module Schema
6
+
7
+ class Toplist
8
+ include Beefcake::Message
9
+ end
10
+
11
+ class DecorationData
12
+ include Beefcake::Message
13
+ end
14
+
15
+ class Toplist
16
+ repeated :items, :string, 1
17
+ end
18
+
19
+
20
+ class DecorationData
21
+ optional :username, :string, 1
22
+ optional :full_name, :string, 2
23
+ optional :image_url, :string, 3
24
+ optional :large_image_url, :string, 5
25
+ optional :first_name, :string, 6
26
+ optional :last_name, :string, 7
27
+ optional :facebook_uid, :string, 8
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,66 @@
1
+ ## Generated from mercury.proto for spotify.mercury.proto
2
+ require "beefcake"
3
+
4
+ module SpotifyWeb
5
+ module Schema
6
+ module Mercury
7
+
8
+ class UserField
9
+ include Beefcake::Message
10
+ end
11
+
12
+ class MercuryMultiGetRequest
13
+ include Beefcake::Message
14
+ end
15
+
16
+ class MercuryMultiGetReply
17
+ include Beefcake::Message
18
+ end
19
+
20
+ class MercuryRequest
21
+ include Beefcake::Message
22
+ end
23
+
24
+ class MercuryReply
25
+ include Beefcake::Message
26
+
27
+ module CachePolicy
28
+ CACHE_NO = 1
29
+ CACHE_PRIVATE = 2
30
+ CACHE_PUBLIC = 3
31
+ end
32
+ end
33
+
34
+ class MercuryMultiGetRequest
35
+ repeated :request, MercuryRequest, 1
36
+ end
37
+
38
+
39
+ class MercuryMultiGetReply
40
+ repeated :reply, MercuryReply, 1
41
+ end
42
+
43
+
44
+ class MercuryRequest
45
+ optional :uri, :string, 1
46
+ optional :content_type, :string, 2
47
+ optional :method, :bytes, 3
48
+ optional :status_code, :sint32, 4
49
+ optional :source, :string, 5
50
+ repeated :user_fields, UserField, 6
51
+ end
52
+
53
+
54
+ class MercuryReply
55
+ optional :status_code, :sint32, 1
56
+ optional :status_message, :string, 2
57
+ optional :cache_policy, MercuryReply::CachePolicy, 3
58
+ optional :ttl, :sint32, 4
59
+ optional :etag, :bytes, 5
60
+ optional :content_type, :string, 6
61
+ optional :body, :bytes, 7
62
+ end
63
+
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,257 @@
1
+ ## Generated from metadata.proto for spotify.metadata.proto
2
+ require "beefcake"
3
+
4
+ module SpotifyWeb
5
+ module Schema
6
+ module Metadata
7
+
8
+ class TopTracks
9
+ include Beefcake::Message
10
+ end
11
+
12
+ class ActivityPeriod
13
+ include Beefcake::Message
14
+ end
15
+
16
+ class Artist
17
+ include Beefcake::Message
18
+ end
19
+
20
+ class AlbumGroup
21
+ include Beefcake::Message
22
+ end
23
+
24
+ class Date
25
+ include Beefcake::Message
26
+ end
27
+
28
+ class Album
29
+ include Beefcake::Message
30
+
31
+ module Type
32
+ ALBUM = 1
33
+ SINGLE = 2
34
+ COMPILATION = 3
35
+ end
36
+ end
37
+
38
+ class Track
39
+ include Beefcake::Message
40
+ end
41
+
42
+ class Image
43
+ include Beefcake::Message
44
+
45
+ module Size
46
+ DEFAULT = 0
47
+ SMALL = 1
48
+ LARGE = 2
49
+ XLARGE = 3
50
+ end
51
+ end
52
+
53
+ class ImageGroup
54
+ include Beefcake::Message
55
+ end
56
+
57
+ class Biography
58
+ include Beefcake::Message
59
+ end
60
+
61
+ class Disc
62
+ include Beefcake::Message
63
+ end
64
+
65
+ class Copyright
66
+ include Beefcake::Message
67
+
68
+ module Type
69
+ P = 0
70
+ C = 1
71
+ end
72
+ end
73
+
74
+ class Restriction
75
+ include Beefcake::Message
76
+
77
+ module Catalogue
78
+ AD = 0
79
+ SUBSCRIPTION = 1
80
+ SHUFFLE = 3
81
+ end
82
+
83
+ module Type
84
+ STREAMING = 0
85
+ end
86
+ end
87
+
88
+ class SalePeriod
89
+ include Beefcake::Message
90
+ end
91
+
92
+ class ExternalId
93
+ include Beefcake::Message
94
+ end
95
+
96
+ class AudioFile
97
+ include Beefcake::Message
98
+
99
+ module Format
100
+ OGG_VORBIS_96 = 0
101
+ OGG_VORBIS_160 = 1
102
+ OGG_VORBIS_320 = 2
103
+ MP3_256 = 3
104
+ MP3_320 = 4
105
+ MP3_160 = 5
106
+ MP3_96 = 6
107
+ end
108
+ end
109
+
110
+ class TopTracks
111
+ optional :country, :string, 1
112
+ repeated :track, Track, 2
113
+ end
114
+
115
+
116
+ class ActivityPeriod
117
+ optional :start_year, :sint32, 1
118
+ optional :end_year, :sint32, 2
119
+ optional :decade, :sint32, 3
120
+ end
121
+
122
+
123
+ class Artist
124
+ optional :gid, :bytes, 1
125
+ optional :name, :string, 2
126
+ optional :popularity, :sint32, 3
127
+ repeated :top_track, TopTracks, 4
128
+ repeated :album_group, AlbumGroup, 5
129
+ repeated :single_group, AlbumGroup, 6
130
+ repeated :compilation_group, AlbumGroup, 7
131
+ repeated :appears_on_group, AlbumGroup, 8
132
+ repeated :genre, :string, 9
133
+ repeated :external_id, ExternalId, 10
134
+ repeated :portrait, Image, 11
135
+ repeated :biography, Biography, 12
136
+ repeated :activity_period, ActivityPeriod, 13
137
+ repeated :restriction, Restriction, 14
138
+ repeated :related, Artist, 15
139
+ optional :is_portrait_album_cover, :bool, 16
140
+ optional :portrait_group, ImageGroup, 17
141
+ end
142
+
143
+
144
+ class AlbumGroup
145
+ repeated :album, Album, 1
146
+ end
147
+
148
+
149
+ class Date
150
+ optional :year, :sint32, 1
151
+ optional :month, :sint32, 2
152
+ optional :day, :sint32, 3
153
+ end
154
+
155
+
156
+ class Album
157
+ optional :gid, :bytes, 1
158
+ optional :name, :string, 2
159
+ repeated :artist, Artist, 3
160
+ optional :type, Album::Type, 4
161
+ optional :label, :string, 5
162
+ optional :date, Date, 6
163
+ optional :popularity, :sint32, 7
164
+ repeated :genre, :string, 8
165
+ repeated :cover, Image, 9
166
+ repeated :external_id, ExternalId, 10
167
+ repeated :disc, Disc, 11
168
+ repeated :review, :string, 12
169
+ repeated :copyright, Copyright, 13
170
+ repeated :restriction, Restriction, 14
171
+ repeated :related, Album, 15
172
+ repeated :sale_period, SalePeriod, 16
173
+ optional :cover_group, ImageGroup, 17
174
+ end
175
+
176
+
177
+ class Track
178
+ optional :gid, :bytes, 1
179
+ optional :name, :string, 2
180
+ optional :album, Album, 3
181
+ repeated :artist, Artist, 4
182
+ optional :number, :sint32, 5
183
+ optional :disc_number, :sint32, 6
184
+ optional :duration, :sint32, 7
185
+ optional :popularity, :sint32, 8
186
+ optional :explicit, :bool, 9
187
+ repeated :external_id, ExternalId, 10
188
+ repeated :restriction, Restriction, 11
189
+ repeated :file, AudioFile, 12
190
+ repeated :alternative, Track, 13
191
+ repeated :sale_period, SalePeriod, 14
192
+ repeated :preview, AudioFile, 15
193
+ end
194
+
195
+
196
+ class Image
197
+ optional :file_id, :bytes, 1
198
+ optional :size, Image::Size, 2
199
+ optional :width, :sint32, 3
200
+ optional :height, :sint32, 4
201
+ end
202
+
203
+
204
+ class ImageGroup
205
+ repeated :image, Image, 1
206
+ end
207
+
208
+
209
+ class Biography
210
+ optional :text, :string, 1
211
+ repeated :portrait, Image, 2
212
+ repeated :portrait_group, ImageGroup, 3
213
+ end
214
+
215
+
216
+ class Disc
217
+ optional :number, :sint32, 1
218
+ optional :name, :string, 2
219
+ repeated :track, Track, 3
220
+ end
221
+
222
+
223
+ class Copyright
224
+ optional :type, Copyright::Type, 1
225
+ optional :text, :string, 2
226
+ end
227
+
228
+
229
+ class Restriction
230
+ repeated :catalogue, Restriction::Catalogue, 1
231
+ optional :countries_allowed, :string, 2
232
+ optional :countries_forbidden, :string, 3
233
+ optional :type, Restriction::Type, 4
234
+ end
235
+
236
+
237
+ class SalePeriod
238
+ repeated :restriction, Restriction, 1
239
+ optional :start, Date, 2
240
+ optional :end, Date, 3
241
+ end
242
+
243
+
244
+ class ExternalId
245
+ optional :type, :string, 1
246
+ optional :id, :string, 2
247
+ end
248
+
249
+
250
+ class AudioFile
251
+ optional :file_id, :bytes, 1
252
+ optional :format, AudioFile::Format, 2
253
+ end
254
+
255
+ end
256
+ end
257
+ end