hallon 0.16.0 → 0.17.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/.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