hallon 0.12.0 → 0.13.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 (47) hide show
  1. data/CHANGELOG.md +43 -0
  2. data/Gemfile +3 -1
  3. data/README.markdown +41 -11
  4. data/Rakefile +12 -0
  5. data/examples/audio_driver.rb +55 -0
  6. data/examples/playing_audio.rb +10 -50
  7. data/hallon.gemspec +1 -1
  8. data/lib/hallon.rb +1 -1
  9. data/lib/hallon/album_browse.rb +22 -11
  10. data/lib/hallon/artist_browse.rb +64 -33
  11. data/lib/hallon/audio_driver.rb +138 -0
  12. data/lib/hallon/audio_queue.rb +110 -0
  13. data/lib/hallon/enumerator.rb +55 -16
  14. data/lib/hallon/error.rb +9 -2
  15. data/lib/hallon/image.rb +6 -5
  16. data/lib/hallon/link.rb +7 -4
  17. data/lib/hallon/linkable.rb +27 -0
  18. data/lib/hallon/observable/player.rb +18 -1
  19. data/lib/hallon/observable/session.rb +5 -1
  20. data/lib/hallon/player.rb +180 -54
  21. data/lib/hallon/playlist.rb +33 -20
  22. data/lib/hallon/playlist_container.rb +78 -64
  23. data/lib/hallon/search.rb +51 -33
  24. data/lib/hallon/session.rb +1 -1
  25. data/lib/hallon/toplist.rb +36 -18
  26. data/lib/hallon/track.rb +12 -6
  27. data/lib/hallon/version.rb +1 -1
  28. data/spec/hallon/artist_browse_spec.rb +3 -4
  29. data/spec/hallon/audio_queue_spec.rb +89 -0
  30. data/spec/hallon/enumerator_spec.rb +50 -25
  31. data/spec/hallon/error_spec.rb +2 -2
  32. data/spec/hallon/image_spec.rb +12 -5
  33. data/spec/hallon/link_spec.rb +8 -9
  34. data/spec/hallon/linkable_spec.rb +11 -0
  35. data/spec/hallon/observable/session_spec.rb +4 -0
  36. data/spec/hallon/player_spec.rb +118 -5
  37. data/spec/hallon/playlist_container_spec.rb +2 -2
  38. data/spec/hallon/playlist_spec.rb +32 -37
  39. data/spec/hallon/search_spec.rb +3 -3
  40. data/spec/hallon/user_spec.rb +0 -6
  41. data/spec/spec_helper.rb +10 -0
  42. data/spec/support/audio_driver_mock.rb +23 -0
  43. data/spec/support/context_stub_session.rb +5 -0
  44. data/spec/support/shared_for_linkable_objects.rb +22 -2
  45. metadata +26 -20
  46. data/lib/hallon/queue.rb +0 -71
  47. data/spec/hallon/queue_spec.rb +0 -35
@@ -42,8 +42,8 @@ describe Hallon::Error do
42
42
  subject.maybe_raise(nil).should eq nil
43
43
  end
44
44
 
45
- it "should return nil when the error is timeout" do
46
- subject.maybe_raise(:timeout).should eq nil
45
+ it "should return nil if the given symbol is also ignored" do
46
+ subject.maybe_raise(:is_loading, ignore: :is_loading).should eq nil
47
47
  end
48
48
  end
49
49
 
@@ -47,12 +47,19 @@ describe Hallon::Image do
47
47
  end
48
48
  end
49
49
 
50
- describe "#==" do
51
- it "should not fail given an object that does not respond to id" do
50
+ describe "#===" do
51
+ it "should compare ids (but only if other is an Image)" do
52
+ other = double
53
+ other.should_receive(:is_a?).with(Hallon::Image).and_return(true)
54
+ other.should_receive(:id).with(true).and_return(image.id(true))
55
+
56
+ image.should === other
57
+ image.should_not === double
58
+ end
59
+
60
+ it "should not call #id if other is not an image" do
52
61
  o = Object.new
53
- def o.id
54
- raise NoMethodError
55
- end
62
+ o.should_not_receive(:id)
56
63
 
57
64
  image.should_not eq o
58
65
  end
@@ -77,17 +77,16 @@ describe Hallon::Link do
77
77
  end
78
78
 
79
79
  describe "#==" do
80
- it "should compare using #to_str" do
81
- obj = Object.new
82
- obj.should_receive(:to_str).and_return(subject.to_str)
80
+ it "should compare using #to_str *if* other is a Link" do
81
+ objA = double
82
+ objA.should_not_receive(:to_str)
83
83
 
84
- subject.should eq obj
85
- end
84
+ objB = Hallon::Link.new(subject.to_str)
85
+ objB.should_receive(:pointer).and_return(null_pointer)
86
+ objB.should_receive(:to_str).and_return(subject.to_str)
86
87
 
87
- it "should not fail when #to_str is unavailable" do
88
- object = Object.new
89
- object.should_not respond_to :to_str
90
- subject.should_not eq object
88
+ subject.should_not eq objA
89
+ subject.should eq objB
91
90
  end
92
91
 
93
92
  it "should compare underlying pointers if #to_str is unavailable" do
@@ -35,6 +35,17 @@ describe Hallon::Linkable do
35
35
  end
36
36
  end
37
37
 
38
+ describe "#to_str" do
39
+ it "should convert object to a link, and then to a string" do
40
+ link = mock
41
+ link.should_receive(:to_str).and_return("spotify:link")
42
+ object.should_receive(:to_link).and_return(link)
43
+
44
+ klass.instance_eval { to_link(:from_user) }
45
+ object.to_str.should eq "spotify:link"
46
+ end
47
+ end
48
+
38
49
  describe "#from_link" do
39
50
  it "should call the appropriate Spotify function" do
40
51
  Spotify.should_receive(:link_as_search!).and_return(pointer)
@@ -68,6 +68,10 @@ describe Hallon::Observable::Session do
68
68
  it "should ensure the resulting value is an integer" do
69
69
  subject_callback.call(*input).should eq 0
70
70
  end
71
+
72
+ it "should not go ballistic when there is no audio data" do
73
+ subject_callback.call(a_pointer, format, FFI::Pointer::NULL, 0)
74
+ end
71
75
  end
72
76
 
73
77
  specification_for_callback "play_token_lost" do
@@ -1,6 +1,17 @@
1
+ # coding: utf-8
1
2
  describe Hallon::Player do
2
- let(:player) { Hallon::Player.new(session) }
3
- let(:track) { Hallon::Track.new(mock_track) }
3
+ let(:player) { Hallon::Player.new(session, AudioDriverMock) }
4
+ let(:track) { Hallon::Track.new(mock_track) }
5
+ let(:driver) { player.instance_variable_get('@driver') }
6
+ let(:queue) { player.instance_variable_get('@queue') } # black box? WHAT?
7
+
8
+ describe "events" do
9
+ %w(end_of_track streaming_error play_token_lost).each do |e|
10
+ it "should support listening for #{e}" do
11
+ expect { player.on(e) {} }.to_not raise_error
12
+ end
13
+ end
14
+ end
4
15
 
5
16
  describe ".bitrates" do
6
17
  it "should be a list of symbols in ascending order" do
@@ -24,9 +35,14 @@ describe Hallon::Player do
24
35
  player.load(track)
25
36
  end
26
37
 
38
+ it "should try to instantiate the track if it’s not a track" do
39
+ Spotify.should_receive(:session_player_load).with(session.pointer, track.pointer)
40
+ player.load(track.to_str)
41
+ end
42
+
27
43
  it "should raise an error if load was unsuccessful" do
28
- Spotify.should_receive(:session_player_load).and_return(:track_not_playable)
29
- expect { player.load(track) }.to raise_error(Hallon::Error, /TRACK_NOT_PLAYABLE/)
44
+ Spotify.should_receive(:session_player_load).and_return(:is_loading)
45
+ expect { player.load(track) }.to raise_error(Hallon::Error, /IS_LOADING/)
30
46
  end
31
47
  end
32
48
 
@@ -51,8 +67,8 @@ describe Hallon::Player do
51
67
  end
52
68
 
53
69
  it "should load and play given track if one was given" do
54
- Spotify.should_receive(:session_player_load).with(session.pointer, track.pointer)
55
70
  Spotify.should_receive(:session_player_play).with(session.pointer, true)
71
+ player.should_receive(:load).with(track)
56
72
  player.play(track)
57
73
  end
58
74
  end
@@ -78,4 +94,101 @@ describe Hallon::Player do
78
94
  player.volume_normalization?.should be_true
79
95
  end
80
96
  end
97
+
98
+ context "playing audio" do
99
+ before { session.class.send(:public, :trigger) }
100
+
101
+ it "should correctly report the status to libspotify" do
102
+ queue.should_receive(:size).and_return(7)
103
+ driver.should_receive(:drops).and_return(19)
104
+ session.trigger(:get_audio_buffer_stats).should eq [7, 19]
105
+ end
106
+
107
+ it "should assume no drops in audio if driver does not support checking" do
108
+ driver.should_receive(:respond_to?).with(:drops).and_return(false)
109
+ driver.should_not_receive(:drops)
110
+ session.trigger(:get_audio_buffer_stats).should eq [0, 0]
111
+ end
112
+
113
+ it "should tell the driver to start playback when commanded so by libspotify" do
114
+ driver.should_receive(:play)
115
+ session.trigger(:start_playback)
116
+ end
117
+
118
+ it "should tell the driver to stop playback when commanded so by libspotify" do
119
+ driver.should_receive(:pause)
120
+ session.trigger(:stop_playback)
121
+ end
122
+
123
+ it "should tell the driver to pause when pause is requested" do
124
+ driver.should_receive(:pause)
125
+ player.pause
126
+ end
127
+
128
+ it "should tell the driver to stop when stop is requested" do
129
+ queue.should_receive(:clear)
130
+ driver.should_receive(:stop)
131
+ player.stop
132
+ end
133
+
134
+ it "should not set the format on music delivery if it’s the same" do
135
+ queue.should_not_receive(:format=)
136
+ session.trigger(:music_delivery, queue.format, [1, 2, 3])
137
+ end
138
+
139
+ it "should set the format on music delivery if format changes" do
140
+ queue.should_receive(:format=).with(:new_format)
141
+ session.trigger(:music_delivery, :new_format, [1, 2, 3])
142
+ end
143
+
144
+ # why? it says so in the docs!
145
+ it "should clear the audio queue when receiving 0 audio frames" do
146
+ queue.should_receive(:clear)
147
+ session.trigger(:music_delivery, driver.format, [])
148
+ end
149
+
150
+ context "the output streaming" do
151
+ it "should feed music to the output stream if the format stays the same" do
152
+ Thread.stub(:start).and_return{ |*args, block| block[*args] }
153
+
154
+ player # create the Player
155
+ session.trigger(:music_delivery, queue.format, [1, 2, 3])
156
+
157
+ # it should block while player is stopped
158
+ begin
159
+ player.status.should be :stopped
160
+ Timeout::timeout(0.1) { driver.stream.call and "call was not blocking" }
161
+ rescue
162
+ :timeout
163
+ end.should eq :timeout
164
+
165
+ session.trigger(:start_playback)
166
+ player.status.should be :playing
167
+ driver.stream.call(1).should eq [1]
168
+ driver.stream.call(nil).should eq [2, 3]
169
+ end
170
+
171
+ it "should set the driver format and return no audio if audio format has changed" do
172
+ Thread.stub(:start).and_return{ |*args, block| block[*args] }
173
+
174
+ player # create the Player
175
+ session.trigger(:start_playback)
176
+ session.trigger(:music_delivery, :new_format, [1, 2, 3])
177
+
178
+ driver.should_receive(:format=).with(:new_format)
179
+ driver.stream.call.should be_nil
180
+
181
+ # driver.should_not_receive(:format)
182
+ driver.should_receive(:format).and_return(:new_format)
183
+ driver.stream.call.should eq [1, 2, 3]
184
+ end
185
+
186
+ it "should set the format on initialization" do
187
+ Thread.stub(:start).and_return{ |*args, block| block[*args] }
188
+ AudioDriverMock.any_instance.should_receive(:format=)
189
+ Hallon::AudioQueue.any_instance.should_receive(:format=)
190
+ player
191
+ end
192
+ end
193
+ end
81
194
  end
@@ -152,12 +152,12 @@ describe Hallon::PlaylistContainer do
152
152
  end
153
153
 
154
154
  it "should support retrieving folders from their start" do
155
- folder = Hallon::PlaylistContainer::Folder.new(container, 1..3)
155
+ folder = Hallon::PlaylistContainer::Folder.new(container.pointer, 1..3)
156
156
  container.contents[1].should eq folder
157
157
  end
158
158
 
159
159
  it "should support retrieving folders from their end" do
160
- folder = Hallon::PlaylistContainer::Folder.new(container, 1..3)
160
+ folder = Hallon::PlaylistContainer::Folder.new(container.pointer, 1..3)
161
161
  container.contents[3].should eq folder
162
162
  end
163
163
  end
@@ -4,35 +4,30 @@ require 'time'
4
4
  describe Hallon::Playlist do
5
5
  it_should_behave_like "a Linkable object" do
6
6
  let(:spotify_uri) { "spotify:user:burgestrand:playlist:07AX9IY9Hqmj1RqltcG0fi" }
7
- let(:described_class) do
8
- real_session = session
9
- Hallon::Playlist.dup.tap do |klass|
10
- klass.class_eval do
11
- define_method(:session) { real_session }
12
- end
13
- end
14
- end
7
+ let(:described_class) { Hallon::Playlist.tap { |o| stub_session(o.any_instance) } }
15
8
  end
16
9
 
17
- let(:playlist) { Hallon::Playlist.new(mock_playlist) }
18
10
  subject { playlist }
11
+ let(:playlist) do
12
+ Hallon::Playlist.new(mock_playlist)
13
+ end
19
14
 
20
15
  it { should be_loaded }
21
16
  it { should be_collaborative }
22
17
  it { should_not be_pending }
23
- it { mock_session { should be_in_ram } }
24
- it { mock_session { should_not be_available_offline } }
18
+ it { stub_session { should be_in_ram } }
19
+ it { stub_session { should_not be_available_offline } }
25
20
 
26
21
  its(:name) { should eq "Megaplaylist" }
27
22
  its(:owner) { should eq Hallon::User.new(mock_user) }
28
23
  its(:description) { should eq "Playlist description...?" }
29
- its(:image) { mock_session { should eq Hallon::Image.new(mock_image_id) } }
24
+ its(:image) { stub_session { should eq Hallon::Image.new(mock_image_id) } }
30
25
  its(:total_subscribers) { should eq 1000 }
31
- its(:sync_progress) { mock_session { should eq 67 } }
26
+ its(:sync_progress) { stub_session { should eq 67 } }
32
27
  its(:size) { should eq 4 }
33
28
 
34
29
  its('tracks.size') { should eq 4 }
35
- its('tracks.to_a') { should eq instantiate(Hallon::Playlist::Track, *(0...4).map { |index| [Spotify.playlist_track!(playlist.pointer, index), playlist, index] }) }
30
+ its('tracks.to_a') { should eq instantiate(Hallon::Playlist::Track, *(0...4).map { |index| [Spotify.playlist_track!(playlist.pointer, index), playlist.pointer, index] }) }
36
31
 
37
32
  describe "tracks#[]" do
38
33
  let(:track) { subject }
@@ -83,24 +78,24 @@ describe Hallon::Playlist do
83
78
  end
84
79
  end
85
80
 
86
- describe "#insert" do
81
+ describe "#insert", :stub_session do
87
82
  let(:tracks) { instantiate(Hallon::Track, mock_track, mock_track_two) }
88
83
 
89
84
  it "should add the given tracks to the playlist at correct index" do
90
85
  old_tracks = playlist.tracks.to_a
91
86
  new_tracks = old_tracks.insert(1, *tracks)
92
- mock_session { playlist.insert(1, tracks) }
87
+ playlist.insert(1, tracks)
93
88
 
94
89
  playlist.tracks.to_a.should eq new_tracks
95
90
  end
96
91
 
97
92
  it "should default to adding tracks at the end" do
98
- mock_session { playlist.insert(tracks) }
93
+ playlist.insert(tracks)
99
94
  playlist.tracks[2, 2].should eq tracks
100
95
  end
101
96
 
102
97
  it "should raise an error if the operation cannot be completed" do
103
- expect { mock_session { playlist.insert(-1, nil) } }.to raise_error(Hallon::Error)
98
+ expect { playlist.insert(-1, nil) }.to raise_error(Hallon::Error)
104
99
  end
105
100
  end
106
101
 
@@ -169,55 +164,55 @@ describe Hallon::Playlist do
169
164
  end
170
165
  end
171
166
 
172
- describe "#in_ram=" do
167
+ describe "#in_ram=", :stub_session do
173
168
  it "should set in_ram status" do
174
- mock_session do
175
- playlist.should be_in_ram
176
- playlist.in_ram = false
177
- playlist.should_not be_in_ram
178
- end
169
+ playlist.should be_in_ram
170
+ playlist.in_ram = false
171
+ playlist.should_not be_in_ram
179
172
  end
180
173
  end
181
174
 
182
- describe "#offline_mode=" do
175
+ describe "#offline_mode=", :stub_session do
183
176
  it "should set offline mode" do
184
- mock_session do
185
- playlist.should_not be_available_offline
186
- playlist.offline_mode = true
187
- playlist.should be_available_offline
188
- end
177
+ playlist.should_not be_available_offline
178
+ playlist.offline_mode = true
179
+ playlist.should be_available_offline
189
180
  end
190
181
  end
191
182
 
192
- describe "#update_subscribers" do
183
+ describe "#update_subscribers", :stub_session do
193
184
  it "should ask libspotify to update the subscribers" do
194
- expect { mock_session { playlist.update_subscribers } }.to_not raise_error
185
+ expect { playlist.update_subscribers }.to_not raise_error
186
+ end
187
+
188
+ it "should return the playlist" do
189
+ playlist.update_subscribers.should eq playlist
195
190
  end
196
191
  end
197
192
 
198
- describe "offline status methods" do
193
+ describe "offline status methods", :stub_session do
199
194
  def symbol_for(number)
200
195
  Spotify.enum_type(:playlist_offline_status)[number]
201
196
  end
202
197
 
203
198
  specify "#available_offline?" do
204
199
  Spotify.should_receive(:playlist_get_offline_status).and_return symbol_for(1)
205
- mock_session { should be_available_offline }
200
+ should be_available_offline
206
201
  end
207
202
 
208
203
  specify "#syncing?" do
209
204
  Spotify.should_receive(:playlist_get_offline_status).and_return symbol_for(2)
210
- mock_session { should be_syncing }
205
+ should be_syncing
211
206
  end
212
207
 
213
208
  specify "#waiting?" do
214
209
  Spotify.should_receive(:playlist_get_offline_status).and_return symbol_for(3)
215
- mock_session { should be_waiting }
210
+ should be_waiting
216
211
  end
217
212
 
218
213
  specify "#offline_mode?" do
219
214
  Spotify.should_receive(:playlist_get_offline_status).and_return symbol_for(0)
220
- mock_session { should_not be_offline_mode }
215
+ should_not be_offline_mode
221
216
  end
222
217
  end
223
218
  end
@@ -69,15 +69,15 @@ describe Hallon::Search do
69
69
 
70
70
  its('tracks.size') { should eq 2 }
71
71
  its('tracks.to_a') { should eq instantiate(Hallon::Track, mock_track, mock_track_two) }
72
- its('total_tracks') { should eq 1337 }
72
+ its('tracks.total') { should eq 1337 }
73
73
 
74
74
  its('albums.size') { should eq 1 }
75
75
  its('albums.to_a') { should eq instantiate(Hallon::Album, mock_album) }
76
- its('total_albums') { should eq 42 }
76
+ its('albums.total') { should eq 42 }
77
77
 
78
78
  its('artists.size') { should eq 2 }
79
79
  its('artists.to_a') { should eq instantiate(Hallon::Artist, mock_artist, mock_artist_two) }
80
- its('total_artists') { should eq 81104 }
80
+ its('artists.total') { should eq 81104 }
81
81
 
82
82
  its(:to_link) { should eq Hallon::Link.new("spotify:search:#{search.query}") }
83
83
  end
@@ -14,12 +14,6 @@ describe Hallon::User do
14
14
  end
15
15
  end
16
16
 
17
- describe "#to_link" do
18
- it "should return a Link for this user" do
19
- user.to_link.should eq "spotify:user:burgestrand"
20
- end
21
- end
22
-
23
17
  describe "#name" do
24
18
  it "should be the canonical name" do
25
19
  user.name.should eq "burgestrand"
data/spec/spec_helper.rb CHANGED
@@ -37,6 +37,16 @@ RSpec.configure do |config|
37
37
  yield
38
38
  end
39
39
 
40
+ def stub_session(target = nil)
41
+ if target
42
+ target.stub(:session).and_return(session)
43
+ else
44
+ Hallon::Session.stub(:instance).and_return(session)
45
+ end
46
+
47
+ target.tap { yield if block_given? }
48
+ end
49
+
40
50
  def pointer_array_with(*args)
41
51
  ary = FFI::MemoryPointer.new(:pointer, args.size)
42
52
  ary.write_array_of_pointer args