hallon 0.14.0 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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