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.
- data/.gitmodules +3 -0
- data/.travis.yml +2 -0
- data/CHANGELOG +30 -6
- data/README.markdown +7 -7
- data/Rakefile +70 -16
- data/examples/logging_in.rb +3 -3
- data/examples/printing_link_information.rb +1 -1
- data/examples/show_published_playlists_of_user.rb +92 -0
- data/hallon.gemspec +7 -4
- data/lib/hallon.rb +16 -4
- data/lib/hallon/album.rb +16 -6
- data/lib/hallon/album_browse.rb +78 -0
- data/lib/hallon/artist.rb +59 -0
- data/lib/hallon/artist_browse.rb +89 -0
- data/lib/hallon/base.rb +7 -0
- data/lib/hallon/enumerator.rb +64 -0
- data/lib/hallon/error.rb +8 -6
- data/lib/hallon/ext/spotify.rb +3 -3
- data/lib/hallon/image.rb +25 -12
- data/lib/hallon/link.rb +4 -4
- data/lib/hallon/linkable.rb +4 -2
- data/lib/hallon/observable.rb +1 -4
- data/lib/hallon/player.rb +130 -0
- data/lib/hallon/search.rb +128 -0
- data/lib/hallon/session.rb +226 -25
- data/lib/hallon/toplist.rb +83 -0
- data/lib/hallon/track.rb +62 -7
- data/lib/hallon/user.rb +6 -6
- data/lib/hallon/version.rb +1 -1
- data/spec/hallon/album_browse_spec.rb +20 -0
- data/spec/hallon/album_spec.rb +12 -7
- data/spec/hallon/artist_browse_spec.rb +29 -0
- data/spec/hallon/artist_spec.rb +32 -0
- data/spec/hallon/enumerator_spec.rb +106 -0
- data/spec/hallon/error_spec.rb +10 -0
- data/spec/hallon/hallon_spec.rb +5 -1
- data/spec/hallon/image_spec.rb +39 -25
- data/spec/hallon/linkable_spec.rb +12 -4
- data/spec/hallon/observable_spec.rb +5 -0
- data/spec/hallon/player_spec.rb +73 -0
- data/spec/hallon/search_spec.rb +80 -0
- data/spec/hallon/session_spec.rb +187 -6
- data/spec/hallon/toplist_spec.rb +40 -0
- data/spec/hallon/track_spec.rb +43 -8
- data/spec/mockspotify.rb +47 -0
- data/spec/mockspotify/.gitignore +5 -0
- data/spec/mockspotify/extconf.rb +5 -0
- data/spec/mockspotify/mockspotify_spec.rb +41 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/common_objects.rb +84 -7
- metadata +72 -20
- 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
|
data/lib/hallon/base.rb
CHANGED
@@ -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
|
data/lib/hallon/error.rb
CHANGED
@@ -9,7 +9,7 @@ module Hallon
|
|
9
9
|
#
|
10
10
|
# @return [Hash<Symbol, Integer>]
|
11
11
|
def table
|
12
|
-
Spotify
|
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
|
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
|
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(
|
49
|
-
|
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 = []
|
data/lib/hallon/ext/spotify.rb
CHANGED
@@ -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
|
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
|
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
|
34
|
+
Spotify.send(:"#{type}_release", ptr)
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
data/lib/hallon/image.rb
CHANGED
@@ -7,21 +7,21 @@ module Hallon
|
|
7
7
|
extend Linkable
|
8
8
|
|
9
9
|
from_link :as_image do |link, session|
|
10
|
-
Spotify
|
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
|
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
|
22
|
+
def initialize(link)
|
23
23
|
if link.is_a?(String)
|
24
|
-
link = to_id($1) if link =~ %r|image[:/](
|
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
|
37
|
-
Spotify
|
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
|
-
#
|
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
|
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
|
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
|
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
|
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
|
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]
|
data/lib/hallon/link.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
60
|
+
Spotify.link_as_string(@pointer, b, b.size)
|
61
61
|
return b.get_string(0)
|
62
62
|
end
|
63
63
|
end
|
data/lib/hallon/linkable.rb
CHANGED
@@ -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
|
-
|
56
|
+
Link.new(link)
|
55
57
|
end
|
56
58
|
end
|
57
59
|
end
|
data/lib/hallon/observable.rb
CHANGED
@@ -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
|