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