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
@@ -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
|
data/lib/hallon/enumerator.rb
CHANGED
@@ -8,29 +8,66 @@ module Hallon
|
|
8
8
|
class Enumerator
|
9
9
|
include Enumerable
|
10
10
|
|
11
|
-
# @return [
|
12
|
-
attr_reader :
|
11
|
+
# @return [Spotify::Pointer]
|
12
|
+
attr_reader :pointer
|
13
13
|
|
14
|
-
#
|
14
|
+
# @macro [attach] size
|
15
|
+
# @method size
|
16
|
+
# @return [Integer] size of this enumerator
|
15
17
|
#
|
16
|
-
# @param [
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
32
|
-
|
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
|
-
|
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(
|
93
|
+
result.map { |index| at(index) }
|
55
94
|
else
|
56
|
-
result
|
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
|
-
|
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
|
-
#
|
72
|
+
# Overridden to first and foremost compare by id if applicable.
|
73
|
+
#
|
73
74
|
# @param [Object] other
|
74
|
-
# @return [Boolean]
|
75
|
+
# @return [Boolean]
|
75
76
|
def ==(other)
|
76
|
-
super or
|
77
|
-
|
78
|
-
|
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]
|
72
|
+
# @return [Boolean]
|
70
73
|
def ==(other)
|
71
|
-
|
72
|
-
|
73
|
-
|
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.
|
data/lib/hallon/linkable.rb
CHANGED
@@ -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
|
-
|
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
|