hallon 0.12.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
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