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
data/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ [HEAD][]
2
+ -----------
3
+ This release breaks backwards-compatibility, as functions will no
4
+ longer accept pointers of any other type of what they expect. This
5
+ means that you must wrap any pointers in a Spotify::ManagedPointer
6
+ of the appropriate type before using them to call API functions.
7
+
8
+ Callbacks now receive actual structs (by reference) in their params,
9
+ instead of a pointer which needed to be manually casted.
10
+
11
+ - [1fe1abed6] refactor retaining class for all managed pointer
12
+ - [af54c02e1] **naive type-checking for all spotify objects**
13
+ - [1d6939cd8] monkeypatch-add FFI::AbstractMemory#read_size_t
14
+ - [b9ce8941c] unlock mutex for Spotify::Error.explain (considered thread-safe)
15
+ - [fd2490728] add Spotify::Struct#to_s/to_h
16
+ - [1cbaf4a64] **callbacks now receive structs by reference instead of raw pointer**
17
+
1
18
  [v12.3.0][]
2
19
  -----------
3
20
  Lots of internal and external changes. You could almost say it’s a rewrite.
@@ -147,6 +164,8 @@ v0.0.0
147
164
  ------
148
165
  - release to register rubygems.org name
149
166
 
167
+ [HEAD]: https://github.com/Burgestrand/spotify/compare/v12.3.0...HEAD
168
+
150
169
  [v12.3.0]: https://github.com/Burgestrand/spotify/compare/v12.2.0...v12.3.0
151
170
  [v12.2.0]: https://github.com/Burgestrand/spotify/compare/v12.0.3...v12.2.0
152
171
  [v12.0.3]: https://github.com/Burgestrand/spotify/compare/v12.0.2...v12.0.3
data/Gemfile CHANGED
@@ -4,3 +4,4 @@ gemspec
4
4
  gem 'pry'
5
5
  gem 'yard'
6
6
  gem 'redcarpet'
7
+ gem "plaything", "~> 1.0"
data/README.markdown CHANGED
@@ -17,12 +17,12 @@ The Spotify gem has:
17
17
  - [100% API coverage][], including callback support. You’ll be able to use any function from the libspotify library.
18
18
  - [Automatic garbage collection][]. Piggybacking on Ruby’s GC to manage pointer lifecycle.
19
19
  - [Parallell function call protection][]. libspotify is not thread-safe, but Spotify protects you.
20
- - [Type conversion][]. Special pointers for every Spotify type.
20
+ - [Type conversion and type safety][]. Special pointers for every Spotify type, protecting you from accidental mix-ups.
21
21
 
22
22
  [100% API coverage]: http://rdoc.info/github/Burgestrand/spotify/master/Spotify/API
23
23
  [Automatic garbage collection]: http://rdoc.info/github/Burgestrand/spotify/master/Spotify/ManagedPointer
24
24
  [Parallell function call protection]: http://rdoc.info/github/Burgestrand/spotify/master/Spotify#method_missing-class_method
25
- [Type conversion]: http://rdoc.info/github/Burgestrand/spotify/master/Spotify/ManagedPointer
25
+ [Type conversion and type safety]: http://rdoc.info/github/Burgestrand/spotify/master/Spotify/ManagedPointer
26
26
 
27
27
  The Spotify gem is aimed at experienced developers
28
28
  --------------------------------------------------
@@ -32,14 +32,15 @@ you can write using it. The Spotify gem itself, however, has very few opinions.
32
32
 
33
33
  Known supporting libraries:
34
34
 
35
- - [Hallon](https://github.com/Burgestrand/Hallon), the original. Currently Hallon is simply
36
- a more convenient Spotify gem, written on top of the Spotify gem. It is diverging from its
37
- previous path, now towards a more focused and opinionated framework. If you’re unsure of
38
- what to use, start with the Hallon gem!
35
+ - [Hallon](https://github.com/Burgestrand/Hallon). Hallon attempted to be a one-gem-does-all,
36
+ at a higher abstraction level than the Spotify gem. It features a slightly more convenient
37
+ API for certain things, and requires no ruby FFI knowledge, but it is not really an expert
38
+ on any use case.
39
39
 
40
- Do not let this stop you! Despite my harsh words the Spotify API is well suited for experimentation,
41
- and it can be awfully fun to play with. If you need any assitance feel free to post a message
42
- on the mailing list: [ruby-hallon@googlegroups.com][] (<https://groups.google.com/d/forum/ruby-hallon>).
40
+ The Spotify API is well suited for experimentation, and it can be awfully fun to play with.
41
+ The examples/ directory contains example code for achieving certain tasks with the Spotify gem.
42
+
43
+ If you need any assitance feel free to post a message on the mailing list: [ruby-hallon@googlegroups.com][].
43
44
 
44
45
  Manually installing libspotify
45
46
  ------------------------------
@@ -63,7 +64,25 @@ Given a version `X.Y.Z`, each segment corresponds to:
63
64
 
64
65
  License
65
66
  -------
66
- X11 license, see the LICENSE document for details.
67
+ Copyright (c) 2012 Kim Burgestrand <kim@burgestrand.se>
68
+
69
+ Permission is hereby granted, free of charge, to any person obtaining a copy
70
+ of this software and associated documentation files (the "Software"), to deal
71
+ in the Software without restriction, including without limitation the rights
72
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
73
+ copies of the Software, and to permit persons to whom the Software is
74
+ furnished to do so, subject to the following conditions:
75
+
76
+ The above copyright notice and this permission notice shall be included in
77
+ all copies or substantial portions of the Software.
78
+
79
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
80
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
81
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
82
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
83
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
84
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
85
+ THE SOFTWARE.
67
86
 
68
87
  [semantic versioning (semver.org)]: http://semver.org/
69
88
  [ruby-hallon@googlegroups.com]: mailto:ruby-hallon@googlegroups.com
data/Rakefile CHANGED
@@ -20,9 +20,16 @@ rescue LoadError
20
20
  puts "WARN: YARD not available. You may install documentation dependencies via bundler."
21
21
  end
22
22
 
23
+ desc "Run code benchmarks"
24
+ task :bench do
25
+ sh "ruby", "spec/bench_helper.rb"
26
+ end
27
+
23
28
  desc "re-generate spec/api.h.xml"
24
29
  task :gen do
25
- sh 'gccxml spec/api.h -fxml=spec/api.h.xml'
30
+ Dir["spec/support/api-*.h"].each do |header|
31
+ sh "gccxml", header, "-fxml=#{header.sub('.h', '.xml')}"
32
+ end
26
33
  end
27
34
 
28
35
  task :console do
@@ -31,11 +38,11 @@ end
31
38
 
32
39
  require 'rspec/core/rake_task'
33
40
  RSpec::Core::RakeTask.new(:test_mac) do |spec|
34
- spec.ruby_opts = ['-r ./spec/mac-platform']
41
+ spec.ruby_opts = ['-r ./spec/support/mac-platform', '-W']
35
42
  end
36
43
 
37
44
  RSpec::Core::RakeTask.new(:test_linux) do |spec|
38
- spec.ruby_opts = ['-r ./spec/linux-platform']
45
+ spec.ruby_opts = ['-r ./spec/support/linux-platform', '-W']
39
46
  end
40
47
 
41
48
  desc "Run the tests for both Linux and Mac OS"
@@ -0,0 +1,4 @@
1
+ spotify_appkey.key
2
+ .spotify
3
+ spotify_tracefile.txt
4
+ Gemfile.lock
@@ -0,0 +1,15 @@
1
+ # How to run the examples:
2
+
3
+ 1. Install the dependencies. Preferrably via bundler with `bundle install`.
4
+ 2. Configure your environment variables SPOTIFY\_USERNAME, SPOTIFY\_PASSWORD.
5
+ 3. Download your binary application key from <https://developer.spotify.com/technologies/libspotify/keys/>.
6
+ 4. Put your `spotify_appkey.key` application key in the example directory.
7
+ 5. Run your specific example, e.g. `bundle exec ruby logging-in.rb`.
8
+
9
+ ## Available examples and what they do
10
+
11
+ - **logging-in.rb**
12
+
13
+ a fairly small example on how you could login with the spotify API. It
14
+ has some callbacks to provide more information on what happens during
15
+ the process.
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require "bundler/setup"
5
+ require "spotify"
6
+ require "logger"
7
+ require "json"
8
+ require "pry"
9
+ require "plaything"
10
+
11
+ Thread.abort_on_exception = true
12
+
13
+ # We use a logger to print some information on when things are happening.
14
+ $logger = Logger.new($stderr)
15
+ $logger.level = Logger::INFO
16
+
17
+ # libspotify supports callbacks, but they are not useful for waiting on
18
+ # operations (how they fire can be strange at times, and sometimes they
19
+ # might not fire at all). As a result, polling is the way to go.
20
+ def poll(session)
21
+ until yield
22
+ FFI::MemoryPointer.new(:int) do |ptr|
23
+ Spotify.session_process_events(session, ptr)
24
+ end
25
+ sleep(0.1)
26
+ end
27
+ end
28
+
29
+ # For making sure fetching configuration options fail with a useful error
30
+ # message when running the examples.
31
+ def env(name)
32
+ ENV.fetch(name) do
33
+ raise "Missing ENV[#{name}]. Please: export #{name}=\"your value\""
34
+ end
35
+ end
36
+
37
+ def play_track(uri)
38
+ link = Spotify.link_create_from_string(uri)
39
+ track = Spotify.link_as_track(link)
40
+ poll($session) { Spotify.track_is_loaded(track) }
41
+ Spotify.try(:session_player_play, $session, false)
42
+ Spotify.try(:session_player_load, $session, track)
43
+ Spotify.try(:session_player_play, $session, true)
44
+ end
45
+
46
+ class FrameReader
47
+ include Enumerable
48
+
49
+ def initialize(channels, sample_type, frames_count, frames_ptr)
50
+ @channels = channels
51
+ @sample_type = sample_type
52
+ @size = frames_count * @channels
53
+ # strip type information, we work with bytes
54
+ @pointer = FFI::Pointer.new(frames_ptr)
55
+ end
56
+
57
+ attr_reader :size
58
+
59
+ def each
60
+ return enum_for(__method__) unless block_given?
61
+
62
+ ffi_read = :"get_#{@sample_type}"
63
+ ffi_size = FFI.type_size(@sample_type)
64
+
65
+ (0...size).each do |index|
66
+ yield @pointer.public_send(ffi_read, index * ffi_size)
67
+ end
68
+ end
69
+ end
70
+
71
+ plaything = Plaything.new
72
+
73
+ #
74
+ # Global callback procs.
75
+ #
76
+ # They are global variables to protect from ever being garbage collected.
77
+ #
78
+ # You must not allow the callbacks to ever be garbage collected, or libspotify
79
+ # will hold information about callbacks that no longer exist, and crash upon
80
+ # calling the first missing callback. This is *very* important!
81
+
82
+ $session_callbacks = {
83
+ log_message: proc do |session, message|
84
+ $logger.info("session (log message)") { message }
85
+ end,
86
+
87
+ logged_in: proc do |session, error|
88
+ $logger.debug("session (logged in)") { Spotify::Error.explain(error) }
89
+ end,
90
+
91
+ logged_out: proc do |session|
92
+ $logger.debug("session (logged out)") { "logged out!" }
93
+ end,
94
+
95
+ streaming_error: proc do |session, error|
96
+ $logger.error("session (player)") { "streaming error %s" % Spotify::Error.explain(error) }
97
+ end,
98
+
99
+ start_playback: proc do |session|
100
+ $logger.debug("session (player)") { "start playback" }
101
+ plaything.play
102
+ end,
103
+
104
+ stop_playback: proc do |session|
105
+ $logger.debug("session (player)") { "stop playback" }
106
+ plaything.stop
107
+ end,
108
+
109
+ get_audio_buffer_stats: proc do |session, stats|
110
+ stats[:samples] = plaything.queue_size
111
+ stats[:stutter] = plaything.drops
112
+ $logger.debug("session (player)") { "queue size [#{stats[:samples]}, #{stats[:stutter]}]" }
113
+ end,
114
+
115
+ music_delivery: proc do |session, format, frames, num_frames|
116
+ if num_frames == 0
117
+ plaything.stop
118
+ $logger.debug("session (player)") { "music delivery audio discontuity" }
119
+ else
120
+ frames = FrameReader.new(format[:channels], format[:sample_type], num_frames, frames)
121
+ consumed_samples = plaything << frames
122
+ consumed_frames = consumed_samples / format[:channels]
123
+ $logger.debug("session (player)") { "music delivery #{consumed_frames} of #{num_frames}" }
124
+ consumed_frames
125
+ end
126
+ end,
127
+
128
+ end_of_track: proc do |session|
129
+ $end_of_track = true
130
+ $logger.debug("session (player)") { "end of track" }
131
+ plaything.stop
132
+ end,
133
+ }
134
+
135
+ #
136
+ # Main work code.
137
+ #
138
+
139
+ # You can read about what these session configuration options do in the
140
+ # libspotify documentation:
141
+ # https://developer.spotify.com/technologies/libspotify/docs/12.1.45/structsp__session__config.html
142
+ config = Spotify::SessionConfig.new({
143
+ api_version: Spotify::API_VERSION.to_i,
144
+ application_key: IO.read("./spotify_appkey.key"),
145
+ cache_location: ".spotify/",
146
+ settings_location: ".spotify/",
147
+ tracefile: "spotify_tracefile.txt",
148
+ user_agent: "spotify for ruby",
149
+ callbacks: Spotify::SessionCallbacks.new($session_callbacks),
150
+ })
151
+
152
+ $logger.info "Creating session."
153
+ FFI::MemoryPointer.new(Spotify::Session) do |ptr|
154
+ Spotify.try(:session_create, config, ptr)
155
+ $session = Spotify::Session.new(ptr.read_pointer)
156
+ end
157
+
158
+ $logger.info "Created! Logging in."
159
+ Spotify.session_login($session, env("SPOTIFY_USERNAME"), env("SPOTIFY_PASSWORD"), false, nil)
160
+
161
+ $logger.info "Log in requested. Waiting forever until logged in."
162
+ poll($session) { Spotify.session_connectionstate($session) == :logged_in }
163
+
164
+ $logger.info "Logged in as #{Spotify.session_user_name($session)}."
165
+
166
+ print "Spotify track URI: "
167
+ play_track gets.chomp
168
+
169
+ $logger.info "Playing track until end. Use ^C to exit."
170
+ poll($session) { $end_of_track }
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require 'spotify'
5
+ require 'logger'
6
+
7
+ # We use a logger to print some information on when things are happening.
8
+ $logger = Logger.new($stderr)
9
+
10
+ #
11
+ # Some utility.
12
+ #
13
+
14
+ # This method is just for convenience. Calling the process_events function
15
+ # is slightly cumbersome.
16
+ def process_events(session)
17
+ FFI::MemoryPointer.new(:int) do |ptr|
18
+ Spotify.session_process_events(session, ptr)
19
+ return Rational(ptr.read_int, 1000)
20
+ end
21
+ end
22
+
23
+ # libspotify supports callbacks, but they are not useful for waiting on
24
+ # operations (how they fire can be strange at times, and sometimes they
25
+ # might not fire at all). As a result, polling is the way to go.
26
+ def poll(session)
27
+ until yield
28
+ process_events(session)
29
+ sleep(0.01)
30
+ end
31
+ end
32
+
33
+ # For making sure fetching configuration options fail with a useful error
34
+ # message when running the examples.
35
+ def env(name)
36
+ ENV.fetch(name) do
37
+ raise "Missing ENV['#{name}']. Please: export #{name}='your value'"
38
+ end
39
+ end
40
+
41
+ #
42
+ # Global callback procs.
43
+ #
44
+ # They are global variables to protect from ever being garbage collected.
45
+ #
46
+ # You must not allow the callbacks to ever be garbage collected, or libspotify
47
+ # will hold information about callbacks that no longer exist, and crash upon
48
+ # calling the first missing callback. This is *very* important!
49
+
50
+ $session_callbacks = {
51
+ log_message: lambda do |session, message|
52
+ $logger.info('session (log message)') { message }
53
+ end,
54
+
55
+ logged_in: lambda do |session, error|
56
+ $logger.info('session (logged in)') { Spotify::Error.explain(error) }
57
+ end,
58
+
59
+ logged_out: lambda do |session|
60
+ $logger.info('session (logged out)') { 'logged out!' }
61
+ end,
62
+ }
63
+
64
+ #
65
+ # Main work code.
66
+ #
67
+
68
+ # You can read about what these session configuration options do in the
69
+ # libspotify documentation:
70
+ # https://developer.spotify.com/technologies/libspotify/docs/12.1.45/structsp__session__config.html
71
+ config = Spotify::SessionConfig.new({
72
+ api_version: Spotify::API_VERSION.to_i,
73
+ application_key: IO.read('./spotify_appkey.key'),
74
+ cache_location: ".spotify/",
75
+ settings_location: ".spotify/",
76
+ tracefile: "spotify_tracefile.txt",
77
+ user_agent: "spotify for ruby",
78
+ callbacks: Spotify::SessionCallbacks.new($session_callbacks),
79
+ })
80
+
81
+ $logger.info "Creating session."
82
+ FFI::MemoryPointer.new(Spotify::Session) do |ptr|
83
+ # Spotify.try is a special method. It raises a ruby exception if the returned spotify
84
+ # error code is an error.
85
+ Spotify.try(:session_create, config, ptr)
86
+ $session = Spotify::Session.new(ptr.read_pointer)
87
+ end
88
+
89
+ $logger.info "Created! Logging in."
90
+ Spotify.session_login($session, env('SPOTIFY_USERNAME'), env('SPOTIFY_PASSWORD'), false, nil)
91
+
92
+ $logger.info "Log in requested. Waiting forever until logged in."
93
+ poll($session) { Spotify.session_connectionstate($session) == :logged_in }
94
+
95
+ $logger.info "Logged in as #{Spotify.session_user_name($session)}."
data/lib/spotify.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
2
  require 'ffi'
3
+ require 'spotify/monkey_patches/ffi_pointer'
3
4
  require 'libspotify'
4
5
  require 'monitor'
5
6
 
@@ -28,7 +29,7 @@ module Spotify
28
29
  extend FFI::Library
29
30
 
30
31
  begin
31
- ffi_lib [LIBSPOTIFY_BIN, 'libspotify', '/Library/Frameworks/libspotify.framework/libspotify']
32
+ ffi_lib [LIBSPOTIFY_BIN, 'spotify', 'libspotify', '/Library/Frameworks/libspotify.framework/libspotify']
32
33
  rescue LoadError
33
34
  puts <<-ERROR.gsub(/^ */, '')
34
35
  Failed to load the `libspotify` library. It is possible that the libspotify gem
data/lib/spotify/error.rb CHANGED
@@ -4,6 +4,9 @@ module Spotify
4
4
  class << self
5
5
  # Explain a Spotify error with a descriptive message.
6
6
  #
7
+ # @note this method calls the API directly, since the
8
+ # underlying API call is considered thread-safe.
9
+ #
7
10
  # @param [Symbol, Integer] error
8
11
  # @return [String] a decriptive string of the error
9
12
  def explain(error)
@@ -11,7 +14,7 @@ module Spotify
11
14
 
12
15
  message = []
13
16
  message << "[#{symbol.to_s.upcase}]"
14
- message << Spotify.error_message(error)
17
+ message << Spotify::API.error_message(error)
15
18
  message << "(#{error})"
16
19
 
17
20
  message.join(' ')