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
@@ -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