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.
- data/CHANGELOG.md +43 -0
- data/Gemfile +3 -1
- data/README.markdown +41 -11
- data/Rakefile +12 -0
- data/examples/audio_driver.rb +55 -0
- data/examples/playing_audio.rb +10 -50
- data/hallon.gemspec +1 -1
- data/lib/hallon.rb +1 -1
- data/lib/hallon/album_browse.rb +22 -11
- data/lib/hallon/artist_browse.rb +64 -33
- data/lib/hallon/audio_driver.rb +138 -0
- data/lib/hallon/audio_queue.rb +110 -0
- data/lib/hallon/enumerator.rb +55 -16
- data/lib/hallon/error.rb +9 -2
- data/lib/hallon/image.rb +6 -5
- data/lib/hallon/link.rb +7 -4
- data/lib/hallon/linkable.rb +27 -0
- data/lib/hallon/observable/player.rb +18 -1
- data/lib/hallon/observable/session.rb +5 -1
- data/lib/hallon/player.rb +180 -54
- data/lib/hallon/playlist.rb +33 -20
- data/lib/hallon/playlist_container.rb +78 -64
- data/lib/hallon/search.rb +51 -33
- data/lib/hallon/session.rb +1 -1
- data/lib/hallon/toplist.rb +36 -18
- data/lib/hallon/track.rb +12 -6
- data/lib/hallon/version.rb +1 -1
- data/spec/hallon/artist_browse_spec.rb +3 -4
- data/spec/hallon/audio_queue_spec.rb +89 -0
- data/spec/hallon/enumerator_spec.rb +50 -25
- data/spec/hallon/error_spec.rb +2 -2
- data/spec/hallon/image_spec.rb +12 -5
- data/spec/hallon/link_spec.rb +8 -9
- data/spec/hallon/linkable_spec.rb +11 -0
- data/spec/hallon/observable/session_spec.rb +4 -0
- data/spec/hallon/player_spec.rb +118 -5
- data/spec/hallon/playlist_container_spec.rb +2 -2
- data/spec/hallon/playlist_spec.rb +32 -37
- data/spec/hallon/search_spec.rb +3 -3
- data/spec/hallon/user_spec.rb +0 -6
- data/spec/spec_helper.rb +10 -0
- data/spec/support/audio_driver_mock.rb +23 -0
- data/spec/support/context_stub_session.rb +5 -0
- data/spec/support/shared_for_linkable_objects.rb +22 -2
- metadata +26 -20
- data/lib/hallon/queue.rb +0 -71
- 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 =
|
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)
|
27
|
-
#
|
28
|
-
#
|
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
|
-
#
|
31
|
-
#
|
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
|
-
#
|
34
|
-
#
|
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
|
-
#
|
37
|
-
#
|
163
|
+
# @example
|
164
|
+
# player.play("spotify:track:44FHDONpdYeDpmqyS3BLRP")
|
38
165
|
#
|
39
|
-
#
|
40
|
-
#
|
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
|
-
#
|
43
|
-
|
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
|
-
#
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
51
|
-
# @
|
52
|
-
def
|
53
|
-
|
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
|
-
|
56
|
-
|
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
|
-
|
59
|
-
|
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
|
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
|
data/lib/hallon/playlist.rb
CHANGED
@@ -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,
|
27
|
+
def initialize(pointer, playlist_pointer, index)
|
18
28
|
super(pointer)
|
19
29
|
|
20
|
-
@index
|
21
|
-
@
|
22
|
-
@create_time
|
23
|
-
@message
|
24
|
-
@seen
|
25
|
-
@creator
|
26
|
-
creator = Spotify.playlist_track_creator!(
|
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(
|
83
|
+
error = Spotify.playlist_track_set_seen(playlist_ptr, index, !! seen)
|
68
84
|
Error.maybe_raise(error)
|
69
|
-
@seen = Spotify.playlist_track_seen(
|
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(
|
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 [
|
272
|
+
# @return [Tracks] a list of playlist tracks.
|
257
273
|
def tracks
|
258
|
-
|
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(
|
46
|
-
@
|
47
|
-
@begin = indices.begin
|
48
|
-
@end = indices.end
|
101
|
+
def initialize(container_pointer, indices)
|
102
|
+
@container_ptr = container_pointer
|
49
103
|
|
50
|
-
@
|
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(
|
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, :
|
63
|
-
|
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 [
|
123
|
+
# @return [Array<Playlist, Folder>] contents of this folder
|
68
124
|
def contents
|
69
|
-
container
|
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(
|
75
|
-
Spotify.playlistcontainer_playlist_folder_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 [
|
166
|
+
# @return [Contents] an enumerator of folders and playlists.
|
110
167
|
def contents
|
111
|
-
|
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
|
236
|
+
error = case Spotify.playlistcontainer_playlist_type(pointer, index)
|
189
237
|
when :start_folder, :end_folder
|
190
|
-
|
238
|
+
folder = contents[index]
|
191
239
|
|
192
|
-
Error.maybe_raise(remove[
|
193
|
-
remove[
|
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
|