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.
- data/.travis.yml +2 -0
- data/CHANGELOG +43 -0
- data/Gemfile +2 -0
- data/README.markdown +21 -13
- data/Rakefile +84 -23
- data/dev/login.rb +16 -0
- data/examples/adding_tracks_to_playlist.rb +49 -0
- data/examples/logging_in.rb +1 -6
- data/examples/show_published_playlists_of_user.rb +9 -19
- data/hallon.gemspec +1 -1
- data/lib/hallon.rb +3 -2
- data/lib/hallon/album.rb +55 -41
- data/lib/hallon/album_browse.rb +41 -37
- data/lib/hallon/artist.rb +30 -21
- data/lib/hallon/artist_browse.rb +59 -41
- data/lib/hallon/base.rb +68 -5
- data/lib/hallon/enumerator.rb +1 -0
- data/lib/hallon/error.rb +3 -0
- data/lib/hallon/ext/spotify.rb +169 -36
- data/lib/hallon/image.rb +30 -44
- data/lib/hallon/link.rb +29 -43
- data/lib/hallon/linkable.rb +68 -20
- data/lib/hallon/observable.rb +0 -1
- data/lib/hallon/player.rb +21 -7
- data/lib/hallon/playlist.rb +291 -0
- data/lib/hallon/playlist_container.rb +27 -0
- data/lib/hallon/search.rb +52 -45
- data/lib/hallon/session.rb +129 -81
- data/lib/hallon/toplist.rb +37 -19
- data/lib/hallon/track.rb +68 -45
- data/lib/hallon/user.rb +69 -33
- data/lib/hallon/version.rb +1 -1
- data/spec/hallon/album_browse_spec.rb +15 -9
- data/spec/hallon/album_spec.rb +15 -15
- data/spec/hallon/artist_browse_spec.rb +28 -9
- data/spec/hallon/artist_spec.rb +30 -14
- data/spec/hallon/enumerator_spec.rb +0 -1
- data/spec/hallon/hallon_spec.rb +20 -1
- data/spec/hallon/image_spec.rb +18 -41
- data/spec/hallon/link_spec.rb +10 -12
- data/spec/hallon/linkable_spec.rb +37 -18
- data/spec/hallon/player_spec.rb +8 -0
- data/spec/hallon/playlist_container_spec.rb +75 -0
- data/spec/hallon/playlist_spec.rb +204 -0
- data/spec/hallon/search_spec.rb +19 -16
- data/spec/hallon/session_spec.rb +61 -29
- data/spec/hallon/spotify_spec.rb +30 -0
- data/spec/hallon/toplist_spec.rb +22 -14
- data/spec/hallon/track_spec.rb +62 -21
- data/spec/hallon/user_spec.rb +47 -36
- data/spec/mockspotify.rb +35 -10
- data/spec/mockspotify/mockspotify_spec.rb +22 -0
- data/spec/spec_helper.rb +7 -3
- data/spec/support/common_objects.rb +91 -16
- data/spec/support/shared_for_linkable_objects.rb +39 -0
- metadata +30 -20
- data/Termfile +0 -7
- data/lib/hallon/synchronizable.rb +0 -32
- 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
|
-
#
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
data/lib/hallon/enumerator.rb
CHANGED
data/lib/hallon/error.rb
CHANGED
data/lib/hallon/ext/spotify.rb
CHANGED
@@ -4,22 +4,125 @@
|
|
4
4
|
#
|
5
5
|
# @see https://github.com/Burgestrand/libspotify-ruby
|
6
6
|
module Spotify
|
7
|
-
|
8
|
-
|
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
|
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
|
-
|
15
|
-
|
16
|
-
# @param [FFI::Pointer]
|
17
|
-
# @param [
|
18
|
-
# @param [Boolean] add_ref
|
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(
|
21
|
-
super
|
22
|
-
|
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 |
|
32
|
-
unless
|
33
|
-
$stdout.puts "Spotify::#{type}_release(#{
|
34
|
-
Spotify.send(:"#{type}_release",
|
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
|
-
|
41
|
-
|
42
|
-
#
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
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
|
-
|
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 [#[]=] 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 [#[]=] 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
|
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
|
-
# @
|
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.
|
24
|
-
link = to_id($1)
|
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 =
|
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(
|
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
|
-
#
|
48
|
-
#
|
49
|
-
# @return [Boolean]
|
43
|
+
# @return [Boolean] true if the image is loaded.
|
50
44
|
def loaded?
|
51
|
-
Spotify.image_is_loaded(
|
45
|
+
Spotify.image_is_loaded(pointer)
|
52
46
|
end
|
53
47
|
|
54
|
-
#
|
55
|
-
#
|
56
|
-
# @return [Symbol] error
|
48
|
+
# @see Error.explain
|
49
|
+
# @return [Symbol] image error status.
|
57
50
|
def status
|
58
|
-
Spotify.image_error(
|
51
|
+
Spotify.image_error(pointer)
|
59
52
|
end
|
60
53
|
|
61
|
-
#
|
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(
|
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(
|
62
|
+
id = Spotify.image_image_id(pointer).read_string(20)
|
74
63
|
raw ? id : to_hex(id)
|
75
64
|
end
|
76
65
|
|
77
|
-
#
|
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(
|
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
|