hallon 0.9.1 → 0.10.1
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 +16 -0
- data/README.markdown +11 -12
- data/Rakefile +1 -0
- data/examples/show_published_playlists_of_user.rb +7 -19
- data/hallon.gemspec +2 -2
- data/lib/hallon/ext/spotify.rb +35 -19
- data/lib/hallon/link.rb +5 -0
- data/lib/hallon/playlist.rb +10 -2
- data/lib/hallon/playlist_container.rb +202 -18
- data/lib/hallon/session.rb +1 -1
- data/lib/hallon/version.rb +1 -1
- data/spec/hallon/link_spec.rb +9 -0
- data/spec/hallon/playlist_container_spec.rb +159 -49
- data/spec/hallon/playlist_spec.rb +8 -0
- data/spec/hallon/user_spec.rb +1 -0
- data/spec/support/common_objects.rb +9 -1
- metadata +17 -16
data/CHANGELOG
CHANGED
@@ -1,6 +1,22 @@
|
|
1
1
|
Hallon’s Changelog
|
2
2
|
==================
|
3
3
|
|
4
|
+
v10.0.0
|
5
|
+
------------------
|
6
|
+
[ Added ]
|
7
|
+
- Add PlaylistContainer::Folder#rename
|
8
|
+
- Add PlaylistContainer#insert_folder
|
9
|
+
- Add PlaylistContainer#move (do see #57)
|
10
|
+
- Add PlaylistContainer#remove
|
11
|
+
- Add ability to retrieve PlaylistContainer folders
|
12
|
+
- Make PlaylistContainer#add accept a spotify playlist URI
|
13
|
+
- Link.new now supports any #to_link’able object
|
14
|
+
- Playlist::Track#moved?
|
15
|
+
- PlaylistContainer callback support (to be changed, see #56)
|
16
|
+
|
17
|
+
[ Fixed ]
|
18
|
+
- A lot of random deadlocks (updated spotify gem dependency)
|
19
|
+
|
4
20
|
v0.9.1
|
5
21
|
------------------
|
6
22
|
[ Added ]
|
data/README.markdown
CHANGED
@@ -3,17 +3,17 @@
|
|
3
3
|
|
4
4
|
We rubyists have this awesome [spotify gem][] allowing us to use [libspotify][] from within Ruby, but it has a significant drawback: the `libspotify` API is very hard to use. Now, we can’t have that, so what do we do? We make Hallon!
|
5
5
|
|
6
|
-
Hallon is Swedish for “Raspberry”, and has been written to satisfy my needs for API simplicity.
|
6
|
+
Hallon is Swedish for “[Raspberry][]”, and has been written to satisfy my needs for API simplicity. Hallon is written on top of [Spotify for Ruby][], but with the goal of making the experience of using `libspotify` from Ruby much more enjoyable.
|
7
7
|
|
8
8
|
Hallon would not have been possible if not for these people:
|
9
9
|
|
10
10
|
- Per Reimers, cracking synchronization bugs with me in the deep night (4 AM) and correcting me when I didn’t know better
|
11
|
-
-
|
12
|
-
-
|
11
|
+
- Spotify, providing a service worth attention (and my money!)
|
12
|
+
- Linus Oleander, originally inspiring me to write Hallon (for the radiofy.se project)
|
13
13
|
|
14
14
|
Also, these people are worthy of mention simply for their contribution:
|
15
15
|
|
16
|
-
-
|
16
|
+
- Jesper Särnesjö, unknowingly providing me a starting point with [Greenstripes][]
|
17
17
|
- Emil “@mrevilme” Palm, for his patience in helping me debug Hallon deadlock issues
|
18
18
|
|
19
19
|
Code samples can be found under `examples/` directory.
|
@@ -59,11 +59,10 @@ License
|
|
59
59
|
-------
|
60
60
|
Hallon is licensed under a 2-clause (Simplified) BSD license. More information can be found in the `LICENSE.txt` file.
|
61
61
|
|
62
|
-
[
|
63
|
-
[
|
64
|
-
[
|
65
|
-
[
|
66
|
-
[
|
67
|
-
[
|
68
|
-
[
|
69
|
-
[Build Status]: https://secure.travis-ci.org/Burgestrand/Hallon.png
|
62
|
+
[Raspberry]: http://images.google.com/search?q=raspberry&tbm=isch
|
63
|
+
[Spotify for Ruby]: https://github.com/Burgestrand/libspotify-ruby
|
64
|
+
[spotify gem]: https://rubygems.org/gems/spotify
|
65
|
+
[libspotify]: http://developer.spotify.com/en/libspotify/overview/
|
66
|
+
[Greenstripes]: http://github.com/sarnesjo/greenstripes
|
67
|
+
[What is Hallon?]: http://burgestrand.se/articles/hallon-delicious-ruby-bindings-to-libspotify.html
|
68
|
+
[Build Status]: https://secure.travis-ci.org/Burgestrand/Hallon.png
|
data/Rakefile
CHANGED
@@ -69,6 +69,7 @@ task 'spotify:coverage' do
|
|
69
69
|
'define_singleton_method', # overloaded by us
|
70
70
|
'image_remove_load_callback', # cleared when Image is GCd
|
71
71
|
'playlist_remove_callbacks', # cleared when Playlist is GCd
|
72
|
+
'playlistcontainer_remove_callbacks', # cleared when Playlist is GCd
|
72
73
|
]
|
73
74
|
|
74
75
|
covered -= ignored
|
@@ -43,37 +43,25 @@ session.login!(ENV['HALLON_USERNAME'], ENV['HALLON_PASSWORD'])
|
|
43
43
|
|
44
44
|
puts "Successfully logged in!"
|
45
45
|
|
46
|
-
# Hallon does not have support for the below operations, so we resort
|
47
|
-
# to using the raw Spotify gem and FFI for now.
|
48
46
|
while username = prompt("Enter a Spotify username: ")
|
49
47
|
begin
|
50
|
-
|
48
|
+
puts "Fetching container for #{username}..."
|
49
|
+
published = Hallon::User.new(username).published
|
50
|
+
session.wait_for { published.loaded? }
|
51
51
|
|
52
|
-
puts "
|
53
|
-
|
54
|
-
|
55
|
-
puts "Failed (unknown reason)."
|
56
|
-
next
|
57
|
-
end
|
58
|
-
|
59
|
-
session.wait_for { Spotify::playlistcontainer_is_loaded(container) }
|
60
|
-
|
61
|
-
num_playlists = Spotify::playlistcontainer_num_playlists(container)
|
62
|
-
puts "Listing #{num_playlists} playlists."
|
52
|
+
puts "Listing #{published.size} playlists."
|
53
|
+
published.contents.each do |playlist|
|
54
|
+
next if playlist.nil? # folder or somesuch
|
63
55
|
|
64
|
-
num_playlists.times do |i|
|
65
|
-
playlist = Spotify::playlistcontainer_playlist!(container, i)
|
66
|
-
playlist = Hallon::Playlist.new(playlist)
|
67
56
|
session.wait_for { playlist.loaded? }
|
68
57
|
|
69
58
|
puts
|
70
59
|
puts playlist.name << ": "
|
71
60
|
|
72
|
-
num_tracks = playlist.tracks.size
|
73
61
|
playlist.tracks.each_with_index do |track, i|
|
74
62
|
session.wait_for { track.loaded? }
|
75
63
|
|
76
|
-
puts "\t (#{i+1}/#{
|
64
|
+
puts "\t (#{i+1}/#{playlist.size}) #{track.name}"
|
77
65
|
end
|
78
66
|
end
|
79
67
|
rescue Interrupt
|
data/hallon.gemspec
CHANGED
@@ -5,7 +5,7 @@ require 'hallon/version'
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |gem|
|
7
7
|
gem.name = "hallon"
|
8
|
-
gem.summary = %Q{
|
8
|
+
gem.summary = %Q{Hallon allows you to write Ruby applications utilizing the official Spotify C API.}
|
9
9
|
gem.homepage = "http://github.com/Burgestrand/Hallon"
|
10
10
|
gem.authors = ["Kim Burgestrand"]
|
11
11
|
gem.email = 'kim@burgestrand.se'
|
@@ -21,7 +21,7 @@ Gem::Specification.new do |gem|
|
|
21
21
|
gem.platform = Gem::Platform::RUBY
|
22
22
|
gem.required_ruby_version = '~> 1.8'
|
23
23
|
|
24
|
-
gem.add_dependency 'spotify', '~> 10.1.
|
24
|
+
gem.add_dependency 'spotify', '~> 10.1.1'
|
25
25
|
gem.add_development_dependency 'bundler', '~> 1.0'
|
26
26
|
gem.add_development_dependency 'rake', '~> 0.8'
|
27
27
|
gem.add_development_dependency 'rspec', '~> 2'
|
data/lib/hallon/ext/spotify.rb
CHANGED
@@ -199,8 +199,9 @@ module Spotify
|
|
199
199
|
end
|
200
200
|
end
|
201
201
|
|
202
|
-
#
|
203
|
-
|
202
|
+
# Makes it easier binding callbacks safely to callback structs.
|
203
|
+
# When including this class you *must* define `proc_for(member)`!
|
204
|
+
module CallbackStruct
|
204
205
|
# Assigns the callbacks to call the given target; the callback
|
205
206
|
# procs are stored in the `storage` parameter. **Make sure the
|
206
207
|
# storage does not get garbage collected as long as these callbacks
|
@@ -208,27 +209,42 @@ module Spotify
|
|
208
209
|
#
|
209
210
|
# @param [Object] target
|
210
211
|
# @param [#[]=] storage
|
211
|
-
def
|
212
|
-
|
213
|
-
|
214
|
-
|
212
|
+
def create(target, storage)
|
213
|
+
new.tap do |struct|
|
214
|
+
members.each do |member|
|
215
|
+
struct[member] = storage[member] = proc_for(target, member)
|
216
|
+
end
|
215
217
|
end
|
216
218
|
end
|
217
219
|
end
|
218
220
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
221
|
+
class << SessionCallbacks
|
222
|
+
include CallbackStruct
|
223
|
+
|
224
|
+
private
|
225
|
+
# @see CallbackStruct
|
226
|
+
def proc_for(target, member)
|
227
|
+
lambda { |pointer, *args| target.trigger(member, *args) }
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
class << PlaylistCallbacks
|
232
|
+
include CallbackStruct
|
233
|
+
|
234
|
+
private
|
235
|
+
# @see CallbackStruct
|
236
|
+
def proc_for(target, member)
|
237
|
+
lambda { |pointer, *args, userdata| target.trigger(member, *args) }
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
class << PlaylistContainerCallbacks
|
242
|
+
include CallbackStruct
|
243
|
+
|
244
|
+
private
|
245
|
+
# @see CallbackStruct
|
246
|
+
def proc_for(target, member)
|
247
|
+
lambda { |pointer, *args, userdata| target.trigger(member, *args) }
|
231
248
|
end
|
232
|
-
end
|
233
249
|
end
|
234
250
|
end
|
data/lib/hallon/link.rb
CHANGED
@@ -28,6 +28,11 @@ module Hallon
|
|
28
28
|
raise "Link.new requires an existing Session instance"
|
29
29
|
end
|
30
30
|
|
31
|
+
# we support any #to_link’able object
|
32
|
+
if uri.respond_to?(:to_link)
|
33
|
+
uri = uri.to_link.pointer
|
34
|
+
end
|
35
|
+
|
31
36
|
@pointer = to_pointer(uri, :link) do
|
32
37
|
Spotify.link_create_from_string!(uri.to_str)
|
33
38
|
end
|
data/lib/hallon/playlist.rb
CHANGED
@@ -63,7 +63,7 @@ module Hallon
|
|
63
63
|
# @param [Boolean] seen true if the track is now seen
|
64
64
|
# @return [Playlist::Track] track at the given index
|
65
65
|
def seen=(seen)
|
66
|
-
|
66
|
+
if moved?
|
67
67
|
raise IndexError, "track has moved from #{index}"
|
68
68
|
end
|
69
69
|
|
@@ -71,6 +71,13 @@ module Hallon
|
|
71
71
|
Error.maybe_raise(error)
|
72
72
|
@seen = Spotify.playlist_track_seen(playlist.pointer, index)
|
73
73
|
end
|
74
|
+
|
75
|
+
# @return [Boolean] true if the track has not yet moved.
|
76
|
+
def moved?
|
77
|
+
# using non-GC version deliberately; no need to keep a reference to
|
78
|
+
# this pointer once we’re done here anyway
|
79
|
+
Spotify.playlist_track(playlist.pointer, index) != pointer
|
80
|
+
end
|
74
81
|
end
|
75
82
|
|
76
83
|
from_link :playlist do |pointer|
|
@@ -83,8 +90,9 @@ module Hallon
|
|
83
90
|
#
|
84
91
|
# @param [String, Link, FFI::Pointer] link
|
85
92
|
def initialize(link)
|
86
|
-
callbacks = Spotify::PlaylistCallbacks.new(self, @sp_callbacks = {})
|
87
93
|
@pointer = to_pointer(link, :playlist)
|
94
|
+
|
95
|
+
callbacks = Spotify::PlaylistCallbacks.create(self, @sp_callbacks = {})
|
88
96
|
Spotify.playlist_add_callbacks(pointer, callbacks, nil)
|
89
97
|
end
|
90
98
|
|
@@ -1,7 +1,74 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
module Hallon
|
3
|
+
# PlaylistContainers are the objects that hold playlists. Each User
|
4
|
+
# in libspotify has a container for its’ starred and published playlists,
|
5
|
+
# and every logged in user has its’ own container.
|
6
|
+
#
|
7
|
+
# @see http://developer.spotify.com/en/libspotify/docs/group__playlist.html
|
3
8
|
class PlaylistContainer < Base
|
9
|
+
# Folders are parts of playlist containers in that they surround playlists
|
10
|
+
# with a beginning marker and an ending marker. The playlists between these
|
11
|
+
# markers are considered "inside the playlist".
|
4
12
|
class Folder
|
13
|
+
# @return [PlaylistContainer] playlistcontainer this folder was created from.
|
14
|
+
attr_reader :container
|
15
|
+
|
16
|
+
# @return [Integer] index this folder starts at in the container.
|
17
|
+
attr_reader :begin
|
18
|
+
|
19
|
+
# @return [Integer] index this folder ends at in the container.
|
20
|
+
attr_reader :end
|
21
|
+
|
22
|
+
# @return [Integer]
|
23
|
+
attr_reader :id
|
24
|
+
|
25
|
+
# @return [String]
|
26
|
+
attr_reader :name
|
27
|
+
|
28
|
+
# Rename the folder.
|
29
|
+
#
|
30
|
+
# @note libspotify has no actual folder rename; what happens is that
|
31
|
+
# the folder is removed and then re-created at the same position.
|
32
|
+
# @param [#to_s] new_name
|
33
|
+
# @return [Folder] the new folder
|
34
|
+
def rename(new_name)
|
35
|
+
raise IndexError, "playlist has moved from #{@begin}..#{@end}" if moved?
|
36
|
+
|
37
|
+
insert_at = @begin
|
38
|
+
container.remove(@begin)
|
39
|
+
container.insert_folder(insert_at, new_name)
|
40
|
+
container.move(insert_at + 1, @end)
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param [PlaylistContainer] container
|
44
|
+
# @param [Range] indices
|
45
|
+
def initialize(container, indices)
|
46
|
+
@container = container
|
47
|
+
@begin = indices.begin
|
48
|
+
@end = indices.end
|
49
|
+
|
50
|
+
@id = Spotify.playlistcontainer_playlist_folder_id(container.pointer, @begin)
|
51
|
+
FFI::Buffer.alloc_out(256) do |buffer|
|
52
|
+
error = Spotify.playlistcontainer_playlist_folder_name(container.pointer, @begin, buffer, buffer.size)
|
53
|
+
Error.maybe_raise(error) # should not fail, but just to be safe!
|
54
|
+
|
55
|
+
@name = buffer.get_string(0)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# @param [Folder] other
|
60
|
+
# @return [Boolean] true if the two folders are the same (same indices, same id).
|
61
|
+
def ==(other)
|
62
|
+
!! [:id, :container, :begin, :end].all? do |attr|
|
63
|
+
public_send(attr) == other.public_send(attr)
|
64
|
+
end if other.is_a?(Folder)
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [Boolean] true if the folder has moved.
|
68
|
+
def moved?
|
69
|
+
Spotify.playlistcontainer_playlist_folder_id(container.pointer, @begin) != id or
|
70
|
+
Spotify.playlistcontainer_playlist_folder_id(container.pointer, @end) != id
|
71
|
+
end
|
5
72
|
end
|
6
73
|
|
7
74
|
include Observable
|
@@ -11,6 +78,9 @@ module Hallon
|
|
11
78
|
# @param [Spotify::Pointer] pointer
|
12
79
|
def initialize(pointer)
|
13
80
|
@pointer = to_pointer(pointer, :playlistcontainer)
|
81
|
+
|
82
|
+
callbacks = Spotify::PlaylistContainerCallbacks.create(self, @sp_callbacs = {})
|
83
|
+
Spotify.playlistcontainer_add_callbacks(pointer, callbacks, nil)
|
14
84
|
end
|
15
85
|
|
16
86
|
# @return [Boolean] true if the container is loaded.
|
@@ -32,40 +102,154 @@ module Hallon
|
|
32
102
|
# @return [Enumerator<Playlist, Folder, nil>] an enumerator of folders and playlists.
|
33
103
|
def contents
|
34
104
|
Enumerator.new(size) do |i|
|
35
|
-
|
36
|
-
|
37
|
-
case type
|
105
|
+
case playlist_type(i)
|
38
106
|
when :playlist
|
39
107
|
playlist = Spotify.playlistcontainer_playlist!(pointer, i)
|
40
108
|
Playlist.new(playlist)
|
41
|
-
when :start_folder
|
42
|
-
|
109
|
+
when :start_folder, :end_folder
|
110
|
+
Folder.new(self, folder_range(i))
|
43
111
|
else # :unknown
|
44
112
|
end
|
45
113
|
end
|
46
114
|
end
|
47
115
|
|
48
|
-
#
|
49
|
-
#
|
116
|
+
# Add the given playlist to the end of the container.
|
117
|
+
#
|
118
|
+
# If the given `name` is a valid spotify playlist URI, Hallon will add
|
119
|
+
# the existing playlist to the container. To always create a new playlist,
|
120
|
+
# set `force_create` to true.
|
121
|
+
#
|
122
|
+
# @example create a new playlist
|
123
|
+
# container.add "New playlist"
|
50
124
|
#
|
51
|
-
#
|
52
|
-
#
|
125
|
+
# @example create a new playlist even if it’s a valid playlist URI
|
126
|
+
# container.add "spotify:user:burgestrand:playlist:07AX9IY9Hqmj1RqltcG0fi", force: true
|
53
127
|
#
|
54
|
-
# @
|
55
|
-
#
|
128
|
+
# @example add existing playlist
|
129
|
+
# playlist = container.add "spotify:user:burgestrand:playlist:07AX9IY9Hqmj1RqltcG0fi"
|
56
130
|
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
|
60
|
-
|
61
|
-
|
131
|
+
# playlist = Hallon::Playlist.new("spotify:user:burgestrand:playlist:07AX9IY9Hqmj1RqltcG0fi")
|
132
|
+
# container.add playlist
|
133
|
+
#
|
134
|
+
# link = Hallon::Link.new("spotify:user:burgestrand:playlist:07AX9IY9Hqmj1RqltcG0fi")
|
135
|
+
# playlist = container.add link
|
136
|
+
#
|
137
|
+
# @param [String, Playlist, Link] playlist
|
138
|
+
# @param [Boolean] force_create force creation of a new playlist
|
139
|
+
# @return [Playlist, nil] the added playlist, or nil if the operation failed
|
140
|
+
def add(name, force_create = false)
|
141
|
+
playlist = if force_create or not Link.valid?(name) and name.is_a?(String)
|
142
|
+
Spotify.playlistcontainer_add_new_playlist!(pointer, name.to_s)
|
62
143
|
else
|
63
|
-
link =
|
64
|
-
link = link.to_link unless link.is_a?(Link)
|
144
|
+
link = Link.new(name)
|
65
145
|
Spotify.playlistcontainer_add_playlist!(pointer, link.pointer)
|
66
146
|
end
|
67
147
|
|
68
148
|
Playlist.new(playlist) unless playlist.null?
|
69
149
|
end
|
150
|
+
|
151
|
+
# Create a new folder with the given name at the end of the container.
|
152
|
+
#
|
153
|
+
# @param [String] name
|
154
|
+
# @return [Folder]
|
155
|
+
# @raise [Error] if the operation failed
|
156
|
+
# @see #insert_folder
|
157
|
+
def add_folder(name)
|
158
|
+
insert_folder(size, name)
|
159
|
+
end
|
160
|
+
|
161
|
+
# Create a new folder with the given name at the specified index.
|
162
|
+
#
|
163
|
+
# @param [Integer] index
|
164
|
+
# @param [String] name
|
165
|
+
# @raise [Error] if the operation failed
|
166
|
+
def insert_folder(index, name)
|
167
|
+
error = Spotify.playlistcontainer_add_folder(pointer, index, name.to_s)
|
168
|
+
Error.maybe_raise(error)
|
169
|
+
contents[index]
|
170
|
+
end
|
171
|
+
|
172
|
+
# Remove a playlist or a folder (but not its’ contents).
|
173
|
+
#
|
174
|
+
# @note When removing a folder, both its’ start and end is removed.
|
175
|
+
# @param [Integer] index
|
176
|
+
# @return [PlaylistContainer]
|
177
|
+
# @raise [Error] if the index is out of range
|
178
|
+
def remove(index)
|
179
|
+
remove = proc { |idx| Spotify.playlistcontainer_remove_playlist(pointer, idx) }
|
180
|
+
|
181
|
+
error = case playlist_type(index)
|
182
|
+
when :start_folder, :end_folder
|
183
|
+
indices = folder_range(index)
|
184
|
+
|
185
|
+
Error.maybe_raise(remove[indices.begin])
|
186
|
+
remove[indices.end - 1] # ^ everything moves down one step
|
187
|
+
else
|
188
|
+
remove[index]
|
189
|
+
end
|
190
|
+
|
191
|
+
tap { Error.maybe_raise(error) }
|
192
|
+
end
|
193
|
+
|
194
|
+
# Move a playlist or a folder.
|
195
|
+
#
|
196
|
+
# @note If moving a folder, only that end of the folder is moved. The folder
|
197
|
+
# size will change!
|
198
|
+
#
|
199
|
+
# @param [Integer] from
|
200
|
+
# @param [Integer] to
|
201
|
+
# @param [Boolean] dry_run don’t really move anything (useful to check if it can be moved)
|
202
|
+
# @return [Playlist, Folder] the entity that was moved
|
203
|
+
# @raise [Error] if the operation failed
|
204
|
+
def move(from, to, dry_run = false)
|
205
|
+
error = Spotify.playlistcontainer_move_playlist(pointer, from, to, !! dry_run)
|
206
|
+
|
207
|
+
if dry_run
|
208
|
+
error, symbol = Error.disambiguate(error)
|
209
|
+
symbol == :ok
|
210
|
+
else
|
211
|
+
Error.maybe_raise(error)
|
212
|
+
contents[from > to ? to : to - 1]
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
protected
|
217
|
+
# Given an index, find out the starting point and ending point
|
218
|
+
# of the folder at that index.
|
219
|
+
#
|
220
|
+
# @param [Integer] index
|
221
|
+
# @return [Range] begin..end
|
222
|
+
def folder_range(index)
|
223
|
+
id = folder_id(index)
|
224
|
+
type = playlist_type(index)
|
225
|
+
same_id = proc { |idx| folder_id(idx) == id }
|
226
|
+
|
227
|
+
case type
|
228
|
+
when :start_folder
|
229
|
+
beginning = index
|
230
|
+
ending = (index + 1).upto(size - 1).find(&same_id)
|
231
|
+
when :end_folder
|
232
|
+
ending = index
|
233
|
+
beginning = (index - 1).downto(0).find(&same_id)
|
234
|
+
end
|
235
|
+
|
236
|
+
if beginning and ending and beginning != ending
|
237
|
+
beginning..ending
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# @return [Symbol] playlist type
|
242
|
+
def playlist_type(index)
|
243
|
+
Spotify.playlistcontainer_playlist_type(pointer, index)
|
244
|
+
end
|
245
|
+
|
246
|
+
# @return [Integer] folder ID of folder at `index`.
|
247
|
+
def folder_id(index)
|
248
|
+
Spotify.playlistcontainer_playlist_folder_id(pointer, index)
|
249
|
+
end
|
250
|
+
|
251
|
+
# playlistcontainer_remove_playlist
|
252
|
+
# playlistcontainer_move_playlist
|
253
|
+
# playlistcontainer_add_folder (#insert)
|
70
254
|
end
|
71
255
|
end
|
data/lib/hallon/session.rb
CHANGED
@@ -110,7 +110,7 @@ module Hallon
|
|
110
110
|
config[:api_version] = Hallon::API_VERSION
|
111
111
|
config.application_key = appkey
|
112
112
|
@options.each { |(key, value)| config.send(:"#{key}=", value) }
|
113
|
-
config[:callbacks] = Spotify::SessionCallbacks.
|
113
|
+
config[:callbacks] = Spotify::SessionCallbacks.create(self, @sp_callbacks = {})
|
114
114
|
|
115
115
|
# Default cache size is 0 (automatic)
|
116
116
|
@cache_size = 0
|
data/lib/hallon/version.rb
CHANGED
data/spec/hallon/link_spec.rb
CHANGED
@@ -20,6 +20,15 @@ describe Hallon::Link do
|
|
20
20
|
Hallon::Session.stub(:instance?).and_return(false)
|
21
21
|
expect { Hallon::Link.new("spotify:user:burgestrand") }.to raise_error(/session/i)
|
22
22
|
end
|
23
|
+
|
24
|
+
it "should accept any object that supplies a #to_link method" do
|
25
|
+
link = Hallon::Link.new("spotify:user:burgestrand")
|
26
|
+
|
27
|
+
to_linkable = double
|
28
|
+
to_linkable.should_receive(:to_link).and_return(link)
|
29
|
+
|
30
|
+
Hallon::Link.new(to_linkable).should eq link
|
31
|
+
end
|
23
32
|
end
|
24
33
|
|
25
34
|
describe "::valid?" do
|
@@ -6,11 +6,11 @@ describe Hallon::PlaylistContainer do
|
|
6
6
|
|
7
7
|
it { should be_loaded }
|
8
8
|
its(:owner) { should eq Hallon::User.new("burgestrand") }
|
9
|
-
its(:size) { should eq
|
9
|
+
its(:size) { should eq 3 }
|
10
10
|
|
11
11
|
describe "#add" do
|
12
|
-
context "given a string" do
|
13
|
-
it "should create a new
|
12
|
+
context "given a string that’s not a valid spotify playlist uri" do
|
13
|
+
it "should create a new playlist at the end of the container" do
|
14
14
|
expect do
|
15
15
|
playlist = container.add("Bogus")
|
16
16
|
|
@@ -20,18 +20,45 @@ describe Hallon::PlaylistContainer do
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
23
|
+
context "given a string that’s a valid spotify playlist uri" do
|
24
|
+
it "should add the existing Playlist at the end of the container" do
|
25
|
+
playlist_uri = "spotify:user:burgestrand:playlist:07AX9IY9Hqmj1RqltcG0fi"
|
26
|
+
playlist = mock_session { Hallon::Playlist.new(playlist_uri) }
|
27
|
+
|
28
|
+
expect do
|
29
|
+
new_playlist = container.add(playlist_uri)
|
30
|
+
|
31
|
+
new_playlist.should eq playlist
|
32
|
+
container.contents[-1].should eq playlist
|
33
|
+
end.to change{ container.size }.by(1)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should create a new playlist at the end of the container if forced to" do
|
37
|
+
playlist_uri = "spotify:user:burgestrand:playlist:07AX9IY9Hqmj1RqltcG0fi"
|
38
|
+
|
39
|
+
expect do
|
40
|
+
new_playlist = container.add(playlist_uri, :force_create)
|
41
|
+
|
42
|
+
new_playlist.name.should eq playlist_uri
|
43
|
+
container.contents[-1].should eq new_playlist
|
44
|
+
end.to change{ container.size }.by(1)
|
45
|
+
end
|
28
46
|
end
|
29
47
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
48
|
+
context "given an existing playlist" do
|
49
|
+
it "should add it to the container if it’s a playlist" do
|
50
|
+
expect do
|
51
|
+
container.add Hallon::Playlist.new(mock_playlist)
|
52
|
+
container.contents[-1].should eq Hallon::Playlist.new(mock_playlist)
|
53
|
+
end.to change{ container.size }.by(1)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should add it to the container if it’s a link" do
|
57
|
+
expect do
|
58
|
+
container.add Hallon::Link.new("spotify:user:burgestrand:playlist:07AX9IY9Hqmj1RqltcG0fi")
|
59
|
+
container.contents[-1].should eq Hallon::Playlist.new(mock_playlist)
|
60
|
+
end.to change{ container.size }.by(1)
|
61
|
+
end
|
35
62
|
end
|
36
63
|
|
37
64
|
it "should return nil when failing to add the item" do
|
@@ -41,63 +68,146 @@ describe Hallon::PlaylistContainer do
|
|
41
68
|
end
|
42
69
|
end
|
43
70
|
|
44
|
-
describe "#
|
45
|
-
it "should add the
|
46
|
-
|
71
|
+
describe "#add_folder" do
|
72
|
+
it "should add a folder at the end of the container with the given name" do
|
73
|
+
size = container.size
|
74
|
+
folder = container.add_folder "Bonkers"
|
75
|
+
|
76
|
+
folder.name.should eq "Bonkers"
|
77
|
+
folder.begin.should be size
|
78
|
+
folder.end.should be(size + 1)
|
79
|
+
|
80
|
+
container.contents[-1].should eq folder
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "#insert_folder" do
|
85
|
+
it "should add a folder at the specified index" do
|
86
|
+
folder = container.insert_folder(2, "Mipmip")
|
87
|
+
|
88
|
+
folder.name.should eq "Mipmip"
|
89
|
+
folder.begin.should be 2
|
90
|
+
folder.end.should be 3
|
91
|
+
|
92
|
+
container.contents[2].should eq folder
|
93
|
+
container.contents[3].should eq folder
|
94
|
+
end
|
47
95
|
end
|
48
96
|
|
49
97
|
describe "#remove" do
|
50
|
-
it "should remove the playlist at the given index"
|
51
|
-
|
98
|
+
it "should remove the playlist at the given index" do
|
99
|
+
expect { container.remove(0) }.to change { container.size }.by(-1)
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should remove the matching :end_folder if removing a :start_folder" do
|
103
|
+
container.contents.map(&:class).should eq [Hallon::Playlist, Hallon::PlaylistContainer::Folder, Hallon::PlaylistContainer::Folder]
|
104
|
+
expect { container.remove(1) }.to change { container.size }.by(-2)
|
105
|
+
container.contents.map(&:class).should eq [Hallon::Playlist]
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should remove the matching :start_folder if removing a :end_folder" do
|
109
|
+
container.contents.map(&:class).should eq [Hallon::Playlist, Hallon::PlaylistContainer::Folder, Hallon::PlaylistContainer::Folder]
|
110
|
+
expect { container.remove(2) }.to change { container.size }.by(-2)
|
111
|
+
container.contents.map(&:class).should eq [Hallon::Playlist]
|
112
|
+
end
|
113
|
+
|
114
|
+
it "should raise an error if the index is out of range" do
|
115
|
+
expect { container.remove(-1) }.to raise_error(Hallon::Error)
|
116
|
+
end
|
52
117
|
end
|
53
118
|
|
54
119
|
describe "#move" do
|
55
|
-
it "should move the
|
120
|
+
it "should move the playlist from the old index to the new index" do
|
121
|
+
playlist = container.contents[0]
|
122
|
+
|
123
|
+
container.contents.map(&:class).should eq [Hallon::Playlist, Hallon::PlaylistContainer::Folder, Hallon::PlaylistContainer::Folder]
|
124
|
+
container.move(0, 2).should eq playlist
|
125
|
+
container.contents.map(&:class).should eq [Hallon::PlaylistContainer::Folder, Hallon::Playlist, Hallon::PlaylistContainer::Folder]
|
126
|
+
container.move(1, 0).should eq playlist
|
127
|
+
container.contents.map(&:class).should eq [Hallon::Playlist, Hallon::PlaylistContainer::Folder, Hallon::PlaylistContainer::Folder]
|
128
|
+
end
|
129
|
+
|
130
|
+
it "should not do anything if it’s a dry-run operation" do
|
131
|
+
container.contents.map(&:class).should eq [Hallon::Playlist, Hallon::PlaylistContainer::Folder, Hallon::PlaylistContainer::Folder]
|
132
|
+
container.move(0, 2, :dry_run).should be_true
|
133
|
+
container.contents.map(&:class).should eq [Hallon::Playlist, Hallon::PlaylistContainer::Folder, Hallon::PlaylistContainer::Folder]
|
134
|
+
end
|
135
|
+
|
136
|
+
it "should not raise an error for an invalid operation when dry_run is active" do
|
137
|
+
container.move(0, -1, :dry_run).should be_false
|
138
|
+
end
|
139
|
+
|
140
|
+
it "should raise an error if the operation failed" do
|
141
|
+
expect { container.move(0, -1) }.to raise_error(Hallon::Error)
|
142
|
+
end
|
56
143
|
end
|
57
144
|
|
58
145
|
describe "#contents" do
|
59
|
-
#
|
60
|
-
# (0) playlist: Hello
|
61
|
-
# (1) start_folder: Hi
|
62
|
-
# (2) playlist: inside Hi
|
63
|
-
# (3) start_folder: Ho
|
64
|
-
# (4) playlist: inside HiHo
|
65
|
-
# (5) end_folder
|
66
|
-
# (6) playlist: inside Hi2
|
67
|
-
# (7) end_folder
|
68
|
-
# (8) playlist: World
|
69
|
-
#
|
70
|
-
# … should become:
|
71
|
-
#
|
72
|
-
# (0) Playlist #1
|
73
|
-
# (1) Folder #1…#7
|
74
|
-
# (2) Playlist #2
|
75
|
-
# (3) Folder #3…#5
|
76
|
-
# (4) Playlist #4
|
77
|
-
# (5) Folder #3…#5
|
78
|
-
# (6) Playlist #6
|
79
|
-
# (7) Folder #1…#7
|
80
|
-
# (8) Playlist #8
|
81
|
-
#
|
82
|
-
it "should be a collection of folders and playlists"
|
83
|
-
|
84
146
|
it "should support retrieving playlists" do
|
85
147
|
container.contents[0].should eq Hallon::Playlist.new(mock_playlist)
|
86
148
|
end
|
149
|
+
|
150
|
+
it "should support retrieving folders from their start" do
|
151
|
+
folder = Hallon::PlaylistContainer::Folder.new(container, 1..2)
|
152
|
+
container.contents[1].should eq folder
|
153
|
+
end
|
154
|
+
|
155
|
+
it "should support retrieving folders from their end" do
|
156
|
+
folder = Hallon::PlaylistContainer::Folder.new(container, 1..2)
|
157
|
+
container.contents[2].should eq folder
|
158
|
+
end
|
87
159
|
end
|
88
160
|
|
89
|
-
describe Hallon::PlaylistContainer::Folder
|
161
|
+
describe Hallon::PlaylistContainer::Folder do
|
90
162
|
subject { container.contents[1] }
|
91
|
-
|
92
|
-
|
93
|
-
its(:
|
163
|
+
let(:folder) { subject }
|
164
|
+
|
165
|
+
its(:id) { should be 1337 }
|
166
|
+
its(:name) { should eq "Boogie" }
|
167
|
+
its(:begin) { should be 1 }
|
168
|
+
its(:end) { should be 2 }
|
169
|
+
|
170
|
+
describe "#moved?" do
|
171
|
+
it "should return true if the folder has moved" do
|
172
|
+
folder.should_not be_moved
|
173
|
+
container.move(folder.begin, 0).id.should eq folder.id
|
174
|
+
folder.should be_moved
|
175
|
+
container.move(0, 2).id.should eq folder.id
|
176
|
+
folder.should_not be_moved
|
177
|
+
end
|
178
|
+
end
|
94
179
|
|
95
180
|
describe "#contents" do
|
96
181
|
it "should be a collection of folders and playlists"
|
97
182
|
end
|
98
183
|
|
99
184
|
describe "#rename" do
|
100
|
-
it "should
|
185
|
+
it "should not touch the original folder data (but it should remove it)" do
|
186
|
+
container.contents.should include(folder)
|
187
|
+
|
188
|
+
folder.rename("Hiphip")
|
189
|
+
|
190
|
+
folder.id.should eq 1337
|
191
|
+
folder.name.should eq "Boogie"
|
192
|
+
folder.begin.should be 1
|
193
|
+
folder.end.should be 2
|
194
|
+
|
195
|
+
container.contents.should_not include(folder)
|
196
|
+
end
|
197
|
+
|
198
|
+
it "should return a new folder with the new data" do
|
199
|
+
new_folder = folder.rename("Hiphip")
|
200
|
+
|
201
|
+
new_folder.id.should_not eq 1337
|
202
|
+
new_folder.name.should eq "Hiphip"
|
203
|
+
new_folder.begin.should be 1
|
204
|
+
new_folder.end.should be 2
|
205
|
+
end
|
206
|
+
|
207
|
+
it "should raise an error if the folder has moved" do
|
208
|
+
container.move(folder.begin, 0)
|
209
|
+
expect { folder.rename "Boogelyboogely" }.to raise_error(IndexError)
|
210
|
+
end
|
101
211
|
end
|
102
212
|
end
|
103
213
|
end
|
@@ -56,6 +56,14 @@ describe Hallon::Playlist do
|
|
56
56
|
track.should_not be_seen
|
57
57
|
end
|
58
58
|
end
|
59
|
+
|
60
|
+
describe "#moved?" do
|
61
|
+
it "should be true if the track has moved" do
|
62
|
+
track.should_not be_moved
|
63
|
+
track.playlist.move(1, 0)
|
64
|
+
track.should be_moved
|
65
|
+
end
|
66
|
+
end
|
59
67
|
end
|
60
68
|
|
61
69
|
describe "#subscribers" do
|
data/spec/hallon/user_spec.rb
CHANGED
@@ -146,7 +146,7 @@ RSpec::Core::ExampleGroup.instance_eval do
|
|
146
146
|
end
|
147
147
|
|
148
148
|
let(:mock_container) do
|
149
|
-
num_items =
|
149
|
+
num_items = 3
|
150
150
|
items_ptr = FFI::MemoryPointer.new(Spotify::Mock::PlaylistTrack, num_items)
|
151
151
|
items = num_items.times.map do |i|
|
152
152
|
Spotify::Mock::PlaylistContainerItem.new(items_ptr + Spotify::Mock::PlaylistContainerItem.size * i)
|
@@ -155,6 +155,14 @@ RSpec::Core::ExampleGroup.instance_eval do
|
|
155
155
|
items[0][:playlist] = mock_playlist
|
156
156
|
items[0][:type] = :playlist
|
157
157
|
|
158
|
+
items[1][:folder_name] = FFI::MemoryPointer.from_string("Boogie")
|
159
|
+
items[1][:type] = :start_folder
|
160
|
+
items[1][:folder_id] = 1337
|
161
|
+
|
162
|
+
items[2][:folder_name] = FFI::Pointer::NULL
|
163
|
+
items[2][:type] = :end_folder
|
164
|
+
items[2][:folder_id] = 1337
|
165
|
+
|
158
166
|
Spotify.mock_playlistcontainer(mock_user, true, num_items, items_ptr, nil, nil)
|
159
167
|
end
|
160
168
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hallon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,22 +9,22 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-11-
|
12
|
+
date: 2011-11-30 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: spotify
|
16
|
-
requirement: &
|
16
|
+
requirement: &70214553379640 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version: 10.1.
|
21
|
+
version: 10.1.1
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70214553379640
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: bundler
|
27
|
-
requirement: &
|
27
|
+
requirement: &70214553395220 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ~>
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '1.0'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70214553395220
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rake
|
38
|
-
requirement: &
|
38
|
+
requirement: &70214553394740 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ~>
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0.8'
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70214553394740
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: rspec
|
49
|
-
requirement: &
|
49
|
+
requirement: &70214553394240 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ~>
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: '2'
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70214553394240
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: yard
|
60
|
-
requirement: &
|
60
|
+
requirement: &70214553393680 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ! '>='
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: '0'
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70214553393680
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rdiscount
|
71
|
-
requirement: &
|
71
|
+
requirement: &70214553393200 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ! '>='
|
@@ -76,7 +76,7 @@ dependencies:
|
|
76
76
|
version: '0'
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *70214553393200
|
80
80
|
description:
|
81
81
|
email: kim@burgestrand.se
|
82
82
|
executables: []
|
@@ -205,7 +205,8 @@ rubyforge_project:
|
|
205
205
|
rubygems_version: 1.8.10
|
206
206
|
signing_key:
|
207
207
|
specification_version: 3
|
208
|
-
summary:
|
208
|
+
summary: Hallon allows you to write Ruby applications utilizing the official Spotify
|
209
|
+
C API.
|
209
210
|
test_files:
|
210
211
|
- spec/fixtures/example_uris.rb
|
211
212
|
- spec/fixtures/pink_cover.jpg
|