hallon 0.16.0 → 0.17.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/.gitignore +2 -1
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +22 -1
  4. data/Gemfile +2 -2
  5. data/README.markdown +2 -2
  6. data/Rakefile +69 -42
  7. data/hallon.gemspec +1 -1
  8. data/lib/hallon.rb +3 -2
  9. data/lib/hallon/album.rb +6 -4
  10. data/lib/hallon/artist.rb +6 -4
  11. data/lib/hallon/audio_queue.rb +1 -1
  12. data/lib/hallon/base.rb +4 -0
  13. data/lib/hallon/blob.rb +6 -0
  14. data/lib/hallon/error.rb +10 -41
  15. data/lib/hallon/ext/spotify.rb +1 -146
  16. data/lib/hallon/image.rb +8 -0
  17. data/lib/hallon/linkable.rb +6 -0
  18. data/lib/hallon/loadable.rb +6 -0
  19. data/lib/hallon/observable.rb +1 -1
  20. data/lib/hallon/observable/playlist_container.rb +2 -2
  21. data/lib/hallon/observable/session.rb +34 -0
  22. data/lib/hallon/player.rb +7 -3
  23. data/lib/hallon/playlist.rb +5 -1
  24. data/lib/hallon/playlist_container.rb +9 -8
  25. data/lib/hallon/scrobbler.rb +103 -0
  26. data/lib/hallon/search.rb +1 -0
  27. data/lib/hallon/session.rb +69 -13
  28. data/lib/hallon/toplist.rb +1 -1
  29. data/lib/hallon/track.rb +2 -2
  30. data/lib/hallon/version.rb +1 -1
  31. data/spec/hallon/album_spec.rb +16 -0
  32. data/spec/hallon/artist_spec.rb +16 -0
  33. data/spec/hallon/base_spec.rb +1 -1
  34. data/spec/hallon/error_spec.rb +3 -3
  35. data/spec/hallon/hallon_spec.rb +1 -1
  36. data/spec/hallon/image_spec.rb +6 -0
  37. data/spec/hallon/observable/session_spec.rb +20 -0
  38. data/spec/hallon/scrobbler_spec.rb +119 -0
  39. data/spec/hallon/session_spec.rb +38 -4
  40. data/spec/hallon/spotify_spec.rb +0 -45
  41. data/spec/mockspotify.rb +6 -1
  42. data/spec/spec_helper.rb +4 -5
  43. metadata +59 -20
  44. data/spec/support/cover_me.rb +0 -7
@@ -4,154 +4,9 @@
4
4
  #
5
5
  # @see https://github.com/Burgestrand/libspotify-ruby
6
6
  module Spotify
7
- # Fetches the associated value of an enum from a given symbol.
8
- #
9
- # @param [Symbol] symbol
10
- # @param [#to_s] type
11
- # @raise ArgumentError on failure
12
- def self.enum_value!(symbol, type)
13
- enum_value(symbol) or raise ArgumentError, "invalid #{type}: #{symbol}"
14
- end
15
-
16
- # Wraps the function `function` so that it always returns
17
- # a Spotify::Pointer with correct refcount. Functions that
18
- # contain the word `create` are assumed to start out with
19
- # a refcount of `+1`.
20
- #
21
- # @param [#to_s] function
22
- # @param [#to_s] return_type
23
- # @raise [NoMethodError] if `function` is not defined
24
- # @see Spotify::Pointer
25
- def self.wrap_function(function, return_type)
26
- method(function)
27
- define_singleton_method("#{function}!") do |*args|
28
- pointer = public_send(function, *args)
29
- Spotify::Pointer.new(pointer, return_type, function !~ /create/)
30
- end
31
- end
32
-
33
- # @macro [attach] wrap_function
34
- # Same as {Spotify}.`$1`, but wraps result in a {Spotify::Pointer}.
35
- #
36
- # @method $1!
37
- # @return [Spotify::Pointer<$2>]
38
- # @see #$1
39
- wrap_function :session_user, :user
40
- wrap_function :session_playlistcontainer, :playlistcontainer
41
- wrap_function :session_inbox_create, :playlist
42
- wrap_function :session_starred_create, :playlist
43
- wrap_function :session_starred_for_user_create, :playlist
44
- wrap_function :session_publishedcontainer_for_user_create, :playlistcontainer
45
-
46
- wrap_function :track_artist, :artist
47
- wrap_function :track_album, :album
48
- wrap_function :localtrack_create, :track
49
- wrap_function :track_get_playable, :track
50
-
51
- wrap_function :album_artist, :artist
52
-
53
- wrap_function :albumbrowse_create, :albumbrowse
54
- wrap_function :albumbrowse_album, :album
55
- wrap_function :albumbrowse_artist, :artist
56
- wrap_function :albumbrowse_track, :track
57
-
58
- wrap_function :artistbrowse_create, :artistbrowse
59
- wrap_function :artistbrowse_artist, :artist
60
- wrap_function :artistbrowse_track, :track
61
- wrap_function :artistbrowse_album, :album
62
- wrap_function :artistbrowse_similar_artist, :artist
63
- wrap_function :artistbrowse_tophit_track, :track
64
-
65
- wrap_function :image_create, :image
66
- wrap_function :image_create_from_link, :image
67
-
68
- wrap_function :link_as_track, :track
69
- wrap_function :link_as_track_and_offset, :track
70
- wrap_function :link_as_album, :album
71
- wrap_function :link_as_artist, :artist
72
- wrap_function :link_as_user, :user
73
-
74
- wrap_function :link_create_from_string, :link
75
- wrap_function :link_create_from_track, :link
76
- wrap_function :link_create_from_album, :link
77
- wrap_function :link_create_from_artist, :link
78
- wrap_function :link_create_from_search, :link
79
- wrap_function :link_create_from_playlist, :link
80
- wrap_function :link_create_from_artist_portrait, :link
81
- wrap_function :link_create_from_artistbrowse_portrait, :link
82
- wrap_function :link_create_from_album_cover, :link
83
- wrap_function :link_create_from_image, :link
84
- wrap_function :link_create_from_user, :link
85
-
86
- wrap_function :search_create, :search
87
- wrap_function :search_track, :track
88
- wrap_function :search_album, :album
89
- wrap_function :search_artist, :artist
90
-
91
- wrap_function :playlist_track, :track
92
- wrap_function :playlist_track_creator, :user
93
- wrap_function :playlist_owner, :user
94
- wrap_function :playlist_create, :playlist
95
-
96
- wrap_function :playlistcontainer_playlist, :playlist
97
- wrap_function :playlistcontainer_add_new_playlist, :playlist
98
- wrap_function :playlistcontainer_add_playlist, :playlist
99
- wrap_function :playlistcontainer_owner, :user
100
-
101
- wrap_function :toplistbrowse_create, :toplistbrowse
102
- wrap_function :toplistbrowse_artist, :artist
103
- wrap_function :toplistbrowse_album, :album
104
- wrap_function :toplistbrowse_track, :track
105
-
106
- wrap_function :inbox_post_tracks, :inbox
107
-
108
- # The Pointer is a kind of AutoPointer specially tailored for Spotify
109
- # objects, that releases the raw pointer on GC.
110
- class Pointer < FFI::AutoPointer
111
- attr_reader :type
112
-
113
- # @param [FFI::Pointer] pointer
114
- # @param [#to_s] type session, link, etc
115
- # @param [Boolean] add_ref
116
- # @return [FFI::AutoPointer]
117
- def initialize(pointer, type, add_ref)
118
- super pointer, self.class.releaser_for(@type = type.to_s)
119
-
120
- unless pointer.null?
121
- Spotify.send(:"#{type}_add_ref", pointer)
122
- end if add_ref
123
- end
124
-
125
- # @return [String] representation of the spotify pointer
126
- def to_s
127
- "<#{self.class} address=0x#{address.to_s(16)} type=#{type}>"
128
- end
129
-
130
- # Create a proc that will accept a pointer of a given type and
131
- # release it with the correct function if it’s not null.
132
- #
133
- # @param [Symbol]
134
- # @return [Proc]
135
- def self.releaser_for(type)
136
- lambda do |pointer|
137
- unless pointer.null?
138
- $stdout.puts "Spotify::#{type}_release(#{pointer})" if $DEBUG
139
- Spotify.send(:"#{type}_release", pointer)
140
- end
141
- end
142
- end
143
-
144
- # @param [Object] pointer
145
- # @param [Symbol] type (optional, no type checking is done if not given)
146
- # @return [Boolean] true if object is a spotify pointer and of correct type
147
- def self.typechecks?(object, type)
148
- !! (object.type == type.to_s) if object.is_a?(Spotify::Pointer)
149
- end
150
- end
151
-
152
7
  # Extensions to SessionConfig, allowing more sensible configuration names.
153
8
  SessionConfig.class_eval do
154
- [:cache_location, :settings_location, :user_agent, :device_id, :tracefile].each do |field|
9
+ [:cache_location, :settings_location, :user_agent, :device_id, :proxy, :proxy_username, :proxy_password, :tracefile].each do |field|
155
10
  method = field.to_s.gsub('location', 'path')
156
11
 
157
12
  define_method(:"#{method}") { self[field].read_string }
data/lib/hallon/image.rb CHANGED
@@ -15,6 +15,14 @@ module Hallon
15
15
  extend Observable::Image
16
16
  include Loadable
17
17
 
18
+ # A list of available image sizes.
19
+ #
20
+ # @see Album#cover
21
+ # @see Artist#portrait
22
+ def self.sizes
23
+ Spotify.enum_type(:image_size).symbols
24
+ end
25
+
18
26
  # Create a new instance of an Image.
19
27
  #
20
28
  # @example from a link
@@ -8,6 +8,9 @@ module Hallon
8
8
  #
9
9
  # @private
10
10
  module Linkable
11
+ # ClassMethods adds `#from_link` and `#to_link` DSL methods, which
12
+ # essentially are convenience methods for defining the way to convert
13
+ # a link to a pointer of a given Spotify object type.
11
14
  module ClassMethods
12
15
  # Defines `#from_link`, used in converting a link to a pointer. You
13
16
  # can either pass it a `method_name`, or a `type` and a block.
@@ -100,6 +103,9 @@ module Hallon
100
103
  end
101
104
  end
102
105
 
106
+ # Will extend `other` with ClassMethods on inclusion.
107
+ #
108
+ # @param [#extend] other
103
109
  def self.included(other)
104
110
  other.extend ClassMethods
105
111
  end
@@ -2,7 +2,13 @@
2
2
  require 'timeout'
3
3
 
4
4
  module Hallon
5
+ # Extends Hallon objects with a method that allows synchronous loading of objects.
5
6
  module Loadable
7
+ # Wait until the object has loaded.
8
+ #
9
+ # @example waiting for a track to load
10
+ # track = Hallon::Track.new(track_uri).load
11
+ #
6
12
  # @param [Numeric] timeout after this time, if the object is not loaded, an error is raised.
7
13
  # @return [self]
8
14
  # @raise [Hallon::TimeoutError] after `timeout` seconds if the object does not load.
@@ -115,7 +115,7 @@ module Hallon
115
115
  #
116
116
  # @note Given block will be called once instantly without parameters.
117
117
  # @note If no events happen for 0.25 seconds, the block will be called without parameters.
118
- # @param [Symbol, ...] *events list of events to wait for
118
+ # @param [Symbol, ...] events list of events to wait for
119
119
  # @yield [Symbol, *args] name of the event that fired, and its’ arguments
120
120
  # @return whatever the block returns
121
121
  def wait_for(*events)
@@ -72,8 +72,8 @@ module Hallon::Observable
72
72
 
73
73
  # @param [Spotify::Pointer] playlist
74
74
  # @return [Hallon::Playlist] a playlist for the given pointer.
75
- def playlist_from(pointer)
76
- pointer = Spotify::Pointer.new(pointer, :playlist, true)
75
+ def playlist_from(playlist)
76
+ pointer = Spotify::Pointer.new(playlist, :playlist, true)
77
77
  Hallon::Playlist.new(pointer)
78
78
  end
79
79
  end
@@ -249,5 +249,39 @@ module Hallon::Observable
249
249
  def credentials_blob_updated_callback(pointer, credentials)
250
250
  trigger(pointer, :credentials_blob_updated, credentials)
251
251
  end
252
+
253
+ # @example listening to this event
254
+ # session.on(:connectionstate_updated) do
255
+ # puts "Yay! Connection state changed… hooray… wee… no?"
256
+ # end
257
+ #
258
+ # @yield
259
+ def connectionstate_updated_callback(pointer)
260
+ trigger(pointer, :connectionstate_updated)
261
+ end
262
+
263
+ # @example listening to this event
264
+ # session.on(:scrobble_error) do |error|
265
+ # Hallon::Error.maybe_raise(error)
266
+ # end
267
+ #
268
+ # @yield
269
+ def scrobble_error_callback(pointer, error)
270
+ trigger(pointer, :scrobble_error, error)
271
+ end
272
+
273
+ # @example listening to this event
274
+ # session.on(:private_session_mode_changed) do |enabled|
275
+ # if enabled
276
+ # puts "Private session enabled!"
277
+ # else
278
+ # puts "Private session disabled!"
279
+ # end
280
+ # end
281
+ #
282
+ # @yield
283
+ def private_session_mode_changed_callback(pointer, enabled)
284
+ trigger(pointer, :private_session_mode_changed, enabled)
285
+ end
252
286
  end
253
287
  end
data/lib/hallon/player.rb CHANGED
@@ -122,7 +122,7 @@ module Hallon
122
122
  # well as allow audio data to stream through the feeder
123
123
  # thread.
124
124
  #
125
- # @param [Symbol] status one of :playing, :paused, :stopped
125
+ # @param [Symbol] new_status one of :playing, :paused, :stopped
126
126
  # @raise [ArgumentError] if given an invalid status
127
127
  def status=(new_status)
128
128
  @queue.synchronize do
@@ -201,10 +201,14 @@ module Hallon
201
201
 
202
202
  # Set preferred playback bitrate.
203
203
  #
204
+ # @note the double possible errors is a result of the same thing as for
205
+ # {Session#offline_bitrate=}, see its documentation for further information.
206
+ #
207
+ # @raise [ArgumentError] if given invalid bitrate
208
+ # @raise [Spotify::Error] if libspotify does not accept the given bitrate
204
209
  # @param [Symbol] bitrate one of :96k, :160k, :320k
205
- # @return [Symbol]
206
210
  def bitrate=(bitrate)
207
- Spotify.session_preferred_bitrate(pointer, bitrate)
211
+ Spotify.session_preferred_bitrate!(pointer, bitrate)
208
212
  end
209
213
 
210
214
  # Loads a Track for playing.
@@ -72,7 +72,6 @@ module Hallon
72
72
  # @raise [IndexError] if the underlying track has moved
73
73
  # @raise [Error] if the operation could not be completed
74
74
  #
75
- # @param [Integer] index
76
75
  # @param [Boolean] seen true if the track is now seen
77
76
  # @return [Playlist::Track] track at the given index
78
77
  def seen=(seen)
@@ -152,6 +151,11 @@ module Hallon
152
151
 
153
152
  # Waits for the playlist to begin updating and blocks until it is done.
154
153
  #
154
+ # @note this is done by waiting for the libspotify callback, where libspotify
155
+ # tells Hallon the playlist update is done.
156
+ #
157
+ # @param [Integer] timeout time until the operation times out
158
+ # @raise [Hallon::TimeoutError] if the upload failed within the load timeout
155
159
  # @return [Playlist]
156
160
  def upload(timeout = Hallon.load_timeout)
157
161
  Timeout.timeout(timeout, Hallon::TimeoutError) do
@@ -96,7 +96,7 @@ module Hallon
96
96
  container.move(insert_at + 1, @end)
97
97
  end
98
98
 
99
- # @param [PlaylistContainer] container
99
+ # @param [PlaylistContainer] container_pointer
100
100
  # @param [Range] indices
101
101
  def initialize(container_pointer, indices)
102
102
  @container_ptr = container_pointer
@@ -193,19 +193,19 @@ module Hallon
193
193
  # @param [String, Playlist, Link] playlist
194
194
  # @param [Boolean] force_create force creation of a new playlist
195
195
  # @return [Playlist, nil] the added playlist, or nil if the operation failed
196
- def add(name, force_create = false)
197
- playlist = if force_create or not Link.valid?(name) and name.is_a?(String)
198
- unless error = Playlist.invalid_name?(name)
199
- Spotify.playlistcontainer_add_new_playlist!(pointer, name)
196
+ def add(playlist, force_create = false)
197
+ resource = if force_create or not Link.valid?(playlist) and playlist.is_a?(String)
198
+ unless error = Playlist.invalid_name?(playlist)
199
+ Spotify.playlistcontainer_add_new_playlist!(pointer, playlist)
200
200
  else
201
201
  raise ArgumentError, error
202
202
  end
203
203
  else
204
- link = Link.new(name)
204
+ link = Link.new(playlist)
205
205
  Spotify.playlistcontainer_add_playlist!(pointer, link.pointer)
206
206
  end
207
207
 
208
- Playlist.from(playlist)
208
+ Playlist.from(resource)
209
209
  end
210
210
 
211
211
  # Create a new folder with the given name at the end of the container.
@@ -331,7 +331,8 @@ module Hallon
331
331
  # Wrapper for original API; adjusts indices accordingly.
332
332
  #
333
333
  # @param [Integer] from
334
- # @param [Integer] from
334
+ # @param [Integer] infront_of
335
+ # @param [Boolean] dry_run
335
336
  # @return [Integer] error
336
337
  def move_playlist(from, infront_of, dry_run)
337
338
  infront_of += 1 if from < infront_of
@@ -0,0 +1,103 @@
1
+ module Hallon
2
+ # The Hallon::Scrobbler is responsible for controlling play scrobbling.
3
+ #
4
+ # You can construct the scrobbler with different providers to control
5
+ # scrobbling for each one individually. The scrobbler includes a list
6
+ # of social providers, methods to adjust the scrobbling of libspotify,
7
+ # and methods to retrieve the current scrobbling state.
8
+ class Scrobbler
9
+ # @return [Array<Symbol>] list of available scrobbling providers
10
+ def self.providers
11
+ Spotify.enum_type(:social_provider).symbols
12
+ end
13
+
14
+ # @return [Symbol] social provider
15
+ attr_reader :provider
16
+
17
+ # Initialize the scrobbler with a social provider.
18
+ #
19
+ # @note it appears that in libspotify v12.1.56, the only valid provider
20
+ # is :facebook — all other providers return errors
21
+ #
22
+ # @raise [ArgumentError] if the given provider is invalid
23
+ # @param [Symbol] provider
24
+ def initialize(provider)
25
+ provider_to_i = Spotify.enum_value!(provider, "social provider")
26
+ @provider = Spotify.enum_type(:social_provider)[provider_to_i]
27
+ end
28
+
29
+ # @note if this returns false, it usually means libspotify either has
30
+ # no scrobbling credentials, or the user has disallowed spotify
31
+ # from scrobbling to the given provider
32
+ #
33
+ # @note this method only works for the :facebook provider; for all other
34
+ # providers it will always return true
35
+ #
36
+ # @return [Boolean] true if scrobbling is possible
37
+ def possible?
38
+ case provider
39
+ when :spotify, :lastfm
40
+ # libspotify v12.1.56 has a bug with all providers except for :facebook
41
+ # where the return value is always :invalid_indata; however, the devs
42
+ # also mentioned the function would always return true for all other
43
+ # providers anyway
44
+ true
45
+ else
46
+ FFI::Buffer.alloc_out(:bool) do |buffer|
47
+ Spotify.session_is_scrobbling_possible!(session.pointer, provider, buffer)
48
+ return ! buffer.read_uchar.zero?
49
+ end
50
+ end
51
+ end
52
+
53
+ # Sets the scrobbling credentials.
54
+ #
55
+ # @example setting username and password
56
+ # scrobbling.credentials = 'kim', 'password'
57
+ #
58
+ # @param [Array<Username, Password>] credentials
59
+ def credentials=(credentials)
60
+ username, password = Array(credentials)
61
+ Spotify.session_set_social_credentials!(session.pointer, provider, username, password)
62
+ end
63
+
64
+ # Enables or disables the local scrobbling setting.
65
+ #
66
+ # @param [Boolean] scrobble true if you want scrobbling to be enabled
67
+ def enabled=(scrobble)
68
+ state = scrobble ? :local_enabled : :local_disabled
69
+ Spotify.session_set_scrobbling!(session.pointer, provider, state)
70
+ end
71
+
72
+ # @return [Boolean] true if scrobbling (global or local) is enabled.
73
+ def enabled?
74
+ FFI::Buffer.alloc_out(:int) do |buffer|
75
+ Spotify.session_is_scrobbling(session.pointer, provider, buffer)
76
+ state = read_state(buffer.read_uint)
77
+ return !! (state =~ /enabled/)
78
+ end
79
+ end
80
+
81
+ # Sets the local scrobbling state to the global state.
82
+ #
83
+ # @return [Scrobbler]
84
+ def reset
85
+ tap { Spotify.session_set_scrobbling!(session.pointer, provider, :use_global_setting) }
86
+ end
87
+
88
+ protected
89
+
90
+ # Convert an integer state to an actual state symbol.
91
+ #
92
+ # @param [Integer] state
93
+ # @return [Symbol] state as a symbol
94
+ def read_state(state)
95
+ Spotify.enum_type(:scrobbling_state)[state]
96
+ end
97
+
98
+ # @return [Hallon::Session]
99
+ def session
100
+ Session.instance
101
+ end
102
+ end
103
+ end