hallon 0.11.0 → 0.12.0

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