hallon 0.13.0 → 0.14.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 (44) hide show
  1. data/CHANGELOG.md +43 -1
  2. data/Gemfile +0 -2
  3. data/LICENSE.txt +1 -1
  4. data/README.markdown +94 -44
  5. data/examples/playing_audio.rb +4 -5
  6. data/lib/hallon.rb +20 -0
  7. data/lib/hallon/album.rb +13 -12
  8. data/lib/hallon/album_browse.rb +1 -0
  9. data/lib/hallon/artist.rb +13 -12
  10. data/lib/hallon/artist_browse.rb +1 -0
  11. data/lib/hallon/base.rb +2 -0
  12. data/lib/hallon/image.rb +18 -10
  13. data/lib/hallon/loadable.rb +24 -0
  14. data/lib/hallon/observable.rb +1 -1
  15. data/lib/hallon/observable/playlist.rb +10 -16
  16. data/lib/hallon/observable/playlist_container.rb +12 -6
  17. data/lib/hallon/player.rb +3 -3
  18. data/lib/hallon/playlist.rb +34 -11
  19. data/lib/hallon/playlist_container.rb +10 -4
  20. data/lib/hallon/search.rb +1 -0
  21. data/lib/hallon/session.rb +2 -2
  22. data/lib/hallon/toplist.rb +17 -12
  23. data/lib/hallon/track.rb +1 -0
  24. data/lib/hallon/user.rb +48 -11
  25. data/lib/hallon/version.rb +1 -1
  26. data/spec/hallon/album_browse_spec.rb +2 -0
  27. data/spec/hallon/album_spec.rb +14 -7
  28. data/spec/hallon/artist_browse_spec.rb +2 -0
  29. data/spec/hallon/artist_spec.rb +14 -8
  30. data/spec/hallon/hallon_spec.rb +12 -0
  31. data/spec/hallon/image_spec.rb +18 -9
  32. data/spec/hallon/loadable_spec.rb +46 -0
  33. data/spec/hallon/observable/playlist_spec.rb +11 -5
  34. data/spec/hallon/observable_spec.rb +6 -0
  35. data/spec/hallon/playlist_container_spec.rb +6 -0
  36. data/spec/hallon/playlist_spec.rb +21 -4
  37. data/spec/hallon/search_spec.rb +2 -0
  38. data/spec/hallon/toplist_spec.rb +40 -23
  39. data/spec/hallon/track_spec.rb +2 -0
  40. data/spec/hallon/user_post_spec.rb +75 -0
  41. data/spec/hallon/user_spec.rb +7 -11
  42. data/spec/spec_helper.rb +2 -2
  43. metadata +20 -16
  44. data/examples/audio_driver.rb +0 -55
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+
3
+ module Hallon
4
+ module Loadable
5
+ # @param [Numeric] timeout after this time, if the object is not loaded, an error is raised.
6
+ # @return [self]
7
+ # @raise [Hallon::TimeoutError] after `timeout` seconds if the object does not load.
8
+ def load(timeout = Hallon.load_timeout)
9
+ Timeout.timeout(timeout, Hallon::TimeoutError) do
10
+ until loaded?
11
+ session.process_events
12
+
13
+ if respond_to?(:status)
14
+ Error.maybe_raise(status, :ignore => :is_loading)
15
+ end
16
+
17
+ sleep(0.001)
18
+ end
19
+
20
+ self
21
+ end
22
+ end
23
+ end
24
+ end
@@ -138,7 +138,7 @@ module Hallon
138
138
  # @return whatever the block returns
139
139
  def subscribe_for_callbacks
140
140
  yield(self.class.callbacks).tap do
141
- self.class.subscribe(self, pointer)
141
+ self.class.subscribe(self, pointer) unless pointer.null?
142
142
  end
143
143
  end
144
144
 
@@ -27,7 +27,12 @@ module Hallon::Observable
27
27
  # @yieldparam [Integer] position
28
28
  # @yieldparam [Playlist] self
29
29
  def tracks_added_callback(pointer, tracks, num_tracks, position, userdata)
30
- trigger(pointer, :tracks_added, callback_make_tracks(tracks, num_tracks), position)
30
+ tracks_ary = tracks.read_array_of_pointer(num_tracks).map do |track|
31
+ ptr = Spotify::Pointer.new(track, :track, true)
32
+ Hallon::Track.new(ptr)
33
+ end
34
+
35
+ trigger(pointer, :tracks_added, tracks_ary, position)
31
36
  end
32
37
 
33
38
  # @example listening to this event
@@ -38,8 +43,8 @@ module Hallon::Observable
38
43
  # @yield [tracks, self] tracks_removed
39
44
  # @yieldparam [Array<Track>] tracks
40
45
  # @yieldparam [Playlist] self
41
- def tracks_removed_callback(pointer, tracks, num_tracks, userdata)
42
- trigger(pointer, :tracks_removed, callback_make_tracks(tracks, num_tracks))
46
+ def tracks_removed_callback(pointer, track_indices, num_indices, userdata)
47
+ trigger(pointer, :tracks_removed, track_indices.read_array_of_int(num_indices))
43
48
  end
44
49
 
45
50
  # @example listening to this event
@@ -51,8 +56,8 @@ module Hallon::Observable
51
56
  # @yieldparam [Array<Track>] tracks
52
57
  # @yieldparam [Integer] new_position
53
58
  # @yieldparam [Playlist] self
54
- def tracks_moved_callback(pointer, tracks, num_tracks, new_position, userdata)
55
- trigger(pointer, :tracks_moved, callback_make_tracks(tracks, num_tracks), new_position)
59
+ def tracks_moved_callback(pointer, track_indices, num_indices, new_position, userdata)
60
+ trigger(pointer, :tracks_moved, track_indices.read_array_of_int(num_indices), new_position)
56
61
  end
57
62
 
58
63
  # @example listening to this event
@@ -179,16 +184,5 @@ module Hallon::Observable
179
184
  def subscribers_changed_callback(pointer, userdata)
180
185
  trigger(pointer, :subscribers_changed)
181
186
  end
182
-
183
- protected
184
- # @param [FFI::Pointer] tracks
185
- # @param [Integer] num_tracks
186
- # @param [Array<Track>]
187
- def callback_make_tracks(tracks, num_tracks)
188
- tracks.read_array_of_pointer(num_tracks).map do |track|
189
- ptr = Spotify::Pointer.new(track, :track, true)
190
- Hallon::Track.new(ptr)
191
- end
192
- end
193
187
  end
194
188
  end
@@ -27,8 +27,7 @@ module Hallon::Observable
27
27
  # @yieldparam [Integer] position
28
28
  # @yieldparam [PlaylistContainer] self
29
29
  def playlist_added_callback(pointer, playlist, position, userdata)
30
- playlist = Spotify::Pointer.new(playlist, :playlist, true)
31
- trigger(pointer, :playlist_added, Hallon::Playlist.new(playlist), position)
30
+ trigger(pointer, :playlist_added, playlist_from(playlist), position)
32
31
  end
33
32
 
34
33
  # @example listening to this event
@@ -41,8 +40,7 @@ module Hallon::Observable
41
40
  # @yieldparam [Integer] position
42
41
  # @yieldparam [PlaylistContainer] self
43
42
  def playlist_removed_callback(pointer, playlist, position, userdata)
44
- playlist = Spotify::Pointer.new(playlist, :playlist, true)
45
- trigger(pointer, :playlist_removed, Hallon::Playlist.new(playlist), position)
43
+ trigger(pointer, :playlist_removed, playlist_from(playlist), position)
46
44
  end
47
45
 
48
46
  # @example listening to this event
@@ -56,8 +54,7 @@ module Hallon::Observable
56
54
  # @yieldparam [Integer] new_position
57
55
  # @yieldparam [PlaylistContainer] self
58
56
  def playlist_moved_callback(pointer, playlist, position, new_position, userdata)
59
- playlist = Spotify::Pointer.new(playlist, :playlist, true)
60
- trigger(pointer, :playlist_moved, Hallon::Playlist.new(playlist), position, new_position)
57
+ trigger(pointer, :playlist_moved, playlist_from(playlist), position, new_position)
61
58
  end
62
59
 
63
60
  # @example listening to this event
@@ -70,5 +67,14 @@ module Hallon::Observable
70
67
  def container_loaded_callback(pointer, userdata)
71
68
  trigger(pointer, :container_loaded)
72
69
  end
70
+
71
+ protected
72
+
73
+ # @param [Spotify::Pointer] playlist
74
+ # @return [Hallon::Playlist] a playlist for the given pointer.
75
+ def playlist_from(pointer)
76
+ pointer = Spotify::Pointer.new(pointer, :playlist, true)
77
+ Hallon::Playlist.new(pointer)
78
+ end
73
79
  end
74
80
  end
data/lib/hallon/player.rb CHANGED
@@ -48,7 +48,7 @@ module Hallon
48
48
  # data to the driver or not (see #status=)
49
49
  @status_c = @queue.new_cond
50
50
  # set initial status (we assume stopped)
51
- self.status = @status = :stopped
51
+ self.status = :stopped
52
52
 
53
53
  # this thread feeds the audio driver with data, but
54
54
  # if we are not playing it’ll wait until we are
@@ -125,9 +125,9 @@ module Hallon
125
125
  #
126
126
  # @param [Symbol] status one of :playing, :paused, :stopped
127
127
  # @raise [ArgumentError] if given an invalid status
128
- def status=(status)
128
+ def status=(new_status)
129
129
  @queue.synchronize do
130
- old_status, @status = @status, status
130
+ old_status, @status = status, new_status
131
131
 
132
132
  case status
133
133
  when :playing
@@ -94,6 +94,7 @@ module Hallon
94
94
  end
95
95
 
96
96
  extend Linkable
97
+ include Loadable
97
98
 
98
99
  # CAN HAZ CALLBAKZ
99
100
  extend Observable::Playlist
@@ -104,6 +105,24 @@ module Hallon
104
105
 
105
106
  to_link :from_playlist
106
107
 
108
+ # Given a string, returns `false` if the string is a valid spotify playlist name.
109
+ # If it’s an invalid spotify playlist name, a string describing the fault is returned.
110
+ #
111
+ # @see http://developer.spotify.com/en/libspotify/docs/group__playlist.html#ga840b82b1074a7ca1c9eacd351bed24c2
112
+ # @param [String] name
113
+ # @return [String, false] description of why the name is invalid, or false if it’s valid
114
+ def self.invalid_name?(name)
115
+ unless name.bytesize < 256
116
+ return "name must be shorter than 256 bytes"
117
+ end
118
+
119
+ unless name =~ /[^[:space:]]/
120
+ return "name must not be blank"
121
+ end
122
+
123
+ return false # no error
124
+ end
125
+
107
126
  # Construct a new Playlist, given a pointer.
108
127
  #
109
128
  # @param [String, Link, FFI::Pointer] link
@@ -185,17 +204,11 @@ module Hallon
185
204
  # @param [#to_s] name new name for playlist
186
205
  # @raise [Error] if name could not be changed
187
206
  def name=(name)
188
- name = name.to_s.encode('UTF-8')
189
-
190
- unless name.bytesize < 256
191
- raise ArgumentError, "name must be shorter than 256 bytes"
207
+ unless error = Playlist.invalid_name?(name)
208
+ Error.maybe_raise(Spotify.playlist_rename(pointer, name))
209
+ else
210
+ raise ArgumentError, error
192
211
  end
193
-
194
- unless name =~ /[^ ]/u
195
- raise ArgumentError, "name must not consist of only spaces"
196
- end unless name.empty?
197
-
198
- Error.maybe_raise(Spotify.playlist_rename(pointer, name))
199
212
  end
200
213
 
201
214
  # @return [User, nil]
@@ -209,7 +222,9 @@ module Hallon
209
222
  Spotify.playlist_get_description(pointer)
210
223
  end
211
224
 
212
- # @return [Image, nil]
225
+ # @note this is not the mosaic image you see in the client. Spotify allows custom images
226
+ # on playlists for promo campaigns etc.
227
+ # @return [Image, nil] custom image for the playlist, if one exists
213
228
  def image
214
229
  buffer = FFI::Buffer.alloc_out(20)
215
230
  if Spotify.playlist_get_image(pointer, buffer)
@@ -297,6 +312,14 @@ module Hallon
297
312
  # @return [Playlist]
298
313
  # @raise [Error] if the operation failed
299
314
  def remove(*indices)
315
+ unless indices == indices.uniq
316
+ raise ArgumentError, "no index may occur twice"
317
+ end
318
+
319
+ unless indices.all? { |i| i.between?(0, size-1) }
320
+ raise ArgumentError, "indices must be inside #{0...size}"
321
+ end
322
+
300
323
  indices_ary = FFI::MemoryPointer.new(:int, indices.size)
301
324
  indices_ary.write_array_of_int(indices)
302
325
 
@@ -58,7 +58,7 @@ module Hallon
58
58
 
59
59
  # Folders are parts of playlist containers in that they surround playlists
60
60
  # with a beginning marker and an ending marker. The playlists between these
61
- # markers are considered "inside the playlist".
61
+ # markers are considered "inside the folder".
62
62
  class Folder
63
63
  # @return [Integer] index this folder starts at in the container.
64
64
  attr_reader :begin
@@ -88,7 +88,7 @@ module Hallon
88
88
  # @param [#to_s] new_name
89
89
  # @return [Folder] the new folder
90
90
  def rename(new_name)
91
- raise IndexError, "playlist has moved from #{@begin}..#{@end}" if moved?
91
+ raise IndexError, "folder has moved from #{@begin}..#{@end}" if moved?
92
92
 
93
93
  insert_at = @begin
94
94
  container.remove(@begin)
@@ -134,6 +134,7 @@ module Hallon
134
134
  end
135
135
 
136
136
  extend Observable::PlaylistContainer
137
+ include Loadable
137
138
 
138
139
  # Wrap an existing PlaylistContainer pointer in an object.
139
140
  #
@@ -194,7 +195,11 @@ module Hallon
194
195
  # @return [Playlist, nil] the added playlist, or nil if the operation failed
195
196
  def add(name, force_create = false)
196
197
  playlist = if force_create or not Link.valid?(name) and name.is_a?(String)
197
- Spotify.playlistcontainer_add_new_playlist!(pointer, name.to_s)
198
+ unless error = Playlist.invalid_name?(name)
199
+ Spotify.playlistcontainer_add_new_playlist!(pointer, name)
200
+ else
201
+ raise ArgumentError, error
202
+ end
198
203
  else
199
204
  link = Link.new(name)
200
205
  Spotify.playlistcontainer_add_playlist!(pointer, link.pointer)
@@ -278,7 +283,8 @@ module Hallon
278
283
  # @param (see #move)
279
284
  # @return [Boolean] true if the operation can be performed
280
285
  def can_move?(from, to)
281
- error = move_playlist(from, to, true)
286
+ dry_run = true
287
+ error = move_playlist(from, to, dry_run)
282
288
  _, symbol = Error.disambiguate(error)
283
289
  symbol == :ok
284
290
  end
data/lib/hallon/search.rb CHANGED
@@ -51,6 +51,7 @@ module Hallon
51
51
  end
52
52
 
53
53
  extend Observable::Search
54
+ include Loadable
54
55
 
55
56
  # @return [Array<Symbol>] a list of radio genres available for search
56
57
  def self.genres
@@ -92,8 +92,8 @@ module Hallon
92
92
  def initialize(appkey, options = {}, &block)
93
93
  @options = {
94
94
  :user_agent => "Hallon",
95
- :settings_path => "tmp",
96
- :cache_path => "",
95
+ :settings_path => "tmp/hallon/",
96
+ :cache_path => "tmp/hallon/",
97
97
  :load_playlists => true,
98
98
  :compress_playlists => true,
99
99
  :cache_playlist_metadata => true,
@@ -37,6 +37,10 @@ module Hallon
37
37
  end
38
38
 
39
39
  extend Observable::Toplist
40
+ include Loadable
41
+
42
+ # @return [Symbol] type of toplist request (one of :artists, :albums or :tracks)
43
+ attr_reader :type
40
44
 
41
45
  # Create a Toplist browsing object.
42
46
  #
@@ -67,6 +71,7 @@ module Hallon
67
71
  end
68
72
 
69
73
  subscribe_for_callbacks do |callback|
74
+ @type = type
70
75
  @pointer = Spotify.toplistbrowse_create!(session.pointer, type, region, user, callback, nil)
71
76
  end
72
77
  end
@@ -82,19 +87,19 @@ module Hallon
82
87
  Spotify.toplistbrowse_error(pointer)
83
88
  end
84
89
 
85
- # @return [Artists] a list of artists.
86
- def artists
87
- Artists.new(self)
88
- end
89
-
90
- # @return [Albums] a list of albums.
91
- def albums
92
- Albums.new(self)
93
- end
90
+ # @note the returned enumerator corresponds to the #type of Toplist.
91
+ # @return [Artists, Albums, Tracks] an enumerator over the collection of results.
92
+ def results
93
+ klass = case type
94
+ when :artists
95
+ Artists
96
+ when :albums
97
+ Albums
98
+ when :tracks
99
+ Tracks
100
+ end
94
101
 
95
- # @return [Tracks] a list of tracks.
96
- def tracks
97
- Tracks.new(self)
102
+ klass.new(self)
98
103
  end
99
104
 
100
105
  # @note If the object is not loaded, the result is undefined.
data/lib/hallon/track.rb CHANGED
@@ -16,6 +16,7 @@ module Hallon
16
16
  end
17
17
 
18
18
  extend Linkable
19
+ include Loadable
19
20
 
20
21
  from_link :as_track_and_offset
21
22
  to_link :from_track
data/lib/hallon/user.rb CHANGED
@@ -7,24 +7,60 @@ module Hallon
7
7
  #
8
8
  # @see http://developer.spotify.com/en/libspotify/docs/group__user.html
9
9
  class User < Base
10
- extend Linkable
11
-
12
10
  # A Post is created upon sending tracks (with an optional message) to a user.
13
11
  #
14
12
  # @see http://developer.spotify.com/en/libspotify/docs/group__inbox.html
15
13
  class Post < Base
16
14
  extend Observable::Post
15
+ include Loadable
16
+
17
+ # Use {.create} instead!
18
+ private_class_method :new
19
+
20
+ # @param (see #initialize)
21
+ # @return [Post, nil] post, or nil if posting failed.
22
+ def self.create(recipient_name, message, tracks)
23
+ post = new(recipient_name, message, tracks)
24
+ post unless post.pointer.null?
25
+ end
26
+
27
+ # @return [Array<Track>] an array of tracks posted.
28
+ attr_reader :tracks
29
+
30
+ # @param [String, nil] message together with the post.
31
+ attr_reader :message
17
32
 
18
- # @param [Spotify::Pointer<inbox>]
19
- def initialize(username, message, tracks, &block)
33
+ # @param [String] the username of the post’s recipient.
34
+ attr_reader :recipient_name
35
+
36
+ # Send a list of tracks to another users’ inbox.
37
+ #
38
+ # @param [String] recipient_name username of person to send post to
39
+ # @param [String, nil] message
40
+ # @param [Track, Array<Track>] tracks
41
+ def initialize(recipient_name, message, tracks)
42
+ tracks = Array(tracks)
20
43
  ary = FFI::MemoryPointer.new(:pointer, tracks.length)
21
44
  ary.write_array_of_pointer tracks.map(&:pointer)
22
45
 
23
46
  subscribe_for_callbacks do |callback|
24
- @pointer = Spotify.inbox_post_tracks!(session.pointer, username, ary, tracks.length, message, callback, nil)
47
+ @tracks = tracks
48
+ @message = message
49
+ @recipient_name = recipient_name
50
+ @pointer = Spotify.inbox_post_tracks!(session.pointer, @recipient_name, ary, tracks.length, @message, callback, nil)
25
51
  end
26
52
  end
27
53
 
54
+ # @return [User] the user named {#recipient_name}.
55
+ def recipient
56
+ User.new(recipient_name)
57
+ end
58
+
59
+ # @return [Boolean] true if the post has been successfully sent.
60
+ def loaded?
61
+ status == :ok
62
+ end
63
+
28
64
  # @see Error.explain
29
65
  # @return [Symbol] error status of inbox post
30
66
  def status
@@ -32,6 +68,9 @@ module Hallon
32
68
  end
33
69
  end
34
70
 
71
+ extend Linkable
72
+ include Loadable
73
+
35
74
  from_link :profile do |link|
36
75
  Spotify.link_as_user!(link)
37
76
  end
@@ -90,16 +129,14 @@ module Hallon
90
129
  #
91
130
  # @overload post(message, tracks)
92
131
  # @param [#to_s] message
93
- # @param [Array<Track>] tracks
132
+ # @param [Track, Array<Track>] tracks
94
133
  #
95
134
  # @overload post(tracks)
96
- # @param [Array<Track>] tracks
135
+ # @param [Track, Array<Track>] tracks
97
136
  #
98
- # @return [Post, nil]
137
+ # @return (see Post.create)
99
138
  def post(message = nil, tracks)
100
- message &&= message.encode('UTF-8')
101
- post = Post.new(name, message, tracks)
102
- post unless post.pointer.null?
139
+ Post.create(name, message, tracks)
103
140
  end
104
141
  end
105
142
  end