hallon 0.13.0 → 0.14.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/CHANGELOG.md +43 -1
- data/Gemfile +0 -2
- data/LICENSE.txt +1 -1
- data/README.markdown +94 -44
- data/examples/playing_audio.rb +4 -5
- data/lib/hallon.rb +20 -0
- data/lib/hallon/album.rb +13 -12
- data/lib/hallon/album_browse.rb +1 -0
- data/lib/hallon/artist.rb +13 -12
- data/lib/hallon/artist_browse.rb +1 -0
- data/lib/hallon/base.rb +2 -0
- data/lib/hallon/image.rb +18 -10
- data/lib/hallon/loadable.rb +24 -0
- data/lib/hallon/observable.rb +1 -1
- data/lib/hallon/observable/playlist.rb +10 -16
- data/lib/hallon/observable/playlist_container.rb +12 -6
- data/lib/hallon/player.rb +3 -3
- data/lib/hallon/playlist.rb +34 -11
- data/lib/hallon/playlist_container.rb +10 -4
- data/lib/hallon/search.rb +1 -0
- data/lib/hallon/session.rb +2 -2
- data/lib/hallon/toplist.rb +17 -12
- data/lib/hallon/track.rb +1 -0
- data/lib/hallon/user.rb +48 -11
- data/lib/hallon/version.rb +1 -1
- data/spec/hallon/album_browse_spec.rb +2 -0
- data/spec/hallon/album_spec.rb +14 -7
- data/spec/hallon/artist_browse_spec.rb +2 -0
- data/spec/hallon/artist_spec.rb +14 -8
- data/spec/hallon/hallon_spec.rb +12 -0
- data/spec/hallon/image_spec.rb +18 -9
- data/spec/hallon/loadable_spec.rb +46 -0
- data/spec/hallon/observable/playlist_spec.rb +11 -5
- data/spec/hallon/observable_spec.rb +6 -0
- data/spec/hallon/playlist_container_spec.rb +6 -0
- data/spec/hallon/playlist_spec.rb +21 -4
- data/spec/hallon/search_spec.rb +2 -0
- data/spec/hallon/toplist_spec.rb +40 -23
- data/spec/hallon/track_spec.rb +2 -0
- data/spec/hallon/user_post_spec.rb +75 -0
- data/spec/hallon/user_spec.rb +7 -11
- data/spec/spec_helper.rb +2 -2
- metadata +20 -16
- data/examples/audio_driver.rb +0 -55
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Hallon
|
4
|
+
module Loadable
|
5
|
+
# @param [Numeric] timeout after this time, if the object is not loaded, an error is raised.
|
6
|
+
# @return [self]
|
7
|
+
# @raise [Hallon::TimeoutError] after `timeout` seconds if the object does not load.
|
8
|
+
def load(timeout = Hallon.load_timeout)
|
9
|
+
Timeout.timeout(timeout, Hallon::TimeoutError) do
|
10
|
+
until loaded?
|
11
|
+
session.process_events
|
12
|
+
|
13
|
+
if respond_to?(:status)
|
14
|
+
Error.maybe_raise(status, :ignore => :is_loading)
|
15
|
+
end
|
16
|
+
|
17
|
+
sleep(0.001)
|
18
|
+
end
|
19
|
+
|
20
|
+
self
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/hallon/observable.rb
CHANGED
@@ -27,7 +27,12 @@ module Hallon::Observable
|
|
27
27
|
# @yieldparam [Integer] position
|
28
28
|
# @yieldparam [Playlist] self
|
29
29
|
def tracks_added_callback(pointer, tracks, num_tracks, position, userdata)
|
30
|
-
|
30
|
+
tracks_ary = tracks.read_array_of_pointer(num_tracks).map do |track|
|
31
|
+
ptr = Spotify::Pointer.new(track, :track, true)
|
32
|
+
Hallon::Track.new(ptr)
|
33
|
+
end
|
34
|
+
|
35
|
+
trigger(pointer, :tracks_added, tracks_ary, position)
|
31
36
|
end
|
32
37
|
|
33
38
|
# @example listening to this event
|
@@ -38,8 +43,8 @@ module Hallon::Observable
|
|
38
43
|
# @yield [tracks, self] tracks_removed
|
39
44
|
# @yieldparam [Array<Track>] tracks
|
40
45
|
# @yieldparam [Playlist] self
|
41
|
-
def tracks_removed_callback(pointer,
|
42
|
-
trigger(pointer, :tracks_removed,
|
46
|
+
def tracks_removed_callback(pointer, track_indices, num_indices, userdata)
|
47
|
+
trigger(pointer, :tracks_removed, track_indices.read_array_of_int(num_indices))
|
43
48
|
end
|
44
49
|
|
45
50
|
# @example listening to this event
|
@@ -51,8 +56,8 @@ module Hallon::Observable
|
|
51
56
|
# @yieldparam [Array<Track>] tracks
|
52
57
|
# @yieldparam [Integer] new_position
|
53
58
|
# @yieldparam [Playlist] self
|
54
|
-
def tracks_moved_callback(pointer,
|
55
|
-
trigger(pointer, :tracks_moved,
|
59
|
+
def tracks_moved_callback(pointer, track_indices, num_indices, new_position, userdata)
|
60
|
+
trigger(pointer, :tracks_moved, track_indices.read_array_of_int(num_indices), new_position)
|
56
61
|
end
|
57
62
|
|
58
63
|
# @example listening to this event
|
@@ -179,16 +184,5 @@ module Hallon::Observable
|
|
179
184
|
def subscribers_changed_callback(pointer, userdata)
|
180
185
|
trigger(pointer, :subscribers_changed)
|
181
186
|
end
|
182
|
-
|
183
|
-
protected
|
184
|
-
# @param [FFI::Pointer] tracks
|
185
|
-
# @param [Integer] num_tracks
|
186
|
-
# @param [Array<Track>]
|
187
|
-
def callback_make_tracks(tracks, num_tracks)
|
188
|
-
tracks.read_array_of_pointer(num_tracks).map do |track|
|
189
|
-
ptr = Spotify::Pointer.new(track, :track, true)
|
190
|
-
Hallon::Track.new(ptr)
|
191
|
-
end
|
192
|
-
end
|
193
187
|
end
|
194
188
|
end
|
@@ -27,8 +27,7 @@ module Hallon::Observable
|
|
27
27
|
# @yieldparam [Integer] position
|
28
28
|
# @yieldparam [PlaylistContainer] self
|
29
29
|
def playlist_added_callback(pointer, playlist, position, userdata)
|
30
|
-
|
31
|
-
trigger(pointer, :playlist_added, Hallon::Playlist.new(playlist), position)
|
30
|
+
trigger(pointer, :playlist_added, playlist_from(playlist), position)
|
32
31
|
end
|
33
32
|
|
34
33
|
# @example listening to this event
|
@@ -41,8 +40,7 @@ module Hallon::Observable
|
|
41
40
|
# @yieldparam [Integer] position
|
42
41
|
# @yieldparam [PlaylistContainer] self
|
43
42
|
def playlist_removed_callback(pointer, playlist, position, userdata)
|
44
|
-
|
45
|
-
trigger(pointer, :playlist_removed, Hallon::Playlist.new(playlist), position)
|
43
|
+
trigger(pointer, :playlist_removed, playlist_from(playlist), position)
|
46
44
|
end
|
47
45
|
|
48
46
|
# @example listening to this event
|
@@ -56,8 +54,7 @@ module Hallon::Observable
|
|
56
54
|
# @yieldparam [Integer] new_position
|
57
55
|
# @yieldparam [PlaylistContainer] self
|
58
56
|
def playlist_moved_callback(pointer, playlist, position, new_position, userdata)
|
59
|
-
|
60
|
-
trigger(pointer, :playlist_moved, Hallon::Playlist.new(playlist), position, new_position)
|
57
|
+
trigger(pointer, :playlist_moved, playlist_from(playlist), position, new_position)
|
61
58
|
end
|
62
59
|
|
63
60
|
# @example listening to this event
|
@@ -70,5 +67,14 @@ module Hallon::Observable
|
|
70
67
|
def container_loaded_callback(pointer, userdata)
|
71
68
|
trigger(pointer, :container_loaded)
|
72
69
|
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
# @param [Spotify::Pointer] playlist
|
74
|
+
# @return [Hallon::Playlist] a playlist for the given pointer.
|
75
|
+
def playlist_from(pointer)
|
76
|
+
pointer = Spotify::Pointer.new(pointer, :playlist, true)
|
77
|
+
Hallon::Playlist.new(pointer)
|
78
|
+
end
|
73
79
|
end
|
74
80
|
end
|
data/lib/hallon/player.rb
CHANGED
@@ -48,7 +48,7 @@ module Hallon
|
|
48
48
|
# data to the driver or not (see #status=)
|
49
49
|
@status_c = @queue.new_cond
|
50
50
|
# set initial status (we assume stopped)
|
51
|
-
self.status =
|
51
|
+
self.status = :stopped
|
52
52
|
|
53
53
|
# this thread feeds the audio driver with data, but
|
54
54
|
# if we are not playing it’ll wait until we are
|
@@ -125,9 +125,9 @@ module Hallon
|
|
125
125
|
#
|
126
126
|
# @param [Symbol] status one of :playing, :paused, :stopped
|
127
127
|
# @raise [ArgumentError] if given an invalid status
|
128
|
-
def status=(
|
128
|
+
def status=(new_status)
|
129
129
|
@queue.synchronize do
|
130
|
-
old_status, @status =
|
130
|
+
old_status, @status = status, new_status
|
131
131
|
|
132
132
|
case status
|
133
133
|
when :playing
|
data/lib/hallon/playlist.rb
CHANGED
@@ -94,6 +94,7 @@ module Hallon
|
|
94
94
|
end
|
95
95
|
|
96
96
|
extend Linkable
|
97
|
+
include Loadable
|
97
98
|
|
98
99
|
# CAN HAZ CALLBAKZ
|
99
100
|
extend Observable::Playlist
|
@@ -104,6 +105,24 @@ module Hallon
|
|
104
105
|
|
105
106
|
to_link :from_playlist
|
106
107
|
|
108
|
+
# Given a string, returns `false` if the string is a valid spotify playlist name.
|
109
|
+
# If it’s an invalid spotify playlist name, a string describing the fault is returned.
|
110
|
+
#
|
111
|
+
# @see http://developer.spotify.com/en/libspotify/docs/group__playlist.html#ga840b82b1074a7ca1c9eacd351bed24c2
|
112
|
+
# @param [String] name
|
113
|
+
# @return [String, false] description of why the name is invalid, or false if it’s valid
|
114
|
+
def self.invalid_name?(name)
|
115
|
+
unless name.bytesize < 256
|
116
|
+
return "name must be shorter than 256 bytes"
|
117
|
+
end
|
118
|
+
|
119
|
+
unless name =~ /[^[:space:]]/
|
120
|
+
return "name must not be blank"
|
121
|
+
end
|
122
|
+
|
123
|
+
return false # no error
|
124
|
+
end
|
125
|
+
|
107
126
|
# Construct a new Playlist, given a pointer.
|
108
127
|
#
|
109
128
|
# @param [String, Link, FFI::Pointer] link
|
@@ -185,17 +204,11 @@ module Hallon
|
|
185
204
|
# @param [#to_s] name new name for playlist
|
186
205
|
# @raise [Error] if name could not be changed
|
187
206
|
def name=(name)
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
raise ArgumentError,
|
207
|
+
unless error = Playlist.invalid_name?(name)
|
208
|
+
Error.maybe_raise(Spotify.playlist_rename(pointer, name))
|
209
|
+
else
|
210
|
+
raise ArgumentError, error
|
192
211
|
end
|
193
|
-
|
194
|
-
unless name =~ /[^ ]/u
|
195
|
-
raise ArgumentError, "name must not consist of only spaces"
|
196
|
-
end unless name.empty?
|
197
|
-
|
198
|
-
Error.maybe_raise(Spotify.playlist_rename(pointer, name))
|
199
212
|
end
|
200
213
|
|
201
214
|
# @return [User, nil]
|
@@ -209,7 +222,9 @@ module Hallon
|
|
209
222
|
Spotify.playlist_get_description(pointer)
|
210
223
|
end
|
211
224
|
|
212
|
-
# @
|
225
|
+
# @note this is not the mosaic image you see in the client. Spotify allows custom images
|
226
|
+
# on playlists for promo campaigns etc.
|
227
|
+
# @return [Image, nil] custom image for the playlist, if one exists
|
213
228
|
def image
|
214
229
|
buffer = FFI::Buffer.alloc_out(20)
|
215
230
|
if Spotify.playlist_get_image(pointer, buffer)
|
@@ -297,6 +312,14 @@ module Hallon
|
|
297
312
|
# @return [Playlist]
|
298
313
|
# @raise [Error] if the operation failed
|
299
314
|
def remove(*indices)
|
315
|
+
unless indices == indices.uniq
|
316
|
+
raise ArgumentError, "no index may occur twice"
|
317
|
+
end
|
318
|
+
|
319
|
+
unless indices.all? { |i| i.between?(0, size-1) }
|
320
|
+
raise ArgumentError, "indices must be inside #{0...size}"
|
321
|
+
end
|
322
|
+
|
300
323
|
indices_ary = FFI::MemoryPointer.new(:int, indices.size)
|
301
324
|
indices_ary.write_array_of_int(indices)
|
302
325
|
|
@@ -58,7 +58,7 @@ module Hallon
|
|
58
58
|
|
59
59
|
# Folders are parts of playlist containers in that they surround playlists
|
60
60
|
# with a beginning marker and an ending marker. The playlists between these
|
61
|
-
# markers are considered "inside the
|
61
|
+
# markers are considered "inside the folder".
|
62
62
|
class Folder
|
63
63
|
# @return [Integer] index this folder starts at in the container.
|
64
64
|
attr_reader :begin
|
@@ -88,7 +88,7 @@ module Hallon
|
|
88
88
|
# @param [#to_s] new_name
|
89
89
|
# @return [Folder] the new folder
|
90
90
|
def rename(new_name)
|
91
|
-
raise IndexError, "
|
91
|
+
raise IndexError, "folder has moved from #{@begin}..#{@end}" if moved?
|
92
92
|
|
93
93
|
insert_at = @begin
|
94
94
|
container.remove(@begin)
|
@@ -134,6 +134,7 @@ module Hallon
|
|
134
134
|
end
|
135
135
|
|
136
136
|
extend Observable::PlaylistContainer
|
137
|
+
include Loadable
|
137
138
|
|
138
139
|
# Wrap an existing PlaylistContainer pointer in an object.
|
139
140
|
#
|
@@ -194,7 +195,11 @@ module Hallon
|
|
194
195
|
# @return [Playlist, nil] the added playlist, or nil if the operation failed
|
195
196
|
def add(name, force_create = false)
|
196
197
|
playlist = if force_create or not Link.valid?(name) and name.is_a?(String)
|
197
|
-
|
198
|
+
unless error = Playlist.invalid_name?(name)
|
199
|
+
Spotify.playlistcontainer_add_new_playlist!(pointer, name)
|
200
|
+
else
|
201
|
+
raise ArgumentError, error
|
202
|
+
end
|
198
203
|
else
|
199
204
|
link = Link.new(name)
|
200
205
|
Spotify.playlistcontainer_add_playlist!(pointer, link.pointer)
|
@@ -278,7 +283,8 @@ module Hallon
|
|
278
283
|
# @param (see #move)
|
279
284
|
# @return [Boolean] true if the operation can be performed
|
280
285
|
def can_move?(from, to)
|
281
|
-
|
286
|
+
dry_run = true
|
287
|
+
error = move_playlist(from, to, dry_run)
|
282
288
|
_, symbol = Error.disambiguate(error)
|
283
289
|
symbol == :ok
|
284
290
|
end
|
data/lib/hallon/search.rb
CHANGED
data/lib/hallon/session.rb
CHANGED
@@ -92,8 +92,8 @@ module Hallon
|
|
92
92
|
def initialize(appkey, options = {}, &block)
|
93
93
|
@options = {
|
94
94
|
:user_agent => "Hallon",
|
95
|
-
:settings_path => "tmp",
|
96
|
-
:cache_path => "",
|
95
|
+
:settings_path => "tmp/hallon/",
|
96
|
+
:cache_path => "tmp/hallon/",
|
97
97
|
:load_playlists => true,
|
98
98
|
:compress_playlists => true,
|
99
99
|
:cache_playlist_metadata => true,
|
data/lib/hallon/toplist.rb
CHANGED
@@ -37,6 +37,10 @@ module Hallon
|
|
37
37
|
end
|
38
38
|
|
39
39
|
extend Observable::Toplist
|
40
|
+
include Loadable
|
41
|
+
|
42
|
+
# @return [Symbol] type of toplist request (one of :artists, :albums or :tracks)
|
43
|
+
attr_reader :type
|
40
44
|
|
41
45
|
# Create a Toplist browsing object.
|
42
46
|
#
|
@@ -67,6 +71,7 @@ module Hallon
|
|
67
71
|
end
|
68
72
|
|
69
73
|
subscribe_for_callbacks do |callback|
|
74
|
+
@type = type
|
70
75
|
@pointer = Spotify.toplistbrowse_create!(session.pointer, type, region, user, callback, nil)
|
71
76
|
end
|
72
77
|
end
|
@@ -82,19 +87,19 @@ module Hallon
|
|
82
87
|
Spotify.toplistbrowse_error(pointer)
|
83
88
|
end
|
84
89
|
|
85
|
-
# @
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
90
|
+
# @note the returned enumerator corresponds to the #type of Toplist.
|
91
|
+
# @return [Artists, Albums, Tracks] an enumerator over the collection of results.
|
92
|
+
def results
|
93
|
+
klass = case type
|
94
|
+
when :artists
|
95
|
+
Artists
|
96
|
+
when :albums
|
97
|
+
Albums
|
98
|
+
when :tracks
|
99
|
+
Tracks
|
100
|
+
end
|
94
101
|
|
95
|
-
|
96
|
-
def tracks
|
97
|
-
Tracks.new(self)
|
102
|
+
klass.new(self)
|
98
103
|
end
|
99
104
|
|
100
105
|
# @note If the object is not loaded, the result is undefined.
|
data/lib/hallon/track.rb
CHANGED
data/lib/hallon/user.rb
CHANGED
@@ -7,24 +7,60 @@ module Hallon
|
|
7
7
|
#
|
8
8
|
# @see http://developer.spotify.com/en/libspotify/docs/group__user.html
|
9
9
|
class User < Base
|
10
|
-
extend Linkable
|
11
|
-
|
12
10
|
# A Post is created upon sending tracks (with an optional message) to a user.
|
13
11
|
#
|
14
12
|
# @see http://developer.spotify.com/en/libspotify/docs/group__inbox.html
|
15
13
|
class Post < Base
|
16
14
|
extend Observable::Post
|
15
|
+
include Loadable
|
16
|
+
|
17
|
+
# Use {.create} instead!
|
18
|
+
private_class_method :new
|
19
|
+
|
20
|
+
# @param (see #initialize)
|
21
|
+
# @return [Post, nil] post, or nil if posting failed.
|
22
|
+
def self.create(recipient_name, message, tracks)
|
23
|
+
post = new(recipient_name, message, tracks)
|
24
|
+
post unless post.pointer.null?
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Array<Track>] an array of tracks posted.
|
28
|
+
attr_reader :tracks
|
29
|
+
|
30
|
+
# @param [String, nil] message together with the post.
|
31
|
+
attr_reader :message
|
17
32
|
|
18
|
-
# @param [
|
19
|
-
|
33
|
+
# @param [String] the username of the post’s recipient.
|
34
|
+
attr_reader :recipient_name
|
35
|
+
|
36
|
+
# Send a list of tracks to another users’ inbox.
|
37
|
+
#
|
38
|
+
# @param [String] recipient_name username of person to send post to
|
39
|
+
# @param [String, nil] message
|
40
|
+
# @param [Track, Array<Track>] tracks
|
41
|
+
def initialize(recipient_name, message, tracks)
|
42
|
+
tracks = Array(tracks)
|
20
43
|
ary = FFI::MemoryPointer.new(:pointer, tracks.length)
|
21
44
|
ary.write_array_of_pointer tracks.map(&:pointer)
|
22
45
|
|
23
46
|
subscribe_for_callbacks do |callback|
|
24
|
-
@
|
47
|
+
@tracks = tracks
|
48
|
+
@message = message
|
49
|
+
@recipient_name = recipient_name
|
50
|
+
@pointer = Spotify.inbox_post_tracks!(session.pointer, @recipient_name, ary, tracks.length, @message, callback, nil)
|
25
51
|
end
|
26
52
|
end
|
27
53
|
|
54
|
+
# @return [User] the user named {#recipient_name}.
|
55
|
+
def recipient
|
56
|
+
User.new(recipient_name)
|
57
|
+
end
|
58
|
+
|
59
|
+
# @return [Boolean] true if the post has been successfully sent.
|
60
|
+
def loaded?
|
61
|
+
status == :ok
|
62
|
+
end
|
63
|
+
|
28
64
|
# @see Error.explain
|
29
65
|
# @return [Symbol] error status of inbox post
|
30
66
|
def status
|
@@ -32,6 +68,9 @@ module Hallon
|
|
32
68
|
end
|
33
69
|
end
|
34
70
|
|
71
|
+
extend Linkable
|
72
|
+
include Loadable
|
73
|
+
|
35
74
|
from_link :profile do |link|
|
36
75
|
Spotify.link_as_user!(link)
|
37
76
|
end
|
@@ -90,16 +129,14 @@ module Hallon
|
|
90
129
|
#
|
91
130
|
# @overload post(message, tracks)
|
92
131
|
# @param [#to_s] message
|
93
|
-
# @param [Array<Track>] tracks
|
132
|
+
# @param [Track, Array<Track>] tracks
|
94
133
|
#
|
95
134
|
# @overload post(tracks)
|
96
|
-
# @param [Array<Track>] tracks
|
135
|
+
# @param [Track, Array<Track>] tracks
|
97
136
|
#
|
98
|
-
# @return
|
137
|
+
# @return (see Post.create)
|
99
138
|
def post(message = nil, tracks)
|
100
|
-
message
|
101
|
-
post = Post.new(name, message, tracks)
|
102
|
-
post unless post.pointer.null?
|
139
|
+
Post.create(name, message, tracks)
|
103
140
|
end
|
104
141
|
end
|
105
142
|
end
|