spotify 12.3.0 → 12.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/CHANGELOG.md +19 -0
  2. data/Gemfile +1 -0
  3. data/README.markdown +29 -10
  4. data/Rakefile +10 -3
  5. data/examples/.gitignore +4 -0
  6. data/examples/README.md +15 -0
  7. data/examples/audio-stream.rb +170 -0
  8. data/examples/logging-in.rb +95 -0
  9. data/lib/spotify.rb +2 -1
  10. data/lib/spotify/error.rb +4 -1
  11. data/lib/spotify/managed_pointer.rb +53 -23
  12. data/lib/spotify/monkey_patches/ffi_pointer.rb +18 -0
  13. data/lib/spotify/structs.rb +14 -0
  14. data/lib/spotify/structs/session_callbacks.rb +2 -2
  15. data/lib/spotify/structs/session_config.rb +1 -1
  16. data/lib/spotify/types/image_id.rb +33 -26
  17. data/lib/spotify/types/nul_string.rb +23 -34
  18. data/lib/spotify/types/utf8_string.rb +19 -25
  19. data/lib/spotify/util.rb +1 -0
  20. data/lib/spotify/version.rb +1 -2
  21. data/spec/bench_helper.rb +29 -0
  22. data/spec/benchmarks/managed_pointer_bench.rb +32 -0
  23. data/spec/spec_helper.rb +1 -1
  24. data/spec/spotify/api/functions_spec.rb +67 -0
  25. data/spec/spotify/api_spec.rb +14 -58
  26. data/spec/spotify/defines_spec.rb +1 -1
  27. data/spec/spotify/managed_pointer_spec.rb +60 -11
  28. data/spec/spotify/monkey_patches/ffi_pointer_spec.rb +9 -0
  29. data/spec/spotify/types/nul_string_spec.rb +3 -3
  30. data/spec/{api-linux.h → support/api-linux.h} +0 -0
  31. data/spec/support/api-linux.xml +1893 -0
  32. data/spec/{api-mac.h → support/api-mac.h} +0 -0
  33. data/spec/{api-mac.xml → support/api-mac.xml} +0 -0
  34. data/spec/support/hook_spotify.rb +1 -1
  35. data/spec/{linux-platform.rb → support/linux-platform.rb} +0 -0
  36. data/spec/{mac-platform.rb → support/mac-platform.rb} +0 -0
  37. metadata +58 -46
  38. data/LICENSE +0 -19
  39. data/spec/api-linux.xml +0 -1887
@@ -23,6 +23,10 @@ module Spotify
23
23
  # @param [FFI::Pointer] pointer
24
24
  def release(pointer)
25
25
  unless pointer.null?
26
+ # this is to circumvent the type protection
27
+ pointer = base_class.new(pointer)
28
+ pointer.autorelease = false
29
+
26
30
  $stderr.puts "Spotify.#{type}_release(#{pointer.inspect})" if $DEBUG
27
31
  Spotify.public_send("#{type}_release", pointer)
28
32
  end
@@ -32,7 +36,7 @@ module Spotify
32
36
  #
33
37
  # This method derives the retain method from the class name.
34
38
  #
35
- # @param [FFI::Pointer] pointer
39
+ # @param [self] pointer must be an instance of {#base_class}
36
40
  def retain(pointer)
37
41
  unless pointer.null?
38
42
  $stderr.puts "Spotify.#{type}_add_ref(#{pointer.inspect})" if $DEBUG
@@ -40,44 +44,70 @@ module Spotify
40
44
  end
41
45
  end
42
46
 
47
+ # Makes all ManagedPointers typesafe in the sense that they will raise
48
+ # an argument error on any value that is not of the same kind.
49
+ def to_native(value, ctx)
50
+ if value.nil? or value.null?
51
+ raise TypeError, "#{name} pointers cannot be null, was #{value.inspect}"
52
+ elsif value.kind_of?(base_class)
53
+ super
54
+ else
55
+ raise TypeError, "expected a kind of #{name}, was #{value.class}"
56
+ end
57
+ end
58
+
59
+ # Retaining class is needed for the functions that return a pointer that
60
+ # does not have its reference count increased. This class is a subclass
61
+ # of the ManagedPointer, and should behave the same in all circumstances
62
+ # except for during initialization.
63
+ #
64
+ # This dynamic method is needed to DRY the pointers up. We have about ten
65
+ # subclasses of ManagedPointer; all of them need a subclass that retains
66
+ # its pointer on initialization. We could create one manually for each
67
+ # Album, Artist, Track, and so on, but that would be annoying.
68
+ #
43
69
  # @return [self] subclass that retains its pointer on initialization.
44
70
  def retaining_class
45
- @klass ||= Class.new(self) do
46
- def initialize(*args, &block)
47
- superclass = self.class.superclass
48
- superclass.instance_method(:initialize).bind(self).call(*args, &block)
49
- superclass.retain(self)
50
- end
51
-
52
- class << self
53
- alias_method :==, :<=
71
+ if defined?(self::Retaining)
72
+ self::Retaining
73
+ else
74
+ subclass = Class.new(self) do
75
+ class << self
76
+ def type
77
+ superclass.type
78
+ end
54
79
 
55
- # @return [String] delegates to the superclass.
56
- def name
57
- superclass.name
58
- end
80
+ protected
59
81
 
60
- # @return [String] delegates to the superclass.
61
- def to_s
62
- superclass.to_s
82
+ def base_class
83
+ superclass
84
+ end
63
85
  end
64
86
 
65
- # @return [String] string representation of object
66
- def inspect
67
- "#{superclass}<retaining>"
87
+ def initialize(*)
88
+ super
89
+ self.class.retain(self)
68
90
  end
69
91
  end
92
+
93
+ const_set("Retaining", subclass)
70
94
  end
71
95
  end
72
96
 
73
- alias_method :==, :>=
74
-
75
97
  protected
76
98
 
77
- # @return [#to_s] the spotify type of this pointer.
99
+ # Retrieves the normalized and downcased name of self, so for
100
+ # Spotify::Album we’ll receive just “album”.
78
101
  def type
79
102
  name.split('::')[-1].downcase
80
103
  end
104
+
105
+ # Retrieves the base class for this pointer. This is overridden
106
+ # by the {.retaining_class}. It is used for type-checking inside
107
+ # {.to_native}.
108
+ def base_class
109
+ self
110
+ end
81
111
  end
82
112
 
83
113
  # @return [String] string representation of self.
@@ -0,0 +1,18 @@
1
+ require "ffi"
2
+
3
+ module FFI
4
+ class AbstractMemory
5
+ unless method_defined?(:read_size_t)
6
+ type = FFI.find_type(:size_t)
7
+ type, _ = FFI::TypeDefs.find do |(name, t)|
8
+ method_defined? "read_#{name}" if t == type
9
+ end
10
+
11
+ if type.nil?
12
+ raise "Missing method to read a size_t from #{klass}"
13
+ end
14
+
15
+ alias_method(:read_size_t, "read_#{type}")
16
+ end
17
+ end
18
+ end
@@ -33,6 +33,20 @@ module Spotify
33
33
  self[key] = value
34
34
  end
35
35
  end
36
+
37
+ # Convert the struct to a hash.
38
+ #
39
+ # @return [Hash]
40
+ def to_h
41
+ Hash[members.zip(values)]
42
+ end
43
+
44
+ # String representation of the struct. Looks like a Hash.
45
+ #
46
+ # @return [String]
47
+ def to_s
48
+ "<#{self.class.name} #{to_h}>"
49
+ end
36
50
  end
37
51
  end
38
52
 
@@ -29,7 +29,7 @@ module Spotify
29
29
  :connection_error => callback([ Session, :error ], :void),
30
30
  :message_to_user => callback([ Session, UTF8String ], :void),
31
31
  :notify_main_thread => callback([ Session ], :void),
32
- :music_delivery => callback([ Session, AudioFormat, :frames, :int ], :int),
32
+ :music_delivery => callback([ Session, AudioFormat.by_ref, :frames, :int ], :int),
33
33
  :play_token_lost => callback([ Session ], :void),
34
34
  :log_message => callback([ Session, UTF8String ], :void),
35
35
  :end_of_track => callback([ Session ], :void),
@@ -37,7 +37,7 @@ module Spotify
37
37
  :userinfo_updated => callback([ Session ], :void),
38
38
  :start_playback => callback([ Session ], :void),
39
39
  :stop_playback => callback([ Session ], :void),
40
- :get_audio_buffer_stats => callback([ Session, AudioBufferStats ], :void),
40
+ :get_audio_buffer_stats => callback([ Session, AudioBufferStats.by_ref ], :void),
41
41
  :offline_status_updated => callback([ Session ], :void),
42
42
  :offline_error => callback([ Session, :error ], :void),
43
43
  :credentials_blob_updated => callback([ Session, :string ], :void),
@@ -51,7 +51,7 @@ module Spotify
51
51
  when :application_key
52
52
  if value.is_a?(String)
53
53
  pointer = FFI::MemoryPointer.new(:char, value.bytesize)
54
- pointer.write_bytes(value)
54
+ pointer.write_string(value)
55
55
  super(key, pointer)
56
56
  self[:application_key_size] = pointer.size
57
57
  else
@@ -8,36 +8,43 @@ module Spotify
8
8
  extend FFI::DataConverter
9
9
  native_type FFI::Type::POINTER
10
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)
11
+ class << self
12
+ # @return [Integer] bytesize of image ID pointers.
13
+ def size
14
+ 20
24
15
  end
25
16
 
26
- super(pointer, ctx)
27
- end
17
+ # Given a string, convert it to an image ID pointer.
18
+ #
19
+ # @param [#to_str, nil] value image id as a string
20
+ # @param ctx
21
+ # @return [FFI::Pointer] pointer to the image ID
22
+ def to_native(value, ctx)
23
+ value && begin
24
+ value = value.to_str
28
25
 
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
26
+ if value.bytesize != size
27
+ raise ArgumentError, "image id bytesize must be #{size}, was #{value.bytesize}"
28
+ end
29
+
30
+ pointer = FFI::MemoryPointer.new(:char, size)
31
+ pointer.write_string(value)
32
+ end
33
+ end
37
34
 
38
- # @see NulString.reference_required?
39
- def self.reference_required?
40
- true
35
+ # Given a pointer, read a {.size}-byte image ID from it.
36
+ #
37
+ # @param [FFI::Pointer] value
38
+ # @param ctx
39
+ # @return [String, nil] the image ID as a string, or nil
40
+ def from_native(value, ctx)
41
+ value.read_string(size) unless value.null?
42
+ end
43
+
44
+ # @see NulString.reference_required?
45
+ def reference_required?
46
+ true
47
+ end
41
48
  end
42
49
  end
43
50
  end
@@ -8,44 +8,33 @@ module Spotify
8
8
  extend FFI::DataConverter
9
9
  native_type FFI::Type::POINTER
10
10
 
11
- # Given either a String or nil, make an actual FFI::Pointer
12
- # of that value.
13
- #
14
- # @param [nil, #to_str] value
15
- # @param [Object] ctx
16
- # @return [FFI::Pointer]
17
- def self.to_native(value, ctx)
18
- return FFI::Pointer::NULL if value.nil?
19
-
20
- unless value.respond_to?(:to_str)
21
- raise TypeError, "#{value.inspect} cannot be converted to string"
11
+ class << self
12
+ # Given either a String or nil, make an actual FFI::Pointer
13
+ # of that value.
14
+ #
15
+ # @param [#to_str, nil] value
16
+ # @param ctx
17
+ # @return [FFI::Pointer]
18
+ def to_native(value, ctx)
19
+ value && FFI::MemoryPointer.from_string(value.to_str)
22
20
  end
23
21
 
24
- # This is alright since MRI and JRuby both
25
- # keep a strong reference to this pointer
26
- # inside the struct where it’s been assigned.
27
- FFI::MemoryPointer.from_string(value.to_str)
28
- end
29
-
30
- # Given a pointer, read out it’s string.
31
- #
32
- # @param [FFI::Pointer] value
33
- # @param [Object] ctx
34
- # @return [String, nil]
35
- def self.from_native(value, ctx)
36
- if value.null?
37
- nil
38
- else
39
- value.read_string
22
+ # Given a pointer, read out it’s string.
23
+ #
24
+ # @param [FFI::Pointer] value
25
+ # @param ctx
26
+ # @return [String, nil]
27
+ def from_native(value, ctx)
28
+ value.read_string unless value.null?
40
29
  end
41
- end
42
30
 
43
- # Used by FFI::StructLayoutField to know if this field
44
- # requires the reference to be maintained by FFI. If we
45
- # return false here, the MemoryPointer from to_native
46
- # will be garbage collected before the struct.
47
- def self.reference_required?
48
- true
31
+ # Used by FFI::StructLayoutField to know if this field
32
+ # requires the reference to be maintained by FFI. If we
33
+ # return false here, the MemoryPointer from to_native
34
+ # will be garbage collected before the struct.
35
+ def reference_required?
36
+ true
37
+ end
49
38
  end
50
39
  end
51
40
  end
@@ -8,33 +8,27 @@ module Spotify
8
8
  extend FFI::DataConverter
9
9
  native_type FFI::Type::STRING
10
10
 
11
- # Given a value, encodes it to UTF-8 no matter what.
12
- #
13
- # @note if the value is already in UTF-8, ruby does nothing
14
- # @note if the given value is falsy, default behaviour is used
15
- #
16
- # @param [String] value
17
- # @param ctx
18
- # @return [String] value, but in UTF-8 if it wasn’t already
19
- def self.to_native(value, ctx)
20
- if value
21
- value.encode('UTF-8')
22
- else
23
- super
11
+ class << self
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, nil] value
18
+ # @param ctx
19
+ # @return [String] value, but in UTF-8 if it wasn’t already
20
+ def to_native(value, ctx)
21
+ value && value.encode('UTF-8')
24
22
  end
25
- end
26
23
 
27
- # Given an original string, assume it is in UTF-8.
28
- #
29
- # @note NO error checking is made, the string is just forced to UTF-8
30
- # @param [String] value can be in any encoding
31
- # @param ctx
32
- # @return [String] value, but with UTF-8 encoding
33
- def self.from_native(value, ctx)
34
- if value
35
- value.force_encoding('UTF-8')
36
- else
37
- super
24
+ # Given an original string, assume it is in UTF-8.
25
+ #
26
+ # @note NO error checking is made, the string is just forced to UTF-8
27
+ # @param [String] value can be in any encoding
28
+ # @param ctx
29
+ # @return [String] value, but with UTF-8 encoding
30
+ def from_native(value, ctx)
31
+ value && value.force_encoding('UTF-8')
38
32
  end
39
33
  end
40
34
  end
data/lib/spotify/util.rb CHANGED
@@ -30,6 +30,7 @@ class << Spotify::API
30
30
  when /mswin/ then :windows
31
31
  else
32
32
  $stderr.puts "[WARN] You are running the Spotify gem on an unknown platform. (#{__FILE__}:#{__LINE__})"
33
+ $stderr.puts "[WARN] Platform: #{FFI::Platform::OS}"
33
34
  :unknown
34
35
  end
35
36
  end
@@ -1,9 +1,8 @@
1
1
  module Spotify
2
2
  # @note See README for versioning policy.
3
3
  # @return [String] Spotify gem version.
4
- VERSION = '12.3.0'
4
+ VERSION = '12.4.0'
5
5
 
6
- # @note May or may not work with other versions.
7
6
  # @return [String] Compatible libspotify API version.
8
7
  API_VERSION = '12.1.51'
9
8
  end
@@ -0,0 +1,29 @@
1
+ require 'spotify'
2
+ require 'benchmark'
3
+ require 'pry'
4
+
5
+ $__benchmarks__ = []
6
+
7
+ def bench(name, iterations = 100_000, &block)
8
+ file, line, _ = caller[0].split(':')
9
+ $__benchmarks__ << {
10
+ file: File.basename(file),
11
+ line: line,
12
+ name: name,
13
+ iterations: iterations,
14
+ block: block
15
+ }
16
+ end
17
+
18
+ at_exit do
19
+ Benchmark.bmbm do |x|
20
+ $__benchmarks__.each do |info|
21
+ benchname = "#{info[:file]}:#{info[:line]} #{info[:name]}"
22
+ x.report(benchname) { info[:iterations].times(&info[:block]) }
23
+ end
24
+ end
25
+ end
26
+
27
+ Dir["./**/*_bench.rb"].each do |benchmark|
28
+ require benchmark
29
+ end
@@ -0,0 +1,32 @@
1
+ module Spotify
2
+ class << API
3
+ def bogus_add_ref(x)
4
+ end
5
+
6
+ def bogus_release(x)
7
+ end
8
+ end
9
+
10
+ class Bogus < ManagedPointer
11
+ end
12
+ end
13
+
14
+ one = FFI::Pointer.new(1)
15
+ null = FFI::Pointer::NULL
16
+
17
+ album = Spotify::Album.new(one)
18
+ album.autorelease = false
19
+
20
+ bench "Album#to_native" do
21
+ Spotify::Album.to_native(album, nil)
22
+ end
23
+
24
+ bench "Bogus#retain" do
25
+ Spotify::Bogus.retain(one)
26
+ Spotify::Bogus.retaining_class.retain(one)
27
+ end
28
+
29
+ bench "Bogus#release" do
30
+ Spotify::Bogus.release(one)
31
+ Spotify::Bogus.retaining_class.release(one)
32
+ end