hallon 0.9.1 → 0.10.1

Sign up to get free protection for your applications and to get access to all the features.
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 ]
@@ -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. It provides you with a wrapper around the spotify gem, making the experience of using `libspotify` from Ruby much more enjoyable.
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
- - [Spotify][], providing a service worth attention (and my money!)
12
- - [Linus Oleander][], spawning the need for Hallon by having me in the [radiofy.se](http://radiofy.se) project
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
- - [Jesper Särnesjö][], unknowingly providing me a starting point with [Greenstripes][]
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
- [spotify gem]: https://rubygems.org/gems/spotify
63
- [libspotify]: http://developer.spotify.com/en/libspotify/overview/
64
- [Greenstripes]: http://github.com/sarnesjo/greenstripes
65
- [Jesper Särnesjö]: http://jesper.sarnesjo.org/
66
- [Linus Oleander]: https://github.com/oleander
67
- [Spotify]: http://spotify.com/
68
- [What is Hallon?]: http://burgestrand.se/articles/hallon-delicious-ruby-bindings-to-libspotify.html
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
- username = nil if username.empty?
48
+ puts "Fetching container for #{username}..."
49
+ published = Hallon::User.new(username).published
50
+ session.wait_for { published.loaded? }
51
51
 
52
- puts "Fetching container for #{username || "current user"}..."
53
- container = Spotify::session_publishedcontainer_for_user_create!(session.pointer, username)
54
- if container.null?
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}/#{num_tracks}) #{track.name}"
64
+ puts "\t (#{i+1}/#{playlist.size}) #{track.name}"
77
65
  end
78
66
  end
79
67
  rescue Interrupt
@@ -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{Delicious Ruby bindings to the official Spotify API}
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.0'
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'
@@ -199,8 +199,9 @@ module Spotify
199
199
  end
200
200
  end
201
201
 
202
- # Extensions to SessionCallbacks, making it easier to define callbacks.
203
- SessionCallbacks.class_eval do
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 [#&#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
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
- 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
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
@@ -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
@@ -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
- unless Spotify.playlist_track(playlist.pointer, index) == pointer
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
- type = Spotify.playlistcontainer_playlist_type(pointer, i)
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
- when :end_folder
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
- # @overload add(name)
49
- # Create a new playlist at the end of the container with the given name.
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
- # @param [String] name
52
- # @return [Playlist, nil] the new playlist, or nil if the operation failed
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
- # @overload add(playlist)
55
- # Add the given playlist to the end of the container.
128
+ # @example add existing playlist
129
+ # playlist = container.add "spotify:user:burgestrand:playlist:07AX9IY9Hqmj1RqltcG0fi"
56
130
  #
57
- # @param [Playlist, Link, #to_link] playlist
58
- # @return [Playlist, nil] the added playlist, or nil if the operation failed
59
- def add(name_or_playlist)
60
- playlist = if name_or_playlist.is_a?(String)
61
- Spotify.playlistcontainer_add_new_playlist!(pointer, name_or_playlist)
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 = name_or_playlist
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
@@ -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.new(self, @sp_callbacks = {})
113
+ config[:callbacks] = Spotify::SessionCallbacks.create(self, @sp_callbacks = {})
114
114
 
115
115
  # Default cache size is 0 (automatic)
116
116
  @cache_size = 0
@@ -3,5 +3,5 @@ module Hallon
3
3
  # Current release version of Hallon
4
4
  #
5
5
  # @see http://semver.org/
6
- VERSION = [0, 9, 1].join('.')
6
+ VERSION = [0, 10, 1].join('.')
7
7
  end
@@ -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 1 }
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 Playlist at the end of the playlist" do
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
- it "should add the given Playlist to the end of the container" do
24
- expect do
25
- playlist = container.add Hallon::Playlist.new(mock_playlist)
26
- container.contents[-1].should eq Hallon::Playlist.new(mock_playlist)
27
- end.to change{ container.size }.by(1)
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
- it "should add the given Playlist Link to the end of the container" do
31
- expect do
32
- playlist = container.add Hallon::Link.new("spotify:user:burgestrand:playlist:07AX9IY9Hqmj1RqltcG0fi")
33
- container.contents[-1].should eq Hallon::Playlist.new(mock_playlist)
34
- end.to change{ container.size }.by(1)
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 "#insert" do
45
- it "should add the given Playlist to the given index"
46
- it "should add the given Folder to the given index"
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
- it "should remove the matching :folder_end if removing a folder"
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 entity at the given index"
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, :pending do
161
+ describe Hallon::PlaylistContainer::Folder do
90
162
  subject { container.contents[1] }
91
-
92
- its(:id) { should be 1337 }
93
- its(:name) { should be "Awesome folder" }
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 rename the playlist container"
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
@@ -82,6 +82,7 @@ describe Hallon::User do
82
82
  end
83
83
 
84
84
  it "should return nil if not logged in" do
85
+ Spotify.should_receive(:session_publishedcontainer_for_user_create).and_return(null_pointer)
85
86
  mock_session { user.published.should be_nil }
86
87
  end
87
88
  end
@@ -146,7 +146,7 @@ RSpec::Core::ExampleGroup.instance_eval do
146
146
  end
147
147
 
148
148
  let(:mock_container) do
149
- num_items = 1
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.9.1
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-22 00:00:00.000000000 Z
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: &70199932063240 !ruby/object:Gem::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.0
21
+ version: 10.1.1
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70199932063240
24
+ version_requirements: *70214553379640
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: bundler
27
- requirement: &70199932060440 !ruby/object:Gem::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: *70199932060440
35
+ version_requirements: *70214553395220
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rake
38
- requirement: &70199932058360 !ruby/object:Gem::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: *70199932058360
46
+ version_requirements: *70214553394740
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rspec
49
- requirement: &70199932057240 !ruby/object:Gem::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: *70199932057240
57
+ version_requirements: *70214553394240
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: yard
60
- requirement: &70199932056420 !ruby/object:Gem::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: *70199932056420
68
+ version_requirements: *70214553393680
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rdiscount
71
- requirement: &70199932055320 !ruby/object:Gem::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: *70199932055320
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: Delicious Ruby bindings to the official Spotify API
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