hallon 0.14.0 → 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +21 -1
- data/README.markdown +64 -10
- data/dev/login.rb +2 -13
- data/examples/adding_tracks_to_playlist.rb +32 -30
- data/examples/example_support.rb +100 -0
- data/examples/playing_audio.rb +11 -47
- data/examples/show_published_playlists_of_user.rb +18 -46
- data/hallon.gemspec +2 -1
- data/lib/hallon.rb +5 -4
- data/lib/hallon/album.rb +1 -1
- data/lib/hallon/artist.rb +1 -1
- data/lib/hallon/base.rb +3 -3
- data/lib/hallon/ext/spotify.rb +1 -1
- data/lib/hallon/image.rb +1 -1
- data/lib/hallon/link.rb +3 -1
- data/lib/hallon/linkable.rb +103 -106
- data/lib/hallon/loadable.rb +1 -0
- data/lib/hallon/observable.rb +4 -2
- data/lib/hallon/observable/session.rb +13 -0
- data/lib/hallon/playlist.rb +21 -1
- data/lib/hallon/search.rb +28 -39
- data/lib/hallon/session.rb +7 -6
- data/lib/hallon/track.rb +1 -1
- data/lib/hallon/user.rb +1 -1
- data/lib/hallon/version.rb +1 -1
- data/spec/hallon/album_spec.rb +1 -1
- data/spec/hallon/hallon_spec.rb +4 -6
- data/spec/hallon/link_spec.rb +4 -0
- data/spec/hallon/linkable_spec.rb +1 -1
- data/spec/hallon/observable_spec.rb +9 -0
- data/spec/hallon/player_spec.rb +4 -3
- data/spec/hallon/playlist_spec.rb +11 -0
- data/spec/hallon/search_spec.rb +19 -39
- data/spec/hallon/session_spec.rb +7 -0
- data/spec/hallon/user_spec.rb +1 -1
- data/spec/mockspotify.rb +6 -4
- data/spec/spec_helper.rb +3 -0
- data/spec/support/common_objects.rb +18 -10
- metadata +29 -17
- data/examples/logging_in.rb +0 -16
- data/examples/printing_link_information.rb +0 -30
@@ -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
|
data/lib/hallon/playlist.rb
CHANGED
@@ -93,7 +93,7 @@ module Hallon
|
|
93
93
|
end
|
94
94
|
end
|
95
95
|
|
96
|
-
|
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
|
-
|
54
|
-
include Loadable
|
55
|
+
include Linkable
|
55
56
|
|
56
|
-
|
57
|
-
|
58
|
-
|
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]
|
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(
|
106
|
-
|
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
|
110
|
-
|
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
|
-
|
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
|
data/lib/hallon/session.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
data/lib/hallon/user.rb
CHANGED
data/lib/hallon/version.rb
CHANGED
data/spec/hallon/album_spec.rb
CHANGED
@@ -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:
|
52
|
+
album.cover_link.should eq Hallon::Link.new("spotify:image:3ad93423add99766e02d563605c6e76ed2b0e400")
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
data/spec/hallon/hallon_spec.rb
CHANGED
@@ -5,7 +5,7 @@ describe Hallon do
|
|
5
5
|
end
|
6
6
|
|
7
7
|
describe "API_VERSION" do
|
8
|
-
specify { Hallon::API_VERSION.should ==
|
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
|
-
|
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.
|
29
|
-
|
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
|
data/spec/hallon/link_spec.rb
CHANGED
@@ -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
|
@@ -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
|
data/spec/hallon/player_spec.rb
CHANGED
@@ -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]
|
data/spec/hallon/search_spec.rb
CHANGED
@@ -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
|
-
|
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
|