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
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(' ')