hallon 0.8.0 → 0.9.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 (59) hide show
  1. data/.travis.yml +2 -0
  2. data/CHANGELOG +43 -0
  3. data/Gemfile +2 -0
  4. data/README.markdown +21 -13
  5. data/Rakefile +84 -23
  6. data/dev/login.rb +16 -0
  7. data/examples/adding_tracks_to_playlist.rb +49 -0
  8. data/examples/logging_in.rb +1 -6
  9. data/examples/show_published_playlists_of_user.rb +9 -19
  10. data/hallon.gemspec +1 -1
  11. data/lib/hallon.rb +3 -2
  12. data/lib/hallon/album.rb +55 -41
  13. data/lib/hallon/album_browse.rb +41 -37
  14. data/lib/hallon/artist.rb +30 -21
  15. data/lib/hallon/artist_browse.rb +59 -41
  16. data/lib/hallon/base.rb +68 -5
  17. data/lib/hallon/enumerator.rb +1 -0
  18. data/lib/hallon/error.rb +3 -0
  19. data/lib/hallon/ext/spotify.rb +169 -36
  20. data/lib/hallon/image.rb +30 -44
  21. data/lib/hallon/link.rb +29 -43
  22. data/lib/hallon/linkable.rb +68 -20
  23. data/lib/hallon/observable.rb +0 -1
  24. data/lib/hallon/player.rb +21 -7
  25. data/lib/hallon/playlist.rb +291 -0
  26. data/lib/hallon/playlist_container.rb +27 -0
  27. data/lib/hallon/search.rb +52 -45
  28. data/lib/hallon/session.rb +129 -81
  29. data/lib/hallon/toplist.rb +37 -19
  30. data/lib/hallon/track.rb +68 -45
  31. data/lib/hallon/user.rb +69 -33
  32. data/lib/hallon/version.rb +1 -1
  33. data/spec/hallon/album_browse_spec.rb +15 -9
  34. data/spec/hallon/album_spec.rb +15 -15
  35. data/spec/hallon/artist_browse_spec.rb +28 -9
  36. data/spec/hallon/artist_spec.rb +30 -14
  37. data/spec/hallon/enumerator_spec.rb +0 -1
  38. data/spec/hallon/hallon_spec.rb +20 -1
  39. data/spec/hallon/image_spec.rb +18 -41
  40. data/spec/hallon/link_spec.rb +10 -12
  41. data/spec/hallon/linkable_spec.rb +37 -18
  42. data/spec/hallon/player_spec.rb +8 -0
  43. data/spec/hallon/playlist_container_spec.rb +75 -0
  44. data/spec/hallon/playlist_spec.rb +204 -0
  45. data/spec/hallon/search_spec.rb +19 -16
  46. data/spec/hallon/session_spec.rb +61 -29
  47. data/spec/hallon/spotify_spec.rb +30 -0
  48. data/spec/hallon/toplist_spec.rb +22 -14
  49. data/spec/hallon/track_spec.rb +62 -21
  50. data/spec/hallon/user_spec.rb +47 -36
  51. data/spec/mockspotify.rb +35 -10
  52. data/spec/mockspotify/mockspotify_spec.rb +22 -0
  53. data/spec/spec_helper.rb +7 -3
  54. data/spec/support/common_objects.rb +91 -16
  55. data/spec/support/shared_for_linkable_objects.rb +39 -0
  56. metadata +30 -20
  57. data/Termfile +0 -7
  58. data/lib/hallon/synchronizable.rb +0 -32
  59. data/spec/hallon/synchronizable_spec.rb +0 -19
data/lib/hallon/base.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # coding: utf-8
1
2
  module Hallon
2
3
  # All objects in Hallon are mere representations of Spotify objects.
3
4
  # Hallon::Base covers basic functionality shared by all of these.
@@ -17,11 +18,73 @@ module Hallon
17
18
  super
18
19
  end
19
20
 
20
- # The current Session instance.
21
- #
22
- # @return [Session]
23
- def session
24
- Session.instance
21
+ # Default string representation of self.
22
+ def to_s
23
+ name = self.class.name
24
+ address = pointer.address.to_s(16)
25
+ "<#{name} address=0x#{address}>"
25
26
  end
27
+
28
+ private
29
+ # @macro [attach] to_link
30
+ # @method to_link
31
+ # @scope instance
32
+ # @return [Hallon::Link] {Link} for the current object.
33
+ def self.to_link(cmethod)
34
+ # this is here to work around a YARD limitation, see
35
+ # {Linkable} for the actual source
36
+ end
37
+
38
+ # @macro [attach] from_link
39
+ # @method from_link
40
+ # @scope instance
41
+ # @visibility private
42
+ # @param [String, Hallon::Link, Spotify::Pointer] link
43
+ # @return [Spotify::Pointer] pointer representation of given link.
44
+ def self.from_link(as_object, &block)
45
+ # this is here to work around a YARD limitation, see
46
+ # {Linkable} for the actual source
47
+ end
48
+
49
+ # The current Session instance.
50
+ #
51
+ # @return [Session]
52
+ def session
53
+ Session.instance
54
+ end
55
+
56
+ # Convert a given object to a pointer by best of ability.
57
+ #
58
+ # @param [Spotify::Pointer, String, Link] resource
59
+ # @return [Spotify::Pointer]
60
+ # @raise [TypeError] when pointer could not be created, or null
61
+ def to_pointer(resource, type, *args)
62
+ if resource.is_a?(FFI::Pointer) and not resource.is_a?(Spotify::Pointer)
63
+ raise TypeError, "Hallon does not support raw FFI::Pointers, wrap it in a Spotify::Pointer"
64
+ end
65
+
66
+ pointer = if Spotify::Pointer.typechecks?(resource, type)
67
+ resource
68
+ elsif is_linkable? and Spotify::Pointer.typechecks?(resource, :link)
69
+ from_link(resource, *args)
70
+ elsif is_linkable? and Link.valid?(resource)
71
+ from_link(resource, *args)
72
+ elsif block_given?
73
+ yield(resource, *args)
74
+ end
75
+
76
+ if pointer.nil? or pointer.null?
77
+ raise ArgumentError, "#{resource.inspect} is not a valid spotify #{type} URI or pointer"
78
+ elsif not Spotify::Pointer.typechecks?(pointer, type)
79
+ raise TypeError, "“#{resource}” is of type #{resource.type}, #{type} expected"
80
+ else
81
+ pointer
82
+ end
83
+ end
84
+
85
+ # @return [Boolean] true if the object can convert links to pointers
86
+ def is_linkable?
87
+ respond_to?(:from_link, true)
88
+ end
26
89
  end
27
90
  end
@@ -1,3 +1,4 @@
1
+ # coding: utf-8
1
2
  module Hallon
2
3
  # Hallon::Enumerator is like a lazy Array.
3
4
  #
data/lib/hallon/error.rb CHANGED
@@ -35,6 +35,9 @@ module Hallon
35
35
 
36
36
  # Explain a Spotify error with a string message.
37
37
  #
38
+ # @example
39
+ # Hallon::Error.explain(:ok) # => "No error"
40
+ #
38
41
  # @param [Fixnum, Symbol]
39
42
  # @return [String]
40
43
  def explain(error)
@@ -4,22 +4,125 @@
4
4
  #
5
5
  # @see https://github.com/Burgestrand/libspotify-ruby
6
6
  module Spotify
7
- extend FFI::Library
8
- ffi_lib ['libspotify', '/Library/Frameworks/libspotify.framework/libspotify']
7
+ # Fetches the associated value of an enum from a given symbol.
8
+ #
9
+ # @param [Symbol] symbol
10
+ # @param [#to_s] type
11
+ # @raise ArgumentError on failure
12
+ def self.enum_value!(symbol, type)
13
+ enum_value(symbol) or raise ArgumentError, "invalid #{type}: #{symbol}"
14
+ end
15
+
16
+ # Wraps the function `function` so that it always returns
17
+ # a Spotify::Pointer with correct refcount. Functions that
18
+ # contain the word `create` are assumed to start out with
19
+ # a refcount of `+1`.
20
+ #
21
+ # @param [#to_s] function
22
+ # @param [#to_s] return_type
23
+ # @raise [NoMethodError] if `function` is not defined
24
+ # @see Spotify::Pointer
25
+ def self.wrap_function(function, return_type)
26
+ define_singleton_method("#{function}!") do |*args|
27
+ pointer = public_send(function, *args)
28
+ Spotify::Pointer.new(pointer, return_type, function !~ /create/)
29
+ end
30
+ end
31
+
32
+ # @macro [attach] wrap_function
33
+ # Same as {Spotify}.`$1`, but wraps result in a {Spotify::Pointer}.
34
+ #
35
+ # @method $1!
36
+ # @return [Spotify::Pointer<$2>]
37
+ # @see #$1
38
+ wrap_function :session_user, :user
39
+ wrap_function :session_playlistcontainer, :playlistcontainer
40
+ wrap_function :session_inbox_create, :playlist
41
+ wrap_function :session_starred_create, :playlist
42
+ wrap_function :session_starred_for_user_create, :playlist
43
+ wrap_function :session_publishedcontainer_for_user_create, :playlistcontainer
44
+
45
+ wrap_function :track_artist, :artist
46
+ wrap_function :track_album, :album
47
+ wrap_function :localtrack_create, :track
48
+
49
+ wrap_function :album_artist, :artist
50
+
51
+ wrap_function :albumbrowse_create, :albumbrowse
52
+ wrap_function :albumbrowse_album, :album
53
+ wrap_function :albumbrowse_artist, :artist
54
+ wrap_function :albumbrowse_track, :track
55
+
56
+ wrap_function :artistbrowse_create, :artistbrowse
57
+ wrap_function :artistbrowse_artist, :artist
58
+ wrap_function :artistbrowse_track, :track
59
+ wrap_function :artistbrowse_album, :album
60
+ wrap_function :artistbrowse_similar_artist, :artist
61
+
62
+ wrap_function :image_create, :image
63
+ wrap_function :image_create_from_link, :image
64
+
65
+ wrap_function :link_as_track, :track
66
+ wrap_function :link_as_track_and_offset, :track
67
+ wrap_function :link_as_album, :album
68
+ wrap_function :link_as_artist, :artist
69
+ wrap_function :link_as_user, :user
70
+
71
+ wrap_function :link_create_from_string, :link
72
+ wrap_function :link_create_from_track, :link
73
+ wrap_function :link_create_from_album, :link
74
+ wrap_function :link_create_from_artist, :link
75
+ wrap_function :link_create_from_search, :link
76
+ wrap_function :link_create_from_playlist, :link
77
+ wrap_function :link_create_from_artist_portrait, :link
78
+ wrap_function :link_create_from_artistbrowse_portrait, :link
79
+ wrap_function :link_create_from_album_cover, :link
80
+ wrap_function :link_create_from_image, :link
81
+ wrap_function :link_create_from_user, :link
82
+
83
+ wrap_function :search_create, :search
84
+ wrap_function :radio_search_create, :search
85
+ wrap_function :search_track, :track
86
+ wrap_function :search_album, :album
87
+ wrap_function :search_artist, :artist
88
+
89
+ wrap_function :playlist_track, :track
90
+ wrap_function :playlist_track_creator, :user
91
+ wrap_function :playlist_owner, :user
92
+ wrap_function :playlist_create, :playlist
93
+
94
+ wrap_function :playlistcontainer_playlist, :playlist
95
+ wrap_function :playlistcontainer_add_new_playlist, :playlist
96
+ wrap_function :playlistcontainer_add_playlist, :playlist
97
+ wrap_function :playlistcontainer_owner, :user
98
+
99
+ wrap_function :toplistbrowse_create, :toplistbrowse
100
+ wrap_function :toplistbrowse_artist, :artist
101
+ wrap_function :toplistbrowse_album, :album
102
+ wrap_function :toplistbrowse_track, :track
103
+
104
+ wrap_function :inbox_post_tracks, :inbox
9
105
 
10
106
  # The Pointer is a kind of AutoPointer specially tailored for Spotify
11
- # objects. It will automatically release the inner pointer with the
12
- # proper function, based on the given type to #initialize.
107
+ # objects, that releases the raw pointer on GC.
13
108
  class Pointer < FFI::AutoPointer
14
- # Initialize the Spotify::Pointer
15
- #
16
- # @param [FFI::Pointer] ptr
17
- # @param [Symbol] type session, link, etc
18
- # @param [Boolean] add_ref increase reference count
109
+ attr_reader :type
110
+
111
+ # @param [FFI::Pointer] pointer
112
+ # @param [#to_s] type session, link, etc
113
+ # @param [Boolean] add_ref
19
114
  # @return [FFI::AutoPointer]
20
- def initialize(ptr, type, add_ref = false)
21
- super ptr, releaser_for(@type = type)
22
- Spotify.send(:"#{type}_add_ref", ptr) if add_ref
115
+ def initialize(pointer, type, add_ref)
116
+ super pointer, self.class.releaser_for(@type = type.to_s)
117
+
118
+ unless pointer.null?
119
+ Spotify.send(:"#{type}_add_ref", pointer)
120
+ end if add_ref
121
+ end
122
+
123
+ # @return [String] representation of the spotify pointer
124
+ def to_s
125
+ "<#{self.class} address=#{address} type=#{type}>"
23
126
  end
24
127
 
25
128
  # Create a proc that will accept a pointer of a given type and
@@ -27,45 +130,42 @@ module Spotify
27
130
  #
28
131
  # @param [Symbol]
29
132
  # @return [Proc]
30
- def releaser_for(type)
31
- lambda do |ptr|
32
- unless ptr.null?
33
- $stdout.puts "Spotify::#{type}_release(#{ptr})" if $DEBUG
34
- Spotify.send(:"#{type}_release", ptr)
133
+ def self.releaser_for(type)
134
+ lambda do |pointer|
135
+ unless pointer.null?
136
+ $stdout.puts "Spotify::#{type}_release(#{pointer})" if $DEBUG
137
+ Spotify.send(:"#{type}_release", pointer)
35
138
  end
36
139
  end
37
140
  end
38
- end
39
141
 
40
- # Extensions to SessionCallbacks, making it easier to define callbacks.
41
- class SessionCallbacks < FFI::Struct
42
- # Assigns the callbacks to call the given target; the callback
43
- # procs are stored in the `storage` parameter. **Make sure the
44
- # storage does not get garbage collected as long as these callbacks
45
- # are needed!**
46
- #
47
- # @param [Object] target
48
- # @param [#&#91;&#93;&#61;] storage
49
- def initialize(target, storage)
50
- members.each do |member|
51
- callback = lambda { |ptr, *args| target.trigger(member, *args) }
52
- self[member] = storage[member] = callback
142
+ # @param [Object] pointer
143
+ # @param [Symbol] type (optional, no type checking is done if not given)
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
53
152
  end
54
153
  end
55
154
  end
56
155
 
57
156
  # Extensions to SessionConfig, allowing more sensible configuration names.
58
- class SessionConfig < FFI::Struct
59
- [:cache_location, :settings_location, :user_agent].each do |field|
157
+ SessionConfig.class_eval do
158
+ [:cache_location, :settings_location, :user_agent, :device_id, :tracefile].each do |field|
60
159
  method = field.to_s.gsub('location', 'path')
160
+
61
161
  define_method(:"#{method}") { self[field].read_string }
62
162
  define_method(:"#{method}=") do |string|
63
- self[field] = FFI::MemoryPointer.from_string(string)
163
+ string &&= FFI::MemoryPointer.from_string(string)
164
+ self[field] = string
64
165
  end
65
166
  end
66
167
 
67
- # Also sets application_key_size.
68
- #
168
+ # @note Also sets application_key_size.
69
169
  # @param [#to_s]
70
170
  def application_key=(appkey)
71
171
  self[:application_key] = FFI::MemoryPointer.from_string(appkey)
@@ -98,4 +198,37 @@ module Spotify
98
198
  self[:dont_save_metadata_for_playlists] = ! bool
99
199
  end
100
200
  end
201
+
202
+ # Extensions to SessionCallbacks, making it easier to define callbacks.
203
+ SessionCallbacks.class_eval do
204
+ # Assigns the callbacks to call the given target; the callback
205
+ # procs are stored in the `storage` parameter. **Make sure the
206
+ # storage does not get garbage collected as long as these callbacks
207
+ # are needed!**
208
+ #
209
+ # @param [Object] target
210
+ # @param [#&#91;&#93;&#61;] storage
211
+ def initialize(target, storage)
212
+ members.each do |member|
213
+ callback = lambda { |ptr, *args| target.trigger(member, *args) }
214
+ self[member] = storage[member] = callback
215
+ end
216
+ end
217
+ end
218
+
219
+ PlaylistCallbacks.class_eval do
220
+ # Assigns the callbacks to call the given target; the callback
221
+ # procs are stored in the `storage` parameter. **Make sure the
222
+ # storage does not get garbage collected as long as these callbacks
223
+ # are needed!**
224
+ #
225
+ # @param [Object] target
226
+ # @param [#&#91;&#93;&#61;] storage
227
+ def initialize(target, storage)
228
+ members.each do |member|
229
+ callback = lambda { |ptr, *args| target.trigger(member, *args[0...-1]) }
230
+ self[member] = storage[member] = callback
231
+ end
232
+ end
233
+ end
101
234
  end
data/lib/hallon/image.rb CHANGED
@@ -6,8 +6,8 @@ module Hallon
6
6
  class Image < Base
7
7
  extend Linkable
8
8
 
9
- from_link :as_image do |link, session|
10
- Spotify.image_create_from_link(session, link)
9
+ from_link :as_image do |link|
10
+ Spotify.image_create_from_link!(session.pointer, link)
11
11
  end
12
12
 
13
13
  to_link :from_image
@@ -17,77 +17,63 @@ module Hallon
17
17
 
18
18
  # Create a new instance of an Image.
19
19
  #
20
- # @param [String, Link, FFI::Pointer] link link or image id
20
+ # @example from a link
21
+ # image = Hallon::Image.new("spotify:image:3ad93423add99766e02d563605c6e76ed2b0e450")
22
+ #
23
+ # @example from an image id
24
+ # image = Hallon::Image.new("3ad93423add99766e02d563605c6e76ed2b0e450")
25
+ #
26
+ # @param [String, Link, Spotify::Pointer] link link or image id
21
27
  # @param [Hallon::Session] session
22
28
  def initialize(link)
23
- if link.is_a?(String)
24
- link = to_id($1) if link =~ %r|image[:/]([a-fA-F0-9]{40})|
25
-
26
- FFI::MemoryPointer.new(:char, 20) do |ptr|
27
- ptr.write_bytes link
28
- link = Spotify.image_create(session.pointer, ptr)
29
- end
30
- else
31
- link = from_link(link, session.pointer)
29
+ if link.respond_to?(:=~) and link =~ %r~(?:image[:/]|\A)([a-fA-F0-9]{40})\z~
30
+ link = to_id($1)
32
31
  end
33
32
 
34
- @pointer = Spotify::Pointer.new link, :image
33
+ @pointer = to_pointer(link, :image) do
34
+ ptr = FFI::MemoryPointer.new(:char, 20)
35
+ ptr.write_bytes(link)
36
+ Spotify.image_create!(session.pointer, ptr)
37
+ end
35
38
 
36
39
  @callback = proc { trigger :load }
37
- Spotify.image_add_load_callback(@pointer, @callback, nil)
38
-
39
- # TODO: remove load_callback when @pointer is released
40
- # NOTE: on(:load) will trigger while load callback is still executing,
41
- # and removing the load callback from within the load callback
42
- # does not make libspotify happy, and thus segfaults D:
43
- #
44
- # on(:load) { Spotify::image_remove_load_callback(@pointer, @callback, nil) }
40
+ Spotify.image_add_load_callback(pointer, @callback, nil)
45
41
  end
46
42
 
47
- # True if the image has been loaded.
48
- #
49
- # @return [Boolean]
43
+ # @return [Boolean] true if the image is loaded.
50
44
  def loaded?
51
- Spotify.image_is_loaded(@pointer)
45
+ Spotify.image_is_loaded(pointer)
52
46
  end
53
47
 
54
- # Retrieve the current error status.
55
- #
56
- # @return [Symbol] error
48
+ # @see Error.explain
49
+ # @return [Symbol] image error status.
57
50
  def status
58
- Spotify.image_error(@pointer)
51
+ Spotify.image_error(pointer)
59
52
  end
60
53
 
61
- # Retrieve image format.
62
- #
63
- # @return [Symbol] `:jpeg` or `:unknown`
54
+ # @return [Symbol] image format, one of `:jpeg` or `:unknown`
64
55
  def format
65
- Spotify.image_format(@pointer)
56
+ Spotify.image_format(pointer)
66
57
  end
67
58
 
68
- # Retrieve image ID as a string.
69
- #
70
59
  # @param [Boolean] raw true if you want the image id as a hexadecimal string
71
- # @return [String]
60
+ # @return [String] image ID as a string.
72
61
  def id(raw = false)
73
- id = Spotify.image_image_id(@pointer).read_string(20)
62
+ id = Spotify.image_image_id(pointer).read_string(20)
74
63
  raw ? id : to_hex(id)
75
64
  end
76
65
 
77
- # Raw image data as a binary encoded string.
78
- #
79
- # @return [String]
66
+ # @return [String] raw image data as a binary encoded string.
80
67
  def data
81
68
  FFI::MemoryPointer.new(:size_t) do |size|
82
- data = Spotify.image_data(@pointer, size)
69
+ data = Spotify.image_data(pointer, size)
83
70
  return data.read_bytes(size.read_size_t)
84
71
  end
85
72
  end
86
73
 
87
- # True if the images both have the same ID, or if their
88
- # pointers are the same.
89
- #
90
74
  # @see Base#==
75
+ # @param [Object] other
76
+ # @return [Boolean] true if the images are the same object or have the same ID.
91
77
  def ==(other)
92
78
  super or id(true) == other.id(true)
93
79
  rescue NoMethodError, ArgumentError