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.
- 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
|