hallon 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. data/CHANGELOG.md +43 -0
  2. data/Gemfile +3 -1
  3. data/README.markdown +41 -11
  4. data/Rakefile +12 -0
  5. data/examples/audio_driver.rb +55 -0
  6. data/examples/playing_audio.rb +10 -50
  7. data/hallon.gemspec +1 -1
  8. data/lib/hallon.rb +1 -1
  9. data/lib/hallon/album_browse.rb +22 -11
  10. data/lib/hallon/artist_browse.rb +64 -33
  11. data/lib/hallon/audio_driver.rb +138 -0
  12. data/lib/hallon/audio_queue.rb +110 -0
  13. data/lib/hallon/enumerator.rb +55 -16
  14. data/lib/hallon/error.rb +9 -2
  15. data/lib/hallon/image.rb +6 -5
  16. data/lib/hallon/link.rb +7 -4
  17. data/lib/hallon/linkable.rb +27 -0
  18. data/lib/hallon/observable/player.rb +18 -1
  19. data/lib/hallon/observable/session.rb +5 -1
  20. data/lib/hallon/player.rb +180 -54
  21. data/lib/hallon/playlist.rb +33 -20
  22. data/lib/hallon/playlist_container.rb +78 -64
  23. data/lib/hallon/search.rb +51 -33
  24. data/lib/hallon/session.rb +1 -1
  25. data/lib/hallon/toplist.rb +36 -18
  26. data/lib/hallon/track.rb +12 -6
  27. data/lib/hallon/version.rb +1 -1
  28. data/spec/hallon/artist_browse_spec.rb +3 -4
  29. data/spec/hallon/audio_queue_spec.rb +89 -0
  30. data/spec/hallon/enumerator_spec.rb +50 -25
  31. data/spec/hallon/error_spec.rb +2 -2
  32. data/spec/hallon/image_spec.rb +12 -5
  33. data/spec/hallon/link_spec.rb +8 -9
  34. data/spec/hallon/linkable_spec.rb +11 -0
  35. data/spec/hallon/observable/session_spec.rb +4 -0
  36. data/spec/hallon/player_spec.rb +118 -5
  37. data/spec/hallon/playlist_container_spec.rb +2 -2
  38. data/spec/hallon/playlist_spec.rb +32 -37
  39. data/spec/hallon/search_spec.rb +3 -3
  40. data/spec/hallon/user_spec.rb +0 -6
  41. data/spec/spec_helper.rb +10 -0
  42. data/spec/support/audio_driver_mock.rb +23 -0
  43. data/spec/support/context_stub_session.rb +5 -0
  44. data/spec/support/shared_for_linkable_objects.rb +22 -2
  45. metadata +26 -20
  46. data/lib/hallon/queue.rb +0 -71
  47. data/spec/hallon/queue_spec.rb +0 -35
@@ -107,7 +107,11 @@ module Hallon::Observable
107
107
  format[:type] = struct[:sample_type]
108
108
 
109
109
  # read the frames of the given type
110
- frames = frames.public_send("read_array_of_#{format[:type]}", num_frames * format[:channels])
110
+ frames = unless num_frames.zero?
111
+ frames.public_send("read_array_of_#{format[:type]}", num_frames * format[:channels])
112
+ else
113
+ [] # when seeking, for example, num_frames will be zero and frames will be nil
114
+ end
111
115
 
112
116
  # pass the frames to the callback, allowing it to do whatever
113
117
  consumed_frames = trigger(pointer, :music_delivery, format, frames.each_slice(format[:channels]))
data/lib/hallon/player.rb CHANGED
@@ -1,9 +1,10 @@
1
1
  # coding: utf-8
2
+ require 'monitor'
3
+
2
4
  module Hallon
3
5
  # A wrapper around Session for playing, stopping and otherwise
4
6
  # controlling the playback features of libspotify.
5
7
  #
6
- # @note This is very much a work in progress.
7
8
  # @see Session
8
9
  class Player
9
10
  # meep?
@@ -12,6 +13,9 @@ module Hallon
12
13
  # @return [Spotify::Pointer<Session>] session pointer
13
14
  attr_reader :pointer
14
15
 
16
+ # @return [Symbol] one of :playing, :paused, :stopped
17
+ attr_reader :status
18
+
15
19
  # @return [Array<Symbol>] a list of available playback bitrates.
16
20
  def self.bitrates
17
21
  Spotify.enum_type(:bitrate).symbols.sort_by do |sym|
@@ -20,46 +24,189 @@ module Hallon
20
24
  end
21
25
  end
22
26
 
23
- # Constructs a Player, given a Session.
27
+ # Constructs a Player, given a Session and an audio driver.
24
28
  #
25
29
  # @example
26
- # Hallon::Player.new(session) do
27
- # on(:music_delivery) do |format, frames|
28
- # end
30
+ # player = Hallon::Player.new(session, Hallon::OpenAL)
31
+ # player.play(track)
32
+ #
33
+ # @note for instructions on how to write your own audio driver, see Hallons’ README
34
+ # @param [Session] session
35
+ # @param [AudioDriver] driver
36
+ # @yield instance_evals itself, allowing you to define callbacks using `on`
37
+ def initialize(session, driver, &block)
38
+ @session = session
39
+ @pointer = @session.pointer
40
+
41
+ # sample rate is often (if not always) 44.1KHz, so
42
+ # we keep an audio queue that can store 3s of audio
43
+ @queue = AudioQueue.new(44100)
44
+ @driver = driver.new
45
+ @queue.format = @driver.format = { rate: 44100, channels: 2, type: :int16 }
46
+
47
+ # used for feeder thread to know if it should stream
48
+ # data to the driver or not (see #status=)
49
+ @status_c = @queue.new_cond
50
+ # set initial status (we assume stopped)
51
+ self.status = @status = :stopped
52
+
53
+ # this thread feeds the audio driver with data, but
54
+ # if we are not playing it’ll wait until we are
55
+ @thread = Thread.start(@driver, @queue, @status_c) do |output, queue, cond|
56
+ output.stream do |num_frames|
57
+ queue.synchronize do
58
+ cond.wait_until { status == :playing }
59
+
60
+ if output.format != queue.format
61
+ output.format = queue.format
62
+ next # format changed, so we return nil
63
+ end
64
+
65
+ queue.pop(*num_frames)
66
+ end
67
+ end
68
+ end
69
+
70
+ @session.on(:start_playback, &method(:start_playback))
71
+ @session.on(:stop_playback, &method(:stop_playback))
72
+ @session.on(:music_delivery, &method(:music_delivery))
73
+ @session.on(:get_audio_buffer_stats, &method(:get_audio_buffer_stats))
74
+
75
+ @session.on(:end_of_track) { |*args| trigger(:end_of_track, *args) }
76
+ @session.on(:streaming_error) { |*args| trigger(:streaming_error, *args) }
77
+ @session.on(:play_token_lost) { |*args| trigger(:play_token_lost, *args) }
78
+
79
+ instance_eval(&block) if block_given?
80
+ end
81
+
82
+ protected
83
+
84
+ # Called by libspotify when the driver should start audio playback.
29
85
  #
30
- # on(:start_playback) do
31
- # end
86
+ # Will be called after calling our buffers are full enough to support
87
+ # continous playback.
88
+ def start_playback(session)
89
+ self.status = :playing
90
+ end
91
+
92
+ # Called by libspotify when the driver should pause audio playback.
93
+ #
94
+ # Might happen if we’re playing audio faster than we can stream it.
95
+ def stop_playback(session)
96
+ self.status = :paused
97
+ end
98
+
99
+ # Called by libspotify on music delivery; format is
100
+ # a hash of (sample) rate, channels and (sample) type.
101
+ def music_delivery(format, frames, session)
102
+ @queue.synchronize do
103
+ if frames.none?
104
+ @queue.clear
105
+ elsif @queue.format != format
106
+ @queue.format = format
107
+ end
108
+
109
+ @queue.push(frames)
110
+ end
111
+ end
112
+
113
+ # Called by libspotify to request information about our
114
+ # audio buffer. Required if we want libspotify to tell
115
+ # us when we should start and stop playback.
116
+ def get_audio_buffer_stats(session)
117
+ drops = @driver.drops if @driver.respond_to?(:drops)
118
+ [@queue.size, drops.to_i]
119
+ end
120
+
121
+ # This is essentially a mini state machine. Setting the
122
+ # status will also put the driver in the correct mode, as
123
+ # well as allow audio data to stream through the feeder
124
+ # thread.
32
125
  #
33
- # on(:stop_playback) do
34
- # end
126
+ # @param [Symbol] status one of :playing, :paused, :stopped
127
+ # @raise [ArgumentError] if given an invalid status
128
+ def status=(status)
129
+ @queue.synchronize do
130
+ old_status, @status = @status, status
131
+
132
+ case status
133
+ when :playing
134
+ @driver.play
135
+ when :paused
136
+ @driver.pause
137
+ when :stopped
138
+ @queue.clear
139
+ @driver.stop
140
+ else
141
+ @status = old_status
142
+ raise ArgumentError, "invalid status"
143
+ end
144
+
145
+ @status_c.signal
146
+ end
147
+ end
148
+
149
+ public
150
+
151
+ # @note default output also shows all our instance variables, that
152
+ # is kind of unnecessary and might take some time to display,
153
+ # for example if the audio queue is full
154
+ # @return [String]
155
+ def to_s
156
+ name = self.class.name
157
+ address = pointer.address.to_s(16)
158
+ "<#{name} session=0x#{address} driver=#{@driver.class} status=#{status}>"
159
+ end
160
+
161
+ # Start playing the currently loaded, or given, Track.
35
162
  #
36
- # on(:play_token_lost) do
37
- # end
163
+ # @example
164
+ # player.play("spotify:track:44FHDONpdYeDpmqyS3BLRP")
38
165
  #
39
- # on(:end_of_track) do
40
- # end
166
+ # @note If no track is given, will try to play currently {#load}ed track.
167
+ # @param [Track, Link, String, nil] track
168
+ # @return [Player]
169
+ def play(track = nil)
170
+ load(track) unless track.nil?
171
+ tap { Spotify.session_player_play(pointer, true) }
172
+ end
173
+
174
+ # Pause playback of a Track.
41
175
  #
42
- # on(:streaming_error) do |error|
43
- # end
176
+ # @return nothing
177
+ def pause
178
+ self.status = :paused
179
+ Spotify.session_player_play(pointer, false)
180
+ end
181
+
182
+ # Stop playing current track and unload it.
44
183
  #
45
- # on(:buffer_size?) do
46
- # # return the pair of [samples, dropouts] of your audiobuffer
47
- # end
48
- # end
184
+ # @return nothing
185
+ def stop
186
+ self.status = :stopped
187
+ Spotify.session_player_unload(pointer)
188
+ end
189
+
190
+ # Like {#play}, but blocks until the track has finished playing.
49
191
  #
50
- # @param [Session] session
51
- # @yield instance_evals itself, allowing you to define callbacks using `on`
52
- def initialize(session, &block)
53
- instance_eval(&block) if block_given?
192
+ # @param (see #play)
193
+ # @return (see #play)
194
+ def play!(track = nil)
195
+ monitor = Monitor.new
196
+ condvar = monitor.new_cond
54
197
 
55
- @session = session
56
- @pointer = @session.pointer
198
+ monitor.synchronize do
199
+ end_of_track = false
200
+
201
+ on(:end_of_track) do
202
+ monitor.synchronize do
203
+ end_of_track = true
204
+ condvar.signal
205
+ end
206
+ end
57
207
 
58
- %w[
59
- start_playback stop_playback play_token_lost end_of_track
60
- streaming_error get_audio_buffer_stats music_delivery
61
- ].each do |cb|
62
- @session.on(cb) { |*args| trigger(cb, *args) }
208
+ play(track)
209
+ condvar.wait_until { end_of_track }
63
210
  end
64
211
  end
65
212
 
@@ -73,15 +220,16 @@ module Hallon
73
220
 
74
221
  # Loads a Track for playing.
75
222
  #
76
- # @param [Track] track
223
+ # @param [Track, Link, String] track
77
224
  # @return [Player]
78
225
  # @raise [Error] if the track could not be loaded
79
226
  def load(track)
227
+ track = Track.new(track) unless track.is_a?(Track)
80
228
  error = Spotify.session_player_load(pointer, track.pointer)
81
229
  tap { Error.maybe_raise(error) }
82
230
  end
83
231
 
84
- # Prepares a Track for playing, without loading it.
232
+ # Prepares a Track for playing, without {#load}ing it.
85
233
  #
86
234
  # @note You can only prefetch if caching is on.
87
235
  # @param [Track] track
@@ -91,28 +239,6 @@ module Hallon
91
239
  tap { Error.maybe_raise(error) }
92
240
  end
93
241
 
94
- # Starts playing a Track by feeding data to your application.
95
- #
96
- # @return [Player]
97
- def play(track = nil)
98
- load(track) unless track.nil?
99
- tap { Spotify.session_player_play(pointer, true) }
100
- end
101
-
102
- # Pause playback of a Track.
103
- #
104
- # @return [Player]
105
- def pause
106
- tap { Spotify.session_player_play(pointer, false) }
107
- end
108
-
109
- # Stop playing current track and unload it.
110
- #
111
- # @return [Player]
112
- def stop
113
- tap { Spotify.session_player_unload(pointer) }
114
- end
115
-
116
242
  # Seek to the desired position of the currently loaded Track.
117
243
  #
118
244
  # @param [Numeric] seconds offset position in seconds
@@ -6,6 +6,16 @@ module Hallon
6
6
  #
7
7
  # @see http://developer.spotify.com/en/libspotify/docs/group__playlist.html
8
8
  class Playlist < Base
9
+ # Enumerates through all tracks of a playlist.
10
+ class Tracks < Enumerator
11
+ size :playlist_num_tracks
12
+
13
+ # @return [Track, nil]
14
+ item :playlist_track! do |track, index, pointer|
15
+ Playlist::Track.from(track, pointer, index)
16
+ end
17
+ end
18
+
9
19
  # Playlist::Track is a {Track} with additional information attached to it,
10
20
  # that is specific to the playlist it was created from. The returned track
11
21
  # is a snapshot of the information, so even if the underlying track moves,
@@ -14,27 +24,28 @@ module Hallon
14
24
  # There is no way to refresh the information. You’ll have to retrieve the
15
25
  # track again.
16
26
  class Track < Hallon::Track
17
- def initialize(pointer, playlist, index)
27
+ def initialize(pointer, playlist_pointer, index)
18
28
  super(pointer)
19
29
 
20
- @index = index
21
- @playlist = playlist
22
- @create_time = Time.at Spotify.playlist_track_create_time(playlist.pointer, index)
23
- @message = Spotify.playlist_track_message(playlist.pointer, index)
24
- @seen = Spotify.playlist_track_seen(playlist.pointer, index)
25
- @creator = begin
26
- creator = Spotify.playlist_track_creator!(playlist.pointer, index)
30
+ @index = index
31
+ @playlist_ptr = playlist_pointer
32
+ @create_time = Time.at Spotify.playlist_track_create_time(playlist_ptr, index)
33
+ @message = Spotify.playlist_track_message(playlist_ptr, index)
34
+ @seen = Spotify.playlist_track_seen(playlist_ptr, index)
35
+ @creator = begin
36
+ creator = Spotify.playlist_track_creator!(playlist_ptr, index)
27
37
  User.from(creator)
28
38
  end
29
39
  end
30
40
 
41
+ # @return [Spotify::Pointer<Playlist>] playlist pointer this track was created from.
42
+ attr_reader :playlist_ptr
43
+ private :playlist_ptr
44
+
31
45
  # @note this value never changes, even if the original track is moved/removed
32
46
  # @return [Integer] index this track was created with.
33
47
  attr_reader :index
34
48
 
35
- # @return [Playlist] playlist this track was created from.
36
- attr_reader :playlist
37
-
38
49
  # @return [Time] time when track at {#index} was added to playlist.
39
50
  attr_reader :create_time
40
51
 
@@ -44,6 +55,11 @@ module Hallon
44
55
  # @return [String] message attached to this track at {#index}.
45
56
  attr_reader :message
46
57
 
58
+ # @return [Playlist] playlist this track was created from.
59
+ def playlist
60
+ Playlist.new(playlist_ptr)
61
+ end
62
+
47
63
  # @see Playlist#seen
48
64
  # @return [Boolean] true if track at {#index} has been seen.
49
65
  def seen?
@@ -64,16 +80,16 @@ module Hallon
64
80
  raise IndexError, "track has moved from #{index}"
65
81
  end
66
82
 
67
- error = Spotify.playlist_track_set_seen(playlist.pointer, index, !! seen)
83
+ error = Spotify.playlist_track_set_seen(playlist_ptr, index, !! seen)
68
84
  Error.maybe_raise(error)
69
- @seen = Spotify.playlist_track_seen(playlist.pointer, index)
85
+ @seen = Spotify.playlist_track_seen(playlist_ptr, index)
70
86
  end
71
87
 
72
88
  # @return [Boolean] true if the track has not yet moved.
73
89
  def moved?
74
90
  # using non-GC version deliberately; no need to keep a reference to
75
91
  # this pointer once we’re done here anyway
76
- Spotify.playlist_track(playlist.pointer, index) != pointer
92
+ Spotify.playlist_track(playlist_ptr, index) != pointer
77
93
  end
78
94
  end
79
95
 
@@ -229,7 +245,7 @@ module Hallon
229
245
  #
230
246
  # @return [Playlist]
231
247
  def update_subscribers
232
- Spotify.playlist_update_subscribers(session.pointer, pointer)
248
+ tap { Spotify.playlist_update_subscribers(session.pointer, pointer) }
233
249
  end
234
250
 
235
251
  # @note only applicable if {#offline_status} is `:downloading`
@@ -253,12 +269,9 @@ module Hallon
253
269
  # track = playlist.tracks[3]
254
270
  # puts track.name
255
271
  #
256
- # @return [Enumerable<Playlist::Track>] a list of playlist tracks.
272
+ # @return [Tracks] a list of playlist tracks.
257
273
  def tracks
258
- Enumerator.new(size) do |index|
259
- track = Spotify.playlist_track!(pointer, index)
260
- Playlist::Track.from(track, self, index)
261
- end
274
+ Tracks.new(self)
262
275
  end
263
276
 
264
277
  # Add a list of tracks to the playlist starting at given position.
@@ -1,4 +1,6 @@
1
1
  # coding: utf-8
2
+ require 'ostruct'
3
+
2
4
  module Hallon
3
5
  # PlaylistContainers are the objects that hold playlists. Each User
4
6
  # in libspotify has a container for its’ starred and published playlists,
@@ -6,13 +8,58 @@ module Hallon
6
8
  #
7
9
  # @see http://developer.spotify.com/en/libspotify/docs/group__playlist.html
8
10
  class PlaylistContainer < Base
11
+ # Enumerates through all playlists and folders of a container.
12
+ class Contents < Enumerator
13
+ size :playlistcontainer_num_playlists
14
+
15
+ # @return [Playlist, Folder, nil]
16
+ item :playlistcontainer_playlist_type do |type, index, pointer|
17
+ case type
18
+ when :playlist
19
+ playlist = Spotify.playlistcontainer_playlist!(pointer, index)
20
+ Playlist.from(playlist)
21
+ when :start_folder, :end_folder
22
+ Folder.new(pointer, folder_range(index, type))
23
+ else # :unknown
24
+ end
25
+ end
26
+
27
+ protected
28
+
29
+ # Given an index, find out the starting point and ending point
30
+ # of the folder at that index.
31
+ #
32
+ # @param [Integer] index
33
+ # @param [Symbol] type
34
+ # @return [Range] begin..end
35
+ def folder_range(index, type)
36
+ id = folder_id(index)
37
+ same_id = proc { |idx| folder_id(idx) == id }
38
+
39
+ case type
40
+ when :start_folder
41
+ beginning = index
42
+ ending = (index + 1).upto(size - 1).find(&same_id)
43
+ when :end_folder
44
+ ending = index
45
+ beginning = (index - 1).downto(0).find(&same_id)
46
+ end
47
+
48
+ if beginning and ending and beginning != ending
49
+ beginning..ending
50
+ end
51
+ end
52
+
53
+ # @return [Integer] folder ID of folder at `index`.
54
+ def folder_id(index)
55
+ Spotify.playlistcontainer_playlist_folder_id(pointer, index)
56
+ end
57
+ end
58
+
9
59
  # Folders are parts of playlist containers in that they surround playlists
10
60
  # with a beginning marker and an ending marker. The playlists between these
11
61
  # markers are considered "inside the playlist".
12
62
  class Folder
13
- # @return [PlaylistContainer] playlistcontainer this folder was created from.
14
- attr_reader :container
15
-
16
63
  # @return [Integer] index this folder starts at in the container.
17
64
  attr_reader :begin
18
65
 
@@ -25,6 +72,15 @@ module Hallon
25
72
  # @return [String]
26
73
  attr_reader :name
27
74
 
75
+ # @return [Spotify::Pointer<Container>]
76
+ attr_reader :container_ptr
77
+ private :container_ptr
78
+
79
+ # @return [PlaylistContainer] playlistcontainer this folder was created from.
80
+ def container
81
+ PlaylistContainer.new(container_ptr)
82
+ end
83
+
28
84
  # Rename the folder.
29
85
  #
30
86
  # @note libspotify has no actual folder rename; what happens is that
@@ -42,14 +98,14 @@ module Hallon
42
98
 
43
99
  # @param [PlaylistContainer] container
44
100
  # @param [Range] indices
45
- def initialize(container, indices)
46
- @container = container
47
- @begin = indices.begin
48
- @end = indices.end
101
+ def initialize(container_pointer, indices)
102
+ @container_ptr = container_pointer
49
103
 
50
- @id = Spotify.playlistcontainer_playlist_folder_id(container.pointer, @begin)
104
+ @begin = indices.begin
105
+ @end = indices.end
106
+ @id = Spotify.playlistcontainer_playlist_folder_id(container_ptr, @begin)
51
107
  FFI::Buffer.alloc_out(256) do |buffer|
52
- error = Spotify.playlistcontainer_playlist_folder_name(container.pointer, @begin, buffer, buffer.size)
108
+ error = Spotify.playlistcontainer_playlist_folder_name(container_ptr, @begin, buffer, buffer.size)
53
109
  Error.maybe_raise(error) # should not fail, but just to be safe!
54
110
 
55
111
  @name = buffer.get_string(0)
@@ -59,20 +115,21 @@ module Hallon
59
115
  # @param [Folder] other
60
116
  # @return [Boolean] true if the two folders are the same (same indices, same id).
61
117
  def ==(other)
62
- !! [:id, :container, :begin, :end].all? do |attr|
63
- public_send(attr) == other.public_send(attr)
118
+ !! [:id, :container_ptr, :begin, :end].all? do |attr|
119
+ send(attr) == other.send(attr)
64
120
  end if other.is_a?(Folder)
65
121
  end
66
122
 
67
- # @return [Enumerator<Playlist, Folder>] contents of this folder
123
+ # @return [Array<Playlist, Folder>] contents of this folder
68
124
  def contents
69
- container.contents[(@begin + 1)..(@end - 1)]
125
+ container = OpenStruct.new(:pointer => container_ptr)
126
+ Contents.new(container)[(@begin + 1)..(@end - 1)]
70
127
  end
71
128
 
72
129
  # @return [Boolean] true if the folder has moved.
73
130
  def moved?
74
- Spotify.playlistcontainer_playlist_folder_id(container.pointer, @begin) != id or
75
- Spotify.playlistcontainer_playlist_folder_id(container.pointer, @end) != id
131
+ Spotify.playlistcontainer_playlist_folder_id(container_ptr, @begin) != id or
132
+ Spotify.playlistcontainer_playlist_folder_id(container_ptr, @end) != id
76
133
  end
77
134
  end
78
135
 
@@ -106,18 +163,9 @@ module Hallon
106
163
  Spotify.playlistcontainer_num_playlists(pointer)
107
164
  end
108
165
 
109
- # @return [Enumerator<Playlist, Folder, nil>] an enumerator of folders and playlists.
166
+ # @return [Contents] an enumerator of folders and playlists.
110
167
  def contents
111
- Enumerator.new(size) do |i|
112
- case playlist_type(i)
113
- when :playlist
114
- playlist = Spotify.playlistcontainer_playlist!(pointer, i)
115
- Playlist.new(playlist)
116
- when :start_folder, :end_folder
117
- Folder.new(self, folder_range(i))
118
- else # :unknown
119
- end
120
- end
168
+ Contents.new(self)
121
169
  end
122
170
 
123
171
  # Add the given playlist to the end of the container.
@@ -185,12 +233,12 @@ module Hallon
185
233
  def remove(index)
186
234
  remove = proc { |idx| Spotify.playlistcontainer_remove_playlist(pointer, idx) }
187
235
 
188
- error = case playlist_type(index)
236
+ error = case Spotify.playlistcontainer_playlist_type(pointer, index)
189
237
  when :start_folder, :end_folder
190
- indices = folder_range(index)
238
+ folder = contents[index]
191
239
 
192
- Error.maybe_raise(remove[indices.begin])
193
- remove[indices.end - 1] # ^ everything moves down one step
240
+ Error.maybe_raise(remove[folder.begin])
241
+ remove[folder.end - 1] # ^ everything moves down one step
194
242
  else
195
243
  remove[index]
196
244
  end
@@ -245,39 +293,5 @@ module Hallon
245
293
  infront_of += 1 if from < infront_of
246
294
  Spotify.playlistcontainer_move_playlist(pointer, from, infront_of, dry_run)
247
295
  end
248
-
249
- # Given an index, find out the starting point and ending point
250
- # of the folder at that index.
251
- #
252
- # @param [Integer] index
253
- # @return [Range] begin..end
254
- def folder_range(index)
255
- id = folder_id(index)
256
- type = playlist_type(index)
257
- same_id = proc { |idx| folder_id(idx) == id }
258
-
259
- case type
260
- when :start_folder
261
- beginning = index
262
- ending = (index + 1).upto(size - 1).find(&same_id)
263
- when :end_folder
264
- ending = index
265
- beginning = (index - 1).downto(0).find(&same_id)
266
- end
267
-
268
- if beginning and ending and beginning != ending
269
- beginning..ending
270
- end
271
- end
272
-
273
- # @return [Symbol] playlist type
274
- def playlist_type(index)
275
- Spotify.playlistcontainer_playlist_type(pointer, index)
276
- end
277
-
278
- # @return [Integer] folder ID of folder at `index`.
279
- def folder_id(index)
280
- Spotify.playlistcontainer_playlist_folder_id(pointer, index)
281
- end
282
296
  end
283
297
  end