hallon 0.4.0 → 0.8.0

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