hallon 0.8.0 → 0.9.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 (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