hallon 0.11.0 → 0.12.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 (68) hide show
  1. data/.yardopts +1 -0
  2. data/{CHANGELOG → CHANGELOG.md} +82 -27
  3. data/Gemfile +1 -0
  4. data/README.markdown +1 -1
  5. data/Rakefile +8 -6
  6. data/examples/adding_tracks_to_playlist.rb +3 -0
  7. data/examples/logging_in.rb +3 -0
  8. data/examples/playing_audio.rb +130 -0
  9. data/examples/printing_link_information.rb +3 -0
  10. data/examples/show_published_playlists_of_user.rb +5 -13
  11. data/hallon.gemspec +2 -2
  12. data/lib/hallon.rb +15 -0
  13. data/lib/hallon/album.rb +1 -1
  14. data/lib/hallon/album_browse.rb +4 -3
  15. data/lib/hallon/artist.rb +1 -1
  16. data/lib/hallon/artist_browse.rb +5 -4
  17. data/lib/hallon/base.rb +7 -2
  18. data/lib/hallon/error.rb +1 -1
  19. data/lib/hallon/ext/spotify.rb +26 -42
  20. data/lib/hallon/image.rb +7 -8
  21. data/lib/hallon/observable.rb +134 -62
  22. data/lib/hallon/observable/album_browse.rb +30 -0
  23. data/lib/hallon/observable/artist_browse.rb +31 -0
  24. data/lib/hallon/observable/image.rb +31 -0
  25. data/lib/hallon/observable/player.rb +13 -0
  26. data/lib/hallon/observable/playlist.rb +194 -0
  27. data/lib/hallon/observable/playlist_container.rb +74 -0
  28. data/lib/hallon/observable/post.rb +30 -0
  29. data/lib/hallon/observable/search.rb +29 -0
  30. data/lib/hallon/observable/session.rb +236 -0
  31. data/lib/hallon/observable/toplist.rb +30 -0
  32. data/lib/hallon/player.rb +8 -17
  33. data/lib/hallon/playlist.rb +11 -7
  34. data/lib/hallon/playlist_container.rb +11 -4
  35. data/lib/hallon/queue.rb +71 -0
  36. data/lib/hallon/search.rb +10 -7
  37. data/lib/hallon/session.rb +18 -21
  38. data/lib/hallon/toplist.rb +4 -3
  39. data/lib/hallon/user.rb +5 -5
  40. data/lib/hallon/version.rb +1 -1
  41. data/spec/hallon/album_browse_spec.rb +4 -0
  42. data/spec/hallon/artist_browse_spec.rb +4 -0
  43. data/spec/hallon/base_spec.rb +26 -9
  44. data/spec/hallon/hallon_spec.rb +0 -18
  45. data/spec/hallon/image_spec.rb +0 -1
  46. data/spec/hallon/link_spec.rb +14 -0
  47. data/spec/hallon/observable/album_browse_spec.rb +7 -0
  48. data/spec/hallon/observable/artist_browse_spec.rb +7 -0
  49. data/spec/hallon/observable/image_spec.rb +8 -0
  50. data/spec/hallon/observable/playlist_container_spec.rb +21 -0
  51. data/spec/hallon/observable/playlist_spec.rb +85 -0
  52. data/spec/hallon/observable/post_spec.rb +8 -0
  53. data/spec/hallon/observable/search_spec.rb +7 -0
  54. data/spec/hallon/observable/session_spec.rb +143 -0
  55. data/spec/hallon/observable/toplist_spec.rb +7 -0
  56. data/spec/hallon/observable_spec.rb +134 -65
  57. data/spec/hallon/playlist_container_spec.rb +24 -18
  58. data/spec/hallon/playlist_spec.rb +2 -0
  59. data/spec/hallon/queue_spec.rb +35 -0
  60. data/spec/hallon/session_spec.rb +4 -4
  61. data/spec/hallon/spotify_spec.rb +35 -9
  62. data/spec/mockspotify.rb +2 -3
  63. data/spec/spec_helper.rb +0 -1
  64. data/spec/support/common_objects.rb +27 -15
  65. data/spec/support/enumerable_comparison.rb +9 -0
  66. data/spec/support/shared_for_callbacks.rb +60 -0
  67. data/spec/support/shared_for_linkable_objects.rb +1 -1
  68. metadata +56 -20
@@ -21,8 +21,8 @@ Gem::Specification.new do |gem|
21
21
  gem.platform = Gem::Platform::RUBY
22
22
  gem.required_ruby_version = '~> 1.8'
23
23
 
24
- gem.add_dependency 'spotify', '~> 10.1.1'
25
- gem.add_development_dependency 'bundler', '~> 1.0'
24
+ gem.add_dependency 'ref', '~> 1.0'
25
+ gem.add_dependency 'spotify', '~> 10.3.0'
26
26
  gem.add_development_dependency 'rake', '~> 0.8'
27
27
  gem.add_development_dependency 'rspec', '~> 2'
28
28
  gem.add_development_dependency 'yard'
@@ -9,8 +9,20 @@ require 'hallon/linkable'
9
9
  require 'hallon/version'
10
10
  require 'hallon/error'
11
11
  require 'hallon/base'
12
+ require 'hallon/queue'
12
13
  require 'hallon/enumerator'
13
14
 
15
+ require 'hallon/observable/album_browse'
16
+ require 'hallon/observable/artist_browse'
17
+ require 'hallon/observable/image'
18
+ require 'hallon/observable/playlist_container'
19
+ require 'hallon/observable/playlist'
20
+ require 'hallon/observable/post'
21
+ require 'hallon/observable/session'
22
+ require 'hallon/observable/search'
23
+ require 'hallon/observable/toplist'
24
+ require 'hallon/observable/player'
25
+
14
26
  require 'hallon/session'
15
27
  require 'hallon/link'
16
28
  require 'hallon/user'
@@ -26,6 +38,9 @@ require 'hallon/artist_browse'
26
38
  require 'hallon/player'
27
39
  require 'hallon/search'
28
40
 
41
+ # Why is this not the default in Ruby?
42
+ Thread.abort_on_exception = true
43
+
29
44
  # The Hallon module wraps around all Hallon objects to avoid polluting
30
45
  # the global namespace. To start using Hallon, you most likely want to
31
46
  # be looking for the documentation on {Hallon::Session}.
@@ -84,7 +84,7 @@ module Hallon
84
84
  def cover(as_image = true)
85
85
  if as_image
86
86
  cover = Spotify.album_cover(pointer)
87
- Image.new cover.read_string(20) unless cover.null?
87
+ Image.from(cover)
88
88
  else
89
89
  cover = Spotify.link_create_from_album_cover!(pointer)
90
90
  Link.from(cover)
@@ -7,7 +7,7 @@ module Hallon
7
7
  # @see Album
8
8
  # @see http://developer.spotify.com/en/libspotify/docs/group__albumbrowse.html
9
9
  class AlbumBrowse < Base
10
- include Observable
10
+ extend Observable::AlbumBrowse
11
11
 
12
12
  # Creates an AlbumBrowse instance from an Album or an Album pointer.
13
13
  #
@@ -22,8 +22,9 @@ module Hallon
22
22
  raise TypeError, "expected album pointer, was given #{given}"
23
23
  end
24
24
 
25
- @callback = proc { trigger(:load) }
26
- @pointer = Spotify.albumbrowse_create!(session.pointer, pointer, @callback, nil)
25
+ subscribe_for_callbacks do |callback|
26
+ @pointer = Spotify.albumbrowse_create!(session.pointer, pointer, callback, nil)
27
+ end
27
28
 
28
29
  raise FFI::NullPointerError, "album browsing failed" if @pointer.null?
29
30
  end
@@ -49,7 +49,7 @@ module Hallon
49
49
  def portrait(as_image = true)
50
50
  if as_image
51
51
  portrait = Spotify.artist_portrait(pointer)
52
- Image.new portrait.read_bytes(20) unless portrait.null?
52
+ Image.from(portrait)
53
53
  else
54
54
  portrait = Spotify.link_create_from_artist_portrait!(pointer)
55
55
  Link.from(portrait)
@@ -5,7 +5,7 @@ module Hallon
5
5
  # @see Artist
6
6
  # @see http://developer.spotify.com/en/libspotify/docs/group__artistbrowse.html
7
7
  class ArtistBrowse < Base
8
- include Observable
8
+ extend Observable::ArtistBrowse
9
9
 
10
10
  # @return [Array<Symbol>] artist browsing types for use in {#initialize}
11
11
  def self.types
@@ -26,8 +26,9 @@ module Hallon
26
26
  raise TypeError, "expected artist pointer, was given #{given}"
27
27
  end
28
28
 
29
- @callback = proc { trigger(:load) }
30
- @pointer = Spotify.artistbrowse_create!(session.pointer, pointer, type, @callback, nil)
29
+ subscribe_for_callbacks do |callback|
30
+ @pointer = Spotify.artistbrowse_create!(session.pointer, pointer, type, callback, nil)
31
+ end
31
32
 
32
33
  raise FFI::NullPointerError, "artist browsing failed" if @pointer.null?
33
34
  end
@@ -70,7 +71,7 @@ module Hallon
70
71
  size = Spotify.artistbrowse_num_portraits(pointer)
71
72
  Enumerator.new(size) do |i|
72
73
  if as_image
73
- id = Spotify.artistbrowse_portrait(pointer, i).read_string(20)
74
+ id = Spotify.artistbrowse_portrait(pointer, i)
74
75
  Image.new(id)
75
76
  else
76
77
  link = Spotify.link_create_from_artistbrowse_portrait!(pointer, i)
@@ -3,10 +3,15 @@ module Hallon
3
3
  # All objects in Hallon are mere representations of Spotify objects.
4
4
  # Hallon::Base covers basic functionality shared by all of these.
5
5
  class Base
6
- # @param [#null?] pointer
6
+ # @param [#nil?, #null?] pointer
7
7
  # @return [self, nil] a new instance of self, unless given pointer is #null?
8
8
  def self.from(pointer, *args, &block)
9
- new(pointer, *args, &block) unless pointer.null?
9
+ is_nil = pointer.nil?
10
+ is_null = pointer.null? if pointer.respond_to?(:null?)
11
+
12
+ unless is_nil or is_null
13
+ new(pointer, *args, &block)
14
+ end
10
15
  end
11
16
 
12
17
  # Underlying FFI pointer.
@@ -49,7 +49,7 @@ module Hallon
49
49
  # @param [Fixnum, Symbol] error
50
50
  # @return [nil]
51
51
  def maybe_raise(x)
52
- return nil if [nil, :timeout].include?(x)
52
+ return nil if [nil, :timeout, :is_loading].include?(x)
53
53
 
54
54
  error, symbol = disambiguate(x)
55
55
  return symbol if symbol == :ok
@@ -122,7 +122,7 @@ module Spotify
122
122
 
123
123
  # @return [String] representation of the spotify pointer
124
124
  def to_s
125
- "<#{self.class} address=#{address} type=#{type}>"
125
+ "<#{self.class} address=0x#{address.to_s(16)} type=#{type}>"
126
126
  end
127
127
 
128
128
  # Create a proc that will accept a pointer of a given type and
@@ -142,14 +142,8 @@ module Spotify
142
142
  # @param [Object] pointer
143
143
  # @param [Symbol] type (optional, no type checking is done if not given)
144
144
  # @return [Boolean] true if object is a spotify pointer and of correct type
145
- def self.typechecks?(object, type = nil)
146
- if ! object.is_a?(Spotify::Pointer)
147
- false
148
- elsif type
149
- object.type == type.to_s
150
- else
151
- true
152
- end
145
+ def self.typechecks?(object, type)
146
+ !! (object.type == type.to_s) if object.is_a?(Spotify::Pointer)
153
147
  end
154
148
  end
155
149
 
@@ -200,51 +194,41 @@ module Spotify
200
194
  end
201
195
 
202
196
  # Makes it easier binding callbacks safely to callback structs.
203
- # When including this class you *must* define `proc_for(member)`!
197
+ #
198
+ # @see add
199
+ # @see remove
204
200
  module CallbackStruct
205
- # Assigns the callbacks to call the given target; the callback
206
- # procs are stored in the `storage` parameter. **Make sure the
207
- # storage does not get garbage collected as long as these callbacks
208
- # are needed!**
201
+ # Before assigning [member]=(callback), inspect the arity of
202
+ # said callback and raise an ArgumentError if they don‘t match.
209
203
  #
210
- # @param [Object] target
211
- # @param [#&#91;&#93;&#61;] storage
212
- def create(target, storage)
213
- new.tap do |struct|
214
- members.each do |member|
215
- struct[member] = storage[member] = proc_for(target, member)
216
- end
204
+ # @raise ArgumentError if the arity of the given callback does not match the member
205
+ def []=(member, callback)
206
+ unless callback.arity < 0 or callback.arity == arity_of(member)
207
+ raise ArgumentError, "#{member} callback takes #{arity_of(member)} arguments, was #{callback.arity}"
208
+ else
209
+ super
217
210
  end
218
211
  end
219
- end
220
212
 
221
- class << SessionCallbacks
222
- include CallbackStruct
213
+ protected
223
214
 
224
- private
225
- # @see CallbackStruct
226
- def proc_for(target, member)
227
- lambda { |pointer, *args| target.trigger(member, *args) }
228
- end
215
+ # @param [Symbol] member
216
+ # @return [Integer] arity of the given callback member
217
+ def arity_of(member)
218
+ fn = layout[member].type
219
+ fn.param_types.size
220
+ end
229
221
  end
230
222
 
231
- class << PlaylistCallbacks
223
+ SessionCallbacks.instance_eval do
232
224
  include CallbackStruct
233
-
234
- private
235
- # @see CallbackStruct
236
- def proc_for(target, member)
237
- lambda { |pointer, *args, userdata| target.trigger(member, *args) }
238
- end
239
225
  end
240
226
 
241
- class << PlaylistContainerCallbacks
227
+ PlaylistCallbacks.instance_eval do
242
228
  include CallbackStruct
229
+ end
243
230
 
244
- private
245
- # @see CallbackStruct
246
- def proc_for(target, member)
247
- lambda { |pointer, *args, userdata| target.trigger(member, *args) }
248
- end
231
+ PlaylistContainerCallbacks.instance_eval do
232
+ include CallbackStruct
249
233
  end
250
234
  end
@@ -12,8 +12,7 @@ module Hallon
12
12
 
13
13
  to_link :from_image
14
14
 
15
- # Image triggers `:load` when loaded
16
- include Observable
15
+ extend Observable::Image
17
16
 
18
17
  # Create a new instance of an Image.
19
18
  #
@@ -30,13 +29,13 @@ module Hallon
30
29
  end
31
30
 
32
31
  @pointer = to_pointer(link, :image) do
33
- ptr = FFI::MemoryPointer.new(:char, 20)
34
- ptr.write_bytes(link)
35
- Spotify.image_create!(session.pointer, ptr)
32
+ Spotify.image_create!(session.pointer, link)
36
33
  end
37
34
 
38
- @callback = proc { trigger :load }
39
- Spotify.image_add_load_callback(pointer, @callback, nil)
35
+ subscribe_for_callbacks do |callbacks|
36
+ Spotify.image_remove_load_callback(pointer, callbacks, nil)
37
+ Spotify.image_add_load_callback(pointer, callbacks, nil)
38
+ end
40
39
  end
41
40
 
42
41
  # @return [Boolean] true if the image is loaded.
@@ -58,7 +57,7 @@ module Hallon
58
57
  # @param [Boolean] raw true if you want the image id as a hexadecimal string
59
58
  # @return [String] image ID as a string.
60
59
  def id(raw = false)
61
- id = Spotify.image_image_id(pointer).read_string(20)
60
+ id = Spotify.image_image_id(pointer)
62
61
  raw ? id : to_hex(id)
63
62
  end
64
63
 
@@ -1,87 +1,159 @@
1
1
  # coding: utf-8
2
+ require 'ref'
3
+
2
4
  module Hallon
3
5
  # A module providing event capabilities to Hallon objects.
4
6
  #
5
7
  # @private
6
8
  module Observable
9
+ # This module is responsible for creating methods for registering
10
+ # callbacks. It expects a certain protocol to already available.
11
+ module ClassMethods
12
+ # When extended it’ll call #initialize_observable to set-up `other`.
13
+ def self.extended(other)
14
+ other.send(:initialize_observable)
15
+ end
16
+
17
+ # @return [Method, Struct] callbacks to attach to this object
18
+ attr_reader :callbacks
19
+
20
+ # Subscribe to callbacks for a given pointer.
21
+ #
22
+ # @param [Object] object
23
+ # @param [FFI::Pointer] pointer
24
+ def subscribe(object, pointer)
25
+ key = pointer.address
26
+ ref = Ref::WeakReference.new(object)
27
+
28
+ @lock.synchronize do
29
+ if @subscribers_rev[ref.referenced_object_id]
30
+ raise ArgumentError, "already subscribed to callbacks"
31
+ end
32
+
33
+ @subscribers[key] ||= {} # use a hash for fast reverse lookups
34
+ @subscribers[key][ref.referenced_object_id] = ref
35
+ @subscribers_rev[ref.referenced_object_id] = key
36
+ end
37
+
38
+ ObjectSpace.define_finalizer(object, @unsubscriber)
39
+ end
40
+
41
+ # Retrieve all subscribers for a given pointer.
42
+ #
43
+ # @param [FFI::Pointer] pointer
44
+ def subscribers_for(pointer)
45
+ key = pointer.address
46
+
47
+ @lock.synchronize do
48
+ @subscribers.fetch(key, {}).values.map(&:object).compact
49
+ end
50
+ end
51
+
52
+ protected
53
+
54
+ # Run when ClassMethods are extended.
55
+ #
56
+ # It sets up the callbacks and all book-keeping required to keep
57
+ # track of all subscribers properly.
58
+ def initialize_observable
59
+ @callbacks = initialize_callbacks
60
+
61
+ @lock = Ref::SafeMonitor.new
62
+ @subscribers = {}
63
+ @subscribers_rev = {}
64
+ @unsubscriber = proc do |object_id|
65
+ @lock.synchronize do
66
+ if key = @subscribers_rev.delete(object_id)
67
+ @subscribers[key].delete(object_id)
68
+ @subscribers.delete(key) if @subscribers[key].empty?
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ # @param [#to_s] name
75
+ # @return [Method]
76
+ def callback_for(name)
77
+ method("#{name}_callback")
78
+ end
79
+
80
+ # Scans through the list of subscribers, trying to find any
81
+ # subscriber attached to this pointer. For each subscriber,
82
+ # trigger the appropriate event.
83
+ #
84
+ # @param [FFI::Pointer] pointer
85
+ # @param [Symbol] event
86
+ # @param […] arguments
87
+ # @return whatever the (last) handler returned
88
+ def trigger(pointer, event, *arguments)
89
+ subscribers_for(pointer).inject(nil) do |_, subscriber|
90
+ # trigger is protected, inconvenient but symbolic
91
+ subscriber.send(:trigger, event, *arguments)
92
+ end
93
+ end
94
+ end
95
+
96
+ # Will extend `other` with {ClassMethods}.
97
+ def self.included(other)
98
+ other.extend(ClassMethods)
99
+ end
100
+
7
101
  # Defines a handler for the given event.
8
102
  #
9
- # @example defining a handler and triggering it
10
- # on(:callback) do |message|
11
- # puts message
12
- # end
13
- #
14
- # trigger(:callback, "Moo!") # => prints "Moo!"
15
- #
16
- # @example multiple events with one handler
17
- # on(:a, :b, :c) do |name, *args|
18
- # puts "#{name} called with: #{args.inspect}"
19
- # end
20
- #
21
- # trigger(:a) # => prints ":a called with: []"
22
- # trigger(:b, :c) # => prints ":b called with: [:c]"
23
- #
24
- # @note when defining a handler for multiple events, the
25
- # first argument passed to the handler is the name
26
- # of the event that called it
27
- # @param [#to_sym] event name of event to handle
103
+ # @param [#to_s] event name of event to handle
104
+ # @return [Proc] the given block
28
105
  # @yield (*args) event handler block
29
- def on(*events, &block)
106
+ def on(event, &block)
30
107
  raise ArgumentError, "no block given" unless block
31
- wrap = events.length > 1
32
- events.map(&:to_sym).each do |event|
33
- block = proc { |*args| yield(event, *args) } if wrap
34
- __handlers[event] = [] unless __handlers.has_key?(event)
35
- __handlers[event] << block
36
- end
108
+ raise NameError, "no such callback: #{event}" unless has_callback?(event)
109
+ handlers[event.to_s] = block
37
110
  end
38
111
 
39
- # Trigger a handler for a given event.
40
- #
41
- # @param [#to_sym] event
42
- # @param [Object, ...] params given to each handler
43
- def trigger(event, *params, &block)
44
- catch :return do
45
- return_value = nil
46
- __handlers[event.to_sym].each do |handler|
47
- return_value = handler.call(*params, &block)
48
- end
49
- return_value
50
- end
112
+ # @param [#to_s] name
113
+ # @return [Boolean] true if a callback with `name` exists.
114
+ def has_callback?(name)
115
+ self.class.respond_to?("#{name}_callback", true)
51
116
  end
52
117
 
53
- # Run the given block, protecting all previous event handlers.
54
- #
55
- # @example
56
- # o = Object.new
57
- # o.instance_eval { include Hallon::Base }
58
- # o.on(:method) { "outside" }
118
+ # Run a given block, and once it exits restore all handlers
119
+ # to the way they were before running the block.
59
120
  #
60
- # puts o.on_method # => "outside"
61
- # o.protecting_handlers do
62
- # o.on(:method) { "inside" }
63
- # puts o.on_method # => "inside"
64
- # end
65
- # puts o.on_method # => "outside"
121
+ # This allows you to temporarily use different handlers for
122
+ # some events.
66
123
  #
67
124
  # @yield
68
- # @return whatever the given block returns
69
125
  def protecting_handlers
70
- deep_copy = __handlers.dup.clear
71
- __handlers.each do |k, v|
72
- deep_copy[k] = v.dup
73
- end
126
+ old_handlers = handlers.dup
74
127
  yield
75
128
  ensure
76
- __handlers.replace deep_copy
129
+ handlers.replace(old_handlers)
77
130
  end
78
131
 
79
- private
80
- # Hash mapping events to handlers.
81
- #
82
- # @return [Hash]
83
- def __handlers
84
- @__handlers ||= Hash.new([])
132
+ protected
133
+
134
+ # Register this object as interested in callbacks.
135
+ #
136
+ # @yield [callback]
137
+ # @yieldparam [Method, Struct] callback (always the same object)
138
+ # @return whatever the block returns
139
+ def subscribe_for_callbacks
140
+ yield(self.class.callbacks).tap do
141
+ self.class.subscribe(self, pointer)
85
142
  end
143
+ end
144
+
145
+ # @param [#to_s] name
146
+ # @param [...] arguments
147
+ # @return whatever the handler returns
148
+ def trigger(name, *arguments, &block)
149
+ if handler = handlers[name.to_s]
150
+ handler.call(*arguments, self, &block)
151
+ end
152
+ end
153
+
154
+ # @return [Hash<String, Proc>]
155
+ def handlers
156
+ @__handlers ||= Hash.new(proc {})
157
+ end
86
158
  end
87
159
  end