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.
- 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
|