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,83 @@
1
+ module Hallon
2
+ # Toplists are what they sound like. They’re collections of
3
+ # artists, albums or tracks popular in a certain area either
4
+ # by country, user or everywhere.
5
+ #
6
+ # @see http://developer.spotify.com/en/libspotify/docs/group__toplist.html
7
+ class Toplist < Base
8
+ include Observable
9
+
10
+ # Create a Toplist browsing object.
11
+ #
12
+ # @overload initialize(type, username)
13
+ # @overload initialize(type, country)
14
+ # @overload initialize(type)
15
+ #
16
+ # @param [Symbol] type one of :artists, :albums or :tracks
17
+ # @param [String, Symbol, nil] region username, 2-letter country code or nil
18
+ def initialize(type, region = nil)
19
+ case region
20
+ when String
21
+ user = region
22
+ region = :user
23
+ when NilClass
24
+ region = :anywhere
25
+ when Symbol
26
+ region = to_country(region)
27
+ end
28
+
29
+ @callback = proc { trigger(:load) }
30
+ pointer = Spotify.toplistbrowse_create(session.pointer, type, region, user, @callback, nil)
31
+ @pointer = Spotify::Pointer.new(pointer, :toplistbrowse, false)
32
+ end
33
+
34
+ # @return [Boolean] true if the toplist is loaded
35
+ def loaded?
36
+ Spotify.toplistbrowse_is_loaded(@pointer)
37
+ end
38
+
39
+ # @return [Symbol] toplist error status
40
+ def error
41
+ Spotify.toplistbrowse_error(@pointer)
42
+ end
43
+
44
+ # @return [Enumerator<Artist>]
45
+ def artists
46
+ size = Spotify.toplistbrowse_num_artists(@pointer)
47
+ Enumerator.new(size) do |i|
48
+ artist = Spotify.toplistbrowse_artist(@pointer, i)
49
+ Artist.new(artist)
50
+ end
51
+ end
52
+
53
+ # @return [Enumerator<Album>]
54
+ def albums
55
+ size = Spotify.toplistbrowse_num_albums(@pointer)
56
+ Enumerator.new(size) do |i|
57
+ album = Spotify.toplistbrowse_album(@pointer, i)
58
+ Artist.new(album)
59
+ end
60
+ end
61
+
62
+ # @return [Enumerator<Track>]
63
+ def tracks
64
+ size = Spotify.toplistbrowse_num_tracks(@pointer)
65
+ Enumerator.new(size) do |i|
66
+ track = Spotify.toplistbrowse_track(@pointer, i)
67
+ Artist.new(track)
68
+ end
69
+ end
70
+
71
+ private
72
+ # Convert a given two-character region to a Spotify
73
+ # compliant region (encoded in a 16bit integer).
74
+ #
75
+ # @param [#to_s]
76
+ # @return [Integer]
77
+ def to_country(region)
78
+ code = region.to_s.upcase
79
+ high, low = code.bytes.take(2)
80
+ (high << 8) | low
81
+ end
82
+ end
83
+ end
@@ -104,14 +104,69 @@ module Hallon
104
104
  # @return [Hallon::Album]
105
105
  def album
106
106
  album = Spotify.track_album(@pointer)
107
- Hallon::Album.new(album) unless album.null?
107
+ Album.new(album) unless album.null?
108
108
  end
109
109
 
110
- # TODO: available?(session)
111
- # TODO: local?(session)
112
- # TODO: autolinked?(session)
113
- # TODO: starred?(session)
114
- # TODO: starred = true (session, array of tracks)
115
- # TODO: artists (count, by index)
110
+ # Artist who performed this Track.
111
+ #
112
+ # @note There may be more than one artist, see {#artists} for retrieving them all!
113
+ # @see #artists
114
+ # @return [Hallon::Artist, nil]
115
+ def artist
116
+ artists.first
117
+ end
118
+
119
+ # All {Artist}s who performed this Track.
120
+ #
121
+ # @note Track must be loaded, or you’ll get zero artists.
122
+ # @return [Hallon::Enumerator<Artist>]
123
+ def artists
124
+ size = Spotify.track_num_artists(@pointer)
125
+ Enumerator.new(size) do |i|
126
+ artist = Spotify.track_artist(@pointer, i)
127
+ Artist.new(artist) unless artist.null?
128
+ end
129
+ end
130
+
131
+ # True if the Track is available.
132
+ #
133
+ # @note This’ll always return false unless the track is loaded.
134
+ # @return [Boolean]
135
+ def available?
136
+ Spotify.track_is_available(session.pointer, @pointer)
137
+ end
138
+
139
+ # True if the Track is a local track.
140
+ #
141
+ # @note This’ll always return false unless the track is loaded.
142
+ # @return [Boolean]
143
+ def local?
144
+ Spotify.track_is_local(session.pointer, @pointer)
145
+ end
146
+
147
+ # True if the Track is autolinked.
148
+ #
149
+ # @note This’ll always return false unless the track is loaded.
150
+ # @return [Boolean]
151
+ def autolinked?
152
+ Spotify.track_is_autolinked(session.pointer, @pointer)
153
+ end
154
+
155
+ # True if the track is starred.
156
+ #
157
+ # @note This’ll always return false unless the track is loaded.
158
+ # @return [Boolean]
159
+ def starred?
160
+ Spotify.track_is_starred(session.pointer, @pointer)
161
+ end
162
+
163
+ # Set {#starred?} status of current track.
164
+ #
165
+ # @note It’ll set the starred status for the current Session.instance.
166
+ # @param [Boolean] starred
167
+ # @return [Boolean]
168
+ def starred=(starred)
169
+ starred ? session.star(self) : session.unstar(self)
170
+ end
116
171
  end
117
172
  end
@@ -17,7 +17,7 @@ module Hallon
17
17
  # @param [String, Hallon::Link, FFI::Pointer] link
18
18
  # @return [FFI::Pointer]
19
19
  from_link :profile do |link|
20
- Spotify::link_as_user(link)
20
+ Spotify.link_as_user(link)
21
21
  end
22
22
 
23
23
  # @macro [attach] to_link
@@ -37,7 +37,7 @@ module Hallon
37
37
 
38
38
  # @return [Boolean] true if the user is loaded
39
39
  def loaded?
40
- Spotify::user_is_loaded(@pointer)
40
+ Spotify.user_is_loaded(@pointer)
41
41
  end
42
42
 
43
43
  # Retrieve the name of the current user.
@@ -48,11 +48,11 @@ module Hallon
48
48
  def name(type = :canonical)
49
49
  case type
50
50
  when :display
51
- Spotify::user_display_name(@pointer)
51
+ Spotify.user_display_name(@pointer)
52
52
  when :full
53
- Spotify::user_full_name(@pointer)
53
+ Spotify.user_full_name(@pointer)
54
54
  when :canonical
55
- Spotify::user_canonical_name(@pointer)
55
+ Spotify.user_canonical_name(@pointer)
56
56
  else
57
57
  raise ArgumentError, "expected type to be :display, :full or :canonical, but was #{type}"
58
58
  end.to_s
@@ -62,7 +62,7 @@ module Hallon
62
62
  #
63
63
  # @return [String]
64
64
  def picture
65
- Spotify::user_picture(@pointer).to_s
65
+ Spotify.user_picture(@pointer).to_s
66
66
  end
67
67
  end
68
68
  end
@@ -3,5 +3,5 @@ module Hallon
3
3
  # Current release version of Hallon
4
4
  #
5
5
  # @see http://semver.org/
6
- VERSION = [0, 4, 0].join('.')
6
+ VERSION = [0, 8, 0].join('.')
7
7
  end
@@ -0,0 +1,20 @@
1
+ # coding: utf-8
2
+ describe Hallon::AlbumBrowse do
3
+ subject do
4
+ mock_session do
5
+ album = Hallon::Album.new(mock_album)
6
+ Spotify.should_receive(:albumbrowse_create).and_return(mock_albumbrowse)
7
+ Hallon::AlbumBrowse.new(album)
8
+ end
9
+ end
10
+
11
+ it { should be_loaded }
12
+ its(:error) { should eq :ok }
13
+ its(:album) { should eq Hallon::Album.new(mock_album) }
14
+ its(:artist) { should eq Hallon::Artist.new(mock_artist) }
15
+ its('copyrights.size') { should eq 2 }
16
+ its('copyrights.to_a') { should eq %w[Kim Elin] }
17
+ its('tracks.size') { should eq 2 }
18
+ its('tracks.to_a') { should eq [mock_track, mock_track_two].map{ |p| Hallon::Track.new(p) } }
19
+ its(:review) { should eq "This album is AWESOME" }
20
+ end
@@ -6,6 +6,13 @@ describe Hallon::Album do
6
6
  its(:year) { should be 2004 }
7
7
  its(:type) { should be :single }
8
8
 
9
+ its(:browse) do
10
+ mock_session do
11
+ Spotify.should_receive(:albumbrowse_create).exactly(2).times.and_return(mock_albumbrowse)
12
+ should eq Hallon::AlbumBrowse.new(mock_album)
13
+ end
14
+ end
15
+
9
16
  it { should be_available }
10
17
  it { should be_loaded }
11
18
 
@@ -15,12 +22,12 @@ describe Hallon::Album do
15
22
  subject.artist.should be_nil
16
23
  end
17
24
 
18
- it "should be an artist if it exists"
25
+ it "should be an artist if it exists" do
26
+ subject.artist.should be_a Hallon::Artist
27
+ end
19
28
  end
20
29
 
21
30
  describe "cover" do
22
- before { Hallon::Session.should_receive(:instance).and_return(session) }
23
-
24
31
  it "should be nil if there is no image" do
25
32
  Spotify.should_receive(:album_cover).and_return(null_pointer)
26
33
  subject.cover.should be_nil
@@ -31,14 +38,12 @@ describe Hallon::Album do
31
38
  ptr.write_string(mock_image_id)
32
39
 
33
40
  Spotify.should_receive(:album_cover).and_return(ptr)
34
- subject.cover.id.should eq mock_image_hex
41
+ mock_session { subject.cover.id.should eq mock_image_hex }
35
42
  end
36
43
  end
37
44
  end
38
45
 
39
46
  describe ".types" do
40
- it "should not be an empty hash" do
41
- Hallon::Album.types.should_not be_empty
42
- end
47
+ specify { Hallon::Album.types.should_not be_empty }
43
48
  end
44
49
  end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ describe Hallon::ArtistBrowse do
3
+ subject do
4
+ mock_session do
5
+ artist = Hallon::Artist.new(mock_artist)
6
+ Spotify.should_receive(:artistbrowse_create).and_return(mock_artistbrowse)
7
+ Hallon::ArtistBrowse.new(artist)
8
+ end
9
+ end
10
+
11
+ it { should be_loaded }
12
+ its(:error) { should eq :ok }
13
+ its(:artist) { should eq Hallon::Artist.new(mock_artist) }
14
+
15
+ its('portraits.size') { should eq 2 }
16
+ its('portraits.to_a') do
17
+ Hallon::Session.should_receive(:instance).exactly(2).times.and_return(session)
18
+
19
+ subject.map{ |img| img.id(true) }.should eq [mock_image_id, mock_image_id]
20
+ end
21
+
22
+ its('tracks.size') { should eq 2 }
23
+ its('tracks.to_a') { should eq [mock_track, mock_track_two].map{ |p| Hallon::Track.new(p) } }
24
+ its('albums.size') { should eq 1 }
25
+ its('albums.to_a') { should eq [Hallon::Album.new(mock_album)] }
26
+ its('similar_artists.size') { should eq 2 }
27
+ its('similar_artists.to_a') { should eq [mock_artist, mock_artist_two].map{ |p| Hallon::Artist.new(p) } }
28
+ its(:biography) { should eq 'grew up in DA BLOCK' }
29
+ end
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ describe Hallon::Artist do
3
+ subject { Hallon::Artist.new(mock_artist) }
4
+
5
+ it { should be_loaded }
6
+ its(:name) { should eq "Jem" }
7
+ its(:browse) do
8
+ Hallon::Session.should_receive(:instance).exactly(2).times.and_return(session)
9
+ Spotify.should_receive(:artistbrowse_create).exactly(2).times.and_return(mock_artistbrowse)
10
+
11
+ should eq Hallon::ArtistBrowse.new(mock_artist)
12
+ end
13
+
14
+ describe "#portrait" do
15
+ let(:link) { Hallon::Link.new("spotify:image:c78f091482e555bd2ffacfcd9cbdc0714b221663") }
16
+ let(:link_pointer) { FFI::Pointer.new(link.pointer.address) }
17
+
18
+ before do
19
+ Hallon::Link.new(link_pointer).should eq link
20
+ Spotify.should_receive(:link_create_from_artist_portrait).with(subject.pointer).and_return(link_pointer)
21
+ end
22
+
23
+ specify "as an image" do
24
+ Hallon::Session.should_receive(:instance).twice.and_return(session)
25
+ subject.portrait.should eq Hallon::Image.new(link_pointer)
26
+ end
27
+
28
+ specify "as a link" do
29
+ subject.portrait(false).should eq link
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,106 @@
1
+ describe Hallon::Enumerator do
2
+ let(:item) do
3
+ mock.tap { |x| x.stub(:get).and_return(&alphabet) }
4
+ end
5
+
6
+ let(:enum) do
7
+ Hallon::Enumerator.new(5) { |i| item.get(i) }
8
+ end
9
+
10
+ let(:alphabet) do
11
+ proc { |x| %w[a b c d e][x] }
12
+ end
13
+
14
+
15
+ it "should be an enumerable" do
16
+ enum.should respond_to :each
17
+ enum.should be_an Enumerable
18
+ end
19
+
20
+ describe "#each" do
21
+ it "should call the containing block" do
22
+ calls = []
23
+ enum = Hallon::Enumerator.new(4) { |i| item.get(i) }
24
+ enum.each_with_index { |x, i| x.should eq alphabet[i] }
25
+ end
26
+ end
27
+
28
+ describe "#size" do
29
+ it "should return the given size" do
30
+ Hallon::Enumerator.new(4).size.should eq 4
31
+ end
32
+ end
33
+
34
+ describe "#[]" do
35
+ it "should support #[x] within range" do
36
+ item.should_receive(:get).with(1).and_return(&alphabet)
37
+
38
+ enum[1].should eq "b"
39
+ end
40
+
41
+ it "should support negative #[x] within range" do
42
+ item.should_receive(:get).with(4).and_return(&alphabet)
43
+
44
+ enum[-1].should eq "e"
45
+ end
46
+
47
+ it "should return nil for #[x] outside range" do
48
+ item.should_not_receive(:get)
49
+
50
+ enum[6].should be_nil
51
+ end
52
+
53
+ it "should return nil for #[-x] outside range" do
54
+ item.should_not_receive(:get)
55
+
56
+ enum[-6].should be_nil
57
+ end
58
+
59
+ it "should return a slice of elements for #[x, y]" do
60
+ item.should_receive(:get).with(1).and_return(&alphabet)
61
+ item.should_receive(:get).with(2).and_return(&alphabet)
62
+
63
+ enum[1, 2].should eq %w[b c]
64
+ end
65
+
66
+ it "should return elements for an inclusive range of #[x..y]" do
67
+ item.should_receive(:get).with(1).and_return(&alphabet)
68
+ item.should_receive(:get).with(2).and_return(&alphabet)
69
+ item.should_receive(:get).with(3).and_return(&alphabet)
70
+
71
+ enum[1..3].should eq %w[b c d]
72
+ end
73
+
74
+ it "should return return only existing elements for partly inclusive range of #[x..y]" do
75
+ item.should_receive(:get).with(4).and_return(&alphabet)
76
+
77
+ enum[4..7].should eq %w[e]
78
+ end
79
+
80
+ it "should return nil for a completely outside range of #[x..y]" do
81
+ item.should_not_receive(:get)
82
+
83
+ enum[6..10].should eq nil
84
+ end
85
+
86
+ it "should return the items for #[-x, y]" do
87
+ item.should_receive(:get).with(2).and_return(&alphabet)
88
+ item.should_receive(:get).with(3).and_return(&alphabet)
89
+ item.should_receive(:get).with(4).and_return(&alphabet)
90
+
91
+ enum[-3, 3].should eq %w[c d e]
92
+ end
93
+
94
+ it "should slice between items by #[x, y]" do
95
+ item.should_not_receive(:get)
96
+
97
+ enum[5, 1].should eq []
98
+ end
99
+
100
+ it "should slice between items by #[x..y]" do
101
+ item.should_not_receive(:get)
102
+
103
+ enum[5..10].should eq []
104
+ end
105
+ end
106
+ end