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
@@ -0,0 +1,138 @@
1
+ # coding: utf-8
2
+ module Hallon
3
+ # This is an implementation of a fictionary audio driver for Hallon. Each method is
4
+ # documented with expectations, parameters and other details. This class should serve
5
+ # as a guide on how to write your own audio driver with Hallon.
6
+ #
7
+ # @note this class is not used by Hallon, and is only here for documentation purposes.
8
+ class ExampleAudioDriver
9
+ # Here you can do your initialization of your audio driver. No
10
+ # parameters are given, and at this point no information of the
11
+ # audio format (or similar) is given.
12
+ def initialize
13
+ end
14
+
15
+ # Called when the audio playback should start. ie. when buffered
16
+ # audio previously retrieved from #stream should start blasting
17
+ # from the speakers.
18
+ #
19
+ # This method is only called as a direct result of the libspotify
20
+ # `start_playback` callback. It is recommended for this method to
21
+ # be thread-safe.
22
+ #
23
+ # Once called, audio playback is expected continue until either
24
+ # {#pause} or {#stop} is called.
25
+ #
26
+ # It is very important that this method does not block!
27
+ #
28
+ # Return value is ignored.
29
+ def play
30
+ end
31
+
32
+ # Called when the audio playback should be paused; often this is
33
+ # called as a direct result of {Player#pause}.
34
+ #
35
+ # It may also be called if the audio is stuttering, to allow spotify
36
+ # to buffer up more data before continuing playback. Because of this,
37
+ # this method is recommended to be thread-safe.
38
+ #
39
+ # It is very important that this method does not block!
40
+ #
41
+ # Return value is ignored.
42
+ def pause
43
+ end
44
+
45
+ # Called when audio playback should be stopped. Audio buffers can
46
+ # be cleared and any grip around the users’ speakers should also
47
+ # be released.
48
+ #
49
+ # This is only ever called as a direct result of the user manually
50
+ # stopping the player with {Player#stop}.
51
+ #
52
+ # Return value is ignored.
53
+ def stop
54
+ end
55
+
56
+ # Sets the current audio format.
57
+ #
58
+ # This is only ever called from inside the block given to {#stream}. It
59
+ # should be safe to recreate any existing audio buffers to fit the new
60
+ # audio format, as no frames will be delivered to the audio driver before
61
+ # this call returns.
62
+ #
63
+ # @note see `Spotify.enum_type(:sampletype).symbols` for a list of possible sample types
64
+ # @param [Hash] new_format
65
+ # @option new_format [Integer] :rate sample rate (eg. 44100)
66
+ # @option new_format [Integer] :channels number of audio channels (eg. 2)
67
+ # @option new_format [Symbol] :type sample type (eg. :int16)
68
+ def format=(new_format)
69
+ @format = new_format
70
+ end
71
+
72
+ # This method is expected to return the currently set format, which
73
+ # has been previously set by {#format=}.
74
+ #
75
+ # The player will only ever call this after previously setting the
76
+ # format through {#format=}.
77
+ #
78
+ # It is important that this always returns the same value that was
79
+ # given to {#format=}!
80
+ def format
81
+ @format
82
+ end
83
+
84
+ # Called *once* by the player, to initiate audio streaming to this driver.
85
+ # This method is expected to run indefinitely, and is run inside a separate
86
+ # thread.
87
+ #
88
+ # It is given a block that takes an integer as an argument, which specifies
89
+ # how many audio frames the player may give the driver for audio playback.
90
+ # If the block is given no arguments, the audio driver is expected to be able
91
+ # to consume any number of audio frames for the given call.
92
+ #
93
+ # When the audio driver is ready to consume audio, it should yield to the given
94
+ # block. If it can take only a finite number of audio frames it should be specified
95
+ # in the parameter.
96
+ #
97
+ # Upon yielding to the given block, the player will:
98
+ #
99
+ # - if the player is currently not playing, wait until it is
100
+ # - inspect the format of the audio driver
101
+ # - if the format has changed, set the new format on the driver __and return nil__
102
+ # - if the format has not changed, return an array of audio frames
103
+ #
104
+ # The number of frames returned upon yielding will be less than or equal to
105
+ # the number of frames requested when calling yield.
106
+ #
107
+ # The format of the audio frames can be determined by inspecting {#format} once
108
+ # the yield has returned. It is safe to inspect this format at any point within
109
+ # this method.
110
+ #
111
+ # The audio frames is a ruby array, grouped by channels. So for 2-channeled audio
112
+ # the returned array from a yield will look similar to this:
113
+ #
114
+ # [[1239857, -123087], [34971, 123084], …]
115
+ #
116
+ # Also see the implementation for this method on a more concise explanation.
117
+ #
118
+ # @yield [num_frames] to retrieve audio frames for playback buffering
119
+ # @yieldparam [Integer] num_frames maximum number of frames that should be returned
120
+ # @yieldreturn [Array<[]>, nil] an array of audio frames, or nil if audio format has changed
121
+ def stream
122
+ loop do
123
+ # set up internal buffers for current @format
124
+ loop do
125
+ # calculate size of internal buffers
126
+ audio_data = yield(4048) # can only take 4048 frames of 2-channeled int16ne data
127
+
128
+ if audio_data.nil?
129
+ # audio format has changed, reinitialize buffers
130
+ break
131
+ else
132
+ # playback the audio data
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,110 @@
1
+ # coding: utf-8
2
+ require 'monitor'
3
+
4
+ module Hallon
5
+ # Hallon::AudioQueue is a non-blocking (well, not entirely) sized FIFO queue.
6
+ #
7
+ # You initialize the queue with a `max_size`, and then push data to it.
8
+ # For every push operation, the AudioQueue will tell you how much of your data
9
+ # it could consume. If the queue becomes full, it won’t accept any more
10
+ # data (and will return 0 on the #push operation) until you pull some data
11
+ # out of it with #pop.
12
+ #
13
+ # Hallon::AudioQueue is useful for handling {Hallon::Observable::Session#music_delivery_callback}.
14
+ #
15
+ # @example
16
+ # queue = Hallon::AudioQueue.new(4)
17
+ # queue.push([1, 2]) # => 2
18
+ # queue.push([3]) # => 1
19
+ # queue.push([4, 5, 6]) # => 1
20
+ # queue.push([5, 6]) # => 0
21
+ # queue.pop(1) # => [1]
22
+ # queue.push([5, 6]) # => 1
23
+ # queue.pop # => [2, 3, 4, 5]
24
+ #
25
+ # @private
26
+ class AudioQueue
27
+ attr_reader :max_size
28
+
29
+ # @param [Integer] max_size
30
+ def initialize(max_size)
31
+ @max_size = max_size
32
+ @samples = []
33
+
34
+ @samples.extend(MonitorMixin)
35
+ @condvar = @samples.new_cond
36
+ end
37
+
38
+ # @param [#take] data
39
+ # @return [Integer] how much of the data that was added to the queue
40
+ def push(samples)
41
+ synchronize do
42
+ can_accept = max_size - size
43
+ new_samples = samples.take(can_accept)
44
+
45
+ @samples.concat(new_samples)
46
+ @condvar.signal
47
+
48
+ new_samples.size
49
+ end
50
+ end
51
+
52
+ # @note If the queue is empty, this operation will block until data is available.
53
+ # @param [Integer] num_samples max number of samples to pop off the queue
54
+ # @return [Array] data, where data.size might be less than num_samples but never more
55
+ def pop(num_samples = max_size)
56
+ synchronize do
57
+ @condvar.wait_while { empty? }
58
+ @samples.shift(num_samples)
59
+ end
60
+ end
61
+
62
+ # @return [Integer] number of samples in buffer.
63
+ def size
64
+ synchronize { @samples.size }
65
+ end
66
+
67
+ # @return [Boolean] true if the queue has a {#size} of 0.
68
+ def empty?
69
+ size.zero?
70
+ end
71
+
72
+ # Clear all data from the AudioQueue.
73
+ def clear
74
+ synchronize { @samples.clear }
75
+ end
76
+
77
+ # Attach format metadata to the queue.
78
+ #
79
+ # @note this will clear the queue of all audio data!
80
+ # @param format new audio format
81
+ def format=(format)
82
+ synchronize do
83
+ @format = format
84
+ clear
85
+ end
86
+ end
87
+
88
+ # Returns the format previously set by #format=.
89
+ attr_reader :format
90
+
91
+ # Use this if you wish to perform multiple operations on
92
+ # the AudioQueue atomicly.
93
+ #
94
+ # @note this lock is re-entrant, you can nest it in itself
95
+ # @yield exclusive section around the queue contents
96
+ # @return whatever the given block returns
97
+ def synchronize
98
+ @samples.synchronize { return yield }
99
+ end
100
+
101
+ # Create a condition variable bound to this AudioQueue.
102
+ # Should be used if you want to wait inside {#synchronize}.
103
+ #
104
+ # @return [MonitorMixin::ConditionVariable]
105
+ # @see monitor.rb (ruby stdlib)
106
+ def new_cond
107
+ @samples.new_cond
108
+ end
109
+ end
110
+ end
@@ -8,29 +8,66 @@ module Hallon
8
8
  class Enumerator
9
9
  include Enumerable
10
10
 
11
- # @return [Integer] number of items this enumerator can yield
12
- attr_reader :size
11
+ # @return [Spotify::Pointer]
12
+ attr_reader :pointer
13
13
 
14
- # Construct an enumerator of `size` elements.
14
+ # @macro [attach] size
15
+ # @method size
16
+ # @return [Integer] size of this enumerator
15
17
  #
16
- # @param [Integer] size
17
- # @yield to the given block when an item is requested (through #each, #[] etc)
18
- # @yieldparam [Integer] index item to retrieve
19
- def initialize(size, &yielder)
20
- @size = size
21
- @items = Array.new(size) do |i|
22
- lambda { yielder[i] }
18
+ # @param [String, Symbol] method
19
+ def self.size(method)
20
+ # this method is about twice as fast as define_method/public_send
21
+ class_eval <<-SIZE, __FILE__, __LINE__ + 1
22
+ def size
23
+ Spotify.#{method}(pointer)
24
+ end
25
+ SIZE
26
+ end
27
+
28
+ # @example modifying result with a block
29
+ # item :playlist_track! do |track|
30
+ # Track.from(track)
31
+ # end
32
+ #
33
+ # @note block passed is used to modify return value from Spotify#item_method
34
+ # @param [Symbol, String] method
35
+ # @yield [item, index, pointer] item from calling Spotify#item_method
36
+ # @yieldparam item
37
+ # @yieldparam [Integer] index
38
+ # @yieldparam [Spotify::Pointer] pointer
39
+ #
40
+ # @macro [attach] item
41
+ # @method at(index)
42
+ def self.item(method, &block)
43
+ define_method(:at) do |index|
44
+ item = Spotify.public_send(method, pointer, index)
45
+ item = instance_exec(item, index, pointer, &block) if block_given?
46
+ item
23
47
  end
24
48
  end
25
49
 
50
+ # initialize the enumerator with `subject`.
51
+ #
52
+ # @param [#pointer] subject
53
+ def initialize(subject)
54
+ @pointer = subject.pointer
55
+ end
56
+
26
57
  # Yield each item out of the enumerator.
27
58
  #
28
59
  # @yield obj
29
- # @return [Enumerator]
60
+ # @return [Enumerator] self
30
61
  def each
31
- tap do
32
- size.times { |i| yield(self[i]) }
62
+ index = 0
63
+
64
+ # check size on each iteration, in case it changes
65
+ while index < size
66
+ yield self[index]
67
+ index += 1
33
68
  end
69
+
70
+ self
34
71
  end
35
72
 
36
73
  # @overload [](index)
@@ -46,14 +83,16 @@ module Hallon
46
83
  #
47
84
  # @see http://rdoc.info/stdlib/core/1.9.2/Array:[]
48
85
  def [](*args)
49
- result = @items[*args]
86
+ # crazy inefficient, but also crazy easy, don’t hate me :(
87
+ items = [*0...size]
88
+ result = items[*args]
50
89
 
51
90
  if result.nil?
52
91
  nil
53
92
  elsif result.respond_to?(:map)
54
- result.map(&:call)
93
+ result.map { |index| at(index) }
55
94
  else
56
- result.call
95
+ at(result)
57
96
  end
58
97
  end
59
98
 
data/lib/hallon/error.rb CHANGED
@@ -46,10 +46,17 @@ module Hallon
46
46
 
47
47
  # Raise an {Error} with the given errno, unless it is `nil`, `:timeout`, `0` or `:ok`.
48
48
  #
49
+ # @example
50
+ #
51
+ # Hallon::Error.maybe_raise(error, ignore: :is_loading)
52
+ #
49
53
  # @param [Fixnum, Symbol] error
54
+ # @param [Hash] options
55
+ # @option options [Array] :ignore ([]) other values to ignore of error
50
56
  # @return [nil]
51
- def maybe_raise(x)
52
- return nil if [nil, :timeout, :is_loading].include?(x)
57
+ def maybe_raise(x, options = {})
58
+ ignore = [nil, :timeout] + Array(options[:ignore])
59
+ return nil if ignore.include?(x)
53
60
 
54
61
  error, symbol = disambiguate(x)
55
62
  return symbol if symbol == :ok
data/lib/hallon/image.rb CHANGED
@@ -69,13 +69,14 @@ module Hallon
69
69
  end
70
70
  end
71
71
 
72
- # @see Base#==
72
+ # Overridden to first and foremost compare by id if applicable.
73
+ #
73
74
  # @param [Object] other
74
- # @return [Boolean] true if the images are the same object or have the same ID.
75
+ # @return [Boolean]
75
76
  def ==(other)
76
- super or id(true) == other.id(true)
77
- rescue NoMethodError, ArgumentError
78
- false
77
+ super or if other.is_a?(Image)
78
+ id(true) == other.id(true)
79
+ end
79
80
  end
80
81
 
81
82
  protected
data/lib/hallon/link.rb CHANGED
@@ -65,12 +65,15 @@ module Hallon
65
65
  "http://open.spotify.com/%s" % to_str[8..-1].gsub(':', '/')
66
66
  end
67
67
 
68
+ # Compare the Link to other. If other is a Link, also compare
69
+ # their `to_str` if necessary.
70
+ #
68
71
  # @param [Object] other
69
- # @return [Boolean] true if this link equals `other.to_str`.
72
+ # @return [Boolean]
70
73
  def ==(other)
71
- to_str == other.to_str
72
- rescue NoMethodError
73
- super
74
+ super or if other.is_a?(Link)
75
+ to_str == other.to_str
76
+ end
74
77
  end
75
78
 
76
79
  # @return [String] string representation of the Link.
@@ -100,5 +100,32 @@ module Hallon
100
100
 
101
101
  private :from_link
102
102
  private :to_link
103
+
104
+ def self.extended(other)
105
+ other.send(:include, InstanceMethods)
106
+ end
107
+
108
+ module InstanceMethods
109
+ # Converts the Linkable first to a Link, and then that link to a String.
110
+ #
111
+ # @note Returns an empty string if the #to_link call fails.
112
+ # @return [String]
113
+ def to_str
114
+ link = to_link
115
+ link &&= link.to_str
116
+ link.to_s
117
+ end
118
+
119
+ # Compare the Linkable to other. If other is a Linkable, also
120
+ # compare their `to_link` if necessary.
121
+ #
122
+ # @param [Object] other
123
+ # @return [Boolean]
124
+ def ===(other)
125
+ super or if other.respond_to?(:to_link)
126
+ to_link == other.to_link
127
+ end
128
+ end
129
+ end
103
130
  end
104
131
  end
@@ -8,6 +8,23 @@ module Hallon::Observable
8
8
  other.send(:include, Hallon::Observable)
9
9
  end
10
10
 
11
- include Hallon::Observable::Session
11
+ protected
12
+
13
+ # @return nil
14
+ def initialize_callbacks
15
+ %w(end_of_track streaming_error play_token_lost).map { |m| callback_for(m) }
16
+ end
17
+
18
+ # Dummy callback. See Session#end_of_track_callback.
19
+ def end_of_track_callback(session)
20
+ end
21
+
22
+ # Dummy callback. See Session#streaming_error_callback.
23
+ def streaming_error_callback(session, error)
24
+ end
25
+
26
+ # Dummy callback. See Session#play_token_lost_callback.
27
+ def play_token_lost_callback(session)
28
+ end
12
29
  end
13
30
  end