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.
Files changed (47) hide show
  1. data/CHANGELOG.md +43 -0
  2. data/Gemfile +3 -1
  3. data/README.markdown +41 -11
  4. data/Rakefile +12 -0
  5. data/examples/audio_driver.rb +55 -0
  6. data/examples/playing_audio.rb +10 -50
  7. data/hallon.gemspec +1 -1
  8. data/lib/hallon.rb +1 -1
  9. data/lib/hallon/album_browse.rb +22 -11
  10. data/lib/hallon/artist_browse.rb +64 -33
  11. data/lib/hallon/audio_driver.rb +138 -0
  12. data/lib/hallon/audio_queue.rb +110 -0
  13. data/lib/hallon/enumerator.rb +55 -16
  14. data/lib/hallon/error.rb +9 -2
  15. data/lib/hallon/image.rb +6 -5
  16. data/lib/hallon/link.rb +7 -4
  17. data/lib/hallon/linkable.rb +27 -0
  18. data/lib/hallon/observable/player.rb +18 -1
  19. data/lib/hallon/observable/session.rb +5 -1
  20. data/lib/hallon/player.rb +180 -54
  21. data/lib/hallon/playlist.rb +33 -20
  22. data/lib/hallon/playlist_container.rb +78 -64
  23. data/lib/hallon/search.rb +51 -33
  24. data/lib/hallon/session.rb +1 -1
  25. data/lib/hallon/toplist.rb +36 -18
  26. data/lib/hallon/track.rb +12 -6
  27. data/lib/hallon/version.rb +1 -1
  28. data/spec/hallon/artist_browse_spec.rb +3 -4
  29. data/spec/hallon/audio_queue_spec.rb +89 -0
  30. data/spec/hallon/enumerator_spec.rb +50 -25
  31. data/spec/hallon/error_spec.rb +2 -2
  32. data/spec/hallon/image_spec.rb +12 -5
  33. data/spec/hallon/link_spec.rb +8 -9
  34. data/spec/hallon/linkable_spec.rb +11 -0
  35. data/spec/hallon/observable/session_spec.rb +4 -0
  36. data/spec/hallon/player_spec.rb +118 -5
  37. data/spec/hallon/playlist_container_spec.rb +2 -2
  38. data/spec/hallon/playlist_spec.rb +32 -37
  39. data/spec/hallon/search_spec.rb +3 -3
  40. data/spec/hallon/user_spec.rb +0 -6
  41. data/spec/spec_helper.rb +10 -0
  42. data/spec/support/audio_driver_mock.rb +23 -0
  43. data/spec/support/context_stub_session.rb +5 -0
  44. data/spec/support/shared_for_linkable_objects.rb +22 -2
  45. metadata +26 -20
  46. data/lib/hallon/queue.rb +0 -71
  47. 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 [Enumerator<Track>] list of all tracks in the search result.
135
+ # @return [Tracks] list of all tracks in the search result.
91
136
  def tracks
92
- size = Spotify.search_num_tracks(pointer)
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 [Enumerator<Album>] list of all albums in the search result.
140
+ # @return [Albums] list of all albums in the search result.
105
141
  def albums
106
- size = Spotify.search_num_albums(pointer)
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 [Enumerator<Artist>] list of all artists in the search result.
145
+ # @return [Artists] list of all artists in the search result.
119
146
  def artists
120
- size = Spotify.search_num_artists(pointer)
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.
@@ -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
@@ -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 [Enumerator<Artist>] a list of artists.
85
+ # @return [Artists] a list of artists.
56
86
  def artists
57
- size = Spotify.toplistbrowse_num_artists(pointer)
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 [Enumerator<Album>] a list of albums.
90
+ # @return [Albums] a list of albums.
65
91
  def albums
66
- size = Spotify.toplistbrowse_num_albums(pointer)
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 [Enumerator<Track>] a list of tracks.
95
+ # @return [Tracks] a list of tracks.
74
96
  def tracks
75
- size = Spotify.toplistbrowse_num_tracks(pointer)
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 [Hallon::Enumerator<Artist>] all {Artist}s who performed this Track.
156
+ # @return [Artists] all {Artist}s who performed this Track.
147
157
  def artists
148
- size = Spotify.track_num_artists(pointer)
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.
@@ -3,5 +3,5 @@ module Hallon
3
3
  # Current release version of Hallon
4
4
  #
5
5
  # @see http://semver.org/
6
- VERSION = [0, 12, 0].join('.')
6
+ VERSION = [0, 13, 0].join('.')
7
7
  end
@@ -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
- mock_session(2) { subject.map{ |img| img.id(true) }.should eq [mock_image_id, mock_image_id] }
27
+ stub_session { should eq instantiate(Hallon::Image, mock_image_id, mock_image_id) }
28
28
  end
29
29
 
30
- specify 'portraits(false)' do
31
- browse.portraits(false)[0].should eq Hallon::Link.new(mock_image_link)
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
- let(:item) do
3
- mock.tap { |x| x.stub(:get).and_return(&alphabet) }
4
- end
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
- let(:enum) do
7
- Hallon::Enumerator.new(5) { |i| item.get(i) }
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 call the containing block" do
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
- Hallon::Enumerator.new(4).size.should eq 4
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
- item.should_receive(:get).with(1).and_return(&alphabet)
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
- item.should_receive(:get).with(4).and_return(&alphabet)
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
- item.should_not_receive(:get)
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
- item.should_not_receive(:get)
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
- item.should_receive(:get).with(1).and_return(&alphabet)
60
- item.should_receive(:get).with(2).and_return(&alphabet)
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
- item.should_receive(:get).with(1).and_return(&alphabet)
67
- item.should_receive(:get).with(2).and_return(&alphabet)
68
- item.should_receive(:get).with(3).and_return(&alphabet)
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
- item.should_receive(:get).with(4).and_return(&alphabet)
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
- item.should_not_receive(:get)
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
- item.should_receive(:get).with(2).and_return(&alphabet)
87
- item.should_receive(:get).with(3).and_return(&alphabet)
88
- item.should_receive(:get).with(4).and_return(&alphabet)
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
- item.should_not_receive(:get)
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
- item.should_not_receive(:get)
125
+ alphabet.should_not_receive(:[])
101
126
 
102
127
  enum[5..10].should eq []
103
128
  end