spotify 12.0.0 → 12.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.
- 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
|