hallon 0.14.0 → 0.15.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.
@@ -236,5 +236,18 @@ module Hallon::Observable
236
236
  def offline_error_callback(pointer, error)
237
237
  trigger(pointer, :offline_error, error)
238
238
  end
239
+
240
+
241
+ # @example listening to this event
242
+ # session.on(:credentials_blob_updated) do |credentials|
243
+ # File.open('.spotify-credentials', 'w') { |io| io.write(credentials) }
244
+ # end
245
+ #
246
+ # @yield [credentials, self]
247
+ # @yieldparam [String] credentials
248
+ # @yieldparam [Session] self
249
+ def credentials_blob_updated_callback(pointer, credentials)
250
+ trigger(pointer, :credentials_blob_updated, credentials)
251
+ end
239
252
  end
240
253
  end
@@ -93,7 +93,7 @@ module Hallon
93
93
  end
94
94
  end
95
95
 
96
- extend Linkable
96
+ include Linkable
97
97
  include Loadable
98
98
 
99
99
  # CAN HAZ CALLBAKZ
@@ -150,6 +150,26 @@ module Hallon
150
150
  Spotify.playlist_set_collaborative(pointer, !!collaborative)
151
151
  end
152
152
 
153
+ # Allow the playlist time to update itself and the Spotify backend.
154
+ # This method will block until libspotify says the playlist no longer
155
+ # has pending changes.
156
+ #
157
+ # @return [Playlist]
158
+ def upload(timeout = Hallon.load_timeout)
159
+ unless pending?
160
+ # libspotify has this bug where pending? returns false directly after
161
+ # adding tracks to a playlist; processing events once usually toggles
162
+ # the playlist into pending? mode
163
+ session.process_events
164
+ end
165
+
166
+ Timeout.timeout(timeout, Hallon::TimeoutError) do
167
+ session.wait_for { not pending? }
168
+ end
169
+
170
+ self
171
+ end
172
+
153
173
  # @return [Boolean] true if playlist has pending changes
154
174
  def pending?
155
175
  Spotify.playlist_has_pending_changes(pointer)
data/lib/hallon/search.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  # coding: utf-8
2
+ require 'cgi'
3
+
2
4
  module Hallon
3
5
  # Search allows you to search Spotify for tracks, albums
4
6
  # and artists, just like in the client.
@@ -50,66 +52,59 @@ module Hallon
50
52
  end
51
53
  end
52
54
 
53
- extend Observable::Search
54
- include Loadable
55
+ include Linkable
55
56
 
56
- # @return [Array<Symbol>] a list of radio genres available for search
57
- def self.genres
58
- Spotify.enum_type(:radio_genre).symbols
57
+ to_link :from_search
58
+
59
+ from_link :search do |link|
60
+ link = Link.new(link).to_uri
61
+ ::CGI.unescape(link[/\Aspotify:search:(.+)\z/m, 1])
59
62
  end
60
63
 
64
+ extend Observable::Search
65
+ include Loadable
66
+
61
67
  # @return [Hash] default search parameters
62
68
  def self.defaults
63
69
  @defaults ||= {
64
70
  :tracks => 25,
65
71
  :albums => 25,
66
72
  :artists => 25,
73
+ :playlists => 25,
67
74
  :tracks_offset => 0,
68
75
  :albums_offset => 0,
69
- :artists_offset => 0
76
+ :artists_offset => 0,
77
+ :playlists_offset => 0
70
78
  }
71
79
  end
72
80
 
73
- # @param [Range<Integer>] range (from_year..to_year)
74
- # @param [Symbol, …] genres
75
- # @return [Search] radio search in given period and genres
76
- def self.radio(range, *genres)
77
- from_year, to_year = range.begin, range.end
78
- genres = genres.reduce(0) do |mask, genre|
79
- mask | Spotify.enum_value!(genre, "genre")
80
- end
81
-
82
- search = allocate
83
- search.instance_eval do
84
- subscribe_for_callbacks do |callback|
85
- @pointer = Spotify.radio_search_create!(session.pointer, from_year, to_year, genres, callback, nil)
86
- end
87
-
88
- raise FFI::NullPointerError, "radio search failed" if pointer.null?
89
- end
90
-
91
- search
92
- end
93
-
94
81
  # Construct a new search with given query.
95
82
  #
96
- # @param [String] query search query
83
+ # @param [String, Link] search search query or spotify URI
97
84
  # @param [Hash] options additional search options
98
85
  # @option options [#to_i] :tracks (25) max number of tracks you want in result
99
86
  # @option options [#to_i] :albums (25) max number of albums you want in result
100
87
  # @option options [#to_i] :artists (25) max number of artists you want in result
88
+ # @option options [#to_i] :playlists (25) max number of playlists you want in result
101
89
  # @option options [#to_i] :tracks_offset (0) offset of tracks in search result
102
90
  # @option options [#to_i] :albums_offset (0) offset of albums in search result
103
91
  # @option options [#to_i] :artists_offset (0) offset of artists in search result
92
+ # @option options [#to_i] :playlists_offset (0) offset of playlists in search result
104
93
  # @see http://developer.spotify.com/en/libspotify/docs/group__search.html#gacf0b5e902e27d46ef8b1f40e332766df
105
- def initialize(query, options = {})
106
- o = Search.defaults.merge(options)
94
+ def initialize(search, options = {})
95
+ opts = Search.defaults.merge(options)
96
+ opts = opts.values_at(:tracks_offset, :tracks, :albums_offset, :albums, :artists_offset, :artists, :playlists_offset, :playlists).map(&:to_i)
97
+ search = from_link(search) if Link.valid?(search)
107
98
 
108
99
  subscribe_for_callbacks do |callback|
109
- @pointer = Spotify.search_create!(session.pointer, query, o[:tracks_offset].to_i, o[:tracks].to_i, o[:albums_offset].to_i, o[:albums].to_i, o[:artists_offset].to_i, o[:artists].to_i, callback, nil)
110
- end
100
+ @pointer = if Spotify::Pointer.typechecks?(search, :search)
101
+ search
102
+ else
103
+ Spotify.search_create!(session.pointer, search, *opts, :standard, callback, nil)
104
+ end
111
105
 
112
- raise FFI::NullPointerError, "search for “#{query} failed" if pointer.null?
106
+ raise ArgumentError, "search with #{search} failed" if @pointer.null?
107
+ end
113
108
  end
114
109
 
115
110
  # @return [Boolean] true if the search has been fully loaded.
@@ -147,11 +142,5 @@ module Hallon
147
142
  def artists
148
143
  Artists.new(self)
149
144
  end
150
-
151
- # @return [Link] link for this search query.
152
- def to_link
153
- link = Spotify.link_create_from_search!(pointer)
154
- Link.from(link)
155
- end
156
145
  end
157
146
  end
@@ -120,7 +120,7 @@ module Hallon
120
120
  # You pass a pointer to the session pointer to libspotify >:)
121
121
  FFI::MemoryPointer.new(:pointer) do |p|
122
122
  Error::maybe_raise Spotify.session_create(config, p)
123
- @pointer = p.read_pointer
123
+ @pointer = Spotify::Pointer.new(p.read_pointer, :session, false)
124
124
  end
125
125
  end
126
126
  end
@@ -185,7 +185,11 @@ module Hallon
185
185
  # @return [Session]
186
186
  # @see login!
187
187
  def login(username, password, remember_me = false)
188
- tap { Spotify.session_login(pointer, username, password, remember_me) }
188
+ if username.empty? or password.empty?
189
+ raise ArgumentError, "username and password may not be blank"
190
+ end
191
+
192
+ tap { Spotify.session_login(pointer, username, password, remember_me, nil) }
189
193
  end
190
194
 
191
195
  # Login the remembered user (see {#login}).
@@ -420,10 +424,7 @@ module Hallon
420
424
  # @see login!
421
425
  # @see relogin!
422
426
  def wait_until_logged_in
423
- # if the user does not have premium, libspotify will still fire logged_in as :ok,
424
- # but a few moments later it fires connection_error; waiting for both and checking
425
- # for errors on both hopefully circumvents this!
426
- wait_for(:logged_in, :connection_error) do |error|
427
+ wait_for(:connection_error) do |error|
427
428
  Error.maybe_raise(error, :ignore => :timeout)
428
429
  session.logged_in?
429
430
  end
data/lib/hallon/track.rb CHANGED
@@ -15,7 +15,7 @@ module Hallon
15
15
  end
16
16
  end
17
17
 
18
- extend Linkable
18
+ include Linkable
19
19
  include Loadable
20
20
 
21
21
  from_link :as_track_and_offset
data/lib/hallon/user.rb CHANGED
@@ -68,7 +68,7 @@ module Hallon
68
68
  end
69
69
  end
70
70
 
71
- extend Linkable
71
+ include Linkable
72
72
  include Loadable
73
73
 
74
74
  from_link :profile do |link|
@@ -3,5 +3,5 @@ module Hallon
3
3
  # Current release version of Hallon
4
4
  #
5
5
  # @see http://semver.org/
6
- VERSION = [0, 14, 0].join('.')
6
+ VERSION = [0, 15, 0].join('.')
7
7
  end
@@ -49,7 +49,7 @@ describe Hallon::Album do
49
49
  end
50
50
 
51
51
  it "should be a link if it exists" do
52
- album.cover_link.should eq Hallon::Link.new("spotify:image:3ad93423add99766e02d563605c6e76ed2b0e450")
52
+ album.cover_link.should eq Hallon::Link.new("spotify:image:3ad93423add99766e02d563605c6e76ed2b0e400")
53
53
  end
54
54
  end
55
55
 
@@ -5,7 +5,7 @@ describe Hallon do
5
5
  end
6
6
 
7
7
  describe "API_VERSION" do
8
- specify { Hallon::API_VERSION.should == 10 }
8
+ specify { Hallon::API_VERSION.should == 11 }
9
9
  end
10
10
 
11
11
  describe "API_BUILD" do
@@ -13,9 +13,8 @@ describe Hallon do
13
13
  end
14
14
 
15
15
  describe "URI" do
16
- subject { Hallon::URI }
17
16
  example_uris.keys.each do |uri|
18
- it { should match uri }
17
+ specify(uri) { Hallon::URI.match(uri)[0].should eq uri }
19
18
  end
20
19
  end
21
20
 
@@ -25,9 +24,8 @@ describe Hallon do
25
24
  end
26
25
 
27
26
  it "should allow setting and retrieving the value" do
28
- Hallon.load_timeout.should eq 5
29
- Hallon.load_timeout = 0.2
30
- Hallon.load_timeout.should eq 0.2
27
+ expect { Hallon.load_timeout = 0.2 }.
28
+ to change { Hallon.load_timeout }
31
29
  end
32
30
  end
33
31
  end
@@ -50,6 +50,10 @@ describe Hallon::Link do
50
50
  it "should truncate if given a small maximum length" do
51
51
  subject.to_str(7).should == "spotify"
52
52
  end
53
+
54
+ it "should be in UTF-8 encoding" do
55
+ subject.to_str.encoding.should eq Encoding::UTF_8
56
+ end
53
57
  end
54
58
 
55
59
  describe "#to_url" do
@@ -2,7 +2,7 @@ describe Hallon::Linkable do
2
2
  let(:klass) do
3
3
  klass = Class.new
4
4
  klass.instance_eval do
5
- extend Hallon::Linkable
5
+ include Hallon::Linkable
6
6
  end
7
7
  klass
8
8
  end
@@ -117,6 +117,15 @@ describe Hallon::Observable do
117
117
  x.should eq 0
118
118
  end
119
119
 
120
+ it "should return the previous callback" do
121
+ previous = proc { puts "hey!" }
122
+ new_one = proc { puts "ho!" }
123
+ initial = subject.on(:testing, &previous)
124
+
125
+ subject.on(:testing, &new_one).should eq previous
126
+ subject.on(:testing, &previous).should eq new_one
127
+ end
128
+
120
129
  it "should raise an error trying to bind to a non-existing callback" do
121
130
  expect { subject.on("nonexisting") {} }.to raise_error(NameError)
122
131
  end
@@ -149,7 +149,7 @@ describe Hallon::Player do
149
149
 
150
150
  context "the output streaming" do
151
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] }
152
+ Thread.stub(:start).and_return{ |*args, &block| block[*args] }
153
153
 
154
154
  player # create the Player
155
155
  session.trigger(:music_delivery, queue.format, [1, 2, 3])
@@ -169,7 +169,7 @@ describe Hallon::Player do
169
169
  end
170
170
 
171
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] }
172
+ Thread.stub(:start).and_return{ |*args, &block| block[*args] }
173
173
 
174
174
  player # create the Player
175
175
  session.trigger(:start_playback)
@@ -184,7 +184,8 @@ describe Hallon::Player do
184
184
  end
185
185
 
186
186
  it "should set the format on initialization" do
187
- Thread.stub(:start).and_return{ |*args, block| block[*args] }
187
+ Thread.stub(:start).and_return{ |*args, &block| block[*args] }
188
+
188
189
  AudioDriverMock.any_instance.should_receive(:format=)
189
190
  Hallon::AudioQueue.any_instance.should_receive(:format=)
190
191
  player
@@ -78,6 +78,17 @@ describe Hallon::Playlist do
78
78
  end
79
79
  end
80
80
 
81
+ describe "#upload", :stub_session do
82
+ around(:each) do |example|
83
+ Timeout.timeout(1) { example.run }
84
+ end
85
+
86
+ it "should raise an error if the playlist takes too long to load" do
87
+ playlist.stub(:pending? => true)
88
+ expect { playlist.upload(0.01) }.to raise_error(Hallon::TimeoutError)
89
+ end
90
+ end
91
+
81
92
  describe "#subscribers" do
82
93
  it "should return an array of names for the subscribers" do
83
94
  subject.subscribers.should eq %w[Kim Elin Ylva]
@@ -1,30 +1,40 @@
1
+ # coding: utf-8
2
+ require 'cgi'
3
+
1
4
  describe Hallon::Search do
5
+ it_should_behave_like "a Linkable object" do
6
+ let(:spotify_uri) { "spotify:search:my+%C3%A5+utf8+%EF%A3%BF+query" }
7
+ let(:custom_object) { "http://open.spotify.com/search/my+%C3%A5+utf8+%EF%A3%BF+query" }
8
+ let(:described_class) { stub_session(Hallon::Search) }
9
+ end
10
+
2
11
  it { should be_a Hallon::Loadable }
3
12
 
4
13
  subject { search }
5
14
  let(:search) do
6
- Spotify.registry_add 'spotify:search:my query', mock_search
7
- mock_session { Hallon::Search.new("my query") }
15
+ mock_session { Hallon::Search.new("my å utf8  query") }
8
16
  end
9
17
 
10
18
  describe ".new" do
11
19
  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") }
20
+ Spotify.should_receive(:search_create).with(session.pointer, "my å utf8  query", 0, 25, 0, 25, 0, 25, 0, 25, :standard, anything, anything).and_return(mock_search)
21
+ mock_session { Hallon::Search.new("my å utf8  query") }
14
22
  end
15
23
 
16
24
  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)
25
+ Spotify.should_receive(:search_create).with(session.pointer, "my å utf8  query", 1, 2, 3, 4, 5, 6, 7, 8, :standard, anything, anything).and_return(mock_search)
18
26
  my_params = {
19
27
  :tracks_offset => 1,
20
28
  :tracks => 2,
21
29
  :albums_offset => 3,
22
30
  :albums => 4,
23
31
  :artists_offset => 5,
24
- :artists => 6
32
+ :artists => 6,
33
+ :playlists_offset => 7,
34
+ :playlists => 8
25
35
  }
26
36
 
27
- mock_session { Hallon::Search.new("my query", my_params) }
37
+ mock_session { Hallon::Search.new("my å utf8  query", my_params) }
28
38
  end
29
39
 
30
40
  it "should raise an error if the search failed" do
@@ -33,40 +43,10 @@ describe Hallon::Search do
33
43
  end
34
44
  end
35
45
 
36
- describe ".genres" do
37
- subject { Hallon::Search.genres }
38
-
39
- it { should include :jazz }
40
- it { should be_a Array }
41
- it { should_not be_empty }
42
- end
43
-
44
- describe ".radio" do
45
- subject do
46
- Spotify.registry_add 'spotify:radio:00002200:1990-2010', mock_search
47
- mock_session { Hallon::Search.radio(1990..2010, :jazz, :punk) }
48
- end
49
-
50
- it "should raise an error on invalid genres" do
51
- Spotify.should_not_receive(:radio_search_create)
52
- expect { Hallon::Search.radio(1990..2010, :bogus, :jazz) }.to raise_error(ArgumentError, /bogus/)
53
- end
54
-
55
- it "should raise an error if the search failed" do
56
- Spotify.should_receive(:radio_search_create).and_return(null_pointer)
57
- expect { mock_session { Hallon::Search.radio(1990..1990) } }.to raise_error(/search failed/)
58
- end
59
-
60
- it { should be_loaded }
61
- its(:status) { should eq :ok }
62
- its('tracks.size') { should eq 2 }
63
- # ^ should be enough
64
- end
65
-
66
46
  it { should be_a Hallon::Observable }
67
47
  it { should be_loaded }
68
48
  its(:status) { should eq :ok }
69
- its(:query) { should eq "my query" }
49
+ its(:query) { should eq "my å utf8  query" }
70
50
  its(:did_you_mean) { should eq "another thing" }
71
51
 
72
52
  its('tracks.size') { should eq 2 }
@@ -81,5 +61,5 @@ describe Hallon::Search do
81
61
  its('artists.to_a') { should eq instantiate(Hallon::Artist, mock_artist, mock_artist_two) }
82
62
  its('artists.total') { should eq 81104 }
83
63
 
84
- its(:to_link) { should eq Hallon::Link.new("spotify:search:#{search.query}") }
64
+ its(:to_link) { should eq Hallon::Link.new("spotify:search:#{CGI.escape(search.query)}") }
85
65
  end