spotify_web 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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