hallon 0.4.0 → 0.8.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 (52) hide show
  1. data/.gitmodules +3 -0
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG +30 -6
  4. data/README.markdown +7 -7
  5. data/Rakefile +70 -16
  6. data/examples/logging_in.rb +3 -3
  7. data/examples/printing_link_information.rb +1 -1
  8. data/examples/show_published_playlists_of_user.rb +92 -0
  9. data/hallon.gemspec +7 -4
  10. data/lib/hallon.rb +16 -4
  11. data/lib/hallon/album.rb +16 -6
  12. data/lib/hallon/album_browse.rb +78 -0
  13. data/lib/hallon/artist.rb +59 -0
  14. data/lib/hallon/artist_browse.rb +89 -0
  15. data/lib/hallon/base.rb +7 -0
  16. data/lib/hallon/enumerator.rb +64 -0
  17. data/lib/hallon/error.rb +8 -6
  18. data/lib/hallon/ext/spotify.rb +3 -3
  19. data/lib/hallon/image.rb +25 -12
  20. data/lib/hallon/link.rb +4 -4
  21. data/lib/hallon/linkable.rb +4 -2
  22. data/lib/hallon/observable.rb +1 -4
  23. data/lib/hallon/player.rb +130 -0
  24. data/lib/hallon/search.rb +128 -0
  25. data/lib/hallon/session.rb +226 -25
  26. data/lib/hallon/toplist.rb +83 -0
  27. data/lib/hallon/track.rb +62 -7
  28. data/lib/hallon/user.rb +6 -6
  29. data/lib/hallon/version.rb +1 -1
  30. data/spec/hallon/album_browse_spec.rb +20 -0
  31. data/spec/hallon/album_spec.rb +12 -7
  32. data/spec/hallon/artist_browse_spec.rb +29 -0
  33. data/spec/hallon/artist_spec.rb +32 -0
  34. data/spec/hallon/enumerator_spec.rb +106 -0
  35. data/spec/hallon/error_spec.rb +10 -0
  36. data/spec/hallon/hallon_spec.rb +5 -1
  37. data/spec/hallon/image_spec.rb +39 -25
  38. data/spec/hallon/linkable_spec.rb +12 -4
  39. data/spec/hallon/observable_spec.rb +5 -0
  40. data/spec/hallon/player_spec.rb +73 -0
  41. data/spec/hallon/search_spec.rb +80 -0
  42. data/spec/hallon/session_spec.rb +187 -6
  43. data/spec/hallon/toplist_spec.rb +40 -0
  44. data/spec/hallon/track_spec.rb +43 -8
  45. data/spec/mockspotify.rb +47 -0
  46. data/spec/mockspotify/.gitignore +5 -0
  47. data/spec/mockspotify/extconf.rb +5 -0
  48. data/spec/mockspotify/mockspotify_spec.rb +41 -0
  49. data/spec/spec_helper.rb +20 -0
  50. data/spec/support/common_objects.rb +84 -7
  51. metadata +72 -20
  52. data/lib/hallon/ext/object.rb +0 -16
@@ -0,0 +1,130 @@
1
+ module Hallon
2
+ # A wrapper around Session for playing, stopping and otherwise
3
+ # controlling the playback features of libspotify.
4
+ #
5
+ # @note This is very much a work in progress. Given Session still
6
+ # takes care of all callbacks, and the callbacks themselves
7
+ # must still be handled by means of Ruby FFI.
8
+ # @see Session
9
+ class Player
10
+ include Observable
11
+
12
+ # @return [Array<Symbol>] a list of available playback bitrates.
13
+ def self.bitrates
14
+ Spotify.enum_type(:bitrate).symbols.sort_by do |sym|
15
+ # sort by bitrate quality
16
+ sym.to_s.to_i
17
+ end
18
+ end
19
+
20
+ # Constructs a Player, given a Session.
21
+ #
22
+ # @example
23
+ # Hallon::Player.new(session) do
24
+ # on(:music_delivery) do |*frames|
25
+ # end
26
+ #
27
+ # on(:start_playback) do
28
+ # end
29
+ #
30
+ # on(:stop_playback) do
31
+ # end
32
+ #
33
+ # on(:play_token_lost) do
34
+ # end
35
+ #
36
+ # on(:end_of_track) do
37
+ # end
38
+ #
39
+ # on(:streaming_error) do |error|
40
+ # end
41
+ #
42
+ # on(:buffer_size?) do
43
+ # # return the pair of [samples, dropouts] of your audiobuffer
44
+ # end
45
+ # end
46
+ #
47
+ # @param [Session] session
48
+ # @yield instance_evals itself, allowing you to define callbacks using `on`
49
+ def initialize(session, &block)
50
+ instance_eval(&block) if block_given?
51
+
52
+ @session = session
53
+ @pointer = @session.pointer
54
+
55
+ %w[start_playback stop_playback play_token_lost end_of_track streaming_error].each do |cb|
56
+ @session.on(cb) { |*args| trigger(cb, *args) }
57
+ end
58
+
59
+ @session.on(:audio_buffer_stats) do |stats_ptr|
60
+ stats = Spotify::AudioBufferStats.new(stats_ptr)
61
+ samples, dropouts = trigger(:buffer_size?)
62
+ stats[:samples] = samples || 0
63
+ stats[:dropouts] = dropouts || 0
64
+ end
65
+
66
+ @session.on(:music_delivery) do |format, frames, num_frames|
67
+ trigger(:music_delivery, format, frames, num_frames)
68
+ num_frames # assume we consume all data
69
+ end
70
+ end
71
+
72
+ # Set preferred playback bitrate.
73
+ #
74
+ # @param [Symbol] bitrate one of :96k, :160k, :320k
75
+ # @return [Symbol]
76
+ def bitrate=(bitrate)
77
+ Spotify.session_preferred_bitrate(@pointer, bitrate)
78
+ end
79
+
80
+ # Loads a Track for playing.
81
+ #
82
+ # @param [Track] track
83
+ # @return [Player]
84
+ # @raise [Error] if the track could not be loaded
85
+ def load(track)
86
+ error = Spotify.session_player_load(@pointer, track.pointer)
87
+ tap { Error.maybe_raise(error) }
88
+ end
89
+
90
+ # Prepares a Track for playing, without loading it.
91
+ #
92
+ # @note You can only prefetch if caching is on.
93
+ # @param [Track] track
94
+ # @return [Player]
95
+ def prefetch(track)
96
+ error = Spotify.session_player_prefetch(@pointer, track.pointer)
97
+ tap { Error.maybe_raise(error) }
98
+ end
99
+
100
+ # Starts playing a Track by feeding data to your application.
101
+ #
102
+ # @return [Player]
103
+ def play(track = nil)
104
+ load(track) unless track.nil?
105
+ tap { Spotify.session_player_play(@pointer, true) }
106
+ end
107
+
108
+ # Pause playback of a Track.
109
+ #
110
+ # @return [Player]
111
+ def pause
112
+ tap { Spotify.session_player_play(@pointer, false) }
113
+ end
114
+
115
+ # Stop playing current track and unload it.
116
+ #
117
+ # @return [Player]
118
+ def stop
119
+ tap { Spotify.session_player_unload(@pointer) }
120
+ end
121
+
122
+ # Seek to the desired position of the currently loaded Track.
123
+ #
124
+ # @param [Numeric] seconds offset position in seconds
125
+ # @return [Player]
126
+ def seek(seconds)
127
+ tap { Spotify.session_player_seek(@pointer, seconds * 1000) }
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,128 @@
1
+ # coding: utf-8
2
+ module Hallon
3
+ # Search allows you to search Spotify for tracks, albums
4
+ # and artists, just like in the client.
5
+ #
6
+ # @see http://developer.spotify.com/en/libspotify/docs/group__search.html
7
+ class Search < Base
8
+ include Observable
9
+
10
+ # @return [Array<Symbol>] a list of radio genres available for search
11
+ def self.genres
12
+ Spotify.enum_type(:radio_genre).symbols
13
+ end
14
+
15
+ # @param [Range<Integer>] range (from_year..to_year)
16
+ # @param [Symbol, …] genres
17
+ # @return [Search] radio search in given period and genres
18
+ def self.radio(range, *genres)
19
+ from_year, to_year = range.begin, range.end
20
+ genres = genres.reduce(0) do |mask, genre|
21
+ mask | (Spotify.enum_value(genre) || 0)
22
+ end
23
+
24
+ search = allocate
25
+ search.instance_eval do
26
+ @callback = proc { search.trigger(:load) }
27
+ pointer = Spotify.radio_search_create(session.pointer, from_year, to_year, genres, @callback, nil)
28
+ @pointer = Spotify::Pointer.new(pointer, :search, false)
29
+
30
+ self
31
+ end
32
+ end
33
+
34
+ # Construct a new search with given query.
35
+ #
36
+ # @param [String] query search query
37
+ # @param [Hash] options additional search options
38
+ # @option options [#to_i] :tracks (25) max number of tracks you want in result
39
+ # @option options [#to_i] :albums (25) max number of albums you want in result
40
+ # @option options [#to_i] :artists (25) max number of artists you want in result
41
+ # @option options [#to_i] :tracks_offset (0) offset of tracks in search result
42
+ # @option options [#to_i] :albums_offset (0) offset of albums in search result
43
+ # @option options [#to_i] :artists_offset (0) offset of artists in search result
44
+ # @see http://developer.spotify.com/en/libspotify/docs/group__search.html#gacf0b5e902e27d46ef8b1f40e332766df
45
+ def initialize(query, options = {})
46
+ o = {
47
+ :tracks => 25,
48
+ :albums => 25,
49
+ :artists => 25,
50
+ :tracks_offset => 0,
51
+ :albums_offset => 0,
52
+ :artists_offset => 0
53
+ }.merge(options)
54
+
55
+ @callback = proc { trigger(:load) }
56
+ pointer = Spotify.search_create(session.pointer, query, o[:tracks_offset].to_i, o[:tracks].to_i, o[:albums_offset].to_i, o[:albums].to_i, o[:artists_offset].to_i, o[:artists].to_i, @callback, nil)
57
+ @pointer = Spotify::Pointer.new(pointer, :search, false)
58
+ end
59
+
60
+ # @return [Boolean] true if the search has been fully loaded
61
+ def loaded?
62
+ Spotify.search_is_loaded(@pointer)
63
+ end
64
+
65
+ # @return [Symbol] error status
66
+ def error
67
+ Spotify.search_error(@pointer)
68
+ end
69
+
70
+ # @return [String] search query this search was created with
71
+ def query
72
+ Spotify.search_query(@pointer)
73
+ end
74
+
75
+ # @return [String] “did you mean?” suggestion for current search
76
+ def did_you_mean
77
+ Spotify.search_did_you_mean(@pointer)
78
+ end
79
+
80
+ # @return [Enumerator<Track>] enumerate over all tracks in the search result
81
+ def tracks
82
+ size = Spotify.search_num_tracks(@pointer)
83
+ Enumerator.new(size) do |i|
84
+ track = Spotify.search_track(@pointer, i)
85
+ Track.new(track) unless track.null?
86
+ end
87
+ end
88
+
89
+ # @return [Integer] total tracks available for this search query
90
+ def total_tracks
91
+ Spotify.search_total_tracks(@pointer)
92
+ end
93
+
94
+ # @return [Enumerator<Album>] enumerate over all albums in the search result
95
+ def albums
96
+ size = Spotify.search_num_albums(@pointer)
97
+ Enumerator.new(size) do |i|
98
+ album = Spotify.search_album(@pointer, i)
99
+ Album.new(album) unless album.null?
100
+ end
101
+ end
102
+
103
+ # @return [Integer] total tracks available for this search query
104
+ def total_albums
105
+ Spotify.search_total_albums(@pointer)
106
+ end
107
+
108
+ # @return [Enumerator<Artist>] enumerate over all artists in the search result
109
+ def artists
110
+ size = Spotify.search_num_artists(@pointer)
111
+ Enumerator.new(size) do |i|
112
+ artist = Spotify.search_artist(@pointer, i)
113
+ Artist.new(artist) unless artist.null?
114
+ end
115
+ end
116
+
117
+ # @return [Integer] total tracks available for this search query
118
+ def total_artists
119
+ Spotify.search_total_artists(@pointer)
120
+ end
121
+
122
+ # @return [Link] link for this search query
123
+ def to_link
124
+ pointer = Spotify.link_create_from_search(@pointer)
125
+ Link.new(pointer) unless pointer.null?
126
+ end
127
+ end
128
+ end
@@ -15,6 +15,15 @@ module Hallon
15
15
  # @return [Hash]
16
16
  attr_reader :options
17
17
 
18
+ # The current session cache size (in megabytes).
19
+ #
20
+ # @note This is not provided by libspotify, and the value is only valid
21
+ # as long as the cache size is only adjusted through {#cache_size=}
22
+ # and not the Spotify FFI interface.
23
+ #
24
+ # @return [Integer]
25
+ attr_reader :cache_size
26
+
18
27
  # libspotify only allows one session per process.
19
28
  include Singleton
20
29
  class << self
@@ -22,16 +31,38 @@ module Hallon
22
31
  end
23
32
 
24
33
  # Session allows you to define your own callbacks.
25
- include Hallon::Observable
34
+ include Observable
26
35
 
27
- # Allows you to create a Spotify session. Subsequent calls to this method
28
- # will return the previous instance, ignoring any passed arguments.
36
+ # Initializes the Spotify session. If you need to access the
37
+ # instance at a later time, you can use {instance}.
29
38
  #
30
- # @param (see Session#initialize)
39
+ # @see Session.instance
31
40
  # @see Session#initialize
41
+ #
42
+ # @param (see Session#initialize)
43
+ # @return [Session]
44
+ def Session.initialize(*args, &block)
45
+ raise "Session has already been initialized" if @__instance__
46
+ @__instance__ = new(*args, &block)
47
+ end
48
+
49
+ # Returns the previously initialized Session.
50
+ #
51
+ # @see Session.instance
52
+ #
32
53
  # @return [Session]
33
- def Session.instance(*args, &block)
34
- @__instance__ ||= new(*args, &block)
54
+ def Session.instance
55
+ @__instance__ or raise "Session has not been initialized"
56
+ end
57
+
58
+ # @return [Array<Symbol>] list of available connection types.
59
+ def self.connection_types
60
+ Spotify.enum_type(:connection_type).symbols
61
+ end
62
+
63
+ # @return [Array<Symbol>] list of available connection rules
64
+ def self.connection_rules
65
+ Spotify.enum_type(:connection_rules).symbols
35
66
  end
36
67
 
37
68
  # Create a new Spotify session.
@@ -49,7 +80,6 @@ module Hallon
49
80
  # @raise [Hallon::Error] if `sp_session_create` fails
50
81
  # @see http://developer.spotify.com/en/libspotify/docs/structsp__session__config.html
51
82
  def initialize(appkey, options = {}, &block)
52
- @appkey = appkey.to_s
53
83
  @options = {
54
84
  :user_agent => "Hallon",
55
85
  :settings_path => "tmp",
@@ -66,15 +96,18 @@ module Hallon
66
96
  # Set configuration, as well as callbacks
67
97
  config = Spotify::SessionConfig.new
68
98
  config[:api_version] = Hallon::API_VERSION
69
- config.application_key = @appkey
99
+ config.application_key = appkey
70
100
  @options.each { |(key, value)| config.send(:"#{key}=", value) }
71
101
  config[:callbacks] = Spotify::SessionCallbacks.new(self, @sp_callbacks = {})
72
102
 
103
+ # Default cache size is 0 (automatic)
104
+ @cache_size = 0
105
+
73
106
  instance_eval(&block) if block_given?
74
107
 
75
108
  # You pass a pointer to the session pointer to libspotify >:)
76
109
  FFI::MemoryPointer.new(:pointer) do |p|
77
- Hallon::Error::maybe_raise Spotify::session_create(config, p)
110
+ Error::maybe_raise Spotify.session_create(config, p)
78
111
  @pointer = p.read_pointer
79
112
  end
80
113
  end
@@ -84,20 +117,22 @@ module Hallon
84
117
  # @return [Fixnum] minimum time until it should be called again
85
118
  def process_events
86
119
  FFI::MemoryPointer.new(:int) do |p|
87
- Spotify::session_process_events(@pointer, p)
120
+ Spotify.session_process_events(@pointer, p)
88
121
  return p.read_int
89
122
  end
90
123
  end
91
124
 
92
125
  # Wait for the given callbacks to fire until the block returns true
93
126
  #
127
+ # @note Given block will be called once instantly without parameters.
128
+ # @note If no events happen for 0.25 seconds, the given block will be called
129
+ # with `:timeout` as parameter.
94
130
  # @param [Symbol, ...] *events list of events to wait for
95
131
  # @yield [Symbol, *args] name of the callback that fired, and its’ arguments
96
132
  # @return [Hash<Event, Arguments>]
97
- def process_events_on(*events, &block)
98
- channel = SizedQueue.new(1)
99
-
100
- protecting_handlers do
133
+ def process_events_on(*events)
134
+ yield or protecting_handlers do
135
+ channel = SizedQueue.new(1)
101
136
  on(*events) { |*args| channel << args }
102
137
  on(:notify_main_thread) { channel << :notify }
103
138
 
@@ -107,10 +142,10 @@ module Hallon
107
142
  params = Timeout::timeout(0.25) { channel.pop }
108
143
  redo if params == :notify
109
144
  rescue Timeout::Error
110
- params = nil
145
+ params = :timeout
111
146
  end
112
147
 
113
- if result = block.call(*params)
148
+ if result = yield(*params)
114
149
  return result
115
150
  end
116
151
  end
@@ -122,39 +157,187 @@ module Hallon
122
157
  #
123
158
  # @param [String] username
124
159
  # @param [String] password
160
+ # @param [Boolean] remember_me have libspotify remember credentials for {#relogin}
161
+ # @return [self]
162
+ def login(username, password, remember_me = false)
163
+ tap { Spotify.session_login(@pointer, username, password, @remembered = remember_me) }
164
+ end
165
+
166
+ # Login the remembered user (see {#login}).
167
+ #
168
+ # @raise [Hallon::Error] if no credentials are stored in libspotify
169
+ def relogin
170
+ Error.maybe_raise Spotify.session_relogin(@pointer)
171
+ end
172
+
173
+ # Username of the user stored in libspotify-remembered credentials.
174
+ #
175
+ # @return [String]
176
+ def remembered_user
177
+ bufflen = Spotify.session_remembered_user(@pointer, nil, 0)
178
+ FFI::Buffer.alloc_out(bufflen + 1) do |b|
179
+ Spotify.session_remembered_user(@pointer, b, b.size)
180
+ return b.get_string(0)
181
+ end if bufflen > 0
182
+ end
183
+
184
+ # Remove stored login credentials in libspotify.
185
+ #
186
+ # @note If no credentials are stored nothing’ll happen.
125
187
  # @return [self]
126
- def login(username, password)
127
- Spotify::session_login(@pointer, username, password)
128
- self
188
+ def forget_me!
189
+ tap { Spotify.session_forget_me(@pointer) }
129
190
  end
130
191
 
131
192
  # Logs out of Spotify. Does nothing if not logged in.
132
193
  #
133
194
  # @return [self]
134
195
  def logout
135
- Spotify::session_logout(@pointer) if logged_in?
136
- self
196
+ tap { Spotify.session_logout(@pointer) if logged_in? }
137
197
  end
138
198
 
139
199
  # Retrieve the currently logged in {User}.
140
200
  #
141
201
  # @return [User]
142
202
  def user
143
- User.new Spotify::session_user(@pointer)
203
+ User.new Spotify.session_user(@pointer)
144
204
  end
145
205
 
146
206
  # Retrieve the relation type between logged in {User} and `user`.
147
207
  #
148
208
  # @return [Symbol] :unknown, :none, :unidirectional or :bidirectional
149
209
  def relation_type?(user)
150
- Spotify::user_relation_type(@pointer, user.pointer)
210
+ Spotify.user_relation_type(@pointer, user.pointer)
151
211
  end
152
212
 
153
213
  # Retrieve current connection status.
154
214
  #
155
215
  # @return [Symbol]
156
216
  def status
157
- Spotify::session_connectionstate(@pointer)
217
+ Spotify.session_connectionstate(@pointer)
218
+ end
219
+
220
+ # Set session cache size in megabytes.
221
+ #
222
+ # @param [Integer]
223
+ # @return [Integer]
224
+ def cache_size=(size)
225
+ Spotify.session_set_cache_size(@pointer, @cache_size = size)
226
+ end
227
+
228
+ # @return [String] Currently logged in users’ country.
229
+ def country
230
+ coded = Spotify.session_user_country(@pointer)
231
+ country = ((coded >> 8) & 0xFF).chr
232
+ country << (coded & 0xFF).chr
233
+ end
234
+
235
+ # Star the given tracks.
236
+ #
237
+ # @example
238
+ # track = Hallon::Track.new("spotify:track:2LFQV2u6wXZmmySCWBkYGu")
239
+ # session.star(track)
240
+ #
241
+ # @param [Track…]
242
+ # @return [Session]
243
+ def star(*tracks)
244
+ tap { tracks_starred(tracks, true) }
245
+ end
246
+
247
+ # Unstar the given tracks.
248
+ #
249
+ # @example
250
+ # track = Hallon::Track.new("spotify:track:2LFQV2u6wXZmmySCWBkYGu")
251
+ # session.unstar(track)
252
+ #
253
+ # @param [Track…]
254
+ # @return [Session]
255
+ def unstar(*tracks)
256
+ tap { tracks_starred(tracks, false) }
257
+ end
258
+
259
+ # @note This will be 0 if not logged in.
260
+ # @note As of current writing, I am unsure if there’s a good way to find out
261
+ # when this enumerator will be populated. No callbacks or other status
262
+ # field can tell you when the current sessions’ friends are available.
263
+ # @return [Enumerator<User>] friends of currently logged in user
264
+ def friends
265
+ size = if logged_in?
266
+ # segfaults unless logged in
267
+ Spotify.session_num_friends(@pointer)
268
+ else
269
+ 0
270
+ end
271
+
272
+ Enumerator.new(size) do |i|
273
+ friend = Spotify.session_friend(@pointer, i)
274
+ User.new(friend)
275
+ end
276
+ end
277
+
278
+ # Set the connection rules for this session.
279
+ #
280
+ # @param [Symbol, …] connection_rules
281
+ # @see Session.connection_rules
282
+ def connection_rules=(connection_rules)
283
+ rules = Array(connection_rules).reduce(0) do |mask, rule|
284
+ mask | (Spotify.enum_value(rule) || 0)
285
+ end
286
+
287
+ Spotify.session_set_connection_rules(@pointer, rules)
288
+ end
289
+
290
+ # Set the connection type for this session.
291
+ #
292
+ # @param [Symbol] connection_type
293
+ # @see Session.connection_types
294
+ def connection_type=(connection_type)
295
+ Spotify.session_set_connection_type(@pointer, connection_type)
296
+ end
297
+
298
+ # Remaining time left you can stay offline before needing to relogin.
299
+ #
300
+ # @return [Integer] offline time left in seconds
301
+ def offline_time_left
302
+ Spotify.offline_time_left(@pointer)
303
+ end
304
+
305
+ # Offline synchronization status.
306
+ #
307
+ # @return [Hash, nil] sync status, or nil if not applicable
308
+ # @see http://developer.spotify.com/en/libspotify/docs/structsp__offline__sync__status.html
309
+ def offline_sync_status
310
+ struct = Spotify::OfflineSyncStatus.new
311
+ if Spotify.offline_sync_get_status(@pointer, struct.pointer)
312
+ Hash[struct.members.zip(struct.values)]
313
+ end
314
+ end
315
+
316
+ # Number of playlists marked for offline sync.
317
+ #
318
+ # @return [Integer]
319
+ def offline_playlists_count
320
+ Spotify.offline_num_playlists(@pointer)
321
+ end
322
+
323
+ # Number of offline tracks left to sync for offline mode.
324
+ #
325
+ # @return [Integer]
326
+ def offline_tracks_to_sync
327
+ Spotify.offline_tracks_to_sync(@pointer)
328
+ end
329
+
330
+ # Set preferred offline bitrate.
331
+ #
332
+ # @example
333
+ # session.offline_bitrate = :'96k', true
334
+ #
335
+ # @param [Symbol] bitrate
336
+ # @param [Boolean] resync (default: false)
337
+ # @see Player.bitrates
338
+ def offline_bitrate=(bitrate)
339
+ bitrate, resync = Array(bitrate)
340
+ Spotify.session_preferred_offline_bitrate(@pointer, bitrate, !! resync)
158
341
  end
159
342
 
160
343
  # True if currently logged in.
@@ -175,11 +358,29 @@ module Hallon
175
358
  status == :disconnected
176
359
  end
177
360
 
361
+ # True if offline.
362
+ # @see #status
363
+ def offline?
364
+ status == :offline
365
+ end
366
+
178
367
  # String representation of the Session.
179
368
  #
180
369
  # @return [String]
181
370
  def to_s
182
- "<#{self.class.name}>"
371
+ "<#{self.class.name}:0x#{object_id.to_s(16)} status=#{status} @options=#{options.inspect}>"
183
372
  end
373
+
374
+ private
375
+ # Set starred status of given tracks.
376
+ #
377
+ # @param [Array<Track>] tracks
378
+ # @param [Boolean] starred
379
+ def tracks_starred(tracks, starred)
380
+ FFI::MemoryPointer.new(:pointer, tracks.size) do |ptr|
381
+ ptr.write_array_of_pointer tracks.map(&:pointer)
382
+ Spotify.track_set_starred(pointer, ptr, tracks.size, starred)
383
+ end
384
+ end
184
385
  end
185
386
  end