hallon 0.13.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
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