spotify 12.3.0 → 12.4.0

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.
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