hallon 0.15.0 → 0.16.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 (65) hide show
  1. data/CHANGELOG.md +40 -0
  2. data/README.markdown +1 -1
  3. data/dev/application_key_converter.rb +11 -0
  4. data/examples/example_support.rb +4 -0
  5. data/examples/playing_audio.rb +1 -1
  6. data/lib/hallon.rb +11 -0
  7. data/lib/hallon/album_browse.rb +3 -3
  8. data/lib/hallon/artist_browse.rb +20 -4
  9. data/lib/hallon/blob.rb +11 -0
  10. data/lib/hallon/enumerator.rb +5 -0
  11. data/lib/hallon/ext/spotify.rb +2 -0
  12. data/lib/hallon/image.rb +7 -1
  13. data/lib/hallon/link.rb +5 -2
  14. data/lib/hallon/observable.rb +48 -1
  15. data/lib/hallon/player.rb +15 -26
  16. data/lib/hallon/playlist.rb +16 -25
  17. data/lib/hallon/playlist_container.rb +38 -0
  18. data/lib/hallon/search.rb +75 -4
  19. data/lib/hallon/session.rb +31 -42
  20. data/lib/hallon/toplist.rb +3 -3
  21. data/lib/hallon/track.rb +28 -10
  22. data/lib/hallon/version.rb +1 -1
  23. data/spec/hallon/album_browse_spec.rb +68 -18
  24. data/spec/hallon/album_spec.rb +62 -27
  25. data/spec/hallon/artist_browse_spec.rb +106 -31
  26. data/spec/hallon/artist_spec.rb +32 -18
  27. data/spec/hallon/blob_spec.rb +6 -0
  28. data/spec/hallon/enumerator_spec.rb +10 -0
  29. data/spec/hallon/error_spec.rb +4 -4
  30. data/spec/hallon/hallon_spec.rb +1 -1
  31. data/spec/hallon/image_spec.rb +58 -47
  32. data/spec/hallon/link_spec.rb +51 -43
  33. data/spec/hallon/observable/album_browse_spec.rb +1 -1
  34. data/spec/hallon/observable/artist_browse_spec.rb +1 -1
  35. data/spec/hallon/observable/image_spec.rb +1 -1
  36. data/spec/hallon/observable/playlist_container_spec.rb +4 -4
  37. data/spec/hallon/observable/playlist_spec.rb +14 -14
  38. data/spec/hallon/observable/post_spec.rb +1 -1
  39. data/spec/hallon/observable/search_spec.rb +1 -1
  40. data/spec/hallon/observable/session_spec.rb +17 -17
  41. data/spec/hallon/observable/toplist_spec.rb +1 -1
  42. data/spec/hallon/observable_spec.rb +40 -6
  43. data/spec/hallon/player_spec.rb +1 -1
  44. data/spec/hallon/playlist_container_spec.rb +96 -13
  45. data/spec/hallon/playlist_spec.rb +180 -45
  46. data/spec/hallon/search_spec.rb +211 -28
  47. data/spec/hallon/session_spec.rb +44 -38
  48. data/spec/hallon/toplist_spec.rb +31 -14
  49. data/spec/hallon/track_spec.rb +159 -50
  50. data/spec/hallon/user_post_spec.rb +10 -5
  51. data/spec/hallon/user_spec.rb +60 -50
  52. data/spec/spec_helper.rb +40 -15
  53. data/spec/support/album_mocks.rb +30 -0
  54. data/spec/support/artist_mocks.rb +36 -0
  55. data/spec/support/common_objects.rb +0 -201
  56. data/spec/support/image_mocks.rb +34 -0
  57. data/spec/support/playlist_container_mocks.rb +36 -0
  58. data/spec/support/playlist_mocks.rb +70 -0
  59. data/spec/support/search_mocks.rb +23 -0
  60. data/spec/support/session_mocks.rb +33 -0
  61. data/spec/support/toplist_mocks.rb +19 -0
  62. data/spec/support/track_mocks.rb +28 -0
  63. data/spec/support/user_mocks.rb +20 -0
  64. metadata +40 -18
  65. data/spec/support/context_stub_session.rb +0 -5
@@ -4,6 +4,46 @@ Hallon’s Changelog
4
4
  [HEAD][]
5
5
  ------------------
6
6
 
7
+ [v0.16.0][]
8
+ ------------------
9
+ This release brings a lot of changes to the Hallon test suite, mainly to make
10
+ it more readable and less of a mess.
11
+
12
+ __Added__
13
+
14
+ - ArtistBrowse#top_hits [5ef57a7]
15
+ - Session#flush_caches [693567]
16
+ - Track#playable_track [2d9cdfb]
17
+ - Support for playlists in search results [05f49e4]
18
+ - Support for suggestion search [1ae4b29]
19
+ - Support for unseen tracks for playlists in playlist containers [4bf8496]
20
+ - Support for login with credentials blob instead of password [61ffdf3]
21
+
22
+ __Changed__
23
+
24
+ - Removed the final `self` parameter of all libspotify events [d738a79]
25
+ - Renamed Playlist::Track#create_time to added_at [1adc2d7]
26
+ - Renamed Playlist::Track#creator to adder [1adc2d7]
27
+ - Session#offline_sync_status now returns a hash always [c14d42ae]
28
+ - Make AlbumBrowse#request_duration always return an integer [ee0697c2]
29
+ - Make ArtistBrowse#request_duration always return an integer [ee0697c2]
30
+ - Make Toplist#request_duration always return an integer [ebc64e1a]
31
+ - Track#popularity now returns a value between 0 and 100 [cd85ae7f]
32
+ - Move Session#wait_for onto Observable, now they all support it! [e14da3e]
33
+ - Make Playlist#update rely solely on callbacks [e703c132]
34
+ - Player.new no longer takes a session parameter [7508d18]
35
+ - Session.instance now raises NoSessionError on missing session [3b47b7]
36
+ - Observable#wait_for now calls original handler on events [be236bfd]
37
+
38
+ __Fixed__
39
+
40
+ - Have Playlist#subscribers always return an array [a8d26c9a]
41
+ - Fix Image#data for images with no data [e7d8627]
42
+ - Playlist::Track#message always return a string [72a644a]
43
+ - Session#login! now raises when given the wrong password [f650a36]
44
+ - Do not wrap the Session pointer in a Spotify pointer [9ae047d]
45
+ - Link.valid? segfaulting if having no session [84c9f69c]
46
+
7
47
  [v0.15.0][]
8
48
  ------------------
9
49
  Updated to libspotify v11.1.60 [3c810b0], and improved the examples provided within Hallon codebase significantly.
@@ -64,7 +64,7 @@ For more information about audio support in Hallon, see the section "Audio suppo
64
64
 
65
65
  ### Contact details
66
66
 
67
- - __Got questions?__ Ask on the mailing list: <https://groups.google.com/d/forum/ruby-hallon>
67
+ - __Got questions?__ Ask on the mailing list: <mailto:ruby-hallon@googlegroups.com> (<https://groups.google.com/d/forum/ruby-hallon>)
68
68
  - __Found a bug?__ Report an issue: <https://github.com/Burgestrand/Hallon/issues/new>
69
69
  - __Have feedback?__ I ❤ feedback! Please send it to the mailing list.
70
70
 
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ if ARGF.file == STDIN
4
+ puts "You forgot to give me a file!"
5
+ abort "Usage: ruby application_key_converter.rb spotify_appkey.key > new_spotify_appkey.key"
6
+ end
7
+
8
+ old_format = ARGF.read.split.join
9
+ new_format = [old_format].pack("H*")
10
+
11
+ print new_format
@@ -86,6 +86,10 @@ session = Hallon::Session.initialize(hallon_appkey) do
86
86
  puts "[LOG] #{message}"
87
87
  end
88
88
 
89
+ on(:credentials_blob_updated) do |blob|
90
+ puts "[BLOB] #{blob}"
91
+ end
92
+
89
93
  on(:connection_error) do |error|
90
94
  Hallon::Error.maybe_raise(error)
91
95
  end
@@ -11,7 +11,7 @@ rescue LoadError => e
11
11
  abort "[ERROR] Could not load gem 'hallon-openal', please install with 'gem install hallon-openal'"
12
12
  end
13
13
 
14
- player = Hallon::Player.new(session, Hallon::OpenAL)
14
+ player = Hallon::Player.new(Hallon::OpenAL)
15
15
 
16
16
  # Program flow.
17
17
 
@@ -12,6 +12,7 @@ require 'hallon/error'
12
12
  require 'hallon/base'
13
13
  require 'hallon/enumerator'
14
14
  require 'hallon/audio_queue'
15
+ require 'hallon/blob'
15
16
 
16
17
  require 'hallon/observable/album_browse'
17
18
  require 'hallon/observable/artist_browse'
@@ -70,6 +71,16 @@ module Hallon
70
71
  # Thrown by {Loadable#load} and {Playlist#update} on failure.
71
72
  TimeoutError = Class.new(Hallon::Error)
72
73
 
74
+ # Raised by Session.instance
75
+ NoSessionError = Class.new(StandardError)
76
+
77
+ # Raised by Session#login! and Session#relogin!
78
+ LoginError = Class.new(StandardError)
79
+
80
+ # Raised by PlaylistContainer#num_unseen_tracks_for and PlaylistContainer#unseen_tracks_for.
81
+ # @note most likely raised because of the playlist not being in the playlist container.
82
+ OperationFailedError = Class.new(StandardError)
83
+
73
84
  class << self
74
85
  # @return [Numeric] default load timeout in seconds, used in {Loadable#load}.
75
86
  attr_reader :load_timeout
@@ -77,11 +77,11 @@ module Hallon
77
77
  end
78
78
 
79
79
  # @note If the object is not loaded, the result is undefined.
80
- # @note Returns nil if the request was served from the local libspotify cache.
81
- # @return [Rational, nil] time it took for the albumbrowse request to complete (in seconds).
80
+ # @return [Rational] time it took for the albumbrowse request to complete (in seconds).
82
81
  def request_duration
83
82
  duration = Spotify.albumbrowse_backend_request_duration(pointer)
84
- Rational(duration, 1000) if duration > 0
83
+ duration = 0 if duration < 0
84
+ Rational(duration, 1000)
85
85
  end
86
86
 
87
87
  # @return [Copyrights] enumerator of copyright notices.
@@ -1,6 +1,7 @@
1
1
  # coding: utf-8
2
2
  module Hallon
3
- # ArtistBrowse is like AlbumBrowse, only that it’s for {Track}s.
3
+ # An ArtistBrowse object is for retrieving details about a given artist, such
4
+ # as it’s tracks, albums, similar artists and more.
4
5
  #
5
6
  # @see Artist
6
7
  # @see http://developer.spotify.com/en/libspotify/docs/group__artistbrowse.html
@@ -55,6 +56,16 @@ module Hallon
55
56
  end
56
57
  end
57
58
 
59
+ # Enumerates through all tophit tracks of an album browsing object.
60
+ class TopHits < Enumerator
61
+ size :artistbrowse_num_tophit_tracks
62
+
63
+ # @return [Track, nil]
64
+ item :artistbrowse_tophit_track! do |track|
65
+ Track.from(track)
66
+ end
67
+ end
68
+
58
69
  extend Observable::ArtistBrowse
59
70
  include Loadable
60
71
 
@@ -107,11 +118,11 @@ module Hallon
107
118
  end
108
119
 
109
120
  # @note If the object is not loaded, the result is undefined.
110
- # @note Returns nil if the request was served from the local libspotify cache.
111
- # @return [Rational, nil] time it took for the albumbrowse request to complete (in seconds).
121
+ # @return [Rational] time it took for the albumbrowse request to complete (in seconds).
112
122
  def request_duration
113
123
  duration = Spotify.artistbrowse_backend_request_duration(pointer)
114
- Rational(duration, 1000) if duration > 0
124
+ duration = 0 if duration < 0
125
+ Rational(duration, 1000)
115
126
  end
116
127
 
117
128
  # @return [Portraits] artist portraits as {Image}s.
@@ -138,5 +149,10 @@ module Hallon
138
149
  def similar_artists
139
150
  SimilarArtists.new(self)
140
151
  end
152
+
153
+ # @return [TopHits] enumerator of the artist’s most popular tracks.
154
+ def top_hits
155
+ TopHits.new(self)
156
+ end
141
157
  end
142
158
  end
@@ -0,0 +1,11 @@
1
+ module Hallon
2
+ # Dummy module, allows for infecting strings
3
+ # with a Hallon::Blob to check for if they are
4
+ # blobs or not.
5
+ module Blob
6
+ end
7
+
8
+ def self.Blob(string)
9
+ string.extend(Blob)
10
+ end
11
+ end
@@ -70,6 +70,11 @@ module Hallon
70
70
  self
71
71
  end
72
72
 
73
+ # @return [Boolean] true if the size is zero.
74
+ def empty?
75
+ size.zero?
76
+ end
77
+
73
78
  # @overload [](index)
74
79
  # @return [Object, nil]
75
80
  #
@@ -46,6 +46,7 @@ module Spotify
46
46
  wrap_function :track_artist, :artist
47
47
  wrap_function :track_album, :album
48
48
  wrap_function :localtrack_create, :track
49
+ wrap_function :track_get_playable, :track
49
50
 
50
51
  wrap_function :album_artist, :artist
51
52
 
@@ -59,6 +60,7 @@ module Spotify
59
60
  wrap_function :artistbrowse_track, :track
60
61
  wrap_function :artistbrowse_album, :album
61
62
  wrap_function :artistbrowse_similar_artist, :artist
63
+ wrap_function :artistbrowse_tophit_track, :track
62
64
 
63
65
  wrap_function :image_create, :image
64
66
  wrap_function :image_create_from_link, :image
@@ -71,7 +71,13 @@ module Hallon
71
71
  def data
72
72
  FFI::MemoryPointer.new(:size_t) do |size|
73
73
  data = Spotify.image_data(pointer, size)
74
- return data.read_bytes(size.read_size_t)
74
+ size = size.read_size_t
75
+
76
+ if size > 0
77
+ return data.read_bytes(size)
78
+ else
79
+ return "".force_encoding("BINARY")
80
+ end
75
81
  end
76
82
  end
77
83
 
@@ -9,6 +9,10 @@ module Hallon
9
9
  # @param [#to_s] spotify_uri
10
10
  # @return [Boolean]
11
11
  def self.valid?(spotify_uri)
12
+ unless Session.instance?
13
+ raise NoSessionError, "You must have initialized a session to create links"
14
+ end
15
+
12
16
  if spotify_uri.is_a?(Link)
13
17
  return true
14
18
  elsif spotify_uri.to_s["\x00"] # image ids
@@ -25,9 +29,8 @@ module Hallon
25
29
  # @param [#to_str] uri
26
30
  # @raise [ArgumentError] link could not be parsed
27
31
  def initialize(uri)
28
- # if no session instance exists, libspotify segfaults, so assert that we have one
29
32
  unless Session.instance?
30
- raise "Link.new requires an existing Session instance"
33
+ raise NoSessionError, "You must have initialized a session to create links"
31
34
  end
32
35
 
33
36
  # we support any #to_link’able object
@@ -111,6 +111,53 @@ module Hallon
111
111
  end
112
112
  end
113
113
 
114
+ # Wait for the given callbacks to fire until the block returns true
115
+ #
116
+ # @note Given block will be called once instantly without parameters.
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
119
+ # @yield [Symbol, *args] name of the event that fired, and its’ arguments
120
+ # @return whatever the block returns
121
+ def wait_for(*events)
122
+ channel = SizedQueue.new(10) # sized just to be safe
123
+
124
+ old_handlers = events.each_with_object({}) do |event, hash|
125
+ hash[event] = on(event) do |*args|
126
+ channel << [event, *args]
127
+ hash[event].call(*args)
128
+ end
129
+ end
130
+
131
+ old_notify = session.on(:notify_main_thread) do
132
+ channel << :notify
133
+ end
134
+
135
+ if result = yield
136
+ return result
137
+ end
138
+
139
+ loop do
140
+ begin
141
+ timeout = [session.process_events.fdiv(1000), 2].min # scope to two seconds
142
+ timeout = timeout + 0.010 # minimum of ten miliseconds timeout
143
+ params = Timeout::timeout(timeout) { channel.pop }
144
+ redo if params == :notify
145
+ rescue Timeout::Error
146
+ params = nil
147
+ end
148
+
149
+ if result = yield(*params)
150
+ return result
151
+ end
152
+ end
153
+ ensure
154
+ old_handlers.each_pair do |event, handler|
155
+ on(event, &handler)
156
+ end unless old_handlers.nil?
157
+ session.on(:notify_main_thread, &old_notify) unless old_notify.nil?
158
+ end
159
+
160
+
114
161
  # @param [#to_s] name
115
162
  # @return [Boolean] true if a callback with `name` exists.
116
163
  def has_callback?(name)
@@ -149,7 +196,7 @@ module Hallon
149
196
  # @return whatever the handler returns
150
197
  def trigger(name, *arguments, &block)
151
198
  if handler = handlers[name.to_s]
152
- handler.call(*arguments, self, &block)
199
+ handler.call(*arguments, &block)
153
200
  end
154
201
  end
155
202
 
@@ -6,7 +6,7 @@ module Hallon
6
6
  # controlling the playback features of libspotify.
7
7
  #
8
8
  # @see Session
9
- class Player
9
+ class Player < Base
10
10
  # meep?
11
11
  extend Observable::Player
12
12
 
@@ -24,18 +24,17 @@ module Hallon
24
24
  end
25
25
  end
26
26
 
27
- # Constructs a Player, given a Session and an audio driver.
27
+ # Constructs a Player, given an audio driver.
28
28
  #
29
29
  # @example
30
- # player = Hallon::Player.new(session, Hallon::OpenAL)
30
+ # player = Hallon::Player.new(Hallon::OpenAL)
31
31
  # player.play(track)
32
32
  #
33
33
  # @note for instructions on how to write your own audio driver, see Hallons’ README
34
- # @param [Session] session
35
34
  # @param [AudioDriver] driver
36
35
  # @yield instance_evals itself, allowing you to define callbacks using `on`
37
- def initialize(session, driver, &block)
38
- @session = session
36
+ def initialize(driver, &block)
37
+ @session = Hallon::Session.instance
39
38
  @pointer = @session.pointer
40
39
 
41
40
  # sample rate is often (if not always) 44.1KHz, so
@@ -85,20 +84,20 @@ module Hallon
85
84
  #
86
85
  # Will be called after calling our buffers are full enough to support
87
86
  # continous playback.
88
- def start_playback(session)
87
+ def start_playback
89
88
  self.status = :playing
90
89
  end
91
90
 
92
91
  # Called by libspotify when the driver should pause audio playback.
93
92
  #
94
93
  # Might happen if we’re playing audio faster than we can stream it.
95
- def stop_playback(session)
94
+ def stop_playback
96
95
  self.status = :paused
97
96
  end
98
97
 
99
98
  # Called by libspotify on music delivery; format is
100
99
  # a hash of (sample) rate, channels and (sample) type.
101
- def music_delivery(format, frames, session)
100
+ def music_delivery(format, frames)
102
101
  @queue.synchronize do
103
102
  if frames.none?
104
103
  @queue.clear
@@ -113,7 +112,7 @@ module Hallon
113
112
  # Called by libspotify to request information about our
114
113
  # audio buffer. Required if we want libspotify to tell
115
114
  # us when we should start and stop playback.
116
- def get_audio_buffer_stats(session)
115
+ def get_audio_buffer_stats
117
116
  drops = @driver.drops if @driver.respond_to?(:drops)
118
117
  [@queue.size, drops.to_i]
119
118
  end
@@ -192,22 +191,12 @@ module Hallon
192
191
  # @param (see #play)
193
192
  # @return (see #play)
194
193
  def play!(track = nil)
195
- monitor = Monitor.new
196
- condvar = monitor.new_cond
197
-
198
- monitor.synchronize do
199
- end_of_track = false
200
-
201
- on(:end_of_track) do
202
- monitor.synchronize do
203
- end_of_track = true
204
- condvar.signal
205
- end
206
- end
207
-
208
- play(track)
209
- condvar.wait_until { end_of_track }
210
- end
194
+ end_of_track = false
195
+ old_callback = on(:end_of_track) { end_of_track = true }
196
+ play(track)
197
+ wait_for(:end_of_track) { end_of_track }
198
+ ensure
199
+ on(:end_of_track, &old_callback)
211
200
  end
212
201
 
213
202
  # Set preferred playback bitrate.
@@ -29,10 +29,10 @@ module Hallon
29
29
 
30
30
  @index = index
31
31
  @playlist_ptr = playlist_pointer
32
- @create_time = Time.at Spotify.playlist_track_create_time(playlist_ptr, index)
33
- @message = Spotify.playlist_track_message(playlist_ptr, index)
32
+ @message = Spotify.playlist_track_message(playlist_ptr, index).to_s
34
33
  @seen = Spotify.playlist_track_seen(playlist_ptr, index)
35
- @creator = begin
34
+ @added_at = Time.at(Spotify.playlist_track_create_time(playlist_ptr, index)).utc
35
+ @adder = begin
36
36
  creator = Spotify.playlist_track_creator!(playlist_ptr, index)
37
37
  User.from(creator)
38
38
  end
@@ -46,11 +46,11 @@ module Hallon
46
46
  # @return [Integer] index this track was created with.
47
47
  attr_reader :index
48
48
 
49
- # @return [Time] time when track at {#index} was added to playlist.
50
- attr_reader :create_time
49
+ # @return [Time, nil] time when track at {#index} was added to playlist.
50
+ attr_reader :added_at
51
51
 
52
52
  # @return [User, nil] person who added track at {#index} to this playlist.
53
- attr_reader :creator
53
+ attr_reader :adder
54
54
 
55
55
  # @return [String] message attached to this track at {#index}.
56
56
  attr_reader :message
@@ -150,24 +150,14 @@ module Hallon
150
150
  Spotify.playlist_set_collaborative(pointer, !!collaborative)
151
151
  end
152
152
 
153
- # Allow the playlist time to update itself and the Spotify backend.
154
- # This method will block until libspotify says the playlist no longer
155
- # has pending changes.
153
+ # Waits for the playlist to begin updating and blocks until it is done.
156
154
  #
157
155
  # @return [Playlist]
158
156
  def upload(timeout = Hallon.load_timeout)
159
- unless pending?
160
- # libspotify has this bug where pending? returns false directly after
161
- # adding tracks to a playlist; processing events once usually toggles
162
- # the playlist into pending? mode
163
- session.process_events
164
- end
165
-
166
157
  Timeout.timeout(timeout, Hallon::TimeoutError) do
167
- session.wait_for { not pending? }
158
+ wait_for(:playlist_update_in_progress) { |done| done }
159
+ self
168
160
  end
169
-
170
- self
171
161
  end
172
162
 
173
163
  # @return [Boolean] true if playlist has pending changes
@@ -215,7 +205,7 @@ module Hallon
215
205
  Spotify.playlist_set_offline_mode(session.pointer, pointer, !! available_offline)
216
206
  end
217
207
 
218
- # @return [String]
208
+ # @return [String] playlist name, or an empty string if unavailable.
219
209
  def name
220
210
  Spotify.playlist_name(pointer)
221
211
  end
@@ -258,17 +248,18 @@ module Hallon
258
248
  def subscribers
259
249
  ptr = Spotify.playlist_subscribers(pointer)
260
250
 
261
- begin
251
+ if ptr.null?
252
+ []
253
+ else
262
254
  struct = Spotify::Subscribers.new(ptr)
263
-
264
255
  if struct[:count].zero?
265
256
  []
266
257
  else
267
258
  struct[:subscribers].map(&:read_string)
268
259
  end
269
- ensure
270
- Spotify.playlist_subscribers_free(ptr)
271
- end unless ptr.null?
260
+ end
261
+ ensure
262
+ Spotify.playlist_subscribers_free(ptr) unless ptr.null?
272
263
  end
273
264
 
274
265
  # @return [Integer] total number of subscribers.