hallon 0.12.0 → 0.13.0

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