hallon 0.11.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
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