hallon 0.8.0 → 0.9.0

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