hallon 0.15.0 → 0.16.0

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