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.
@@ -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
@@ -1,4 +1,4 @@
1
1
  module Spotify
2
2
  # See README for versioning policy.
3
- VERSION = [12, 0, 0].join('.')
3
+ VERSION = [12, 0, 1].join('.')
4
4
  end
@@ -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 "UTF8 string" do
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 "Image ID" do
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 }
@@ -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', '~> 2.0'
30
+ gem.add_development_dependency 'minitest', '~> 3.0'
31
31
  end