hallon 0.12.0 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +43 -0
- data/Gemfile +3 -1
- data/README.markdown +41 -11
- data/Rakefile +12 -0
- data/examples/audio_driver.rb +55 -0
- data/examples/playing_audio.rb +10 -50
- data/hallon.gemspec +1 -1
- data/lib/hallon.rb +1 -1
- data/lib/hallon/album_browse.rb +22 -11
- data/lib/hallon/artist_browse.rb +64 -33
- data/lib/hallon/audio_driver.rb +138 -0
- data/lib/hallon/audio_queue.rb +110 -0
- data/lib/hallon/enumerator.rb +55 -16
- data/lib/hallon/error.rb +9 -2
- data/lib/hallon/image.rb +6 -5
- data/lib/hallon/link.rb +7 -4
- data/lib/hallon/linkable.rb +27 -0
- data/lib/hallon/observable/player.rb +18 -1
- data/lib/hallon/observable/session.rb +5 -1
- data/lib/hallon/player.rb +180 -54
- data/lib/hallon/playlist.rb +33 -20
- data/lib/hallon/playlist_container.rb +78 -64
- data/lib/hallon/search.rb +51 -33
- data/lib/hallon/session.rb +1 -1
- data/lib/hallon/toplist.rb +36 -18
- data/lib/hallon/track.rb +12 -6
- data/lib/hallon/version.rb +1 -1
- data/spec/hallon/artist_browse_spec.rb +3 -4
- data/spec/hallon/audio_queue_spec.rb +89 -0
- data/spec/hallon/enumerator_spec.rb +50 -25
- data/spec/hallon/error_spec.rb +2 -2
- data/spec/hallon/image_spec.rb +12 -5
- data/spec/hallon/link_spec.rb +8 -9
- data/spec/hallon/linkable_spec.rb +11 -0
- data/spec/hallon/observable/session_spec.rb +4 -0
- data/spec/hallon/player_spec.rb +118 -5
- data/spec/hallon/playlist_container_spec.rb +2 -2
- data/spec/hallon/playlist_spec.rb +32 -37
- data/spec/hallon/search_spec.rb +3 -3
- data/spec/hallon/user_spec.rb +0 -6
- data/spec/spec_helper.rb +10 -0
- data/spec/support/audio_driver_mock.rb +23 -0
- data/spec/support/context_stub_session.rb +5 -0
- data/spec/support/shared_for_linkable_objects.rb +22 -2
- metadata +26 -20
- data/lib/hallon/queue.rb +0 -71
- data/spec/hallon/queue_spec.rb +0 -35
data/lib/hallon/search.rb
CHANGED
@@ -5,6 +5,51 @@ module Hallon
|
|
5
5
|
#
|
6
6
|
# @see http://developer.spotify.com/en/libspotify/docs/group__search.html
|
7
7
|
class Search < Base
|
8
|
+
# Enumerates through all tracks of a search object.
|
9
|
+
class Tracks < Enumerator
|
10
|
+
size :search_num_tracks
|
11
|
+
|
12
|
+
# @return [Track, nil]
|
13
|
+
item :search_track! do |track|
|
14
|
+
Track.from(track)
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Integer] total number of tracks from connected search result.
|
18
|
+
def total
|
19
|
+
Spotify.search_total_tracks(pointer)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Enumerates through all albums of a search object.
|
24
|
+
class Albums < Enumerator
|
25
|
+
size :search_num_albums
|
26
|
+
|
27
|
+
# @return [Album, nil]
|
28
|
+
item :search_album! do |album|
|
29
|
+
Album.from(album)
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [Integer] total number of tracks from connected search result.
|
33
|
+
def total
|
34
|
+
Spotify.search_total_albums(pointer)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Enumerates through all albums of a search object.
|
39
|
+
class Artists < Enumerator
|
40
|
+
size :search_num_artists
|
41
|
+
|
42
|
+
# @return [Artist, nil]
|
43
|
+
item :search_artist! do |artist|
|
44
|
+
Artist.from(artist)
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Integer] total tracks available from connected search result.
|
48
|
+
def total
|
49
|
+
Spotify.search_total_artists(pointer)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
8
53
|
extend Observable::Search
|
9
54
|
|
10
55
|
# @return [Array<Symbol>] a list of radio genres available for search
|
@@ -87,46 +132,19 @@ module Hallon
|
|
87
132
|
Spotify.search_did_you_mean(pointer)
|
88
133
|
end
|
89
134
|
|
90
|
-
# @return [
|
135
|
+
# @return [Tracks] list of all tracks in the search result.
|
91
136
|
def tracks
|
92
|
-
|
93
|
-
Enumerator.new(size) do |i|
|
94
|
-
track = Spotify.search_track!(pointer, i)
|
95
|
-
Track.new(track)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
# @return [Integer] total tracks available for this search query.
|
100
|
-
def total_tracks
|
101
|
-
Spotify.search_total_tracks(pointer)
|
137
|
+
Tracks.new(self)
|
102
138
|
end
|
103
139
|
|
104
|
-
# @return [
|
140
|
+
# @return [Albums] list of all albums in the search result.
|
105
141
|
def albums
|
106
|
-
|
107
|
-
Enumerator.new(size) do |i|
|
108
|
-
album = Spotify.search_album!(pointer, i)
|
109
|
-
Album.new(album)
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
# @return [Integer] total tracks available for this search query.
|
114
|
-
def total_albums
|
115
|
-
Spotify.search_total_albums(pointer)
|
142
|
+
Albums.new(self)
|
116
143
|
end
|
117
144
|
|
118
|
-
# @return [
|
145
|
+
# @return [Artists] list of all artists in the search result.
|
119
146
|
def artists
|
120
|
-
|
121
|
-
Enumerator.new(size) do |i|
|
122
|
-
artist = Spotify.search_artist!(pointer, i)
|
123
|
-
Artist.new(artist)
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
# @return [Integer] total tracks available for this search query.
|
128
|
-
def total_artists
|
129
|
-
Spotify.search_total_artists(pointer)
|
147
|
+
Artists.new(self)
|
130
148
|
end
|
131
149
|
|
132
150
|
# @return [Link] link for this search query.
|
data/lib/hallon/session.rb
CHANGED
@@ -424,7 +424,7 @@ module Hallon
|
|
424
424
|
# but a few moments later it fires connection_error; waiting for both and checking
|
425
425
|
# for errors on both hopefully circumvents this!
|
426
426
|
wait_for(:logged_in, :connection_error) do |error|
|
427
|
-
Error.maybe_raise(error)
|
427
|
+
Error.maybe_raise(error, :ignore => :timeout)
|
428
428
|
session.logged_in?
|
429
429
|
end
|
430
430
|
end
|
data/lib/hallon/toplist.rb
CHANGED
@@ -6,6 +6,36 @@ module Hallon
|
|
6
6
|
#
|
7
7
|
# @see http://developer.spotify.com/en/libspotify/docs/group__toplist.html
|
8
8
|
class Toplist < Base
|
9
|
+
# Enumerates through all tracks of a toplist object.
|
10
|
+
class Tracks < Enumerator
|
11
|
+
size :toplistbrowse_num_tracks
|
12
|
+
|
13
|
+
# @return [Track, nil]
|
14
|
+
item :toplistbrowse_track! do |track|
|
15
|
+
Track.from(track)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Enumerates through all albums of a toplist object.
|
20
|
+
class Albums < Enumerator
|
21
|
+
size :toplistbrowse_num_albums
|
22
|
+
|
23
|
+
# @return [Album, nil]
|
24
|
+
item :toplistbrowse_album! do |album|
|
25
|
+
Album.from(album)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Enumerates through all albums of a toplist object.
|
30
|
+
class Artists < Enumerator
|
31
|
+
size :toplistbrowse_num_artists
|
32
|
+
|
33
|
+
# @return [Artist, nil]
|
34
|
+
item :toplistbrowse_artist! do |artist|
|
35
|
+
Artist.from(artist)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
9
39
|
extend Observable::Toplist
|
10
40
|
|
11
41
|
# Create a Toplist browsing object.
|
@@ -52,31 +82,19 @@ module Hallon
|
|
52
82
|
Spotify.toplistbrowse_error(pointer)
|
53
83
|
end
|
54
84
|
|
55
|
-
# @return [
|
85
|
+
# @return [Artists] a list of artists.
|
56
86
|
def artists
|
57
|
-
|
58
|
-
Enumerator.new(size) do |i|
|
59
|
-
artist = Spotify.toplistbrowse_artist!(pointer, i)
|
60
|
-
Artist.new(artist)
|
61
|
-
end
|
87
|
+
Artists.new(self)
|
62
88
|
end
|
63
89
|
|
64
|
-
# @return [
|
90
|
+
# @return [Albums] a list of albums.
|
65
91
|
def albums
|
66
|
-
|
67
|
-
Enumerator.new(size) do |i|
|
68
|
-
album = Spotify.toplistbrowse_album!(pointer, i)
|
69
|
-
Album.new(album)
|
70
|
-
end
|
92
|
+
Albums.new(self)
|
71
93
|
end
|
72
94
|
|
73
|
-
# @return [
|
95
|
+
# @return [Tracks] a list of tracks.
|
74
96
|
def tracks
|
75
|
-
|
76
|
-
Enumerator.new(size) do |i|
|
77
|
-
track = Spotify.toplistbrowse_track!(pointer, i)
|
78
|
-
Track.new(track)
|
79
|
-
end
|
97
|
+
Tracks.new(self)
|
80
98
|
end
|
81
99
|
|
82
100
|
# @note If the object is not loaded, the result is undefined.
|
data/lib/hallon/track.rb
CHANGED
@@ -5,6 +5,16 @@ module Hallon
|
|
5
5
|
#
|
6
6
|
# @see http://developer.spotify.com/en/libspotify/docs/group__track.html
|
7
7
|
class Track < Base
|
8
|
+
# Enumerates through all albums of a search object.
|
9
|
+
class Artists < Enumerator
|
10
|
+
size :track_num_artists
|
11
|
+
|
12
|
+
# @return [Artist, nil]
|
13
|
+
item :track_artist! do |artist|
|
14
|
+
Artist.from(artist)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
8
18
|
extend Linkable
|
9
19
|
|
10
20
|
from_link :as_track_and_offset
|
@@ -143,13 +153,9 @@ module Hallon
|
|
143
153
|
end
|
144
154
|
|
145
155
|
# @note Track must be loaded, or you’ll get zero artists.
|
146
|
-
# @return [
|
156
|
+
# @return [Artists] all {Artist}s who performed this Track.
|
147
157
|
def artists
|
148
|
-
|
149
|
-
Enumerator.new(size) do |i|
|
150
|
-
artist = Spotify.track_artist!(pointer, i)
|
151
|
-
Artist.new(artist)
|
152
|
-
end
|
158
|
+
Artists.new(self)
|
153
159
|
end
|
154
160
|
|
155
161
|
# @note This’ll always return false unless the track is loaded.
|
data/lib/hallon/version.rb
CHANGED
@@ -24,12 +24,11 @@ describe Hallon::ArtistBrowse do
|
|
24
24
|
|
25
25
|
its('portraits.size') { should eq 2 }
|
26
26
|
its('portraits.to_a') do
|
27
|
-
|
27
|
+
stub_session { should eq instantiate(Hallon::Image, mock_image_id, mock_image_id) }
|
28
28
|
end
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
end
|
30
|
+
its('portrait_links.size') { should eq 2 }
|
31
|
+
its('portrait_links.to_a') { should eq instantiate(Hallon::Link, mock_image_link, mock_image_link) }
|
33
32
|
|
34
33
|
its('tracks.size') { should eq 2 }
|
35
34
|
its('tracks.to_a') { should eq [mock_track, mock_track_two].map{ |p| Hallon::Track.new(p) } }
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
describe Hallon::AudioQueue do
|
5
|
+
let(:queue) { Hallon::AudioQueue.new(4) }
|
6
|
+
subject { queue }
|
7
|
+
|
8
|
+
it "should conform to the example specification of its’ documentation" do
|
9
|
+
queue.push([1, 2]).should eq 2
|
10
|
+
queue.push([3]).should eq 1
|
11
|
+
queue.push([4, 5, 6]).should eq 1
|
12
|
+
queue.push([5, 6]).should eq 0
|
13
|
+
queue.pop(1).should eq [1]
|
14
|
+
queue.push([5, 6]).should eq 1
|
15
|
+
queue.pop.should eq [2, 3, 4, 5]
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#pop" do
|
19
|
+
it "should not block if the queue is not empty" do
|
20
|
+
queue.push([1, 2])
|
21
|
+
|
22
|
+
start = Time.now
|
23
|
+
queue.pop.should eq [1, 2]
|
24
|
+
(Time.now - start).should be_within(0.001).of(0)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should block if the queue is empty" do
|
28
|
+
queue.size.should be_zero
|
29
|
+
|
30
|
+
# I could mock out ConditionVariable and Mutex, but where’s the fun in that?
|
31
|
+
start = Time.now
|
32
|
+
Thread.start { sleep 0.2; queue.push([1]) }
|
33
|
+
queue.pop.should eq [1]
|
34
|
+
(Time.now - start).should be_within(0.08).of(0.2)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "#clear" do
|
39
|
+
it "should clear the queue" do
|
40
|
+
queue.push([1, 2])
|
41
|
+
queue.should_not be_empty
|
42
|
+
queue.clear
|
43
|
+
queue.should be_empty
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#format" do
|
48
|
+
it "should clear the queue when setting the format" do
|
49
|
+
queue.push([1, 2])
|
50
|
+
queue.should_not be_empty
|
51
|
+
queue.format = :new_format
|
52
|
+
queue.should be_empty
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should allow setting and retrieving the format" do
|
56
|
+
queue.format.should_not be :new_format
|
57
|
+
queue.format = :new_format
|
58
|
+
queue.format.should be :new_format
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "#synchronize" do
|
63
|
+
it "should be re-entrant" do
|
64
|
+
expect { queue.synchronize { queue.synchronize {} } }.to_not raise_error
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#new_cond" do
|
69
|
+
it "should be bound to the queue" do
|
70
|
+
condvar = queue.new_cond
|
71
|
+
inside = false
|
72
|
+
|
73
|
+
Thread.new(queue, condvar) do |q, c|
|
74
|
+
q.synchronize do
|
75
|
+
inside = true
|
76
|
+
c.signal
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
Timeout::timeout(1) do
|
81
|
+
queue.synchronize do
|
82
|
+
condvar.wait_until { inside }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
inside.should be_true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -1,103 +1,128 @@
|
|
1
|
+
# coding: utf-8
|
1
2
|
describe Hallon::Enumerator do
|
2
|
-
|
3
|
-
|
4
|
-
|
3
|
+
def enumerator(items)
|
4
|
+
Spotify.stub(:enumerator_size => items)
|
5
|
+
Spotify.stub(:enumerator_item).and_return { |_, i| alphabet[i] }
|
6
|
+
|
7
|
+
klass = Class.new(Hallon::Enumerator) do
|
8
|
+
size :enumerator_size
|
9
|
+
item :enumerator_item
|
10
|
+
end
|
5
11
|
|
6
|
-
|
7
|
-
|
12
|
+
struct = OpenStruct.new(:pointer => nil)
|
13
|
+
klass.new(struct)
|
8
14
|
end
|
9
15
|
|
16
|
+
# our subject
|
17
|
+
let(:enum) { enumerator(5) }
|
18
|
+
|
19
|
+
# this is a proc so we can pass it to #and_return
|
20
|
+
# we can still access elements with #[] though, ain’t that nice?
|
10
21
|
let(:alphabet) do
|
11
22
|
proc { |x| %w[a b c d e][x] }
|
12
23
|
end
|
13
24
|
|
14
|
-
|
15
25
|
it "should be an enumerable" do
|
16
26
|
enum.should respond_to :each
|
17
27
|
enum.should be_an Enumerable
|
18
28
|
end
|
19
29
|
|
20
30
|
describe "#each" do
|
21
|
-
it "should
|
22
|
-
enum = Hallon::Enumerator.new(4) { |i| item.get(i) }
|
31
|
+
it "should yield items from the collection" do
|
23
32
|
enum.each_with_index { |x, i| x.should eq alphabet[i] }
|
24
33
|
end
|
34
|
+
|
35
|
+
it "should stop enumerating if the size shrinks below current index during iteration" do
|
36
|
+
iterations = 0
|
37
|
+
|
38
|
+
enum.map do |x|
|
39
|
+
enum.should_receive(:size).and_return(0)
|
40
|
+
iterations += 1
|
41
|
+
end
|
42
|
+
|
43
|
+
iterations.should eq 1
|
44
|
+
end
|
25
45
|
end
|
26
46
|
|
27
47
|
describe "#size" do
|
28
48
|
it "should return the given size" do
|
29
|
-
|
49
|
+
enum.size.should eq 5
|
30
50
|
end
|
31
51
|
end
|
32
52
|
|
33
53
|
describe "#[]" do
|
54
|
+
it "should return nil if #[x] is not within the enumerators’ size (no matter if the value exists or not)" do
|
55
|
+
enum.should_receive(:size).and_return(1)
|
56
|
+
enum[1].should be_nil
|
57
|
+
end
|
58
|
+
|
34
59
|
it "should support #[x] within range" do
|
35
|
-
|
60
|
+
alphabet.should_receive(:[]).with(1).and_return(&alphabet)
|
36
61
|
|
37
62
|
enum[1].should eq "b"
|
38
63
|
end
|
39
64
|
|
40
65
|
it "should support negative #[x] within range" do
|
41
|
-
|
66
|
+
alphabet.should_receive(:[]).with(4).and_return(&alphabet)
|
42
67
|
|
43
68
|
enum[-1].should eq "e"
|
44
69
|
end
|
45
70
|
|
46
71
|
it "should return nil for #[x] outside range" do
|
47
|
-
|
72
|
+
alphabet.should_not_receive(:get)
|
48
73
|
|
49
74
|
enum[6].should be_nil
|
50
75
|
end
|
51
76
|
|
52
77
|
it "should return nil for #[-x] outside range" do
|
53
|
-
|
78
|
+
alphabet.should_not_receive(:get)
|
54
79
|
|
55
80
|
enum[-6].should be_nil
|
56
81
|
end
|
57
82
|
|
58
83
|
it "should return a slice of elements for #[x, y]" do
|
59
|
-
|
60
|
-
|
84
|
+
alphabet.should_receive(:[]).with(1).and_return(&alphabet)
|
85
|
+
alphabet.should_receive(:[]).with(2).and_return(&alphabet)
|
61
86
|
|
62
87
|
enum[1, 2].should eq %w[b c]
|
63
88
|
end
|
64
89
|
|
65
90
|
it "should return elements for an inclusive range of #[x..y]" do
|
66
|
-
|
67
|
-
|
68
|
-
|
91
|
+
alphabet.should_receive(:[]).with(1).and_return(&alphabet)
|
92
|
+
alphabet.should_receive(:[]).with(2).and_return(&alphabet)
|
93
|
+
alphabet.should_receive(:[]).with(3).and_return(&alphabet)
|
69
94
|
|
70
95
|
enum[1..3].should eq %w[b c d]
|
71
96
|
end
|
72
97
|
|
73
98
|
it "should return return only existing elements for partly inclusive range of #[x..y]" do
|
74
|
-
|
99
|
+
alphabet.should_receive(:[]).with(4).and_return(&alphabet)
|
75
100
|
|
76
101
|
enum[4..7].should eq %w[e]
|
77
102
|
end
|
78
103
|
|
79
104
|
it "should return nil for a completely outside range of #[x..y]" do
|
80
|
-
|
105
|
+
alphabet.should_not_receive(:[])
|
81
106
|
|
82
107
|
enum[6..10].should eq nil
|
83
108
|
end
|
84
109
|
|
85
110
|
it "should return the items for #[-x, y]" do
|
86
|
-
|
87
|
-
|
88
|
-
|
111
|
+
alphabet.should_receive(:[]).with(2).and_return(&alphabet)
|
112
|
+
alphabet.should_receive(:[]).with(3).and_return(&alphabet)
|
113
|
+
alphabet.should_receive(:[]).with(4).and_return(&alphabet)
|
89
114
|
|
90
115
|
enum[-3, 3].should eq %w[c d e]
|
91
116
|
end
|
92
117
|
|
93
118
|
it "should slice between items by #[x, y]" do
|
94
|
-
|
119
|
+
alphabet.should_not_receive(:[])
|
95
120
|
|
96
121
|
enum[5, 1].should eq []
|
97
122
|
end
|
98
123
|
|
99
124
|
it "should slice between items by #[x..y]" do
|
100
|
-
|
125
|
+
alphabet.should_not_receive(:[])
|
101
126
|
|
102
127
|
enum[5..10].should eq []
|
103
128
|
end
|