hallon 0.4.0 → 0.8.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 (52) hide show
  1. data/.gitmodules +3 -0
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG +30 -6
  4. data/README.markdown +7 -7
  5. data/Rakefile +70 -16
  6. data/examples/logging_in.rb +3 -3
  7. data/examples/printing_link_information.rb +1 -1
  8. data/examples/show_published_playlists_of_user.rb +92 -0
  9. data/hallon.gemspec +7 -4
  10. data/lib/hallon.rb +16 -4
  11. data/lib/hallon/album.rb +16 -6
  12. data/lib/hallon/album_browse.rb +78 -0
  13. data/lib/hallon/artist.rb +59 -0
  14. data/lib/hallon/artist_browse.rb +89 -0
  15. data/lib/hallon/base.rb +7 -0
  16. data/lib/hallon/enumerator.rb +64 -0
  17. data/lib/hallon/error.rb +8 -6
  18. data/lib/hallon/ext/spotify.rb +3 -3
  19. data/lib/hallon/image.rb +25 -12
  20. data/lib/hallon/link.rb +4 -4
  21. data/lib/hallon/linkable.rb +4 -2
  22. data/lib/hallon/observable.rb +1 -4
  23. data/lib/hallon/player.rb +130 -0
  24. data/lib/hallon/search.rb +128 -0
  25. data/lib/hallon/session.rb +226 -25
  26. data/lib/hallon/toplist.rb +83 -0
  27. data/lib/hallon/track.rb +62 -7
  28. data/lib/hallon/user.rb +6 -6
  29. data/lib/hallon/version.rb +1 -1
  30. data/spec/hallon/album_browse_spec.rb +20 -0
  31. data/spec/hallon/album_spec.rb +12 -7
  32. data/spec/hallon/artist_browse_spec.rb +29 -0
  33. data/spec/hallon/artist_spec.rb +32 -0
  34. data/spec/hallon/enumerator_spec.rb +106 -0
  35. data/spec/hallon/error_spec.rb +10 -0
  36. data/spec/hallon/hallon_spec.rb +5 -1
  37. data/spec/hallon/image_spec.rb +39 -25
  38. data/spec/hallon/linkable_spec.rb +12 -4
  39. data/spec/hallon/observable_spec.rb +5 -0
  40. data/spec/hallon/player_spec.rb +73 -0
  41. data/spec/hallon/search_spec.rb +80 -0
  42. data/spec/hallon/session_spec.rb +187 -6
  43. data/spec/hallon/toplist_spec.rb +40 -0
  44. data/spec/hallon/track_spec.rb +43 -8
  45. data/spec/mockspotify.rb +47 -0
  46. data/spec/mockspotify/.gitignore +5 -0
  47. data/spec/mockspotify/extconf.rb +5 -0
  48. data/spec/mockspotify/mockspotify_spec.rb +41 -0
  49. data/spec/spec_helper.rb +20 -0
  50. data/spec/support/common_objects.rb +84 -7
  51. metadata +72 -20
  52. data/lib/hallon/ext/object.rb +0 -16
@@ -35,6 +35,16 @@ describe Hallon::Error do
35
35
  it "should return the error symbol if it's ok" do
36
36
  subject.maybe_raise(0).should eq :ok
37
37
  end
38
+
39
+ # to account for the following case:
40
+ # session.wait_for(:timeout) { |param| Hallon::Error.maybe_raise(param) }
41
+ it "should return nil when the error is nil" do
42
+ subject.maybe_raise(nil).should eq nil
43
+ end
44
+
45
+ it "should return nil when the error is timeout" do
46
+ subject.maybe_raise(:timeout).should eq nil
47
+ end
38
48
  end
39
49
 
40
50
  describe "::table" do
@@ -4,7 +4,11 @@ describe Hallon do
4
4
  end
5
5
 
6
6
  describe "API_VERSION" do
7
- specify { Hallon::API_VERSION.should == 8 }
7
+ specify { Hallon::API_VERSION.should == 9 }
8
+ end
9
+
10
+ describe "API_BUILD" do
11
+ specify { Hallon::API_BUILD.should be_a String }
8
12
  end
9
13
 
10
14
  describe "URI" do
@@ -1,13 +1,10 @@
1
1
  # coding: utf-8
2
- describe Hallon::Image, :session => true do
2
+ describe Hallon::Image do
3
3
  describe "an image instance" do
4
- let(:image) do
5
- Hallon::Session.should_receive(:instance).and_return session
6
- Hallon::Image.new(mock_image)
7
- end
8
-
9
4
  subject { image }
5
+ let(:image) { mock_session { Hallon::Image.new(mock_image) } }
10
6
 
7
+ it { should be_loaded }
11
8
  its(:status) { should be :ok }
12
9
  its(:format) { should be :jpeg }
13
10
 
@@ -34,34 +31,51 @@ describe Hallon::Image, :session => true do
34
31
  image.to_link.should eq Hallon::Link.new("spotify:image:#{image.id}")
35
32
  end
36
33
  end
37
- end
38
34
 
39
- context "created from an url" do
40
- subject { Hallon::Image.new("http://open.spotify.com/image/c78f091482e555bd2ffacfcd9cbdc0714b221663", session) }
41
- its(:id) { should eq "c78f091482e555bd2ffacfcd9cbdc0714b221663" }
42
- end
35
+ describe "#==" do
36
+ it "should not fail given an object that does not respond to id" do
37
+ o = Object.new
38
+ def o.id
39
+ raise NoMethodError
40
+ end
43
41
 
44
- context "created from an uri" do
45
- subject { Hallon::Image.new("spotify:image:c78f091482e555bd2ffacfcd9cbdc0714b221663", session) }
46
- its(:id) { should eq "c78f091482e555bd2ffacfcd9cbdc0714b221663" }
42
+ image.should_not eq o
43
+ end
44
+ end
47
45
  end
48
46
 
49
- context "created from an id" do
50
- subject { Hallon::Image.new(mock_image_id, session) }
51
- its(:id) { should eq mock_image_hex }
52
- end
47
+ describe "instantiation" do
48
+ subject do
49
+ mock_session do
50
+ Hallon::Image.new(image_uri)
51
+ end
52
+ end
53
53
 
54
- context "created from a link" do
55
- subject { Hallon::Image.new(Hallon::Link.new("spotify:image:c78f091482e555bd2ffacfcd9cbdc0714b221663"), session) }
56
- its(:id) { should eq "c78f091482e555bd2ffacfcd9cbdc0714b221663" }
54
+ context "created from an url" do
55
+ let(:image_uri) { "http://open.spotify.com/image/c78f091482e555bd2ffacfcd9cbdc0714b221663" }
56
+ its(:id) { should eq "c78f091482e555bd2ffacfcd9cbdc0714b221663" }
57
+ end
58
+
59
+ context "created from an uri" do
60
+ let(:image_uri) { "spotify:image:c78f091482e555bd2ffacfcd9cbdc0714b221663" }
61
+ its(:id) { should eq "c78f091482e555bd2ffacfcd9cbdc0714b221663" }
62
+ end
63
+
64
+ context "created from an id" do
65
+ let(:image_uri) { mock_image_id }
66
+ its(:id) { should eq mock_image_hex }
67
+ end
68
+
69
+ context "created from a link" do
70
+ let(:image_uri) { Hallon::Link.new("spotify:image:c78f091482e555bd2ffacfcd9cbdc0714b221663") }
71
+ its(:id) { should eq "c78f091482e555bd2ffacfcd9cbdc0714b221663" }
72
+ end
57
73
  end
58
74
 
59
75
  describe "callbacks" do
60
- it "should trigger :load when loaded" do
61
- pending "waiting for mockspotify event system"
62
-
76
+ it "should trigger :load when loaded", :pending => true do
63
77
  uri = "spotify:image:c78f091482e555bd2ffacfcd9cbdc0714b221663"
64
- image = Hallon::Image.new(uri, session)
78
+ image = Hallon::Image.new(uri)
65
79
  image.should_not be_loaded
66
80
  image.should_receive(:trigger).with(:load).once
67
81
 
@@ -8,6 +8,7 @@ describe Hallon::Linkable do
8
8
  end
9
9
 
10
10
  let(:object) { klass.new }
11
+ let(:pointer) { FFI::Pointer.new(1) }
11
12
 
12
13
  before(:each) { Spotify.stub(:link_as_search) }
13
14
 
@@ -19,7 +20,7 @@ describe Hallon::Linkable do
19
20
 
20
21
  describe "#from_link" do
21
22
  it "should call the appropriate Spotify function" do
22
- Spotify.should_receive(:link_as_search)
23
+ Spotify.should_receive(:link_as_search).and_return(pointer)
23
24
 
24
25
  klass.from_link(:as_search)
25
26
  object.from_link 'spotify:search:moo'
@@ -29,13 +30,20 @@ describe Hallon::Linkable do
29
30
  Spotify.should_not_receive(:link_as_search)
30
31
 
31
32
  called = false
32
- klass.from_link(:as_search) { called = true }
33
+ klass.from_link(:as_search) { called = true and pointer }
33
34
  expect { object.from_link 'spotify:search:whatever' }.to change { called }
34
35
  end
35
36
 
36
37
  it "should pass extra parameters to the defining block" do
37
- klass.from_link(:search) { |link, *args| args }
38
- object.from_link("spotify:search:burgestrand", :cool, 5).should eq [:cool, 5]
38
+ passed_args = nil
39
+ klass.from_link(:search) { |link, *args| passed_args = args and pointer }
40
+ object.from_link("spotify:search:burgestrand", :cool, 5)
41
+ passed_args.should eq [:cool, 5]
42
+ end
43
+
44
+ it "should fail, given a null pointer" do
45
+ klass.from_link(:as_search)
46
+ expect { object.from_link FFI::Pointer.new(0) }.to raise_error(Hallon::Error)
39
47
  end
40
48
  end
41
49
  end
@@ -29,6 +29,11 @@ describe Hallon::Observable do
29
29
  subject.on(:a) { |event, *args| event }
30
30
  subject.trigger(:a).should eq nil
31
31
  end
32
+
33
+ it "should convert the event to a symbol" do
34
+ subject.on("a") { raise "hell" }
35
+ expect { subject.trigger(:a) }.to raise_error("hell")
36
+ end
32
37
  end
33
38
 
34
39
  describe "#trigger and #on" do
@@ -0,0 +1,73 @@
1
+ describe Hallon::Player do
2
+ let(:player) { Hallon::Player.new(session) }
3
+ let(:track) { Hallon::Track.new(mock_track) }
4
+
5
+ describe ".bitrates" do
6
+ it "should be a list of symbols in ascending order" do
7
+ Hallon::Player.bitrates.should eq %w[96k 160k 320k].map(&:to_sym)
8
+ end
9
+ end
10
+
11
+ describe "#bitrate=" do
12
+ it "should not fail horribly given a correct bitrate" do
13
+ player.bitrate = :'96k'
14
+ end
15
+
16
+ it "should fail horrible given a bad bitrate" do
17
+ expect { player.bitrate = :'100k' }.to raise_error(ArgumentError)
18
+ end
19
+ end
20
+
21
+ describe "#load" do
22
+ it "should load the given track" do
23
+ Spotify.should_receive(:session_player_load).with(session.pointer, track.pointer)
24
+ player.load(track)
25
+ end
26
+
27
+ 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/)
30
+ end
31
+ end
32
+
33
+ describe "#stop" do
34
+ it "should unload the currently loaded track" do
35
+ Spotify.should_receive(:session_player_unload).with(session.pointer)
36
+ player.stop
37
+ end
38
+ end
39
+
40
+ describe "#prefetch" do
41
+ it "should set up given track for prefetching" do
42
+ Spotify.should_receive(:session_player_prefetch).with(session.pointer, track.pointer)
43
+ player.prefetch(track)
44
+ end
45
+ end
46
+
47
+ describe "#play" do
48
+ it "should start playback of given track" do
49
+ Spotify.should_receive(:session_player_play).with(session.pointer, true)
50
+ player.play
51
+ end
52
+
53
+ 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
+ Spotify.should_receive(:session_player_play).with(session.pointer, true)
56
+ player.play(track)
57
+ end
58
+ end
59
+
60
+ describe "#pause" do
61
+ it "should stop playback of given track" do
62
+ Spotify.should_receive(:session_player_play).with(session.pointer, false)
63
+ player.pause
64
+ end
65
+ end
66
+
67
+ describe "#seek" do
68
+ it "should set up the currently loaded track at given position" do
69
+ Spotify.should_receive(:session_player_seek).with(session.pointer, 1000)
70
+ player.seek(1)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,80 @@
1
+ describe Hallon::Search do
2
+ subject { search }
3
+ let(:search) do
4
+ mock_session do
5
+ Spotify.should_receive(:search_create).and_return(mock_search)
6
+ Hallon::Search.new("my query")
7
+ end
8
+ end
9
+
10
+ describe ".new" do
11
+ it "should have some sane defaults" do
12
+ Spotify.should_receive(:search_create).with(session.pointer, "my query", 0, 25, 0, 25, 0, 25, anything, anything).and_return(mock_search)
13
+ mock_session { Hallon::Search.new("my query") }
14
+ end
15
+
16
+ it "should allow you to customize the defaults" do
17
+ Spotify.should_receive(:search_create).with(session.pointer, "my query", 1, 2, 3, 4, 5, 6, anything, anything).and_return(mock_search)
18
+ my_params = {
19
+ :tracks_offset => 1,
20
+ :tracks => 2,
21
+ :albums_offset => 3,
22
+ :albums => 4,
23
+ :artists_offset => 5,
24
+ :artists => 6
25
+ }
26
+
27
+ mock_session { Hallon::Search.new("my query", my_params) }
28
+ end
29
+ end
30
+
31
+ describe ".genres" do
32
+ subject { Hallon::Search.genres }
33
+
34
+ it { should include :jazz }
35
+ it { should be_a Array }
36
+ it { should_not be_empty }
37
+ end
38
+
39
+ describe ".search" do
40
+ subject do
41
+ Spotify.should_receive(:radio_search_create).and_return(mock_search)
42
+
43
+ mock_session do
44
+ search = Hallon::Search.radio(1990..2010, :jazz, :punk)
45
+ end
46
+ end
47
+
48
+ it "should simply ignore invalid genres" do
49
+ mock_session do
50
+ Spotify.should_receive(:radio_search_create).and_return(mock_search)
51
+ expect { Hallon::Search.radio(1990..2010, :bogus, :hocum) }.to_not raise_error
52
+ end
53
+ end
54
+
55
+ it { should be_loaded }
56
+ its(:error) { should eq :ok }
57
+ its('tracks.size') { should eq 2 }
58
+ # ^ should be enough
59
+ end
60
+
61
+ it { should be_a Hallon::Observable }
62
+ it { should be_loaded }
63
+ its(:error) { should eq :ok }
64
+ its(:query) { should eq "my query" }
65
+ its(:did_you_mean) { should eq "another thing" }
66
+
67
+ its('tracks.size') { should eq 2 }
68
+ its('tracks.to_a') { should eq instantiate(Hallon::Track, mock_track, mock_track_two) }
69
+ its('total_tracks') { should eq 1337 }
70
+
71
+ its('albums.size') { should eq 1 }
72
+ its('albums.to_a') { should eq instantiate(Hallon::Album, mock_album) }
73
+ its('total_albums') { should eq 42 }
74
+
75
+ its('artists.size') { should eq 2 }
76
+ its('artists.to_a') { should eq instantiate(Hallon::Artist, mock_artist, mock_artist_two) }
77
+ its('total_artists') { should eq 81104 }
78
+
79
+ its(:to_link) { should eq Hallon::Link.new("spotify:search:#{search.query}") }
80
+ end
@@ -2,13 +2,27 @@
2
2
  describe Hallon::Session do
3
3
  it { Hallon::Session.should_not respond_to :new }
4
4
 
5
- describe "#instance" do
5
+ describe ".initialize and .instance" do
6
+ before { Hallon.instance_eval { @__instance = nil } }
7
+ after { Hallon.instance_eval { @__instance = nil } }
8
+
9
+ it "should fail if calling instance before initialize" do
10
+ expect { Hallon.instance }.to raise_error
11
+ end
12
+
13
+ it "should fail if calling initialize twice" do
14
+ expect {
15
+ Hallon.initialize
16
+ Hallon.initialize
17
+ }.to raise_error
18
+ end
19
+ end
20
+
21
+ describe ".new" do
6
22
  it "should require an application key" do
7
23
  expect { Hallon::Session.send(:new) }.to raise_error(ArgumentError)
8
24
  end
9
- end
10
25
 
11
- describe "#new" do
12
26
  it "should fail on an invalid application key" do
13
27
  expect { create_session(false) }.to raise_error(Hallon::Error, /BAD_APPLICATION_KEY/)
14
28
  end
@@ -52,12 +66,50 @@ describe Hallon::Session do
52
66
  end
53
67
  end
54
68
 
55
- session.process_events_on(:bogus) { |e| e.inspect }.should eq ":bogus"
69
+ session.process_events_on(:bogus) { |e| e == :bogus }.should be_true
56
70
  end
57
71
 
58
72
  it "should time out if waiting for events too long" do
59
- session.should_receive(:process_events).once.and_return { session.trigger(:whatever) }
60
- session.process_events_on(:whatever) { |e| e.inspect }.should eq "nil"
73
+ session.should_receive(:process_events).once # and do nothing
74
+ session.wait_for(:ever) { |x| x }.should eq :timeout
75
+ end
76
+
77
+ it "should call the given block once before waiting" do
78
+ session.should_not_receive(:process_events)
79
+ session.process_events_on { true }
80
+ end
81
+ end
82
+
83
+ describe "#relogin" do
84
+ it "should raise if no credentials have been saved" do
85
+ expect { session.relogin }.to raise_error(Hallon::Error)
86
+ end
87
+
88
+ it "should not raise if credentials have been saved" do
89
+ session.login 'Kim', 'pass', true
90
+ session.logout
91
+ expect { session.relogin }.to_not raise_error
92
+ session.should be_logged_in
93
+ end
94
+ end
95
+
96
+ describe "#remembered_user" do
97
+ it "should be nil if no username is stored in libspotify" do
98
+ session.remembered_user.should eq nil
99
+ end
100
+
101
+ it "should retrieve the remembered username if stored" do
102
+ session.login 'Kim', 'pass', true
103
+ session.remembered_user.should eq 'Kim'
104
+ end
105
+ end
106
+
107
+ describe "#forget_me!" do
108
+ it "should forget the currently stored user credentials" do
109
+ session.login 'Kim', 'pass', true
110
+ session.remembered_user.should eq 'Kim'
111
+ session.forget_me!
112
+ session.remembered_user.should eq nil
61
113
  end
62
114
  end
63
115
 
@@ -68,6 +120,135 @@ describe Hallon::Session do
68
120
  end
69
121
  end
70
122
 
123
+ describe "#country" do
124
+ it "should retrieve the current sessions’ country as a string" do
125
+ session.country.should eq 'SE'
126
+ end
127
+ end
128
+
129
+ describe "#star and #unstar" do
130
+ it "should be able to star and unstar tracks" do
131
+ # for track#starred?
132
+ Hallon::Session.should_receive(:instance).exactly(6).times.and_return(session)
133
+
134
+ tracks = [mock_track, mock_track_two]
135
+ tracks.map! { |x| Hallon::Track.new(x) }
136
+ tracks.all?(&:starred?).should be_true # starred by default
137
+
138
+ session.unstar(*tracks)
139
+ tracks.none?(&:starred?).should be_true
140
+
141
+ session.star(tracks[0])
142
+ tracks[0].should be_starred
143
+ tracks[1].should_not be_starred
144
+ end
145
+ end
146
+
147
+ describe "#friends" do
148
+ let(:session) { mock_session_object }
149
+
150
+ it "should be an enumerator" do
151
+ session.friends.should be_an Hallon::Enumerator
152
+ end
153
+
154
+ it "should be empty if not logged in" do
155
+ Spotify.should_not_receive(:session_num_friends)
156
+ session.should_receive(:logged_in?).and_return(false)
157
+ session.friends.size.should eq 0
158
+ end
159
+
160
+ it "should be an enumerator of friends" do
161
+ session.login 'Kim', 'pass'
162
+ session.friends[0].should eq Hallon::User.new(mock_user)
163
+ end
164
+ end
165
+
166
+ describe "#cache_size" do
167
+ it "should default to 0" do
168
+ session.cache_size.should eq 0
169
+ end
170
+
171
+ it "should be settable" do
172
+ session.cache_size = 10
173
+ session.cache_size.should eq 10
174
+ end
175
+ end
176
+
177
+ describe ".connection_types" do
178
+ subject { Hallon::Session.connection_types }
179
+
180
+ it { should be_an Array }
181
+ it { should_not be_empty }
182
+ it { should include :wifi }
183
+ end
184
+
185
+ describe "#connection_type=" do
186
+ it "should fail given an invalid connection type" do
187
+ expect { session.connection_type = :bogus }.to raise_error(ArgumentError)
188
+ end
189
+
190
+ it "should succeed given a correct connection type" do
191
+ expect { session.connection_type = :wifi }.to_not raise_error
192
+ end
193
+ end
194
+
195
+ describe ".connection_types" do
196
+ subject { Hallon::Session.connection_rules }
197
+
198
+ it { should be_an Array }
199
+ it { should_not be_empty }
200
+ it { should include :network }
201
+ end
202
+
203
+ describe "#connection_rules=" do
204
+ it "should not fail given an invalid rule" do
205
+ expect { session.connection_rules = :lawly }.to_not raise_error
206
+ end
207
+
208
+ it "should succeed given correct connection thingy" do
209
+ expect { session.connection_rules = :allow_network, :allow_sync_over_mobile }.to_not raise_error
210
+ end
211
+
212
+ it "should combine given rules and feed to libspotify" do
213
+ Spotify.should_receive(:session_set_connection_rules).with(session.pointer, 5)
214
+ session.connection_rules = :network, :allow_sync_over_mobile
215
+ end
216
+ end
217
+
218
+ describe "offline settings readers" do
219
+ subject { mock_session_object }
220
+
221
+ its(:offline_time_left) { should eq 60 * 60 * 24 * 30 } # a month!
222
+ its(:offline_sync_status) { should eq mock_offline_sync_status_hash }
223
+ its(:offline_playlists_count) { should eq 7 }
224
+ its(:offline_tracks_to_sync) { should eq 3 }
225
+
226
+ specify "offline_sync_status when given false as return from libspotify" do
227
+ Spotify.should_receive(:offline_sync_get_status).and_return(false)
228
+ subject.offline_sync_status.should eq nil
229
+ end
230
+ end
231
+
232
+ describe "#offline_bitrate=" do
233
+ it "should not resync unless explicitly told so" do
234
+ Spotify.should_receive(:session_preferred_offline_bitrate).with(session.pointer, :'96k', false)
235
+ session.offline_bitrate = :'96k'
236
+ end
237
+
238
+ it "should resync if asked to" do
239
+ Spotify.should_receive(:session_preferred_offline_bitrate).with(session.pointer, :'96k', true)
240
+ session.offline_bitrate = :'96k', true
241
+ end
242
+
243
+ it "should fail given an invalid value" do
244
+ expect { session.offline_bitrate = :hocum }.to raise_error(ArgumentError)
245
+ end
246
+
247
+ it "should succeed given a proper value" do
248
+ expect { session.offline_bitrate = :'96k' }.to_not raise_error
249
+ end
250
+ end
251
+
71
252
  context "when logged in", :logged_in => true do
72
253
  it "should be logged in" do
73
254
  session.should be_logged_in