hallon 0.12.0 → 0.13.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 +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
|