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