hallon 0.11.0 → 0.12.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 (68) hide show
  1. data/.yardopts +1 -0
  2. data/{CHANGELOG → CHANGELOG.md} +82 -27
  3. data/Gemfile +1 -0
  4. data/README.markdown +1 -1
  5. data/Rakefile +8 -6
  6. data/examples/adding_tracks_to_playlist.rb +3 -0
  7. data/examples/logging_in.rb +3 -0
  8. data/examples/playing_audio.rb +130 -0
  9. data/examples/printing_link_information.rb +3 -0
  10. data/examples/show_published_playlists_of_user.rb +5 -13
  11. data/hallon.gemspec +2 -2
  12. data/lib/hallon.rb +15 -0
  13. data/lib/hallon/album.rb +1 -1
  14. data/lib/hallon/album_browse.rb +4 -3
  15. data/lib/hallon/artist.rb +1 -1
  16. data/lib/hallon/artist_browse.rb +5 -4
  17. data/lib/hallon/base.rb +7 -2
  18. data/lib/hallon/error.rb +1 -1
  19. data/lib/hallon/ext/spotify.rb +26 -42
  20. data/lib/hallon/image.rb +7 -8
  21. data/lib/hallon/observable.rb +134 -62
  22. data/lib/hallon/observable/album_browse.rb +30 -0
  23. data/lib/hallon/observable/artist_browse.rb +31 -0
  24. data/lib/hallon/observable/image.rb +31 -0
  25. data/lib/hallon/observable/player.rb +13 -0
  26. data/lib/hallon/observable/playlist.rb +194 -0
  27. data/lib/hallon/observable/playlist_container.rb +74 -0
  28. data/lib/hallon/observable/post.rb +30 -0
  29. data/lib/hallon/observable/search.rb +29 -0
  30. data/lib/hallon/observable/session.rb +236 -0
  31. data/lib/hallon/observable/toplist.rb +30 -0
  32. data/lib/hallon/player.rb +8 -17
  33. data/lib/hallon/playlist.rb +11 -7
  34. data/lib/hallon/playlist_container.rb +11 -4
  35. data/lib/hallon/queue.rb +71 -0
  36. data/lib/hallon/search.rb +10 -7
  37. data/lib/hallon/session.rb +18 -21
  38. data/lib/hallon/toplist.rb +4 -3
  39. data/lib/hallon/user.rb +5 -5
  40. data/lib/hallon/version.rb +1 -1
  41. data/spec/hallon/album_browse_spec.rb +4 -0
  42. data/spec/hallon/artist_browse_spec.rb +4 -0
  43. data/spec/hallon/base_spec.rb +26 -9
  44. data/spec/hallon/hallon_spec.rb +0 -18
  45. data/spec/hallon/image_spec.rb +0 -1
  46. data/spec/hallon/link_spec.rb +14 -0
  47. data/spec/hallon/observable/album_browse_spec.rb +7 -0
  48. data/spec/hallon/observable/artist_browse_spec.rb +7 -0
  49. data/spec/hallon/observable/image_spec.rb +8 -0
  50. data/spec/hallon/observable/playlist_container_spec.rb +21 -0
  51. data/spec/hallon/observable/playlist_spec.rb +85 -0
  52. data/spec/hallon/observable/post_spec.rb +8 -0
  53. data/spec/hallon/observable/search_spec.rb +7 -0
  54. data/spec/hallon/observable/session_spec.rb +143 -0
  55. data/spec/hallon/observable/toplist_spec.rb +7 -0
  56. data/spec/hallon/observable_spec.rb +134 -65
  57. data/spec/hallon/playlist_container_spec.rb +24 -18
  58. data/spec/hallon/playlist_spec.rb +2 -0
  59. data/spec/hallon/queue_spec.rb +35 -0
  60. data/spec/hallon/session_spec.rb +4 -4
  61. data/spec/hallon/spotify_spec.rb +35 -9
  62. data/spec/mockspotify.rb +2 -3
  63. data/spec/spec_helper.rb +0 -1
  64. data/spec/support/common_objects.rb +27 -15
  65. data/spec/support/enumerable_comparison.rb +9 -0
  66. data/spec/support/shared_for_callbacks.rb +60 -0
  67. data/spec/support/shared_for_linkable_objects.rb +1 -1
  68. metadata +56 -20
@@ -0,0 +1,30 @@
1
+ module Hallon::Observable
2
+ # Callbacks related to the {Hallon::Toplist} object.
3
+ module Toplist
4
+ # Includes {Hallon::Observable} for you.
5
+ def self.extended(other)
6
+ other.send(:include, Hallon::Observable)
7
+ end
8
+
9
+ protected
10
+
11
+ # @return [Method] load callback
12
+ def initialize_callbacks
13
+ callback_for(:load)
14
+ end
15
+
16
+ # This callback is fired when the Image object is fully loaded.
17
+ #
18
+ # @example listening to this callback
19
+ # toplist.on(:load) do
20
+ # puts "the toplist has loaded!"
21
+ # end
22
+ #
23
+ # @yield [self]
24
+ # @yieldparam [Toplist] self
25
+ def load_callback(pointer, userdata)
26
+ trigger(pointer, :load)
27
+ end
28
+ end
29
+ end
30
+
@@ -3,12 +3,11 @@ module Hallon
3
3
  # A wrapper around Session for playing, stopping and otherwise
4
4
  # controlling the playback features of libspotify.
5
5
  #
6
- # @note This is very much a work in progress. Given Session still
7
- # takes care of all callbacks, and the callbacks themselves
8
- # must still be handled by means of Ruby FFI.
6
+ # @note This is very much a work in progress.
9
7
  # @see Session
10
8
  class Player
11
- include Observable
9
+ # meep?
10
+ extend Observable::Player
12
11
 
13
12
  # @return [Spotify::Pointer<Session>] session pointer
14
13
  attr_reader :pointer
@@ -25,7 +24,7 @@ module Hallon
25
24
  #
26
25
  # @example
27
26
  # Hallon::Player.new(session) do
28
- # on(:music_delivery) do |*frames|
27
+ # on(:music_delivery) do |format, frames|
29
28
  # end
30
29
  #
31
30
  # on(:start_playback) do
@@ -56,20 +55,12 @@ module Hallon
56
55
  @session = session
57
56
  @pointer = @session.pointer
58
57
 
59
- %w[start_playback stop_playback play_token_lost end_of_track streaming_error].each do |cb|
58
+ %w[
59
+ start_playback stop_playback play_token_lost end_of_track
60
+ streaming_error get_audio_buffer_stats music_delivery
61
+ ].each do |cb|
60
62
  @session.on(cb) { |*args| trigger(cb, *args) }
61
63
  end
62
-
63
- @session.on(:audio_buffer_stats) do |stats_ptr|
64
- stats = Spotify::AudioBufferStats.new(stats_ptr)
65
- samples, dropouts = trigger(:buffer_size?)
66
- stats[:samples] = samples || 0
67
- stats[:dropouts] = dropouts || 0
68
- end
69
-
70
- @session.on(:music_delivery) do |format, frames, num_frames|
71
- trigger(:music_delivery, format, frames, num_frames)
72
- end
73
64
  end
74
65
 
75
66
  # Set preferred playback bitrate.
@@ -6,9 +6,6 @@ module Hallon
6
6
  #
7
7
  # @see http://developer.spotify.com/en/libspotify/docs/group__playlist.html
8
8
  class Playlist < Base
9
- include Observable
10
- extend Linkable
11
-
12
9
  # Playlist::Track is a {Track} with additional information attached to it,
13
10
  # that is specific to the playlist it was created from. The returned track
14
11
  # is a snapshot of the information, so even if the underlying track moves,
@@ -80,6 +77,11 @@ module Hallon
80
77
  end
81
78
  end
82
79
 
80
+ extend Linkable
81
+
82
+ # CAN HAZ CALLBAKZ
83
+ extend Observable::Playlist
84
+
83
85
  from_link :playlist do |pointer|
84
86
  Spotify.playlist_create!(session.pointer, pointer)
85
87
  end
@@ -92,8 +94,10 @@ module Hallon
92
94
  def initialize(link)
93
95
  @pointer = to_pointer(link, :playlist)
94
96
 
95
- callbacks = Spotify::PlaylistCallbacks.create(self, @sp_callbacks = {})
96
- Spotify.playlist_add_callbacks(pointer, callbacks, nil)
97
+ subscribe_for_callbacks do |callbacks|
98
+ Spotify.playlist_remove_callbacks(pointer, callbacks, nil)
99
+ Spotify.playlist_add_callbacks(pointer, callbacks, nil)
100
+ end
97
101
  end
98
102
 
99
103
  # @return [Boolean] true if the playlist is loaded
@@ -167,8 +171,8 @@ module Hallon
167
171
  def name=(name)
168
172
  name = name.to_s.encode('UTF-8')
169
173
 
170
- unless name.length < 256
171
- raise ArgumentError, "name must be shorter than 256 characters (UTF-8)"
174
+ unless name.bytesize < 256
175
+ raise ArgumentError, "name must be shorter than 256 bytes"
172
176
  end
173
177
 
174
178
  unless name =~ /[^ ]/u
@@ -64,6 +64,11 @@ module Hallon
64
64
  end if other.is_a?(Folder)
65
65
  end
66
66
 
67
+ # @return [Enumerator<Playlist, Folder>] contents of this folder
68
+ def contents
69
+ container.contents[(@begin + 1)..(@end - 1)]
70
+ end
71
+
67
72
  # @return [Boolean] true if the folder has moved.
68
73
  def moved?
69
74
  Spotify.playlistcontainer_playlist_folder_id(container.pointer, @begin) != id or
@@ -71,7 +76,7 @@ module Hallon
71
76
  end
72
77
  end
73
78
 
74
- include Observable
79
+ extend Observable::PlaylistContainer
75
80
 
76
81
  # Wrap an existing PlaylistContainer pointer in an object.
77
82
  #
@@ -79,8 +84,10 @@ module Hallon
79
84
  def initialize(pointer)
80
85
  @pointer = to_pointer(pointer, :playlistcontainer)
81
86
 
82
- callbacks = Spotify::PlaylistContainerCallbacks.create(self, @sp_callbacs = {})
83
- Spotify.playlistcontainer_add_callbacks(pointer, callbacks, nil)
87
+ subscribe_for_callbacks do |callbacks|
88
+ Spotify.playlistcontainer_remove_callbacks(pointer, callbacks, nil)
89
+ Spotify.playlistcontainer_add_callbacks(pointer, callbacks, nil)
90
+ end
84
91
  end
85
92
 
86
93
  # @return [Boolean] true if the container is loaded.
@@ -224,7 +231,7 @@ module Hallon
224
231
  # @return [Boolean] true if the operation can be performed
225
232
  def can_move?(from, to)
226
233
  error = move_playlist(from, to, true)
227
- number, symbol = Error.disambiguate(error)
234
+ _, symbol = Error.disambiguate(error)
228
235
  symbol == :ok
229
236
  end
230
237
 
@@ -0,0 +1,71 @@
1
+ # coding: utf-8
2
+ require 'thread'
3
+
4
+ module Hallon
5
+ # Hallon::Queue is a non-blocking (well, not entirely) sized FIFO queue.
6
+ #
7
+ # You initialize the queue with a `max_size`, and then push data to it.
8
+ # For every push operation, the Queue will tell you how much of your data
9
+ # it could consume. If the queue becomes full, it won’t accept any more
10
+ # data (and will return 0 on the #push operation) until you pull some data
11
+ # out of it with #pop.
12
+ #
13
+ # Hallon::Queue is useful for handling {Hallon::Observable::Session#music_delivery_callback}.
14
+ #
15
+ # @example
16
+ # queue = Hallon::Queue.new(4)
17
+ # queue.push([1, 2]) # => 2
18
+ # queue.push([3]) # => 1
19
+ # queue.push([4, 5, 6]) # => 1
20
+ # queue.push([5, 6]) # => 0
21
+ # queue.pop(1) # => [1]
22
+ # queue.push([5, 6]) # => 1
23
+ # queue.pop # => [2, 3, 4, 5]
24
+ class Queue
25
+ attr_reader :max_size
26
+
27
+ # @param [Integer] max_size
28
+ def initialize(max_size)
29
+ @mutex = Mutex.new
30
+ @condv = ConditionVariable.new
31
+
32
+ @max_size = max_size
33
+ @samples = []
34
+ end
35
+
36
+ # @param [#take] data
37
+ # @return [Integer] how much of the data that was added to the queue
38
+ def push(samples)
39
+ synchronize do
40
+ can_accept = max_size - size
41
+ new_samples = samples.take(can_accept)
42
+
43
+ @samples.concat(new_samples)
44
+ @condv.signal
45
+
46
+ new_samples.size
47
+ end
48
+ end
49
+
50
+ # @note If the queue is empty, this operation will block until data is available.
51
+ # @param [Integer] num_samples max number of samples to pop off the queue
52
+ # @return [Array] data, where data.size might be less than num_samples but never more
53
+ def pop(num_samples = max_size)
54
+ synchronize do
55
+ @condv.wait(@mutex) while @samples.empty?
56
+ @samples.shift(num_samples)
57
+ end
58
+ end
59
+
60
+ # @return [Integer] number of samples in buffer
61
+ def size
62
+ @samples.size
63
+ end
64
+
65
+ private
66
+ # @yield (merely a wrapper over @mutex.synchronize)
67
+ def synchronize
68
+ @mutex.synchronize { return yield }
69
+ end
70
+ end
71
+ end
@@ -5,7 +5,7 @@ module Hallon
5
5
  #
6
6
  # @see http://developer.spotify.com/en/libspotify/docs/group__search.html
7
7
  class Search < Base
8
- include Observable
8
+ extend Observable::Search
9
9
 
10
10
  # @return [Array<Symbol>] a list of radio genres available for search
11
11
  def self.genres
@@ -35,10 +35,11 @@ module Hallon
35
35
 
36
36
  search = allocate
37
37
  search.instance_eval do
38
- @callback = proc { search.trigger(:load) }
39
- @pointer = Spotify.radio_search_create!(session.pointer, from_year, to_year, genres, @callback, nil)
38
+ subscribe_for_callbacks do |callback|
39
+ @pointer = Spotify.radio_search_create!(session.pointer, from_year, to_year, genres, callback, nil)
40
+ end
40
41
 
41
- raise FFI::NullPointerError, "radio search failed" if @pointer.null?
42
+ raise FFI::NullPointerError, "radio search failed" if pointer.null?
42
43
  end
43
44
 
44
45
  search
@@ -57,10 +58,12 @@ module Hallon
57
58
  # @see http://developer.spotify.com/en/libspotify/docs/group__search.html#gacf0b5e902e27d46ef8b1f40e332766df
58
59
  def initialize(query, options = {})
59
60
  o = Search.defaults.merge(options)
60
- @callback = proc { trigger(:load) }
61
- @pointer = Spotify.search_create!(session.pointer, query, o[:tracks_offset].to_i, o[:tracks].to_i, o[:albums_offset].to_i, o[:albums].to_i, o[:artists_offset].to_i, o[:artists].to_i, @callback, nil)
62
61
 
63
- raise FFI::NullPointerError, "search for “#{query}” failed" if @pointer.null?
62
+ subscribe_for_callbacks do |callback|
63
+ @pointer = Spotify.search_create!(session.pointer, query, o[:tracks_offset].to_i, o[:tracks].to_i, o[:albums_offset].to_i, o[:albums].to_i, o[:artists_offset].to_i, o[:artists].to_i, callback, nil)
64
+ end
65
+
66
+ raise FFI::NullPointerError, "search for “#{query}” failed" if pointer.null?
64
67
  end
65
68
 
66
69
  # @return [Boolean] true if the search has been fully loaded.
@@ -30,8 +30,8 @@ module Hallon
30
30
  undef :instance
31
31
  end
32
32
 
33
- # Session allows you to define your own callbacks.
34
- include Observable
33
+ # We have Session callbacks that you can listen to!
34
+ extend Observable::Session
35
35
 
36
36
  # Initializes the Spotify session. If you need to access the
37
37
  # instance at a later time, you can use {instance}.
@@ -105,22 +105,23 @@ module Hallon
105
105
  raise ArgumentError, "User-agent must be less than 256 bytes long"
106
106
  end
107
107
 
108
- # Set configuration, as well as callbacks
109
- config = Spotify::SessionConfig.new
110
- config[:api_version] = Hallon::API_VERSION
111
- config.application_key = appkey
112
- @options.each { |(key, value)| config.send(:"#{key}=", value) }
113
- config[:callbacks] = Spotify::SessionCallbacks.create(self, @sp_callbacks = {})
114
-
115
108
  # Default cache size is 0 (automatic)
116
109
  @cache_size = 0
117
110
 
118
- instance_eval(&block) if block_given?
111
+ subscribe_for_callbacks do |callbacks|
112
+ config = Spotify::SessionConfig.new
113
+ config[:api_version] = Hallon::API_VERSION
114
+ config.application_key = appkey
115
+ @options.each { |(key, value)| config.send(:"#{key}=", value) }
116
+ config[:callbacks] = callbacks
117
+
118
+ instance_eval(&block) if block_given?
119
119
 
120
- # You pass a pointer to the session pointer to libspotify >:)
121
- FFI::MemoryPointer.new(:pointer) do |p|
122
- Error::maybe_raise Spotify.session_create(config, p)
123
- @pointer = p.read_pointer
120
+ # You pass a pointer to the session pointer to libspotify >:)
121
+ FFI::MemoryPointer.new(:pointer) do |p|
122
+ Error::maybe_raise Spotify.session_create(config, p)
123
+ @pointer = p.read_pointer
124
+ end
124
125
  end
125
126
  end
126
127
 
@@ -154,7 +155,8 @@ module Hallon
154
155
  def process_events_on(*events)
155
156
  yield or protecting_handlers do
156
157
  channel = SizedQueue.new(1)
157
- on(*events) { |*args| channel << args }
158
+ block = proc { |*args| channel << args }
159
+ events.each { |event| on(event, &block) }
158
160
  on(:notify_main_thread) { channel << :notify }
159
161
 
160
162
  loop do
@@ -399,11 +401,6 @@ module Hallon
399
401
  status == :offline
400
402
  end
401
403
 
402
- # @return [String] string representation of the Session.
403
- def to_s
404
- "<#{self.class.name}:0x#{object_id.to_s(16)} status=#{status} @options=#{options.inspect}>"
405
- end
406
-
407
404
  private
408
405
  # Set starred status of given tracks.
409
406
  #
@@ -426,7 +423,7 @@ module Hallon
426
423
  # if the user does not have premium, libspotify will still fire logged_in as :ok,
427
424
  # but a few moments later it fires connection_error; waiting for both and checking
428
425
  # for errors on both hopefully circumvents this!
429
- wait_for(:logged_in, :connection_error) do |_, error|
426
+ wait_for(:logged_in, :connection_error) do |error|
430
427
  Error.maybe_raise(error)
431
428
  session.logged_in?
432
429
  end
@@ -6,7 +6,7 @@ module Hallon
6
6
  #
7
7
  # @see http://developer.spotify.com/en/libspotify/docs/group__toplist.html
8
8
  class Toplist < Base
9
- include Observable
9
+ extend Observable::Toplist
10
10
 
11
11
  # Create a Toplist browsing object.
12
12
  #
@@ -36,8 +36,9 @@ module Hallon
36
36
  region = to_country(region)
37
37
  end
38
38
 
39
- @callback = proc { trigger(:load) }
40
- @pointer = Spotify.toplistbrowse_create!(session.pointer, type, region, user, @callback, nil)
39
+ subscribe_for_callbacks do |callback|
40
+ @pointer = Spotify.toplistbrowse_create!(session.pointer, type, region, user, callback, nil)
41
+ end
41
42
  end
42
43
 
43
44
  # @return [Boolean] true if the toplist is loaded.
@@ -13,15 +13,15 @@ module Hallon
13
13
  #
14
14
  # @see http://developer.spotify.com/en/libspotify/docs/group__inbox.html
15
15
  class Post < Base
16
- include Observable
16
+ extend Observable::Post
17
17
 
18
18
  # @param [Spotify::Pointer<inbox>]
19
19
  def initialize(username, message, tracks, &block)
20
- @callback = proc { trigger(:load) }
20
+ ary = FFI::MemoryPointer.new(:pointer, tracks.length)
21
+ ary.write_array_of_pointer tracks.map(&:pointer)
21
22
 
22
- FFI::MemoryPointer.new(:pointer, tracks.length) do |ary|
23
- ary.write_array_of_pointer tracks.map(&:pointer)
24
- @pointer = Spotify.inbox_post_tracks!(session.pointer, username, ary, tracks.length, message, @callback, nil)
23
+ subscribe_for_callbacks do |callback|
24
+ @pointer = Spotify.inbox_post_tracks!(session.pointer, username, ary, tracks.length, message, callback, nil)
25
25
  end
26
26
  end
27
27
 
@@ -3,5 +3,5 @@ module Hallon
3
3
  # Current release version of Hallon
4
4
  #
5
5
  # @see http://semver.org/
6
- VERSION = [0, 11, 0].join('.')
6
+ VERSION = [0, 12, 0].join('.')
7
7
  end
@@ -5,6 +5,10 @@ describe Hallon::AlbumBrowse do
5
5
  Spotify.should_receive(:albumbrowse_create).and_return(null_pointer)
6
6
  expect { mock_session { Hallon::AlbumBrowse.new(mock_album) } }.to raise_error(FFI::NullPointerError)
7
7
  end
8
+
9
+ it "should raise an error given a non-album spotify pointer" do
10
+ expect { Hallon::AlbumBrowse.new(mock_artist) }.to raise_error(TypeError)
11
+ end
8
12
  end
9
13
 
10
14
  let(:browse) do
@@ -5,6 +5,10 @@ describe Hallon::ArtistBrowse do
5
5
  Spotify.should_receive(:artistbrowse_create).and_return(null_pointer)
6
6
  expect { mock_session { Hallon::ArtistBrowse.new(mock_artist) } }.to raise_error(FFI::NullPointerError)
7
7
  end
8
+
9
+ it "should raise an error given a non-album spotify pointer" do
10
+ expect { Hallon::ArtistBrowse.new(mock_album) }.to raise_error(TypeError)
11
+ end
8
12
  end
9
13
 
10
14
  let(:browse) do
@@ -2,34 +2,51 @@ describe Hallon::Base do
2
2
  let(:klass) do
3
3
  Class.new(Hallon::Base) do
4
4
  def initialize(pointer)
5
- @pointer = pointer
5
+ @pointer = to_pointer(pointer, :base) { |x| x }
6
6
  end
7
7
  end
8
8
  end
9
9
 
10
+ let(:base_pointer) do
11
+ Spotify.stub!(:base_add_ref, :base_release)
12
+ Spotify::Pointer.new(a_pointer, :base, true)
13
+ end
14
+
15
+ describe "#to_pointer" do
16
+ it "should not accept raw FFI pointers" do
17
+ expect { klass.new(a_pointer) }.to raise_error(TypeError)
18
+ end
19
+
20
+ it "should raise an error if given an invalid pointer type" do
21
+ expect { klass.new(mock_album) }.to raise_error(TypeError)
22
+ end
23
+ end
24
+
10
25
  describe ".from" do
11
26
  it "should return a new object if given pointer is not null" do
12
- a_pointer.should_receive(:null?).and_return(false)
13
- klass.from(a_pointer).should_not be_nil
27
+ klass.from(base_pointer).should_not be_nil
14
28
  end
15
29
 
16
30
  it "should return nil if given pointer is null" do
17
- a_pointer.should_receive(:null?).and_return(true)
18
- klass.from(a_pointer).should be_nil
31
+ klass.from(null_pointer).should be_nil
32
+ end
33
+
34
+ it "should return nil if given object is nil" do
35
+ klass.from(nil).should be_nil
19
36
  end
20
37
  end
21
38
 
22
39
  describe "#==" do
23
40
  it "should compare the pointers if applicable" do
24
- one = klass.new(a_pointer)
25
- two = klass.new(a_pointer)
41
+ one = klass.new(base_pointer)
42
+ two = klass.new(base_pointer)
26
43
 
27
44
  one.should eq two
28
45
  end
29
46
 
30
47
  it "should fall back to default object comparison" do
31
- one = klass.new(a_pointer)
32
- two = klass.new(a_pointer)
48
+ one = klass.new(base_pointer)
49
+ two = klass.new(base_pointer)
33
50
  two.stub(:respond_to?).and_return(false)
34
51
 
35
52
  one.should_not eq two