spotify 12.0.0 → 12.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +17 -0
- data/README.markdown +1 -1
- data/lib/spotify.rb +7 -762
- data/lib/spotify/error_wrappers.rb +165 -0
- data/lib/spotify/functions.rb +730 -0
- data/lib/spotify/gc_wrappers.rb +104 -0
- data/lib/spotify/types/image_id.rb +38 -0
- data/lib/spotify/types/pointer.rb +61 -0
- data/lib/spotify/types/utf8_string.rb +45 -0
- data/lib/spotify/version.rb +1 -1
- data/spec/spotify_spec.rb +150 -2
- data/spotify.gemspec +1 -1
- metadata +12 -6
@@ -0,0 +1,104 @@
|
|
1
|
+
# This file contains a tiny (!) DSL that wraps existing Spotify
|
2
|
+
# functions into versions that return Spotify::Pointers instead
|
3
|
+
# of the usual FFI::Pointers. The Spotify::Pointer automatically
|
4
|
+
# manages the underlying pointers reference count, which allows
|
5
|
+
# us to piggyback on the Ruby GC mechanism.
|
6
|
+
|
7
|
+
module Spotify
|
8
|
+
# Wraps the function `function` so that it always returns
|
9
|
+
# a Spotify::Pointer with correct refcount. Functions that
|
10
|
+
# contain the word `create` are assumed to start out with
|
11
|
+
# a refcount of `+1`.
|
12
|
+
#
|
13
|
+
# @note This method is removed at the bottom of this file.
|
14
|
+
#
|
15
|
+
# @param [#to_s] function
|
16
|
+
# @param [#to_s] return_type
|
17
|
+
# @raise [NoMethodError] if `function` is not defined
|
18
|
+
# @see Spotify::Pointer
|
19
|
+
def self.wrap_function(function, return_type)
|
20
|
+
method(function) # make sure it exists
|
21
|
+
define_singleton_method("#{function}!") do |*args, &block|
|
22
|
+
pointer = public_send(function, *args, &block)
|
23
|
+
Spotify::Pointer.new(pointer, return_type, function !~ /create/)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# @macro [attach] wrap_function
|
28
|
+
# Same as {Spotify}.`$1`, but wraps result in a {Spotify::Pointer}.
|
29
|
+
#
|
30
|
+
# @method $1!
|
31
|
+
# @return [Spotify::Pointer<$2>]
|
32
|
+
# @see #$1
|
33
|
+
wrap_function :session_user, :user
|
34
|
+
wrap_function :session_playlistcontainer, :playlistcontainer
|
35
|
+
wrap_function :session_inbox_create, :playlist
|
36
|
+
wrap_function :session_starred_create, :playlist
|
37
|
+
wrap_function :session_starred_for_user_create, :playlist
|
38
|
+
wrap_function :session_publishedcontainer_for_user_create, :playlistcontainer
|
39
|
+
|
40
|
+
wrap_function :track_artist, :artist
|
41
|
+
wrap_function :track_album, :album
|
42
|
+
wrap_function :localtrack_create, :track
|
43
|
+
wrap_function :track_get_playable, :track
|
44
|
+
|
45
|
+
wrap_function :album_artist, :artist
|
46
|
+
|
47
|
+
wrap_function :albumbrowse_create, :albumbrowse
|
48
|
+
wrap_function :albumbrowse_album, :album
|
49
|
+
wrap_function :albumbrowse_artist, :artist
|
50
|
+
wrap_function :albumbrowse_track, :track
|
51
|
+
|
52
|
+
wrap_function :artistbrowse_create, :artistbrowse
|
53
|
+
wrap_function :artistbrowse_artist, :artist
|
54
|
+
wrap_function :artistbrowse_track, :track
|
55
|
+
wrap_function :artistbrowse_album, :album
|
56
|
+
wrap_function :artistbrowse_similar_artist, :artist
|
57
|
+
wrap_function :artistbrowse_tophit_track, :track
|
58
|
+
|
59
|
+
wrap_function :image_create, :image
|
60
|
+
wrap_function :image_create_from_link, :image
|
61
|
+
|
62
|
+
wrap_function :link_as_track, :track
|
63
|
+
wrap_function :link_as_track_and_offset, :track
|
64
|
+
wrap_function :link_as_album, :album
|
65
|
+
wrap_function :link_as_artist, :artist
|
66
|
+
wrap_function :link_as_user, :user
|
67
|
+
|
68
|
+
wrap_function :link_create_from_string, :link
|
69
|
+
wrap_function :link_create_from_track, :link
|
70
|
+
wrap_function :link_create_from_album, :link
|
71
|
+
wrap_function :link_create_from_artist, :link
|
72
|
+
wrap_function :link_create_from_search, :link
|
73
|
+
wrap_function :link_create_from_playlist, :link
|
74
|
+
wrap_function :link_create_from_artist_portrait, :link
|
75
|
+
wrap_function :link_create_from_artistbrowse_portrait, :link
|
76
|
+
wrap_function :link_create_from_album_cover, :link
|
77
|
+
wrap_function :link_create_from_image, :link
|
78
|
+
wrap_function :link_create_from_user, :link
|
79
|
+
|
80
|
+
wrap_function :search_create, :search
|
81
|
+
wrap_function :search_track, :track
|
82
|
+
wrap_function :search_album, :album
|
83
|
+
wrap_function :search_artist, :artist
|
84
|
+
|
85
|
+
wrap_function :playlist_track, :track
|
86
|
+
wrap_function :playlist_track_creator, :user
|
87
|
+
wrap_function :playlist_owner, :user
|
88
|
+
wrap_function :playlist_create, :playlist
|
89
|
+
|
90
|
+
wrap_function :playlistcontainer_playlist, :playlist
|
91
|
+
wrap_function :playlistcontainer_add_new_playlist, :playlist
|
92
|
+
wrap_function :playlistcontainer_add_playlist, :playlist
|
93
|
+
wrap_function :playlistcontainer_owner, :user
|
94
|
+
|
95
|
+
wrap_function :toplistbrowse_create, :toplistbrowse
|
96
|
+
wrap_function :toplistbrowse_artist, :artist
|
97
|
+
wrap_function :toplistbrowse_album, :album
|
98
|
+
wrap_function :toplistbrowse_track, :track
|
99
|
+
|
100
|
+
wrap_function :inbox_post_tracks, :inbox
|
101
|
+
|
102
|
+
# Clean up
|
103
|
+
class << self; undef :wrap_function; end
|
104
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Spotify
|
2
|
+
# A custom data type for Spotify image IDs.
|
3
|
+
#
|
4
|
+
# It will convert strings to image ID pointers when handling
|
5
|
+
# values from Ruby to C, and it will convert pointers to Ruby
|
6
|
+
# strings when handling values from C to Ruby.
|
7
|
+
module ImageID
|
8
|
+
extend FFI::DataConverter
|
9
|
+
native_type FFI::Type::POINTER
|
10
|
+
|
11
|
+
# Given a string, convert it to an image ID pointer.
|
12
|
+
#
|
13
|
+
# @param [String] value image id as a string
|
14
|
+
# @param ctx
|
15
|
+
# @return [FFI::Pointer] pointer to the image ID
|
16
|
+
def self.to_native(value, ctx)
|
17
|
+
pointer = if value
|
18
|
+
if value.bytesize != 20
|
19
|
+
raise ArgumentError, "image id bytesize must be 20, was #{value.bytesize}"
|
20
|
+
end
|
21
|
+
|
22
|
+
pointer = FFI::MemoryPointer.new(:char, 20)
|
23
|
+
pointer.write_string(value.to_s)
|
24
|
+
end
|
25
|
+
|
26
|
+
super(pointer, ctx)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Given a pointer, read a 20-byte image ID from it.
|
30
|
+
#
|
31
|
+
# @param [FFI::Pointer] value
|
32
|
+
# @param ctx
|
33
|
+
# @return [String, nil] the image ID as a string, or nil
|
34
|
+
def self.from_native(value, ctx)
|
35
|
+
value.read_string(20) unless value.null?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Spotify
|
4
|
+
# The Pointer is a kind of AutoPointer specially tailored for Spotify
|
5
|
+
# objects, that releases the raw pointer on GC.
|
6
|
+
class Pointer < FFI::AutoPointer
|
7
|
+
# Raised when #releaser_for is given an invalid type.
|
8
|
+
class InvalidTypeError < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# Create a proc that will accept a pointer of a given type and
|
13
|
+
# release it with the correct function if it’s not null.
|
14
|
+
#
|
15
|
+
# @raise [InvalidTypeError] when given an invalid type
|
16
|
+
# @param [Symbol] type
|
17
|
+
# @return [Proc]
|
18
|
+
def releaser_for(type)
|
19
|
+
unless Spotify.respond_to?(:"#{type}_release")
|
20
|
+
raise InvalidTypeError, "#{type} is not a valid Spotify type"
|
21
|
+
end
|
22
|
+
|
23
|
+
lambda do |pointer|
|
24
|
+
$stdout.puts "Spotify::#{type}_release(#{pointer})" if $DEBUG
|
25
|
+
Spotify.send(:"#{type}_release", pointer) unless pointer.null?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Checks an object by pointer kind and type.
|
30
|
+
#
|
31
|
+
# @param [Object] object
|
32
|
+
# @param [Symbol] type
|
33
|
+
# @return [Boolean] true if object is a spotify pointer and of correct type
|
34
|
+
def typechecks?(object, type)
|
35
|
+
object.is_a?(Spotify::Pointer) && (object.type == type.to_s)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Symbol] type
|
40
|
+
attr_reader :type
|
41
|
+
|
42
|
+
# Initialize a Spotify pointer, which will automatically decrease
|
43
|
+
# the reference count of it’s pointer when garbage collected.
|
44
|
+
#
|
45
|
+
# @param [FFI::Pointer] pointer
|
46
|
+
# @param [#to_s] type session, link, etc
|
47
|
+
# @param [Boolean] add_ref will increase refcount by one if true
|
48
|
+
def initialize(pointer, type, add_ref)
|
49
|
+
super pointer, self.class.releaser_for(@type = type.to_s)
|
50
|
+
|
51
|
+
unless pointer.null?
|
52
|
+
Spotify.send(:"#{type}_add_ref", pointer)
|
53
|
+
end if add_ref
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [String] representation of the spotify pointer
|
57
|
+
def to_s
|
58
|
+
"<#{self.class} address=0x#{address.to_s(16)} type=#{type}>"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Spotify
|
2
|
+
# A UTF-8 FFI type, making sure all strings are UTF8 in and out.
|
3
|
+
#
|
4
|
+
# Given an ingoing (ruby to C) string, it will make sure the string
|
5
|
+
# is in UTF-8 encoding. An outgoing (C to Ruby) will be assumed to
|
6
|
+
# actually be in UTF-8, and force-encoded as such.
|
7
|
+
module UTF8String
|
8
|
+
extend FFI::DataConverter
|
9
|
+
native_type FFI::Type::STRING
|
10
|
+
|
11
|
+
if "".respond_to?(:encode)
|
12
|
+
# Given a value, encodes it to UTF-8 no matter what.
|
13
|
+
#
|
14
|
+
# @note if the value is already in UTF-8, ruby does nothing
|
15
|
+
# @note if the given value is falsy, default behaviour is used
|
16
|
+
#
|
17
|
+
# @param [String] value
|
18
|
+
# @param ctx
|
19
|
+
# @return [String] value, but in UTF-8 if it wasn’t already
|
20
|
+
def self.to_native(value, ctx)
|
21
|
+
if value
|
22
|
+
value.encode('UTF-8')
|
23
|
+
else
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
if "".respond_to?(:force_encoding)
|
30
|
+
# Given an original string, assume it is in UTF-8.
|
31
|
+
#
|
32
|
+
# @note NO error checking is made, the string is just forced to UTF-8
|
33
|
+
# @param [String] value can be in any encoding
|
34
|
+
# @param ctx
|
35
|
+
# @return [String] value, but with UTF-8 encoding
|
36
|
+
def self.from_native(value, ctx)
|
37
|
+
if value
|
38
|
+
value.force_encoding('UTF-8')
|
39
|
+
else
|
40
|
+
super
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/spotify/version.rb
CHANGED
data/spec/spotify_spec.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
require 'rubygems' # needed for 1.8, does not matter in 1.9
|
3
3
|
|
4
|
+
require 'ostruct'
|
5
|
+
require 'set'
|
4
6
|
require 'rbgccxml'
|
7
|
+
require 'minitest/mock'
|
5
8
|
require 'minitest/autorun'
|
6
9
|
|
7
10
|
#
|
@@ -44,6 +47,19 @@ module C
|
|
44
47
|
attach_function :strncpy, [ :pointer, :utf8_string, :size_t ], :utf8_string
|
45
48
|
end
|
46
49
|
|
50
|
+
# Used for checking Spotify::Pointer things.
|
51
|
+
module Spotify
|
52
|
+
def bogus_add_ref(pointer)
|
53
|
+
end
|
54
|
+
|
55
|
+
def bogus_release(pointer)
|
56
|
+
end
|
57
|
+
|
58
|
+
# This may be called after our GC test. Randomly.
|
59
|
+
def garbage_release(pointer)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
47
63
|
#
|
48
64
|
# Utility
|
49
65
|
#
|
@@ -67,6 +83,16 @@ describe Spotify do
|
|
67
83
|
end
|
68
84
|
end
|
69
85
|
|
86
|
+
describe ".enum_value!" do
|
87
|
+
it "raises an error if given an invalid enum value" do
|
88
|
+
proc { Spotify.enum_value!(:moo, "error value") }.must_raise(ArgumentError)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "gives back the enum value for that enum" do
|
92
|
+
Spotify.enum_value!(:ok, "error value").must_equal 0
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
70
96
|
describe Spotify::SessionConfig do
|
71
97
|
it "allows setting boolean values with bools" do
|
72
98
|
subject = Spotify::SessionConfig.new
|
@@ -111,7 +137,129 @@ describe Spotify do
|
|
111
137
|
end
|
112
138
|
end
|
113
139
|
|
114
|
-
describe "
|
140
|
+
describe "error wrapped functions" do
|
141
|
+
wrapped_methods = Spotify.attached_methods.find_all { |meth, info| info[:returns] == :error }
|
142
|
+
wrapped_methods.each do |meth, info|
|
143
|
+
it "raises an error if #{meth}! returns non-ok" do
|
144
|
+
Spotify.stub(meth, :bad_application_key) do
|
145
|
+
proc { Spotify.send("#{meth}!") }.must_raise(Spotify::Error, /BAD_APPLICATION_KEY/)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
describe "GC wrapped functions" do
|
152
|
+
gc_types = Set.new([:session, :track, :user, :playlistcontainer, :playlist, :link, :album, :artist, :search, :image, :albumbrowse, :artistbrowse, :toplistbrowse, :inbox])
|
153
|
+
wrapped_methods = Spotify.attached_methods.find_all { |meth, info| gc_types.member?(info[:returns]) }
|
154
|
+
wrapped_methods.each do |meth, info|
|
155
|
+
it "returns a Spotify::Pointer for #{meth}!" do
|
156
|
+
Spotify.stub(meth, lambda { FFI::Pointer.new(0) }) do
|
157
|
+
Spotify.send("#{meth}!").must_be_instance_of Spotify::Pointer
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
it "adds a ref to the pointer if required" do
|
163
|
+
session = FFI::Pointer.new(1)
|
164
|
+
ref_added = false
|
165
|
+
|
166
|
+
Spotify.stub(:session_user, FFI::Pointer.new(1)) do
|
167
|
+
Spotify.stub(:user_add_ref, proc { ref_added = true }) do
|
168
|
+
Spotify.session_user!(session)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
ref_added.must_equal true
|
173
|
+
end
|
174
|
+
|
175
|
+
it "does not add a ref when the result is null" do
|
176
|
+
session = FFI::Pointer.new(1)
|
177
|
+
ref_added = false
|
178
|
+
|
179
|
+
Spotify.stub(:session_user, FFI::Pointer.new(0)) do
|
180
|
+
Spotify.stub(:user_add_ref, proc { ref_added = true }) do
|
181
|
+
Spotify.session_user!(session)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
ref_added.must_equal false
|
186
|
+
end
|
187
|
+
|
188
|
+
it "does not add a ref when the result already has one" do
|
189
|
+
session = FFI::Pointer.new(1)
|
190
|
+
ref_added = false
|
191
|
+
|
192
|
+
Spotify.stub(:albumbrowse_create, FFI::Pointer.new(1)) do
|
193
|
+
Spotify.stub(:albumbrowse_add_ref, proc { ref_added = true }) do
|
194
|
+
# to avoid it trying to GC our fake pointer later, and cause a
|
195
|
+
# segfault in our tests
|
196
|
+
Spotify::Pointer.stub(:releaser_for, proc { proc {} }) do
|
197
|
+
Spotify.albumbrowse_create!(session)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
ref_added.must_equal false
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
describe Spotify::Pointer do
|
207
|
+
describe ".new" do
|
208
|
+
it "adds a reference on the given pointer" do
|
209
|
+
ref_added = false
|
210
|
+
|
211
|
+
Spotify.stub(:bogus_add_ref, proc { ref_added = true }) do
|
212
|
+
Spotify::Pointer.new(FFI::Pointer.new(1), :bogus, true)
|
213
|
+
end
|
214
|
+
|
215
|
+
ref_added.must_equal true
|
216
|
+
end
|
217
|
+
|
218
|
+
it "does not add a reference on the given pointer if it is NULL" do
|
219
|
+
ref_added = false
|
220
|
+
|
221
|
+
Spotify.stub(:bogus_add_ref, proc { ref_added = true }) do
|
222
|
+
Spotify::Pointer.new(FFI::Pointer::NULL, :bogus, true)
|
223
|
+
end
|
224
|
+
|
225
|
+
ref_added.must_equal false
|
226
|
+
end
|
227
|
+
|
228
|
+
it "raises an error when given an invalid type" do
|
229
|
+
proc { Spotify::Pointer.new(FFI::Pointer.new(1), :really_bogus, true) }.
|
230
|
+
must_raise(Spotify::Pointer::InvalidTypeError, /invalid/)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
describe ".typechecks?" do
|
235
|
+
it "typechecks a given spotify pointer" do
|
236
|
+
pointer = Spotify::Pointer.new(FFI::Pointer.new(1), :bogus, true)
|
237
|
+
bogus = OpenStruct.new(:type => :bogus)
|
238
|
+
Spotify::Pointer.typechecks?(bogus, :bogus).must_equal false
|
239
|
+
Spotify::Pointer.typechecks?(pointer, :link).must_equal false
|
240
|
+
Spotify::Pointer.typechecks?(pointer, :bogus).must_equal true
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
describe "garbage collection" do
|
245
|
+
let(:my_pointer) { FFI::Pointer.new(1) }
|
246
|
+
|
247
|
+
it "should work" do
|
248
|
+
gc_count = 0
|
249
|
+
|
250
|
+
Spotify.stub(:garbage_release, proc { gc_count += 1 }) do
|
251
|
+
5.times { Spotify::Pointer.new(my_pointer, :garbage, false) }
|
252
|
+
5.times { GC.start; sleep 0.01 }
|
253
|
+
end
|
254
|
+
|
255
|
+
# GC tests are a bit funky, but as long as we garbage_release at least once, then
|
256
|
+
# we can assume our GC works properly, but up the stakes just for the sake of it
|
257
|
+
gc_count.must_be :>, 3
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
describe Spotify::UTF8String do
|
115
263
|
let(:char) do
|
116
264
|
char = "\xC4"
|
117
265
|
char.force_encoding('ISO-8859-1') if char.respond_to?(:force_encoding)
|
@@ -136,7 +284,7 @@ describe Spotify do
|
|
136
284
|
end unless "".respond_to?(:force_encoding)
|
137
285
|
end
|
138
286
|
|
139
|
-
describe
|
287
|
+
describe Spotify::ImageID do
|
140
288
|
let(:context) { nil }
|
141
289
|
let(:subject) { Spotify.find_type(:image_id) }
|
142
290
|
let(:null_pointer) { FFI::Pointer::NULL }
|
data/spotify.gemspec
CHANGED
@@ -27,5 +27,5 @@ Gem::Specification.new do |gem|
|
|
27
27
|
gem.add_development_dependency 'rake'
|
28
28
|
gem.add_development_dependency 'yard'
|
29
29
|
gem.add_development_dependency 'rbgccxml'
|
30
|
-
gem.add_development_dependency 'minitest', '~>
|
30
|
+
gem.add_development_dependency 'minitest', '~> 3.0'
|
31
31
|
end
|