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 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