hallon 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/.travis.yml +2 -0
  2. data/CHANGELOG +43 -0
  3. data/Gemfile +2 -0
  4. data/README.markdown +21 -13
  5. data/Rakefile +84 -23
  6. data/dev/login.rb +16 -0
  7. data/examples/adding_tracks_to_playlist.rb +49 -0
  8. data/examples/logging_in.rb +1 -6
  9. data/examples/show_published_playlists_of_user.rb +9 -19
  10. data/hallon.gemspec +1 -1
  11. data/lib/hallon.rb +3 -2
  12. data/lib/hallon/album.rb +55 -41
  13. data/lib/hallon/album_browse.rb +41 -37
  14. data/lib/hallon/artist.rb +30 -21
  15. data/lib/hallon/artist_browse.rb +59 -41
  16. data/lib/hallon/base.rb +68 -5
  17. data/lib/hallon/enumerator.rb +1 -0
  18. data/lib/hallon/error.rb +3 -0
  19. data/lib/hallon/ext/spotify.rb +169 -36
  20. data/lib/hallon/image.rb +30 -44
  21. data/lib/hallon/link.rb +29 -43
  22. data/lib/hallon/linkable.rb +68 -20
  23. data/lib/hallon/observable.rb +0 -1
  24. data/lib/hallon/player.rb +21 -7
  25. data/lib/hallon/playlist.rb +291 -0
  26. data/lib/hallon/playlist_container.rb +27 -0
  27. data/lib/hallon/search.rb +52 -45
  28. data/lib/hallon/session.rb +129 -81
  29. data/lib/hallon/toplist.rb +37 -19
  30. data/lib/hallon/track.rb +68 -45
  31. data/lib/hallon/user.rb +69 -33
  32. data/lib/hallon/version.rb +1 -1
  33. data/spec/hallon/album_browse_spec.rb +15 -9
  34. data/spec/hallon/album_spec.rb +15 -15
  35. data/spec/hallon/artist_browse_spec.rb +28 -9
  36. data/spec/hallon/artist_spec.rb +30 -14
  37. data/spec/hallon/enumerator_spec.rb +0 -1
  38. data/spec/hallon/hallon_spec.rb +20 -1
  39. data/spec/hallon/image_spec.rb +18 -41
  40. data/spec/hallon/link_spec.rb +10 -12
  41. data/spec/hallon/linkable_spec.rb +37 -18
  42. data/spec/hallon/player_spec.rb +8 -0
  43. data/spec/hallon/playlist_container_spec.rb +75 -0
  44. data/spec/hallon/playlist_spec.rb +204 -0
  45. data/spec/hallon/search_spec.rb +19 -16
  46. data/spec/hallon/session_spec.rb +61 -29
  47. data/spec/hallon/spotify_spec.rb +30 -0
  48. data/spec/hallon/toplist_spec.rb +22 -14
  49. data/spec/hallon/track_spec.rb +62 -21
  50. data/spec/hallon/user_spec.rb +47 -36
  51. data/spec/mockspotify.rb +35 -10
  52. data/spec/mockspotify/mockspotify_spec.rb +22 -0
  53. data/spec/spec_helper.rb +7 -3
  54. data/spec/support/common_objects.rb +91 -16
  55. data/spec/support/shared_for_linkable_objects.rb +39 -0
  56. metadata +30 -20
  57. data/Termfile +0 -7
  58. data/lib/hallon/synchronizable.rb +0 -32
  59. data/spec/hallon/synchronizable_spec.rb +0 -19
data/lib/hallon/link.rb CHANGED
@@ -6,92 +6,78 @@ module Hallon
6
6
  class Link < Base
7
7
  # True if the given Spotify URI is valid (parsable by libspotify).
8
8
  #
9
- # @param (see Hallon::Link#initialize)
9
+ # @param [#to_s] spotify_uri
10
10
  # @return [Boolean]
11
11
  def self.valid?(spotify_uri)
12
- !! new(spotify_uri)
13
- rescue ArgumentError
14
- false
15
- end
12
+ if spotify_uri.is_a?(Link)
13
+ return true
14
+ end
16
15
 
17
- # Overloaded to short-circuit when given a Link.
18
- #
19
- # @return [Hallon::Link]
20
- def self.new(uri)
21
- uri.is_a?(Link) ? uri : super
16
+ link = Spotify.link_create_from_string!(spotify_uri.to_s)
17
+ not link.null?
22
18
  end
23
19
 
24
20
  # Parse the given Spotify URI into a Link.
25
21
  #
26
- # @note Unless you have a {Session} initialized, this will segfault!
22
+ # @note You must initialize a Session before you call this method.
27
23
  # @param [#to_str] uri
28
24
  # @raise [ArgumentError] link could not be parsed
29
25
  def initialize(uri)
30
- if (link = uri).respond_to? :to_str
31
- link = Spotify.link_create_from_string(link.to_str)
26
+ # if no session instance exists, libspotify segfaults, so assert that we have one
27
+ unless Session.instance?
28
+ raise "Link.new requires an existing Session instance"
32
29
  end
33
30
 
34
- @pointer = Spotify::Pointer.new(link, :link)
35
-
36
- raise ArgumentError, "#{uri} is not a valid Spotify link" if @pointer.null?
31
+ @pointer = to_pointer(uri, :link) do
32
+ Spotify.link_create_from_string!(uri.to_str)
33
+ end
37
34
  end
38
35
 
39
- # Link type as a symbol.
40
- #
41
- # @return [Symbol]
36
+ # @return [Symbol] link type as a symbol (e.g. `:playlist`).
42
37
  def type
43
- Spotify.link_type(@pointer)
38
+ Spotify.link_type(pointer)
44
39
  end
45
40
 
46
- # Spotify URI length.
47
- #
48
- # @return [Fixnum]
41
+ # @return [Fixnum] spotify URI length.
49
42
  def length
50
- Spotify.link_as_string(@pointer, nil, 0)
43
+ Spotify.link_as_string(pointer, nil, 0)
51
44
  end
52
45
 
53
- # Get the Spotify URI this Link represents.
54
- #
55
46
  # @see #length
56
47
  # @param [Fixnum] length truncate to this size
57
- # @return [String]
48
+ # @return [String] spotify URI representation of this Link.
58
49
  def to_str(length = length)
59
50
  FFI::Buffer.alloc_out(length + 1) do |b|
60
- Spotify.link_as_string(@pointer, b, b.size)
51
+ Spotify.link_as_string(pointer, b, b.size)
61
52
  return b.get_string(0)
62
53
  end
63
54
  end
64
55
 
65
- # Retrieve the full Spotify HTTP URL for this Link.
66
- #
67
- # @return [String]
56
+ alias_method :to_uri, :to_str
57
+
58
+ # @return [String] full Spotify HTTP URL.
68
59
  def to_url
69
60
  "http://open.spotify.com/%s" % to_str[8..-1].gsub(':', '/')
70
61
  end
71
62
 
72
- # True if this link equals `other.to_str`
73
- #
74
- # @param [#to_str] other
75
- # @return [Boolean]
63
+ # @param [Object] other
64
+ # @return [Boolean] true if this link equals `other.to_str`.
76
65
  def ==(other)
77
- return super unless other.respond_to?(:to_str)
78
66
  to_str == other.to_str
67
+ rescue NoMethodError
68
+ super
79
69
  end
80
70
 
81
- # String representation of the given Link.
82
- #
83
- # @return [String]
71
+ # @return [String] string representation of the Link.
84
72
  def to_s
85
73
  "<#{self.class.name} #{to_str}>"
86
74
  end
87
75
 
88
- # Retrieve the underlying pointer. Used by {Linkable}.
89
- #
90
76
  # @param [Symbol] expected_type if given, makes sure the link is of this type
91
- # @return [FFI::Pointer]
77
+ # @return [Spotify::Pointer] the underlying Spotify::Pointer.
92
78
  # @raise ArgumentError if `type` is given and does not match link {#type}
93
79
  def pointer(expected_type = nil)
94
- unless type == expected_type
80
+ unless type == expected_type or (expected_type == :playlist and type == :starred)
95
81
  raise ArgumentError, "expected #{expected_type} link, but it is of type #{type}"
96
82
  end if expected_type
97
83
  super()
@@ -3,58 +3,106 @@ module Hallon
3
3
  # Methods shared between objects that can be created from Spotify URIs,
4
4
  # or can be turned into Spotify URIs.
5
5
  #
6
- # @note Linkable is not part of Hallons’ public API.
6
+ # @note Linkable is part of Hallons’ private API. You probably do not
7
+ # not need to care about these methods.
8
+ #
7
9
  # @private
8
10
  module Linkable
9
- # Defines `#from_link`, used in converting a link to a pointer.
11
+ # Defines `#from_link`, used in converting a link to a pointer. You
12
+ # can either pass it a `method_name`, or a `type` and a block.
10
13
  #
11
- # @overload from_link(type)
12
- # Convert from a link using said method.
14
+ # @overload from_link(method_name)
15
+ # Define `#from_link` simply by giving the name of the method,
16
+ # minus the `link_` prefix.
13
17
  #
14
18
  # @example
15
- # from_link :as_album # => Spotify.link_as_album(pointer, *args)
19
+ # class Album
20
+ # extend Linkable
21
+ #
22
+ # from_link :as_album # => Spotify.link_as_album(pointer, *args)
23
+ # # ^ is roughly equivalent to:
24
+ # def from_link(link, *args)
25
+ # unless Spotify::Pointer.typechecks?(link, :link)
26
+ # link = Link.new(link).pointer(:album)
27
+ # end
28
+ #
29
+ # Spotify.link_as_album!(link)
30
+ # end
31
+ # end
16
32
  #
17
- # @param [Symbol] as_object link conversion method, formatted `as_type`
33
+ # @param [Symbol] method_name
18
34
  #
19
35
  # @overload from_link(type) { |*args| … }
20
- # Use the given block to convert the link.
36
+ # Define `#from_link` to use the given block to convert an object
37
+ # from a link. The link is converted to a pointer and typechecked
38
+ # to be of the same type as `type` before given to the block.
21
39
  #
22
40
  # @example
23
- # from_link :profile do |pointer|
24
- # Spotify.link_as_user(pointer)
41
+ # class User
42
+ # extend Linkable
43
+ #
44
+ # from_link :profile do |pointer|
45
+ # Spotify.link_as_user!(pointer)
46
+ # end
47
+ # # ^ is roughly equivalent to:
48
+ # def from_link(link, *args)
49
+ # unless Spotify::Pointer.typechecks?(link, :link)
50
+ # link = Link.new(link).pointer(:profile)
51
+ # end
52
+ #
53
+ # Spotify.link_as_user!(link)
54
+ # end
25
55
  # end
26
56
  #
27
57
  # @param [#to_s] type link type
28
58
  # @yield [link, *args] called when conversion is needed from Link pointer
29
- # @yieldparam [Hallon::Link] link
59
+ # @yieldparam [Spotify::Pointer] link
30
60
  # @yieldparam *args any extra arguments given to `#from_link`
31
61
  #
32
- # @see Link#pointer
62
+ # @note Private API. You probably do not need to care about this method.
33
63
  def from_link(as_object, &block)
34
- block ||= Spotify.method(:"link_#{as_object}")
64
+ block ||= Spotify.method(:"link_#{as_object}!")
35
65
  type = as_object.to_s[/^(as_)?([^_]+)/, 2].to_sym
36
66
 
37
67
  define_method(:from_link) do |link, *args|
38
- link = if link.is_a? FFI::Pointer then link else
39
- block.call Link.new(link).pointer(type), *args
40
- end
68
+ if link.is_a?(FFI::Pointer) and not link.is_a?(Spotify::Pointer)
69
+ link
70
+ else
71
+ unless Spotify::Pointer.typechecks?(link, :link)
72
+ link = Link.new(link).pointer(type)
73
+ end
41
74
 
42
- link.tap { raise Hallon::Error, "invalid link" if link.null? }
75
+ instance_exec(link, *args, &block)
76
+ end
43
77
  end
78
+
79
+ private :from_link
44
80
  end
45
81
 
46
- # Defines `#to_link` method, which converts the the current object to a {Link}
82
+ # Defines `#to_link` method, used in converting the object to a {Link}.
47
83
  #
48
84
  # @example
49
- # to_link :from_artist # => Spotify.link_create_from_artist
85
+ # class Artist
86
+ # extend Linkable
50
87
  #
51
- # @param [Symbol] cmethod object kind
88
+ # to_link :from_artist
89
+ # # ^ is the same as:
90
+ # def to_link(*args)
91
+ # link = Spotify.link_create_from_artist!(pointer, *args)
92
+ # Link.new(link)
93
+ # end
94
+ # end
95
+ #
96
+ # @param [Symbol] cmethod name of the C method, say `from_artist` in `Spotify.link_create_from_artist`.
52
97
  # @return [Link]
53
98
  def to_link(cmethod)
54
99
  define_method(:to_link) do |*args|
55
- link = Spotify.__send__(:"link_create_#{cmethod}", @pointer, *args)
100
+ link = Spotify.__send__(:"link_create_#{cmethod}!", pointer, *args)
56
101
  Link.new(link)
57
102
  end
58
103
  end
104
+
105
+ private :from_link
106
+ private :to_link
59
107
  end
60
108
  end
@@ -26,7 +26,6 @@ module Hallon
26
26
  # of the event that called it
27
27
  # @param [#to_sym] event name of event to handle
28
28
  # @yield (*args) event handler block
29
- # @see #initialize
30
29
  def on(*events, &block)
31
30
  raise ArgumentError, "no block given" unless block
32
31
  wrap = events.length > 1
data/lib/hallon/player.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # coding: utf-8
1
2
  module Hallon
2
3
  # A wrapper around Session for playing, stopping and otherwise
3
4
  # controlling the playback features of libspotify.
@@ -9,6 +10,9 @@ module Hallon
9
10
  class Player
10
11
  include Observable
11
12
 
13
+ # @return [Spotify::Pointer<Session>] session pointer
14
+ attr_reader :pointer
15
+
12
16
  # @return [Array<Symbol>] a list of available playback bitrates.
13
17
  def self.bitrates
14
18
  Spotify.enum_type(:bitrate).symbols.sort_by do |sym|
@@ -74,7 +78,7 @@ module Hallon
74
78
  # @param [Symbol] bitrate one of :96k, :160k, :320k
75
79
  # @return [Symbol]
76
80
  def bitrate=(bitrate)
77
- Spotify.session_preferred_bitrate(@pointer, bitrate)
81
+ Spotify.session_preferred_bitrate(pointer, bitrate)
78
82
  end
79
83
 
80
84
  # Loads a Track for playing.
@@ -83,7 +87,7 @@ module Hallon
83
87
  # @return [Player]
84
88
  # @raise [Error] if the track could not be loaded
85
89
  def load(track)
86
- error = Spotify.session_player_load(@pointer, track.pointer)
90
+ error = Spotify.session_player_load(pointer, track.pointer)
87
91
  tap { Error.maybe_raise(error) }
88
92
  end
89
93
 
@@ -93,7 +97,7 @@ module Hallon
93
97
  # @param [Track] track
94
98
  # @return [Player]
95
99
  def prefetch(track)
96
- error = Spotify.session_player_prefetch(@pointer, track.pointer)
100
+ error = Spotify.session_player_prefetch(pointer, track.pointer)
97
101
  tap { Error.maybe_raise(error) }
98
102
  end
99
103
 
@@ -102,21 +106,21 @@ module Hallon
102
106
  # @return [Player]
103
107
  def play(track = nil)
104
108
  load(track) unless track.nil?
105
- tap { Spotify.session_player_play(@pointer, true) }
109
+ tap { Spotify.session_player_play(pointer, true) }
106
110
  end
107
111
 
108
112
  # Pause playback of a Track.
109
113
  #
110
114
  # @return [Player]
111
115
  def pause
112
- tap { Spotify.session_player_play(@pointer, false) }
116
+ tap { Spotify.session_player_play(pointer, false) }
113
117
  end
114
118
 
115
119
  # Stop playing current track and unload it.
116
120
  #
117
121
  # @return [Player]
118
122
  def stop
119
- tap { Spotify.session_player_unload(@pointer) }
123
+ tap { Spotify.session_player_unload(pointer) }
120
124
  end
121
125
 
122
126
  # Seek to the desired position of the currently loaded Track.
@@ -124,7 +128,17 @@ module Hallon
124
128
  # @param [Numeric] seconds offset position in seconds
125
129
  # @return [Player]
126
130
  def seek(seconds)
127
- tap { Spotify.session_player_seek(@pointer, seconds * 1000) }
131
+ tap { Spotify.session_player_seek(pointer, seconds * 1000) }
132
+ end
133
+
134
+ # @return [Boolean] true if libspotify is set to normalize audio volume.
135
+ def volume_normalization?
136
+ Spotify.session_get_volume_normalization(pointer)
137
+ end
138
+
139
+ # @param [Boolean] normalize_volume true if libspotify should normalize audio volume.
140
+ def volume_normalization=(normalize_volume)
141
+ Spotify.session_set_volume_normalization(pointer, !! normalize_volume)
128
142
  end
129
143
  end
130
144
  end
@@ -0,0 +1,291 @@
1
+ # coding: utf-8
2
+ module Hallon
3
+ # Playlists are playlists. They contain tracks and track information
4
+ # such as when tracks were added or by whom. They also contain some
5
+ # metadata such as their own name.
6
+ #
7
+ # @see http://developer.spotify.com/en/libspotify/docs/group__playlist.html
8
+ class Playlist < Base
9
+ include Observable
10
+ extend Linkable
11
+
12
+ # Playlist::Track is a {Track} with additional information attached to it,
13
+ # that is specific to the playlist it was created from. The returned track
14
+ # is a snapshot of the information, so even if the underlying track moves,
15
+ # this Playlist::Track will still contain the same information.
16
+ #
17
+ # There is no way to refresh the information. You’ll have to retrieve the
18
+ # track again.
19
+ class Track < Hallon::Track
20
+ def initialize(playlist, index)
21
+ super Spotify.playlist_track!(playlist, index)
22
+
23
+ @index = index
24
+ @create_time = Time.at Spotify.playlist_track_create_time(playlist, index)
25
+ @message = Spotify.playlist_track_message(playlist, index)
26
+ @seen = Spotify.playlist_track_seen(playlist, index)
27
+ @creator = begin
28
+ creator = Spotify.playlist_track_creator!(playlist, index)
29
+ User.new(creator) unless creator.null?
30
+ end
31
+ end
32
+
33
+ # @note this value never changes, even if the original track is moved/removed
34
+ # @return [Integer] index this track was created with.
35
+ attr_reader :index
36
+
37
+ # @return [Time] time when track at {#index} was added to playlist.
38
+ def create_time
39
+ @create_time
40
+ end
41
+
42
+ # @return [User, nil] person who added track at {#index} to this playlist.
43
+ def creator
44
+ @creator
45
+ end
46
+
47
+ # @return [String] message attached to this track at {#index}.
48
+ def message
49
+ @message
50
+ end
51
+
52
+ # @see Playlist#seen
53
+ # @return [Boolean] true if track at {#index} has been seen.
54
+ def seen?
55
+ @seen
56
+ end
57
+ end
58
+
59
+ from_link :playlist do |pointer|
60
+ Spotify.playlist_create!(session.pointer, pointer)
61
+ end
62
+
63
+ to_link :from_playlist
64
+
65
+ # Construct a new Playlist, given a pointer.
66
+ #
67
+ # @param [String, Link, FFI::Pointer] link
68
+ def initialize(link)
69
+ callbacks = Spotify::PlaylistCallbacks.new(self, @sp_callbacks = {})
70
+ @pointer = to_pointer(link, :playlist)
71
+ Spotify.playlist_add_callbacks(pointer, callbacks, nil)
72
+ end
73
+
74
+ # @return [Boolean] true if the playlist is loaded
75
+ def loaded?
76
+ Spotify.playlist_is_loaded(pointer)
77
+ end
78
+
79
+ # @return [Boolean] true if the playlist is collaborative
80
+ def collaborative?
81
+ Spotify.playlist_is_collaborative(pointer)
82
+ end
83
+
84
+ # @param [Boolean] collaborative true to set the playlist to collaborative
85
+ def collaborative=(collaborative)
86
+ Spotify.playlist_set_collaborative(pointer, !!collaborative)
87
+ end
88
+
89
+ # @return [Boolean] true if playlist has pending changes
90
+ def pending?
91
+ Spotify.playlist_has_pending_changes(pointer)
92
+ end
93
+
94
+ # @return [Boolean] true if the playlist is in RAM
95
+ def in_ram?
96
+ Spotify.playlist_is_in_ram(session.pointer, pointer)
97
+ end
98
+
99
+ # @param [Boolean] in_ram true if you want to store the playlist in RAM
100
+ def in_ram=(in_ram)
101
+ Spotify.playlist_set_in_ram(session.pointer, pointer, !! in_ram)
102
+ end
103
+
104
+ # @return [Boolean] true if playlist is available offline (fully synced)
105
+ def available_offline?
106
+ offline_status == :yes
107
+ end
108
+
109
+ # @return [Boolean] true if playlist is currently syncing
110
+ def syncing?
111
+ offline_status == :downloading
112
+ end
113
+
114
+ # @return [Boolean] true if playlist is queued for offline syncing
115
+ def waiting?
116
+ offline_status == :waiting
117
+ end
118
+
119
+ # @return [Boolean] true if playlist is requested to be available offline
120
+ def offline_mode?
121
+ offline_status != :no
122
+ end
123
+
124
+ # @return [Symbol] one of :no, :yes, :downloading, :waiting
125
+ def offline_status
126
+ Spotify.playlist_get_offline_status(session.pointer, pointer)
127
+ end
128
+
129
+ # @param [Boolean] available_offline true if you want this playlist available offline
130
+ def offline_mode=(available_offline)
131
+ Spotify.playlist_set_offline_mode(session.pointer, pointer, !! available_offline)
132
+ end
133
+
134
+ # @return [String]
135
+ def name
136
+ Spotify.playlist_name(pointer)
137
+ end
138
+
139
+ # @note The name must not consist of only spaces and it must be shorter than 256 characters.
140
+ # @param [#to_s] name new name for playlist
141
+ # @raise [Error] if name could not be changed
142
+ def name=(name)
143
+ name = name.to_s.encode('UTF-8')
144
+
145
+ unless name.length < 256
146
+ raise ArgumentError, "name must be shorter than 256 characters (UTF-8)"
147
+ end
148
+
149
+ unless name =~ /[^ ]/u
150
+ raise ArgumentError, "name must not consist of only spaces"
151
+ end unless name.empty?
152
+
153
+ Error.maybe_raise(Spotify.playlist_rename(pointer, name))
154
+ end
155
+
156
+ # @return [User, nil]
157
+ def owner
158
+ user = Spotify.playlist_owner!(pointer)
159
+ User.new(user) unless user.null?
160
+ end
161
+
162
+ # @return [String]
163
+ def description
164
+ Spotify.playlist_get_description(pointer)
165
+ end
166
+
167
+ # @return [Image, nil]
168
+ def image
169
+ buffer = FFI::Buffer.alloc_out(20)
170
+ if Spotify.playlist_get_image(pointer, buffer)
171
+ Image.new buffer.read_bytes(20)
172
+ end
173
+ end
174
+
175
+ # @note this list might be shorter than {#total_subscribers}, as
176
+ # libspotify does not store more than 500 subscriber names
177
+ # @return [Array<String>] list of canonical usernames
178
+ def subscribers
179
+ ptr = Spotify.playlist_subscribers(pointer)
180
+ struct = Spotify::Subscribers.new(ptr)
181
+
182
+ if struct[:count].zero?
183
+ []
184
+ else
185
+ struct[:subscribers].map(&:read_string)
186
+ end
187
+ ensure
188
+ Spotify.playlist_subscribers_free(ptr)
189
+ end
190
+
191
+ # @return [Integer] total number of subscribers.
192
+ def total_subscribers
193
+ Spotify.playlist_num_subscribers(pointer)
194
+ end
195
+
196
+ # Ask libspotify to update subscriber information
197
+ #
198
+ # @return [Playlist]
199
+ def update_subscribers
200
+ Spotify.playlist_update_subscribers(session.pointer, pointer)
201
+ end
202
+
203
+ # @note only applicable if {#offline_status} is `:downloading`
204
+ # @return [Integer] percentage done of playlist offline sync
205
+ def sync_progress
206
+ Spotify.playlist_get_offline_download_completed(session.pointer, pointer)
207
+ end
208
+
209
+ # @param [Boolean] autolink_tracks if you want unplayable tracks to be linked to playable tracks (if possible)
210
+ def autolink_tracks=(autolink_tracks)
211
+ Spotify.playlist_set_autolink_tracks(pointer, !! autolink_tracks)
212
+ end
213
+
214
+ # @note Will be 0 unless {#loaded?}.
215
+ # @return [Integer] number of tracks in playlist
216
+ def size
217
+ Spotify.playlist_num_tracks(pointer)
218
+ end
219
+
220
+ # @example retrieve track at index 3
221
+ # track = playlist.tracks[3]
222
+ # puts track.name
223
+ #
224
+ # @return [Enumerable<Playlist::Track>] a list of playlist tracks.
225
+ def tracks
226
+ Enumerator.new(size) { |i| Playlist::Track.new(pointer, i) }
227
+ end
228
+
229
+ # Set seen status of the Playlist::Track at the given index.
230
+ #
231
+ # @see #tracks
232
+ # @raise [Error] if the operation could not be completed
233
+ # @param [Integer] index
234
+ # @param [Boolean] seen true if the track is now seen
235
+ # @return [Playlist::Track] track at the given index
236
+ def seen(index, seen)
237
+ error = Spotify.playlist_track_set_seen(pointer, index, !! seen)
238
+ Error.maybe_raise(error)
239
+ tracks[index]
240
+ end
241
+
242
+ # Add a list of tracks to the playlist starting at given position.
243
+ #
244
+ # @param [Integer] index starting index to add tracks from (between 0..{#size})
245
+ # @param [Track, Array<Track>] tracks
246
+ # @return [Playlist]
247
+ # @raise [Hallon::Error] if the operation failed
248
+ def insert(index = size, tracks)
249
+ tracks = Array(tracks).map(&:pointer)
250
+ tracks_ary = FFI::MemoryPointer.new(:pointer, tracks.size)
251
+ tracks_ary.write_array_of_pointer(tracks)
252
+
253
+ tap do
254
+ error = Spotify.playlist_add_tracks(pointer, tracks_ary, tracks.size, index, session.pointer)
255
+ Error.maybe_raise(error)
256
+ end
257
+ end
258
+
259
+ # Remove tracks at given indices.
260
+ #
261
+ # @param [Integer, ...] indices
262
+ # @return [Playlist]
263
+ # @raise [Error] if the operation failed
264
+ def remove(*indices)
265
+ indices_ary = FFI::MemoryPointer.new(:int, indices.size)
266
+ indices_ary.write_array_of_int(indices)
267
+
268
+ tap do
269
+ error = Spotify.playlist_remove_tracks(pointer, indices_ary, indices.size)
270
+ Error.maybe_raise(error)
271
+ end
272
+ end
273
+
274
+ # Move tracks at given indices to given index.
275
+ #
276
+ # @param [Integer] destination index to move tracks to
277
+ # @param [Integer, Array<Integer>] indices
278
+ # @return [Playlist]
279
+ # @raise [Error] if the operation failed
280
+ def move(destination, indices)
281
+ indices = Array(indices)
282
+ indices_ary = FFI::MemoryPointer.new(:int, indices.size)
283
+ indices_ary.write_array_of_int(indices)
284
+
285
+ tap do
286
+ error = Spotify.playlist_reorder_tracks(pointer, indices_ary, indices.size, destination)
287
+ Error.maybe_raise(error)
288
+ end
289
+ end
290
+ end
291
+ end