hallon 0.4.0 → 0.8.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 (52) hide show
  1. data/.gitmodules +3 -0
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG +30 -6
  4. data/README.markdown +7 -7
  5. data/Rakefile +70 -16
  6. data/examples/logging_in.rb +3 -3
  7. data/examples/printing_link_information.rb +1 -1
  8. data/examples/show_published_playlists_of_user.rb +92 -0
  9. data/hallon.gemspec +7 -4
  10. data/lib/hallon.rb +16 -4
  11. data/lib/hallon/album.rb +16 -6
  12. data/lib/hallon/album_browse.rb +78 -0
  13. data/lib/hallon/artist.rb +59 -0
  14. data/lib/hallon/artist_browse.rb +89 -0
  15. data/lib/hallon/base.rb +7 -0
  16. data/lib/hallon/enumerator.rb +64 -0
  17. data/lib/hallon/error.rb +8 -6
  18. data/lib/hallon/ext/spotify.rb +3 -3
  19. data/lib/hallon/image.rb +25 -12
  20. data/lib/hallon/link.rb +4 -4
  21. data/lib/hallon/linkable.rb +4 -2
  22. data/lib/hallon/observable.rb +1 -4
  23. data/lib/hallon/player.rb +130 -0
  24. data/lib/hallon/search.rb +128 -0
  25. data/lib/hallon/session.rb +226 -25
  26. data/lib/hallon/toplist.rb +83 -0
  27. data/lib/hallon/track.rb +62 -7
  28. data/lib/hallon/user.rb +6 -6
  29. data/lib/hallon/version.rb +1 -1
  30. data/spec/hallon/album_browse_spec.rb +20 -0
  31. data/spec/hallon/album_spec.rb +12 -7
  32. data/spec/hallon/artist_browse_spec.rb +29 -0
  33. data/spec/hallon/artist_spec.rb +32 -0
  34. data/spec/hallon/enumerator_spec.rb +106 -0
  35. data/spec/hallon/error_spec.rb +10 -0
  36. data/spec/hallon/hallon_spec.rb +5 -1
  37. data/spec/hallon/image_spec.rb +39 -25
  38. data/spec/hallon/linkable_spec.rb +12 -4
  39. data/spec/hallon/observable_spec.rb +5 -0
  40. data/spec/hallon/player_spec.rb +73 -0
  41. data/spec/hallon/search_spec.rb +80 -0
  42. data/spec/hallon/session_spec.rb +187 -6
  43. data/spec/hallon/toplist_spec.rb +40 -0
  44. data/spec/hallon/track_spec.rb +43 -8
  45. data/spec/mockspotify.rb +47 -0
  46. data/spec/mockspotify/.gitignore +5 -0
  47. data/spec/mockspotify/extconf.rb +5 -0
  48. data/spec/mockspotify/mockspotify_spec.rb +41 -0
  49. data/spec/spec_helper.rb +20 -0
  50. data/spec/support/common_objects.rb +84 -7
  51. metadata +72 -20
  52. data/lib/hallon/ext/object.rb +0 -16
@@ -0,0 +1,59 @@
1
+ module Hallon
2
+ # Artists in Hallon are the people behind the songs. Methods
3
+ # are defined for retrieving their names and loaded status.
4
+ #
5
+ # To retrieve more information about an artist, you can browse
6
+ # it. This will give access to more detailed data such as bio,
7
+ # portraits and more. Hallon does not support this as of yet,
8
+ # but you can use the underlying Spotify API for this, just like
9
+ # we have for {Album}s.
10
+ #
11
+ # Both Albums and Tracks can have more than one artist.
12
+ #
13
+ # @see http://developer.spotify.com/en/libspotify/docs/group__artist.html
14
+ class Artist < Base
15
+ extend Linkable
16
+
17
+ from_link :as_artist
18
+ to_link :from_artist
19
+
20
+ # Construct an artist given a link.
21
+ #
22
+ # @param [String, Link, FFI::Pointer] link
23
+ def initialize(link)
24
+ @pointer = Spotify::Pointer.new from_link(link), :artist, true
25
+ end
26
+
27
+ # Retrieve Artist name. Empty string if Artist is not loaded.
28
+ #
29
+ # @return [String]
30
+ def name
31
+ Spotify.artist_name(@pointer)
32
+ end
33
+
34
+ # True if the Artist is loaded.
35
+ #
36
+ # @return [Boolean]
37
+ def loaded?
38
+ Spotify.artist_is_loaded(@pointer)
39
+ end
40
+
41
+ # @param [Boolean] as_image true if you want it as an Image
42
+ # @return [Image, Link, nil] artist portrait, or the link to it, or nil
43
+ def portrait(as_image = true)
44
+ portrait = Spotify.link_create_from_artist_portrait(@pointer)
45
+ unless portrait.null?
46
+ klass = as_image ? Image : Link
47
+ klass.new(portrait)
48
+ end
49
+ end
50
+
51
+ # Browse the Artist, giving you the ability to explore its’
52
+ # portraits, biography and more.
53
+ #
54
+ # @return [ArtistBrowse] an artist browsing object
55
+ def browse
56
+ ArtistBrowse.new(pointer)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,89 @@
1
+ module Hallon
2
+ # ArtistBrowse is like AlbumBrowse, only that it’s for {Track}s.
3
+ #
4
+ # When it loads, it triggers the load callback on itself, that
5
+ # can be utilized by giving {#on}(:load) a block to execute.
6
+ #
7
+ # @example
8
+ # browse = artist.browse # artist is a Hallon::Artist
9
+ # browse.on(:load) do
10
+ # puts "#{browse.artist.name} browser has been loaded!"
11
+ # end
12
+ # session.wait_for { browse.loaded? }
13
+ #
14
+ # @see Artist
15
+ # @see http://developer.spotify.com/en/libspotify/docs/group__artistbrowse.html
16
+ class ArtistBrowse < Base
17
+ include Observable
18
+
19
+ # Creates an ArtistBrowse instance from an Artist or an Artist pointer.
20
+ #
21
+ # @note Use {Artist#browse} to browse an Artist.
22
+ # @param [Artist, FFI::Pointer] artist
23
+ def initialize(artist)
24
+ artist = artist.pointer if artist.respond_to?(:pointer)
25
+ @callback = proc { trigger(:load) }
26
+
27
+ artistbrowse = Spotify.artistbrowse_create(session.pointer, artist, @callback, nil)
28
+ @pointer = Spotify::Pointer.new(artistbrowse, :artistbrowse, false)
29
+ end
30
+
31
+ # @return [Boolean] true if the album browser is loaded
32
+ def loaded?
33
+ Spotify.artistbrowse_is_loaded(@pointer)
34
+ end
35
+
36
+ # @see Error
37
+ # @return [Symbol] artist browser error status
38
+ def error
39
+ Spotify.artistbrowse_error(@pointer)
40
+ end
41
+
42
+ # @return [Artist, nil] artist this browser is browsing
43
+ def artist
44
+ artist = Spotify.artistbrowse_artist(@pointer)
45
+ Artist.new(artist) unless artist.null?
46
+ end
47
+
48
+ # @return [String] artist biography
49
+ def biography
50
+ Spotify.artistbrowse_biography(@pointer)
51
+ end
52
+
53
+ # @return [Enumerator<Image>] artist portraits
54
+ def portraits
55
+ size = Spotify.artistbrowse_num_portraits(@pointer)
56
+ Enumerator.new(size) do |i|
57
+ id = Spotify.artistbrowse_portrait(@pointer, i).read_string(20)
58
+ Image.new(id)
59
+ end
60
+ end
61
+
62
+ # @return [Enumerator<Track>] artist authored tracks
63
+ def tracks
64
+ size = Spotify.artistbrowse_num_tracks(@pointer)
65
+ Enumerator.new(size) do |i|
66
+ track = Spotify.artistbrowse_track(@pointer, i)
67
+ Track.new(track)
68
+ end
69
+ end
70
+
71
+ # @return [Enumerator<Album>] artist authored albums
72
+ def albums
73
+ size = Spotify.artistbrowse_num_albums(@pointer)
74
+ Enumerator.new(size) do |i|
75
+ album = Spotify.artistbrowse_album(@pointer, i)
76
+ Album.new(album)
77
+ end
78
+ end
79
+
80
+ # @return [Enumartor<Artist>] similar artists to this artist
81
+ def similar_artists
82
+ size = Spotify.artistbrowse_num_similar_artists(@pointer)
83
+ Enumerator.new(size) do |i|
84
+ artist = Spotify.artistbrowse_similar_artist(@pointer, i)
85
+ Artist.new(artist)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -16,5 +16,12 @@ module Hallon
16
16
  rescue NoMethodError
17
17
  super
18
18
  end
19
+
20
+ # The current Session instance.
21
+ #
22
+ # @return [Session]
23
+ def session
24
+ Session.instance
25
+ end
19
26
  end
20
27
  end
@@ -0,0 +1,64 @@
1
+ module Hallon
2
+ # Hallon::Enumerator is like a lazy Array.
3
+ #
4
+ # It provides methods from Enumerable to enumerate through its’ contents,
5
+ # size information and Array access methods. It’s used throughout Hallon
6
+ # for collections of items such as artist tracks, albums and so on.
7
+ class Enumerator
8
+ include Enumerable
9
+
10
+ # @return [Integer] number of items this enumerator can yield
11
+ attr_reader :size
12
+
13
+ # Construct an enumerator of `size` elements.
14
+ #
15
+ # @param [Integer] size
16
+ # @yield to the given block when an item is requested (through #each, #[] etc)
17
+ # @yieldparam [Integer] index item to retrieve
18
+ def initialize(size, &yielder)
19
+ @size = size
20
+ @items = Array.new(size) do |i|
21
+ lambda { yielder[i] }
22
+ end
23
+ end
24
+
25
+ # Yield each item out of the enumerator.
26
+ #
27
+ # @yield obj
28
+ # @return [Enumerator]
29
+ def each
30
+ tap do
31
+ size.times { |i| yield(self[i]) }
32
+ end
33
+ end
34
+
35
+ # @overload [](index)
36
+ # @return [Object, nil]
37
+ #
38
+ # @overload [](start, length)
39
+ # @return [Array, nil]
40
+ #
41
+ # @overload [](range)
42
+ # @return [Array, nil]
43
+ #
44
+ # Works exactly the same as Array#[], including the special cases.
45
+ #
46
+ # @see http://rdoc.info/stdlib/core/1.9.2/Array:[]
47
+ def [](*args)
48
+ result = @items[*args]
49
+
50
+ if result.nil?
51
+ nil
52
+ elsif result.respond_to?(:map)
53
+ result.map(&:call)
54
+ else
55
+ result.call
56
+ end
57
+ end
58
+
59
+ # @return [String] String representation of the Enumerator.
60
+ def to_s
61
+ "<#{self.class.name}:0x#{object_id.to_s(16)} @size=#{size}>"
62
+ end
63
+ end
64
+ end
@@ -9,7 +9,7 @@ module Hallon
9
9
  #
10
10
  # @return [Hash<Symbol, Integer>]
11
11
  def table
12
- Spotify::enum_type(:error).to_hash
12
+ Spotify.enum_type(:error).to_hash
13
13
  end
14
14
 
15
15
  # Given a number or a symbol, find both the symbol and the error
@@ -18,7 +18,7 @@ module Hallon
18
18
  # @param [Symbol, Fixnum] error
19
19
  # @return [[Fixnum, Symbol]] (error code, error symbol)
20
20
  def disambiguate(error)
21
- @enum ||= Spotify::enum_type(:error)
21
+ @enum ||= Spotify.enum_type(:error)
22
22
 
23
23
  if error.is_a? Symbol
24
24
  error = @enum[symbol = error]
@@ -38,15 +38,17 @@ module Hallon
38
38
  # @param [Fixnum, Symbol]
39
39
  # @return [String]
40
40
  def explain(error)
41
- Spotify::error_message disambiguate(error)[0]
41
+ Spotify.error_message disambiguate(error)[0]
42
42
  end
43
43
 
44
- # Raise an {Error} with the given errno, unless it is `0` or `:ok`.
44
+ # Raise an {Error} with the given errno, unless it is `nil`, `:timeout`, `0` or `:ok`.
45
45
  #
46
46
  # @param [Fixnum, Symbol] error
47
47
  # @return [nil]
48
- def maybe_raise(error)
49
- error, symbol = disambiguate(error)
48
+ def maybe_raise(x)
49
+ return nil if [nil, :timeout].include?(x)
50
+
51
+ error, symbol = disambiguate(x)
50
52
  return symbol if symbol == :ok
51
53
 
52
54
  message = []
@@ -15,11 +15,11 @@ module Spotify
15
15
  #
16
16
  # @param [FFI::Pointer] ptr
17
17
  # @param [Symbol] type session, link, etc
18
- # @param [Boolean[ add_ref increase reference count
18
+ # @param [Boolean] add_ref increase reference count
19
19
  # @return [FFI::AutoPointer]
20
20
  def initialize(ptr, type, add_ref = false)
21
21
  super ptr, releaser_for(@type = type)
22
- Spotify::send(:"#{type}_add_ref", ptr) if add_ref
22
+ Spotify.send(:"#{type}_add_ref", ptr) if add_ref
23
23
  end
24
24
 
25
25
  # Create a proc that will accept a pointer of a given type and
@@ -31,7 +31,7 @@ module Spotify
31
31
  lambda do |ptr|
32
32
  unless ptr.null?
33
33
  $stdout.puts "Spotify::#{type}_release(#{ptr})" if $DEBUG
34
- Spotify::send(:"#{type}_release", ptr)
34
+ Spotify.send(:"#{type}_release", ptr)
35
35
  end
36
36
  end
37
37
  end
@@ -7,21 +7,21 @@ module Hallon
7
7
  extend Linkable
8
8
 
9
9
  from_link :as_image do |link, session|
10
- Spotify::image_create_from_link(session, link)
10
+ Spotify.image_create_from_link(session, link)
11
11
  end
12
12
 
13
13
  to_link :from_image
14
14
 
15
15
  # Image triggers `:load` when loaded
16
- include Hallon::Observable
16
+ include Observable
17
17
 
18
18
  # Create a new instance of an Image.
19
19
  #
20
20
  # @param [String, Link, FFI::Pointer] link link or image id
21
21
  # @param [Hallon::Session] session
22
- def initialize(link, session = Session.instance)
22
+ def initialize(link)
23
23
  if link.is_a?(String)
24
- link = to_id($1) if link =~ %r|image[:/](\h{40})|
24
+ link = to_id($1) if link =~ %r|image[:/]([a-fA-F0-9]{40})|
25
25
 
26
26
  FFI::MemoryPointer.new(:char, 20) do |ptr|
27
27
  ptr.write_bytes link
@@ -33,11 +33,14 @@ module Hallon
33
33
 
34
34
  @pointer = Spotify::Pointer.new link, :image
35
35
 
36
- @callback = proc { trigger(:load) }
37
- Spotify::image_add_load_callback(@pointer, @callback, nil)
36
+ @callback = proc { trigger :load }
37
+ Spotify.image_add_load_callback(@pointer, @callback, nil)
38
38
 
39
39
  # TODO: remove load_callback when @pointer is released
40
- # TODO: this makes libspotify segfault, figure out why
40
+ # NOTE: on(:load) will trigger while load callback is still executing,
41
+ # and removing the load callback from within the load callback
42
+ # does not make libspotify happy, and thus segfaults D:
43
+ #
41
44
  # on(:load) { Spotify::image_remove_load_callback(@pointer, @callback, nil) }
42
45
  end
43
46
 
@@ -45,21 +48,21 @@ module Hallon
45
48
  #
46
49
  # @return [Boolean]
47
50
  def loaded?
48
- Spotify::image_is_loaded(@pointer)
51
+ Spotify.image_is_loaded(@pointer)
49
52
  end
50
53
 
51
54
  # Retrieve the current error status.
52
55
  #
53
56
  # @return [Symbol] error
54
57
  def status
55
- Spotify::image_error(@pointer)
58
+ Spotify.image_error(@pointer)
56
59
  end
57
60
 
58
61
  # Retrieve image format.
59
62
  #
60
63
  # @return [Symbol] `:jpeg` or `:unknown`
61
64
  def format
62
- Spotify::image_format(@pointer)
65
+ Spotify.image_format(@pointer)
63
66
  end
64
67
 
65
68
  # Retrieve image ID as a string.
@@ -67,7 +70,7 @@ module Hallon
67
70
  # @param [Boolean] raw true if you want the image id as a hexadecimal string
68
71
  # @return [String]
69
72
  def id(raw = false)
70
- id = Spotify::image_image_id(@pointer).read_string(20)
73
+ id = Spotify.image_image_id(@pointer).read_string(20)
71
74
  raw ? id : to_hex(id)
72
75
  end
73
76
 
@@ -76,11 +79,21 @@ module Hallon
76
79
  # @return [String]
77
80
  def data
78
81
  FFI::MemoryPointer.new(:size_t) do |size|
79
- data = Spotify::image_data(@pointer, size)
82
+ data = Spotify.image_data(@pointer, size)
80
83
  return data.read_bytes(size.read_size_t)
81
84
  end
82
85
  end
83
86
 
87
+ # True if the images both have the same ID, or if their
88
+ # pointers are the same.
89
+ #
90
+ # @see Base#==
91
+ def ==(other)
92
+ super or id(true) == other.id(true)
93
+ rescue NoMethodError, ArgumentError
94
+ false
95
+ end
96
+
84
97
  protected
85
98
  # @param [String]
86
99
  # @return [String]
@@ -28,7 +28,7 @@ module Hallon
28
28
  # @raise [ArgumentError] link could not be parsed
29
29
  def initialize(uri)
30
30
  if (link = uri).respond_to? :to_str
31
- link = Spotify::link_create_from_string(link.to_str)
31
+ link = Spotify.link_create_from_string(link.to_str)
32
32
  end
33
33
 
34
34
  @pointer = Spotify::Pointer.new(link, :link)
@@ -40,14 +40,14 @@ module Hallon
40
40
  #
41
41
  # @return [Symbol]
42
42
  def type
43
- Spotify::link_type(@pointer)
43
+ Spotify.link_type(@pointer)
44
44
  end
45
45
 
46
46
  # Spotify URI length.
47
47
  #
48
48
  # @return [Fixnum]
49
49
  def length
50
- Spotify::link_as_string(@pointer, nil, 0)
50
+ Spotify.link_as_string(@pointer, nil, 0)
51
51
  end
52
52
 
53
53
  # Get the Spotify URI this Link represents.
@@ -57,7 +57,7 @@ module Hallon
57
57
  # @return [String]
58
58
  def to_str(length = length)
59
59
  FFI::Buffer.alloc_out(length + 1) do |b|
60
- Spotify::link_as_string(@pointer, b, b.size)
60
+ Spotify.link_as_string(@pointer, b, b.size)
61
61
  return b.get_string(0)
62
62
  end
63
63
  end
@@ -35,9 +35,11 @@ module Hallon
35
35
  type = as_object.to_s[/^(as_)?([^_]+)/, 2].to_sym
36
36
 
37
37
  define_method(:from_link) do |link, *args|
38
- if link.is_a? FFI::Pointer then link else
38
+ link = if link.is_a? FFI::Pointer then link else
39
39
  block.call Link.new(link).pointer(type), *args
40
40
  end
41
+
42
+ link.tap { raise Hallon::Error, "invalid link" if link.null? }
41
43
  end
42
44
  end
43
45
 
@@ -51,7 +53,7 @@ module Hallon
51
53
  def to_link(cmethod)
52
54
  define_method(:to_link) do |*args|
53
55
  link = Spotify.__send__(:"link_create_#{cmethod}", @pointer, *args)
54
- Hallon::Link.new(link)
56
+ Link.new(link)
55
57
  end
56
58
  end
57
59
  end
@@ -4,9 +4,6 @@ module Hallon
4
4
  #
5
5
  # @private
6
6
  module Observable
7
- # Required for maintaining thread-safety around #handlers
8
- include Hallon::Synchronizable
9
-
10
7
  # Defines a handler for the given event.
11
8
  #
12
9
  # @example defining a handler and triggering it
@@ -33,7 +30,7 @@ module Hallon
33
30
  def on(*events, &block)
34
31
  raise ArgumentError, "no block given" unless block
35
32
  wrap = events.length > 1
36
- events.each do |event|
33
+ events.map(&:to_sym).each do |event|
37
34
  block = proc { |*args| yield(event, *args) } if wrap
38
35
  __handlers[event] = [] unless __handlers.has_key?(event)
39
36
  __handlers[event] << block