hallon 0.12.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
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