hallon 0.12.0 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +43 -0
- data/Gemfile +3 -1
- data/README.markdown +41 -11
- data/Rakefile +12 -0
- data/examples/audio_driver.rb +55 -0
- data/examples/playing_audio.rb +10 -50
- data/hallon.gemspec +1 -1
- data/lib/hallon.rb +1 -1
- data/lib/hallon/album_browse.rb +22 -11
- data/lib/hallon/artist_browse.rb +64 -33
- data/lib/hallon/audio_driver.rb +138 -0
- data/lib/hallon/audio_queue.rb +110 -0
- data/lib/hallon/enumerator.rb +55 -16
- data/lib/hallon/error.rb +9 -2
- data/lib/hallon/image.rb +6 -5
- data/lib/hallon/link.rb +7 -4
- data/lib/hallon/linkable.rb +27 -0
- data/lib/hallon/observable/player.rb +18 -1
- data/lib/hallon/observable/session.rb +5 -1
- data/lib/hallon/player.rb +180 -54
- data/lib/hallon/playlist.rb +33 -20
- data/lib/hallon/playlist_container.rb +78 -64
- data/lib/hallon/search.rb +51 -33
- data/lib/hallon/session.rb +1 -1
- data/lib/hallon/toplist.rb +36 -18
- data/lib/hallon/track.rb +12 -6
- data/lib/hallon/version.rb +1 -1
- data/spec/hallon/artist_browse_spec.rb +3 -4
- data/spec/hallon/audio_queue_spec.rb +89 -0
- data/spec/hallon/enumerator_spec.rb +50 -25
- data/spec/hallon/error_spec.rb +2 -2
- data/spec/hallon/image_spec.rb +12 -5
- data/spec/hallon/link_spec.rb +8 -9
- data/spec/hallon/linkable_spec.rb +11 -0
- data/spec/hallon/observable/session_spec.rb +4 -0
- data/spec/hallon/player_spec.rb +118 -5
- data/spec/hallon/playlist_container_spec.rb +2 -2
- data/spec/hallon/playlist_spec.rb +32 -37
- data/spec/hallon/search_spec.rb +3 -3
- data/spec/hallon/user_spec.rb +0 -6
- data/spec/spec_helper.rb +10 -0
- data/spec/support/audio_driver_mock.rb +23 -0
- data/spec/support/context_stub_session.rb +5 -0
- data/spec/support/shared_for_linkable_objects.rb +22 -2
- metadata +26 -20
- data/lib/hallon/queue.rb +0 -71
- data/spec/hallon/queue_spec.rb +0 -35
data/spec/hallon/error_spec.rb
CHANGED
@@ -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
|
46
|
-
subject.maybe_raise(:
|
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
|
|
data/spec/hallon/image_spec.rb
CHANGED
@@ -47,12 +47,19 @@ describe Hallon::Image do
|
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
|
-
describe "
|
51
|
-
it "should
|
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
|
-
|
54
|
-
raise NoMethodError
|
55
|
-
end
|
62
|
+
o.should_not_receive(:id)
|
56
63
|
|
57
64
|
image.should_not eq o
|
58
65
|
end
|
data/spec/hallon/link_spec.rb
CHANGED
@@ -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
|
-
|
82
|
-
|
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.
|
85
|
-
|
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
|
-
|
88
|
-
|
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
|
data/spec/hallon/player_spec.rb
CHANGED
@@ -1,6 +1,17 @@
|
|
1
|
+
# coding: utf-8
|
1
2
|
describe Hallon::Player do
|
2
|
-
let(:player) { Hallon::Player.new(session) }
|
3
|
-
let(: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(:
|
29
|
-
expect { player.load(track) }.to raise_error(Hallon::Error, /
|
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)
|
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 {
|
24
|
-
it {
|
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) {
|
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) {
|
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
|
-
|
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
|
-
|
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 {
|
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
|
-
|
175
|
-
|
176
|
-
|
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
|
-
|
185
|
-
|
186
|
-
|
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 {
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
215
|
+
should_not be_offline_mode
|
221
216
|
end
|
222
217
|
end
|
223
218
|
end
|
data/spec/hallon/search_spec.rb
CHANGED
@@ -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('
|
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('
|
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('
|
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
|
data/spec/hallon/user_spec.rb
CHANGED
@@ -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
|