hallon 0.16.0 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/.gitignore +2 -1
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +22 -1
  4. data/Gemfile +2 -2
  5. data/README.markdown +2 -2
  6. data/Rakefile +69 -42
  7. data/hallon.gemspec +1 -1
  8. data/lib/hallon.rb +3 -2
  9. data/lib/hallon/album.rb +6 -4
  10. data/lib/hallon/artist.rb +6 -4
  11. data/lib/hallon/audio_queue.rb +1 -1
  12. data/lib/hallon/base.rb +4 -0
  13. data/lib/hallon/blob.rb +6 -0
  14. data/lib/hallon/error.rb +10 -41
  15. data/lib/hallon/ext/spotify.rb +1 -146
  16. data/lib/hallon/image.rb +8 -0
  17. data/lib/hallon/linkable.rb +6 -0
  18. data/lib/hallon/loadable.rb +6 -0
  19. data/lib/hallon/observable.rb +1 -1
  20. data/lib/hallon/observable/playlist_container.rb +2 -2
  21. data/lib/hallon/observable/session.rb +34 -0
  22. data/lib/hallon/player.rb +7 -3
  23. data/lib/hallon/playlist.rb +5 -1
  24. data/lib/hallon/playlist_container.rb +9 -8
  25. data/lib/hallon/scrobbler.rb +103 -0
  26. data/lib/hallon/search.rb +1 -0
  27. data/lib/hallon/session.rb +69 -13
  28. data/lib/hallon/toplist.rb +1 -1
  29. data/lib/hallon/track.rb +2 -2
  30. data/lib/hallon/version.rb +1 -1
  31. data/spec/hallon/album_spec.rb +16 -0
  32. data/spec/hallon/artist_spec.rb +16 -0
  33. data/spec/hallon/base_spec.rb +1 -1
  34. data/spec/hallon/error_spec.rb +3 -3
  35. data/spec/hallon/hallon_spec.rb +1 -1
  36. data/spec/hallon/image_spec.rb +6 -0
  37. data/spec/hallon/observable/session_spec.rb +20 -0
  38. data/spec/hallon/scrobbler_spec.rb +119 -0
  39. data/spec/hallon/session_spec.rb +38 -4
  40. data/spec/hallon/spotify_spec.rb +0 -45
  41. data/spec/mockspotify.rb +6 -1
  42. data/spec/spec_helper.rb +4 -5
  43. metadata +59 -20
  44. data/spec/support/cover_me.rb +0 -7
data/lib/hallon/search.rb CHANGED
@@ -52,6 +52,7 @@ module Hallon
52
52
  end
53
53
  end
54
54
 
55
+ # Enumerates through all playlists of a search object.
55
56
  class PlaylistEnumerator < Enumerator
56
57
  size :search_num_playlists
57
58
 
@@ -2,6 +2,7 @@
2
2
  require 'singleton'
3
3
  require 'timeout'
4
4
  require 'thread'
5
+ require 'uri'
5
6
 
6
7
  module Hallon
7
8
  # The Session is fundamental for all communication with Spotify.
@@ -82,6 +83,7 @@ module Hallon
82
83
  # @option options [String] :cache_path ("") where to save cache files (`""` to disable)
83
84
  # @option options [String] :tracefile (nil) path to libspotify API tracefile (`nil` to disable)
84
85
  # @option options [String] :device_id (nil) device ID for offline synchronization (`nil` to disable)
86
+ # @option options [String] :proxy (nil) proxy URI (supports http, https, socks4, socks5)
85
87
  # @option options [Bool] :load_playlists (true) load playlists into RAM on startup
86
88
  # @option options [Bool] :compress_playlists (true) compress local copies of playlists
87
89
  # @option options [Bool] :cache_playlist_metadata (true) cache metadata for playlists locally
@@ -90,6 +92,14 @@ module Hallon
90
92
  # @raise [Hallon::Error] if `sp_session_create` fails
91
93
  # @see http://developer.spotify.com/en/libspotify/docs/structsp__session__config.html
92
94
  def initialize(appkey, options = {}, &block)
95
+ if options[:proxy]
96
+ proxy_uri = URI(options[:proxy])
97
+ options[:proxy_username] ||= proxy_uri.user
98
+ options[:proxy_password] ||= proxy_uri.password
99
+ proxy_uri.user = proxy_uri.password = nil
100
+ options[:proxy] = proxy_uri.to_s
101
+ end
102
+
93
103
  @options = {
94
104
  :user_agent => "Hallon",
95
105
  :settings_path => "tmp/hallon/",
@@ -98,6 +108,9 @@ module Hallon
98
108
  :compress_playlists => true,
99
109
  :cache_playlist_metadata => true,
100
110
  :device_id => nil,
111
+ :proxy => nil,
112
+ :proxy_username => nil,
113
+ :proxy_password => nil,
101
114
  :tracefile => nil,
102
115
  }.merge(options)
103
116
 
@@ -164,7 +177,7 @@ module Hallon
164
177
  # @note it also supports logging in via a credentials blob, if you pass
165
178
  # a Hallon::Blob(blob_string) as the password instead of the real password
166
179
  # @param [String] username
167
- # @param [String] password_or_blob
180
+ # @param [String] password your password, or user credentials blob
168
181
  # @param [Boolean] remember_me have libspotify remember credentials for {#relogin}
169
182
  # @return [Session]
170
183
  # @see login!
@@ -179,10 +192,10 @@ module Hallon
179
192
 
180
193
  # Login the remembered user (see {#login}).
181
194
  #
182
- # @raise [Hallon::Error] if no credentials are stored in libspotify
195
+ # @raise [Spotify::Error] if no credentials are stored in libspotify
183
196
  # @see #relogin!
184
197
  def relogin
185
- Error.maybe_raise Spotify.session_relogin(pointer)
198
+ Spotify.session_relogin!(pointer)
186
199
  end
187
200
 
188
201
  # Log in to Spotify using the given credentials.
@@ -219,7 +232,13 @@ module Hallon
219
232
  tap { wait_for(:logged_out) { logged_out? } }
220
233
  end
221
234
 
222
- # @return [String] username of the user stored in libspotify-remembered credentials.
235
+ # @return [String, nil] username of the currently logged in user.
236
+ def username
237
+ username = Spotify.session_user_name(pointer)
238
+ username unless username.nil? or username.empty?
239
+ end
240
+
241
+ # @return [String, nil] username of the user stored in libspotify-remembered credentials.
223
242
  def remembered_user
224
243
  bufflen = Spotify.session_remembered_user(pointer, nil, 0)
225
244
  FFI::Buffer.alloc_out(bufflen + 1) do |b|
@@ -254,15 +273,30 @@ module Hallon
254
273
  Spotify.session_connectionstate(pointer)
255
274
  end
256
275
 
257
- # Set session cache size in megabytes.
276
+ # @return [Boolean] true if the session is currently set to private.
277
+ def private?
278
+ Spotify.session_is_private_session(pointer)
279
+ end
280
+ alias_method :britney_spears_mode?, :private?
281
+
282
+ # Set private session.
258
283
  #
259
- # @param [Integer]
260
- # @return [Integer]
284
+ # @note mode is reverted to normal after some time without user activity,
285
+ # see official libspotify documentation for details.
286
+ # @param [Boolean] is_private
287
+ def private=(is_private)
288
+ Spotify.session_set_private_session(pointer, !! is_private)
289
+ end
290
+ alias_method :britney_spears_mode=, :private=
291
+
292
+ # Set session cache size.
293
+ #
294
+ # @param [Integer] size new session cache size, in megabytes.
261
295
  def cache_size=(size)
262
296
  Spotify.session_set_cache_size(pointer, @cache_size = size)
263
297
  end
264
298
 
265
- # @return [String] currently logged in users’ country.
299
+ # @return [String] currently logged in users country.
266
300
  def country
267
301
  coded = Spotify.session_user_country(pointer)
268
302
  country = ((coded >> 8) & 0xFF).chr
@@ -275,7 +309,10 @@ module Hallon
275
309
  # track = Hallon::Track.new("spotify:track:2LFQV2u6wXZmmySCWBkYGu")
276
310
  # session.star(track)
277
311
  #
278
- # @param [Track…]
312
+ # @note (see #unstar)
313
+ # @raise (see #unstar)
314
+ #
315
+ # @param [Track…] tracks
279
316
  # @return [Session]
280
317
  def star(*tracks)
281
318
  tap { tracks_starred(tracks, true) }
@@ -287,7 +324,13 @@ module Hallon
287
324
  # track = Hallon::Track.new("spotify:track:2LFQV2u6wXZmmySCWBkYGu")
288
325
  # session.unstar(track)
289
326
  #
290
- # @param [Track…]
327
+ # @note this method might raise a Spotify::Error, however when this might
328
+ # occur is not documented in libspotify (and I have yet to find any
329
+ # way to trigger it myself). it’s entirely possible that this method
330
+ # never returns an error, but we can’t know for sure.
331
+ #
332
+ # @raise [Spotify:Error] if libspotify reports an error (when this happens is unknown and undocumented)
333
+ # @param [Track…] tracks
291
334
  # @return [Session]
292
335
  def unstar(*tracks)
293
336
  tap { tracks_starred(tracks, false) }
@@ -295,6 +338,7 @@ module Hallon
295
338
 
296
339
  # Set the connection rules for this session.
297
340
  #
341
+ # @raise [ArgumentError] if given invalid connection rules
298
342
  # @param [Symbol, …] connection_rules
299
343
  # @see Session.connection_rules
300
344
  def connection_rules=(connection_rules)
@@ -307,6 +351,7 @@ module Hallon
307
351
 
308
352
  # Set the connection type for this session.
309
353
  #
354
+ # @raise [ArgumentError] if given invalid connection type
310
355
  # @param [Symbol] connection_type
311
356
  # @see Session.connection_types
312
357
  def connection_type=(connection_type)
@@ -345,15 +390,25 @@ module Hallon
345
390
 
346
391
  # Set preferred offline bitrate.
347
392
  #
348
- # @example
393
+ # @example setting offline bitrate without resync
394
+ # session.offline_bitrate = :'96k'
395
+ #
396
+ # @example setting offline bitrate and resync already-synced tracks
349
397
  # session.offline_bitrate = :'96k', true
350
398
  #
399
+ # @note under normal circumstances, ArgumentError is the error that will
400
+ # be raised on an invalid bitrate. However, if Hallon fails the type
401
+ # checking (for whatever reason), libspotify will itself return an
402
+ # error as well.
403
+ #
404
+ # @raise [ArgumentError] if given invalid bitrate
405
+ # @raise [Spotify::Error] if libspotify does not accept the given bitrate (see note)
351
406
  # @param [Symbol] bitrate
352
407
  # @param [Boolean] resync (default: false)
353
408
  # @see Player.bitrates
354
409
  def offline_bitrate=(bitrate)
355
410
  bitrate, resync = Array(bitrate)
356
- Spotify.session_preferred_offline_bitrate(pointer, bitrate, !! resync)
411
+ Spotify.session_preferred_offline_bitrate!(pointer, bitrate, !! resync)
357
412
  end
358
413
 
359
414
  # @note Returns nil when no user is logged in.
@@ -397,12 +452,13 @@ module Hallon
397
452
  private
398
453
  # Set starred status of given tracks.
399
454
  #
455
+ # @raise [Spotify::Error] … maybe, it’s undocumented in libspotify, who knows?
400
456
  # @param [Array<Track>] tracks
401
457
  # @param [Boolean] starred
402
458
  def tracks_starred(tracks, starred)
403
459
  FFI::MemoryPointer.new(:pointer, tracks.size) do |ptr|
404
460
  ptr.write_array_of_pointer tracks.map(&:pointer)
405
- Spotify.track_set_starred(pointer, ptr, tracks.size, starred)
461
+ Spotify.track_set_starred!(pointer, ptr, tracks.size, starred)
406
462
  end
407
463
  end
408
464
 
@@ -114,7 +114,7 @@ module Hallon
114
114
  # Convert a given two-character region to a Spotify
115
115
  # compliant region (encoded in a 16bit integer).
116
116
  #
117
- # @param [#to_s]
117
+ # @param [#to_s] region
118
118
  # @return [Integer]
119
119
  def to_country(region)
120
120
  code = region.to_s.upcase
data/lib/hallon/track.rb CHANGED
@@ -210,9 +210,9 @@ module Hallon
210
210
 
211
211
  # Set {#starred?} status of current track.
212
212
  #
213
- # @note It’ll set the starred status for the current Session.instance.
213
+ # @note (see Session#star)
214
+ # @raise (see Session#star)
214
215
  # @param [Boolean] starred
215
- # @return [Boolean]
216
216
  def starred=(starred)
217
217
  starred ? session.star(self) : session.unstar(self)
218
218
  end
@@ -3,5 +3,5 @@ module Hallon
3
3
  # Current release version of Hallon
4
4
  #
5
5
  # @see http://semver.org/
6
- VERSION = [0, 16, 0].join('.')
6
+ VERSION = [0, 17, 0].join('.')
7
7
  end
@@ -80,6 +80,14 @@ describe Hallon::Album do
80
80
  it "should be nil if there is no image" do
81
81
  empty_album.cover.should be_nil
82
82
  end
83
+
84
+ it "should support specifying size" do
85
+ album.cover(:large).should eq Hallon::Image.new(mock_image_id)
86
+ end
87
+
88
+ it "should raise an error when given an invalid size" do
89
+ expect { album.cover(:lawl) }.to raise_error(ArgumentError)
90
+ end
83
91
  end
84
92
 
85
93
  describe "#cover_link" do
@@ -90,5 +98,13 @@ describe Hallon::Album do
90
98
  it "should be nil if there is no image" do
91
99
  empty_album.cover_link.should be_nil
92
100
  end
101
+
102
+ it "should support specifying size" do
103
+ album.cover_link(:large).should eq Hallon::Link.new("spotify:image:3ad93423add99766e02d563605c6e76ed2b0e400")
104
+ end
105
+
106
+ it "should raise an error when given an invalid size" do
107
+ expect { album.cover_link(:lawl) }.to raise_error(ArgumentError)
108
+ end
93
109
  end
94
110
  end
@@ -54,6 +54,14 @@ describe Hallon::Artist do
54
54
  it "should be nil if an image is not available" do
55
55
  empty_artist.portrait.should be_nil
56
56
  end
57
+
58
+ it "should support specifying size" do
59
+ artist.portrait(:large).should eq Hallon::Image.new(mock_image_id)
60
+ end
61
+
62
+ it "should raise an error when given an invalid size" do
63
+ expect { artist.portrait(:lawl) }.to raise_error(ArgumentError)
64
+ end
57
65
  end
58
66
 
59
67
  describe "#portrait_link" do
@@ -64,5 +72,13 @@ describe Hallon::Artist do
64
72
  it "should be nil if an image is not available" do
65
73
  empty_artist.portrait_link.should be_nil
66
74
  end
75
+
76
+ it "should support specifying size" do
77
+ artist.portrait_link(:large).should eq Hallon::Link.new(mock_image_uri)
78
+ end
79
+
80
+ it "should raise an error when given an invalid size" do
81
+ expect { artist.portrait_link(:lawl) }.to raise_error(ArgumentError)
82
+ end
67
83
  end
68
84
  end
@@ -8,7 +8,7 @@ describe Hallon::Base do
8
8
  end
9
9
 
10
10
  let(:base_pointer) do
11
- Spotify.stub!(:base_add_ref, :base_release)
11
+ Spotify.stub(:base_add_ref! => nil, :base_release! => nil)
12
12
  Spotify::Pointer.new(a_pointer, :base, true)
13
13
  end
14
14
 
@@ -1,7 +1,7 @@
1
1
  describe Hallon::Error do
2
2
  subject { described_class }
3
3
 
4
- it { should <= RuntimeError }
4
+ it { should <= StandardError }
5
5
 
6
6
  describe ".disambiguate" do
7
7
  it "should not fail on invalid numbers" do
@@ -15,11 +15,11 @@ describe Hallon::Error do
15
15
 
16
16
  describe ".explain" do
17
17
  it "should work properly given an integer" do
18
- subject.explain(0).should eq 'sp_error: 0'
18
+ subject.explain(0).should match 'sp_error: 0'
19
19
  end
20
20
 
21
21
  it "should work properly given a symbol" do
22
- subject.explain(:bad_api_version).should eq 'sp_error: 1'
22
+ subject.explain(:bad_api_version).should match 'sp_error: 1'
23
23
  end
24
24
  end
25
25
 
@@ -5,7 +5,7 @@ describe Hallon do
5
5
  end
6
6
 
7
7
  describe "API_VERSION" do
8
- specify { Hallon::API_VERSION.should == 11 }
8
+ specify { Hallon::API_VERSION.should == 12 }
9
9
  end
10
10
 
11
11
  describe "API_BUILD" do
@@ -18,6 +18,12 @@ describe Hallon::Image do
18
18
 
19
19
  specify { image.should be_a Hallon::Loadable }
20
20
 
21
+ describe ".sizes" do
22
+ it "should list all sizes" do
23
+ Hallon::Image.sizes.should eq [:normal, :small, :large]
24
+ end
25
+ end
26
+
21
27
  describe "#loaded?" do
22
28
  it "returns true when the image is loaded" do
23
29
  image.should be_loaded
@@ -144,4 +144,24 @@ describe Hallon::Observable::Session do
144
144
  let(:input) { [a_pointer, :ok] }
145
145
  let(:output) { [:ok] }
146
146
  end
147
+
148
+ specification_for_callback "credentials_blob_updated" do
149
+ let(:input) { [a_pointer, "credentials"] }
150
+ let(:output) { ["credentials"] }
151
+ end
152
+
153
+ specification_for_callback "connectionstate_updated" do
154
+ let(:input) { [a_pointer] }
155
+ let(:output) { [] }
156
+ end
157
+
158
+ specification_for_callback "scrobble_error" do
159
+ let(:input) { [a_pointer, :ok] }
160
+ let(:output) { [:ok] }
161
+ end
162
+
163
+ specification_for_callback "private_session_mode_changed" do
164
+ let(:input) { [a_pointer, true] }
165
+ let(:output) { [true] }
166
+ end
147
167
  end
@@ -0,0 +1,119 @@
1
+ describe Hallon::Scrobbler do
2
+ let(:scrobbling) { Hallon::Scrobbler.new(:facebook) }
3
+
4
+ describe ".providers" do
5
+ it "returns a list of social providers" do
6
+ Hallon::Scrobbler.providers.should include :facebook
7
+ end
8
+ end
9
+
10
+ describe "#initialize" do
11
+ it "raises an error if given an invalid social provider" do
12
+ expect { Hallon::Scrobbler.new(:invalid_provider) }.to raise_error(ArgumentError, /social provider/)
13
+ end
14
+ end
15
+
16
+ describe "#provider" do
17
+ it "returns the social provider the scrobbler was instantiated with" do
18
+ scrobbling.provider.should eq :facebook
19
+ end
20
+ end
21
+
22
+ describe "#credentials=" do
23
+ it "sets the credentials for the scrobbler provider" do
24
+ Spotify.should_receive(:session_set_social_credentials).with(anything, :facebook, "Kim", "password").and_return(:ok)
25
+ scrobbling.credentials = "Kim", "password"
26
+ end
27
+ end
28
+
29
+ describe "#possible?" do
30
+ it "returns true if scrobbling is possible" do
31
+ Spotify.mocksp_session_set_is_scrobbling_possible(session.pointer, scrobbling.provider, true)
32
+ scrobbling.should be_possible
33
+ end
34
+
35
+ it "returns false if scrobbling is not possible" do
36
+ Spotify.mocksp_session_set_is_scrobbling_possible(session.pointer, scrobbling.provider, false)
37
+ scrobbling.should_not be_possible
38
+ end
39
+
40
+ it "raises an error if libspotify does not like us" do
41
+ Spotify.should_receive(:session_is_scrobbling_possible).and_return(:invalid_indata)
42
+ expect { scrobbling.possible? }.to raise_error(Spotify::Error)
43
+ end
44
+ end
45
+
46
+ describe "#enabled=" do
47
+ it "sets the local scrobbling" do
48
+ scrobbling.should_not be_enabled
49
+ scrobbling.enabled = true
50
+ scrobbling.should be_enabled
51
+ scrobbling.enabled = false
52
+ scrobbling.should_not be_enabled
53
+ end
54
+
55
+ it "raises an error if setting scrobbling state fails" do
56
+ Spotify.should_receive(:session_set_scrobbling).and_return(:invalid_indata)
57
+ expect { scrobbling.enabled = true }.to raise_error(Spotify::Error, /INVALID_INDATA/)
58
+ end
59
+ end
60
+
61
+ describe "#enabled?" do
62
+ before do
63
+ Spotify.should_receive(:session_is_scrobbling).and_return do |session, provider, buffer|
64
+ buffer.write_int(Spotify.enum_value(state_symbol))
65
+ end
66
+ end
67
+
68
+ context "if the state is locally enabled" do
69
+ let(:state_symbol) { :local_enabled }
70
+
71
+ it "returns true" do
72
+ scrobbling.should be_enabled
73
+ end
74
+ end
75
+
76
+ context "if the state is locally disabled" do
77
+ let(:state_symbol) { :local_disabled }
78
+
79
+ it "returns false" do
80
+ scrobbling.should_not be_enabled
81
+ end
82
+ end
83
+
84
+ context "if the state is globally enabled" do
85
+ let(:state_symbol) { :global_enabled }
86
+
87
+ it "returns true" do
88
+ scrobbling.should be_enabled
89
+ end
90
+ end
91
+
92
+ context "if the state is globally disabled" do
93
+ let(:state_symbol) { :global_disabled }
94
+
95
+ it "returns false" do
96
+ scrobbling.should_not be_enabled
97
+ end
98
+ end
99
+ end
100
+
101
+ describe "#reset" do
102
+ def state(scrobbler)
103
+ session = scrobbling.send(:session)
104
+ state = nil
105
+ FFI::Buffer.alloc_out(:int) do |buffer|
106
+ Spotify.session_is_scrobbling!(session.pointer, scrobbling.provider, buffer)
107
+ state = buffer.read_int
108
+ end
109
+ Spotify.enum_type(:scrobbling_state)[state]
110
+ end
111
+
112
+ it "sets the local scrobbling state to use the global state" do
113
+ scrobbling.enabled = true
114
+ state(scrobbling).should eq :local_enabled
115
+ scrobbling.reset
116
+ state(scrobbling).should eq :global_enabled
117
+ end
118
+ end
119
+ end