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,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