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
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ module Hallon
3
+ class PlaylistContainer < Base
4
+ class Folder
5
+ end
6
+
7
+ include Observable
8
+
9
+ # Wrap an existing PlaylistContainer pointer in an object.
10
+ #
11
+ # @param [Spotify::Pointer] pointer
12
+ def initialize(pointer)
13
+ @pointer = to_pointer(pointer, :playlistcontainer)
14
+ end
15
+
16
+ # @return [Boolean] true if the container is loaded
17
+ def loaded?
18
+ Spotify.playlistcontainer_is_loaded(pointer)
19
+ end
20
+
21
+ # @return [User, nil] owner of the container (nil if unknown or no owner)
22
+ def owner
23
+ owner = Spotify.playlistcontainer_owner!(pointer)
24
+ User.new(owner) unless owner.null?
25
+ end
26
+ end
27
+ end
data/lib/hallon/search.rb CHANGED
@@ -12,23 +12,36 @@ module Hallon
12
12
  Spotify.enum_type(:radio_genre).symbols
13
13
  end
14
14
 
15
+ # @return [Hash] default search parameters
16
+ def self.defaults
17
+ @defaults ||= {
18
+ :tracks => 25,
19
+ :albums => 25,
20
+ :artists => 25,
21
+ :tracks_offset => 0,
22
+ :albums_offset => 0,
23
+ :artists_offset => 0
24
+ }
25
+ end
26
+
15
27
  # @param [Range<Integer>] range (from_year..to_year)
16
28
  # @param [Symbol, …] genres
17
29
  # @return [Search] radio search in given period and genres
18
30
  def self.radio(range, *genres)
19
31
  from_year, to_year = range.begin, range.end
20
32
  genres = genres.reduce(0) do |mask, genre|
21
- mask | (Spotify.enum_value(genre) || 0)
33
+ mask | Spotify.enum_value!(genre, "genre")
22
34
  end
23
35
 
24
36
  search = allocate
25
37
  search.instance_eval do
26
38
  @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)
39
+ @pointer = Spotify.radio_search_create!(session.pointer, from_year, to_year, genres, @callback, nil)
29
40
 
30
- self
41
+ raise FFI::NullPointerError, "radio search failed" if @pointer.null?
31
42
  end
43
+
44
+ search
32
45
  end
33
46
 
34
47
  # Construct a new search with given query.
@@ -43,86 +56,80 @@ module Hallon
43
56
  # @option options [#to_i] :artists_offset (0) offset of artists in search result
44
57
  # @see http://developer.spotify.com/en/libspotify/docs/group__search.html#gacf0b5e902e27d46ef8b1f40e332766df
45
58
  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
-
59
+ o = Search.defaults.merge(options)
55
60
  @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)
61
+ @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)
62
+
63
+ raise FFI::NullPointerError, "search for “#{query}” failed" if @pointer.null?
58
64
  end
59
65
 
60
- # @return [Boolean] true if the search has been fully loaded
66
+ # @return [Boolean] true if the search has been fully loaded.
61
67
  def loaded?
62
- Spotify.search_is_loaded(@pointer)
68
+ Spotify.search_is_loaded(pointer)
63
69
  end
64
70
 
65
- # @return [Symbol] error status
66
- def error
67
- Spotify.search_error(@pointer)
71
+ # @see Error.explain
72
+ # @return [Symbol] search error status.
73
+ def status
74
+ Spotify.search_error(pointer)
68
75
  end
69
76
 
70
- # @return [String] search query this search was created with
77
+ # @return [String] search query this search was created with.
71
78
  def query
72
- Spotify.search_query(@pointer)
79
+ Spotify.search_query(pointer)
73
80
  end
74
81
 
75
- # @return [String] “did you mean?” suggestion for current search
82
+ # @return [String] “did you mean?” suggestion for current search.
76
83
  def did_you_mean
77
- Spotify.search_did_you_mean(@pointer)
84
+ Spotify.search_did_you_mean(pointer)
78
85
  end
79
86
 
80
- # @return [Enumerator<Track>] enumerate over all tracks in the search result
87
+ # @return [Enumerator<Track>] list of all tracks in the search result.
81
88
  def tracks
82
- size = Spotify.search_num_tracks(@pointer)
89
+ size = Spotify.search_num_tracks(pointer)
83
90
  Enumerator.new(size) do |i|
84
- track = Spotify.search_track(@pointer, i)
85
- Track.new(track) unless track.null?
91
+ track = Spotify.search_track!(pointer, i)
92
+ Track.new(track)
86
93
  end
87
94
  end
88
95
 
89
- # @return [Integer] total tracks available for this search query
96
+ # @return [Integer] total tracks available for this search query.
90
97
  def total_tracks
91
- Spotify.search_total_tracks(@pointer)
98
+ Spotify.search_total_tracks(pointer)
92
99
  end
93
100
 
94
- # @return [Enumerator<Album>] enumerate over all albums in the search result
101
+ # @return [Enumerator<Album>] list of all albums in the search result.
95
102
  def albums
96
- size = Spotify.search_num_albums(@pointer)
103
+ size = Spotify.search_num_albums(pointer)
97
104
  Enumerator.new(size) do |i|
98
- album = Spotify.search_album(@pointer, i)
99
- Album.new(album) unless album.null?
105
+ album = Spotify.search_album!(pointer, i)
106
+ Album.new(album)
100
107
  end
101
108
  end
102
109
 
103
- # @return [Integer] total tracks available for this search query
110
+ # @return [Integer] total tracks available for this search query.
104
111
  def total_albums
105
- Spotify.search_total_albums(@pointer)
112
+ Spotify.search_total_albums(pointer)
106
113
  end
107
114
 
108
- # @return [Enumerator<Artist>] enumerate over all artists in the search result
115
+ # @return [Enumerator<Artist>] list of all artists in the search result.
109
116
  def artists
110
- size = Spotify.search_num_artists(@pointer)
117
+ size = Spotify.search_num_artists(pointer)
111
118
  Enumerator.new(size) do |i|
112
- artist = Spotify.search_artist(@pointer, i)
113
- Artist.new(artist) unless artist.null?
119
+ artist = Spotify.search_artist!(pointer, i)
120
+ Artist.new(artist)
114
121
  end
115
122
  end
116
123
 
117
- # @return [Integer] total tracks available for this search query
124
+ # @return [Integer] total tracks available for this search query.
118
125
  def total_artists
119
- Spotify.search_total_artists(@pointer)
126
+ Spotify.search_total_artists(pointer)
120
127
  end
121
128
 
122
- # @return [Link] link for this search query
129
+ # @return [Link] link for this search query.
123
130
  def to_link
124
- pointer = Spotify.link_create_from_search(@pointer)
125
- Link.new(pointer) unless pointer.null?
131
+ link = Spotify.link_create_from_search!(pointer)
132
+ Link.new(link) unless link.null?
126
133
  end
127
134
  end
128
135
  end
@@ -37,30 +37,38 @@ module Hallon
37
37
  # instance at a later time, you can use {instance}.
38
38
  #
39
39
  # @see Session.instance
40
- # @see Session#initialize
41
40
  #
42
41
  # @param (see Session#initialize)
42
+ # @option (see Session#initialize)
43
+ # @yield (see Session#initialize)
44
+ # @raise (see Session#initialize)
45
+ # @see (see Session#initialize)
43
46
  # @return [Session]
44
- def Session.initialize(*args, &block)
47
+ def Session.initialize(appkey, options = {}, &block)
45
48
  raise "Session has already been initialized" if @__instance__
46
- @__instance__ = new(*args, &block)
49
+ @__instance__ = new(appkey, options, &block)
47
50
  end
48
51
 
49
52
  # Returns the previously initialized Session.
50
53
  #
51
- # @see Session.instance
54
+ # @see Session.initialize
52
55
  #
53
56
  # @return [Session]
54
57
  def Session.instance
55
58
  @__instance__ or raise "Session has not been initialized"
56
59
  end
57
60
 
61
+ # @return [Boolean] true if a Session instance exists.
62
+ def Session.instance?
63
+ !! @__instance__
64
+ end
65
+
58
66
  # @return [Array<Symbol>] list of available connection types.
59
67
  def self.connection_types
60
68
  Spotify.enum_type(:connection_type).symbols
61
69
  end
62
70
 
63
- # @return [Array<Symbol>] list of available connection rules
71
+ # @return [Array<Symbol>] list of available connection rules.
64
72
  def self.connection_rules
65
73
  Spotify.enum_type(:connection_rules).symbols
66
74
  end
@@ -69,13 +77,15 @@ module Hallon
69
77
  #
70
78
  # @param [#to_s] appkey
71
79
  # @param [Hash] options
72
- # @option options [String] :user_agent ("Hallon") User-Agent to use (length < 256)
80
+ # @option options [String] :user_agent ("Hallon") User-Agent to use (length < `256`)
73
81
  # @option options [String] :settings_path ("tmp") where to save settings and user-specific cache
74
- # @option options [String] :cache_path ("") where to save cache files (set to "" to disable)
82
+ # @option options [String] :cache_path ("") where to save cache files (`""` to disable)
83
+ # @option options [String] :tracefile (nil) path to libspotify API tracefile (`nil` to disable)
84
+ # @option options [String] :device_id (nil) device ID for offline synchronization (`nil` to disable)
75
85
  # @option options [Bool] :load_playlists (true) load playlists into RAM on startup
76
86
  # @option options [Bool] :compress_playlists (true) compress local copies of playlists
77
87
  # @option options [Bool] :cache_playlist_metadata (true) cache metadata for playlists locally
78
- # @yield allows you to define handlers for events (see {Hallon::Base#on})
88
+ # @yield allows you to define handlers for events (see {Observable#on})
79
89
  # @raise [ArgumentError] if `options[:user_agent]` is more than 256 characters long
80
90
  # @raise [Hallon::Error] if `sp_session_create` fails
81
91
  # @see http://developer.spotify.com/en/libspotify/docs/structsp__session__config.html
@@ -86,7 +96,9 @@ module Hallon
86
96
  :cache_path => "",
87
97
  :load_playlists => true,
88
98
  :compress_playlists => true,
89
- :cache_playlist_metadata => true
99
+ :cache_playlist_metadata => true,
100
+ :device_id => nil,
101
+ :tracefile => nil,
90
102
  }.merge(options)
91
103
 
92
104
  if @options[:user_agent].bytesize > 255
@@ -94,7 +106,7 @@ module Hallon
94
106
  end
95
107
 
96
108
  # Set configuration, as well as callbacks
97
- config = Spotify::SessionConfig.new
109
+ config = Spotify::SessionConfig.new
98
110
  config[:api_version] = Hallon::API_VERSION
99
111
  config.application_key = appkey
100
112
  @options.each { |(key, value)| config.send(:"#{key}=", value) }
@@ -112,12 +124,21 @@ module Hallon
112
124
  end
113
125
  end
114
126
 
127
+ # PlaylistContainer for the currently logged in session.
128
+ #
129
+ # @note returns nil if the session is not logged in.
130
+ # @return [PlaylistContainer, nil]
131
+ def container
132
+ container = Spotify.session_playlistcontainer!(pointer)
133
+ PlaylistContainer.new(container) unless container.null?
134
+ end
135
+
115
136
  # Process pending Spotify events (might fire callbacks).
116
137
  #
117
- # @return [Fixnum] minimum time until it should be called again
138
+ # @return [Fixnum] time (in milliseconds) until it should be called again
118
139
  def process_events
119
140
  FFI::MemoryPointer.new(:int) do |p|
120
- Spotify.session_process_events(@pointer, p)
141
+ Spotify.session_process_events(pointer, p)
121
142
  return p.read_int
122
143
  end
123
144
  end
@@ -138,8 +159,8 @@ module Hallon
138
159
 
139
160
  loop do
140
161
  begin
141
- process_events
142
- params = Timeout::timeout(0.25) { channel.pop }
162
+ timeout = [process_events.fdiv(1000), 5].min # scope to five seconds
163
+ params = Timeout::timeout(timeout) { channel.pop }
143
164
  redo if params == :notify
144
165
  rescue Timeout::Error
145
166
  params = :timeout
@@ -158,25 +179,57 @@ module Hallon
158
179
  # @param [String] username
159
180
  # @param [String] password
160
181
  # @param [Boolean] remember_me have libspotify remember credentials for {#relogin}
161
- # @return [self]
182
+ # @return [Session]
183
+ # @see login!
162
184
  def login(username, password, remember_me = false)
163
- tap { Spotify.session_login(@pointer, username, password, @remembered = remember_me) }
185
+ tap { Spotify.session_login(pointer, username, password, remember_me) }
164
186
  end
165
187
 
166
188
  # Login the remembered user (see {#login}).
167
189
  #
168
190
  # @raise [Hallon::Error] if no credentials are stored in libspotify
191
+ # @see #relogin!
169
192
  def relogin
170
- Error.maybe_raise Spotify.session_relogin(@pointer)
193
+ Error.maybe_raise Spotify.session_relogin(pointer)
171
194
  end
172
195
 
173
- # Username of the user stored in libspotify-remembered credentials.
196
+ # Log in to Spotify using the given credentials.
174
197
  #
175
- # @return [String]
198
+ # @note This function will not return until you’ve either logged in successfully,
199
+ # or until an error is raised.
200
+ # @param (see #login)
201
+ # @return [Session]
202
+ # @raise [Error] if failed to log in
203
+ # @see #login
204
+ def login!(username, password, remember_me = false)
205
+ login(username, password, remember_me)
206
+ tap { wait_until_logged_in }
207
+ end
208
+
209
+ # Log in the remembered user.
210
+ #
211
+ # @note This method will not return until you’ve either logged in successfully
212
+ # or until an error is raised.
213
+ # @return [Session]
214
+ # @raise [Error] if failed to log in
215
+ # @see #relogin
216
+ def relogin!
217
+ tap { relogin; wait_until_logged_in }
218
+ end
219
+
220
+ # Log out the current user.
221
+ #
222
+ # @note This method will not return until you’ve logged out successfully.
223
+ # @return [Session]
224
+ def logout!
225
+ tap { logout; wait_for(:logged_out) { logged_out? } }
226
+ end
227
+
228
+ # @return [String] username of the user stored in libspotify-remembered credentials.
176
229
  def remembered_user
177
- bufflen = Spotify.session_remembered_user(@pointer, nil, 0)
230
+ bufflen = Spotify.session_remembered_user(pointer, nil, 0)
178
231
  FFI::Buffer.alloc_out(bufflen + 1) do |b|
179
- Spotify.session_remembered_user(@pointer, b, b.size)
232
+ Spotify.session_remembered_user(pointer, b, b.size)
180
233
  return b.get_string(0)
181
234
  end if bufflen > 0
182
235
  end
@@ -186,35 +239,25 @@ module Hallon
186
239
  # @note If no credentials are stored nothing’ll happen.
187
240
  # @return [self]
188
241
  def forget_me!
189
- tap { Spotify.session_forget_me(@pointer) }
242
+ tap { Spotify.session_forget_me(pointer) }
190
243
  end
191
244
 
192
245
  # Logs out of Spotify. Does nothing if not logged in.
193
246
  #
194
247
  # @return [self]
195
248
  def logout
196
- tap { Spotify.session_logout(@pointer) if logged_in? }
249
+ tap { Spotify.session_logout(pointer) if logged_in? }
197
250
  end
198
251
 
199
- # Retrieve the currently logged in {User}.
200
- #
201
- # @return [User]
252
+ # @return [User] the User currently logged in.
202
253
  def user
203
- User.new Spotify.session_user(@pointer)
204
- end
205
-
206
- # Retrieve the relation type between logged in {User} and `user`.
207
- #
208
- # @return [Symbol] :unknown, :none, :unidirectional or :bidirectional
209
- def relation_type?(user)
210
- Spotify.user_relation_type(@pointer, user.pointer)
254
+ user = Spotify.session_user!(pointer)
255
+ User.new(user) unless user.null?
211
256
  end
212
257
 
213
- # Retrieve current connection status.
214
- #
215
- # @return [Symbol]
258
+ # @return [Symbol] current connection status.
216
259
  def status
217
- Spotify.session_connectionstate(@pointer)
260
+ Spotify.session_connectionstate(pointer)
218
261
  end
219
262
 
220
263
  # Set session cache size in megabytes.
@@ -222,12 +265,12 @@ module Hallon
222
265
  # @param [Integer]
223
266
  # @return [Integer]
224
267
  def cache_size=(size)
225
- Spotify.session_set_cache_size(@pointer, @cache_size = size)
268
+ Spotify.session_set_cache_size(pointer, @cache_size = size)
226
269
  end
227
270
 
228
- # @return [String] Currently logged in users’ country.
271
+ # @return [String] currently logged in users’ country.
229
272
  def country
230
- coded = Spotify.session_user_country(@pointer)
273
+ coded = Spotify.session_user_country(pointer)
231
274
  country = ((coded >> 8) & 0xFF).chr
232
275
  country << (coded & 0xFF).chr
233
276
  end
@@ -256,35 +299,16 @@ module Hallon
256
299
  tap { tracks_starred(tracks, false) }
257
300
  end
258
301
 
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
302
  # Set the connection rules for this session.
279
303
  #
280
304
  # @param [Symbol, …] connection_rules
281
305
  # @see Session.connection_rules
282
306
  def connection_rules=(connection_rules)
283
307
  rules = Array(connection_rules).reduce(0) do |mask, rule|
284
- mask | (Spotify.enum_value(rule) || 0)
308
+ mask | Spotify.enum_value!(rule, "connection rule")
285
309
  end
286
310
 
287
- Spotify.session_set_connection_rules(@pointer, rules)
311
+ Spotify.session_set_connection_rules(pointer, rules)
288
312
  end
289
313
 
290
314
  # Set the connection type for this session.
@@ -292,14 +316,14 @@ module Hallon
292
316
  # @param [Symbol] connection_type
293
317
  # @see Session.connection_types
294
318
  def connection_type=(connection_type)
295
- Spotify.session_set_connection_type(@pointer, connection_type)
319
+ Spotify.session_set_connection_type(pointer, connection_type)
296
320
  end
297
321
 
298
322
  # Remaining time left you can stay offline before needing to relogin.
299
323
  #
300
324
  # @return [Integer] offline time left in seconds
301
325
  def offline_time_left
302
- Spotify.offline_time_left(@pointer)
326
+ Spotify.offline_time_left(pointer)
303
327
  end
304
328
 
305
329
  # Offline synchronization status.
@@ -308,23 +332,19 @@ module Hallon
308
332
  # @see http://developer.spotify.com/en/libspotify/docs/structsp__offline__sync__status.html
309
333
  def offline_sync_status
310
334
  struct = Spotify::OfflineSyncStatus.new
311
- if Spotify.offline_sync_get_status(@pointer, struct.pointer)
335
+ if Spotify.offline_sync_get_status(pointer, struct.pointer)
312
336
  Hash[struct.members.zip(struct.values)]
313
337
  end
314
338
  end
315
339
 
316
- # Number of playlists marked for offline sync.
317
- #
318
- # @return [Integer]
340
+ # @return [Integer] number of playlists marked for offline sync.
319
341
  def offline_playlists_count
320
- Spotify.offline_num_playlists(@pointer)
342
+ Spotify.offline_num_playlists(pointer)
321
343
  end
322
344
 
323
- # Number of offline tracks left to sync for offline mode.
324
- #
325
- # @return [Integer]
345
+ # @return [Integer] number of offline tracks left to sync for offline mode.
326
346
  def offline_tracks_to_sync
327
- Spotify.offline_tracks_to_sync(@pointer)
347
+ Spotify.offline_tracks_to_sync(pointer)
328
348
  end
329
349
 
330
350
  # Set preferred offline bitrate.
@@ -337,36 +357,48 @@ module Hallon
337
357
  # @see Player.bitrates
338
358
  def offline_bitrate=(bitrate)
339
359
  bitrate, resync = Array(bitrate)
340
- Spotify.session_preferred_offline_bitrate(@pointer, bitrate, !! resync)
360
+ Spotify.session_preferred_offline_bitrate(pointer, bitrate, !! resync)
361
+ end
362
+
363
+ # @note Returns nil when no user is logged in.
364
+ # @return [Playlist, nil] currently logged in user’s starred playlist.
365
+ def starred
366
+ playlist = Spotify.session_starred_create!(pointer)
367
+ Playlist.new(playlist) unless playlist.null?
368
+ end
369
+
370
+ # @note Returns nil when no user is logged in.
371
+ # @return [Playlist, nil] currently logged in user’s inbox playlist.
372
+ def inbox
373
+ playlist = Spotify.session_inbox_create!(pointer)
374
+ Playlist.new(playlist) unless playlist.null?
341
375
  end
342
376
 
343
- # True if currently logged in.
344
377
  # @see #status
378
+ # @return [Boolean] true if logged in.
345
379
  def logged_in?
346
380
  status == :logged_in
347
381
  end
348
382
 
349
- # True if logged out.
350
383
  # @see #status
384
+ # @return [Boolean] true if logged out.
351
385
  def logged_out?
352
386
  status == :logged_out
353
387
  end
354
388
 
355
- # True if session has been disconnected.
356
389
  # @see #status
390
+ # @return [Boolean] true if session has been disconnected.
357
391
  def disconnected?
358
392
  status == :disconnected
359
393
  end
360
394
 
361
- # True if offline.
362
395
  # @see #status
396
+ # @return [Boolean] true if offline.
363
397
  def offline?
364
398
  status == :offline
365
399
  end
366
400
 
367
- # String representation of the Session.
368
- #
369
- # @return [String]
401
+ # @return [String] string representation of the Session.
370
402
  def to_s
371
403
  "<#{self.class.name}:0x#{object_id.to_s(16)} status=#{status} @options=#{options.inspect}>"
372
404
  end
@@ -382,5 +414,21 @@ module Hallon
382
414
  Spotify.track_set_starred(pointer, ptr, tracks.size, starred)
383
415
  end
384
416
  end
417
+
418
+ # Waits until we’re either logged in or a failure occurs.
419
+ #
420
+ # @note You must call {#login} or {#relogin} before you call this method, or
421
+ # it will hang forever!
422
+ # @see login!
423
+ # @see relogin!
424
+ def wait_until_logged_in
425
+ # if the user does not have premium, libspotify will still fire logged_in as :ok,
426
+ # but a few moments later it fires connection_error; waiting for both and checking
427
+ # for errors on both hopefully circumvents this!
428
+ wait_for(:logged_in, :connection_error) do |_, error|
429
+ Error.maybe_raise(error)
430
+ session.logged_in?
431
+ end
432
+ end
385
433
  end
386
434
  end