hallon 0.16.0 → 0.17.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 (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