hallon 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/.travis.yml +2 -0
  2. data/CHANGELOG +43 -0
  3. data/Gemfile +2 -0
  4. data/README.markdown +21 -13
  5. data/Rakefile +84 -23
  6. data/dev/login.rb +16 -0
  7. data/examples/adding_tracks_to_playlist.rb +49 -0
  8. data/examples/logging_in.rb +1 -6
  9. data/examples/show_published_playlists_of_user.rb +9 -19
  10. data/hallon.gemspec +1 -1
  11. data/lib/hallon.rb +3 -2
  12. data/lib/hallon/album.rb +55 -41
  13. data/lib/hallon/album_browse.rb +41 -37
  14. data/lib/hallon/artist.rb +30 -21
  15. data/lib/hallon/artist_browse.rb +59 -41
  16. data/lib/hallon/base.rb +68 -5
  17. data/lib/hallon/enumerator.rb +1 -0
  18. data/lib/hallon/error.rb +3 -0
  19. data/lib/hallon/ext/spotify.rb +169 -36
  20. data/lib/hallon/image.rb +30 -44
  21. data/lib/hallon/link.rb +29 -43
  22. data/lib/hallon/linkable.rb +68 -20
  23. data/lib/hallon/observable.rb +0 -1
  24. data/lib/hallon/player.rb +21 -7
  25. data/lib/hallon/playlist.rb +291 -0
  26. data/lib/hallon/playlist_container.rb +27 -0
  27. data/lib/hallon/search.rb +52 -45
  28. data/lib/hallon/session.rb +129 -81
  29. data/lib/hallon/toplist.rb +37 -19
  30. data/lib/hallon/track.rb +68 -45
  31. data/lib/hallon/user.rb +69 -33
  32. data/lib/hallon/version.rb +1 -1
  33. data/spec/hallon/album_browse_spec.rb +15 -9
  34. data/spec/hallon/album_spec.rb +15 -15
  35. data/spec/hallon/artist_browse_spec.rb +28 -9
  36. data/spec/hallon/artist_spec.rb +30 -14
  37. data/spec/hallon/enumerator_spec.rb +0 -1
  38. data/spec/hallon/hallon_spec.rb +20 -1
  39. data/spec/hallon/image_spec.rb +18 -41
  40. data/spec/hallon/link_spec.rb +10 -12
  41. data/spec/hallon/linkable_spec.rb +37 -18
  42. data/spec/hallon/player_spec.rb +8 -0
  43. data/spec/hallon/playlist_container_spec.rb +75 -0
  44. data/spec/hallon/playlist_spec.rb +204 -0
  45. data/spec/hallon/search_spec.rb +19 -16
  46. data/spec/hallon/session_spec.rb +61 -29
  47. data/spec/hallon/spotify_spec.rb +30 -0
  48. data/spec/hallon/toplist_spec.rb +22 -14
  49. data/spec/hallon/track_spec.rb +62 -21
  50. data/spec/hallon/user_spec.rb +47 -36
  51. data/spec/mockspotify.rb +35 -10
  52. data/spec/mockspotify/mockspotify_spec.rb +22 -0
  53. data/spec/spec_helper.rb +7 -3
  54. data/spec/support/common_objects.rb +91 -16
  55. data/spec/support/shared_for_linkable_objects.rb +39 -0
  56. metadata +30 -20
  57. data/Termfile +0 -7
  58. data/lib/hallon/synchronizable.rb +0 -32
  59. data/spec/hallon/synchronizable_spec.rb +0 -19
@@ -1,16 +1,17 @@
1
1
  # coding: utf-8
2
2
  describe Hallon::Album do
3
- subject { Hallon::Album.new(mock_album) }
3
+ it_should_behave_like "a Linkable object" do
4
+ let(:spotify_uri) { "spotify:album:1xvnWMz2PNFf7mXOSRuLws" }
5
+ end
6
+
7
+ let(:album) { Hallon::Album.new(mock_album) }
8
+ subject { album }
4
9
 
5
10
  its(:name) { should eq "Finally Woken" }
6
- its(:year) { should be 2004 }
11
+ its(:release_year) { should be 2004 }
7
12
  its(:type) { should be :single }
8
-
9
13
  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
+ mock_session { should eq Hallon::AlbumBrowse.new(album) }
14
15
  end
15
16
 
16
17
  it { should be_available }
@@ -19,27 +20,26 @@ describe Hallon::Album do
19
20
  describe "artist" do
20
21
  it "should be nil if there is no artist" do
21
22
  Spotify.should_receive(:album_artist).and_return(null_pointer)
22
- subject.artist.should be_nil
23
+ album.artist.should be_nil
23
24
  end
24
25
 
25
26
  it "should be an artist if it exists" do
26
- subject.artist.should be_a Hallon::Artist
27
+ album.artist.should eq Hallon::Artist.new(mock_artist)
27
28
  end
28
29
  end
29
30
 
30
31
  describe "cover" do
31
32
  it "should be nil if there is no image" do
32
33
  Spotify.should_receive(:album_cover).and_return(null_pointer)
33
- subject.cover.should be_nil
34
+ album.cover.should be_nil
34
35
  end
35
36
 
36
37
  it "should be an image if it exists" do
37
- FFI::MemoryPointer.new(:string, 20) do |ptr|
38
- ptr.write_string(mock_image_id)
38
+ mock_session { album.cover.id.should eq mock_image_hex }
39
+ end
39
40
 
40
- Spotify.should_receive(:album_cover).and_return(ptr)
41
- mock_session { subject.cover.id.should eq mock_image_hex }
42
- end
41
+ it "should be a link if requested" do
42
+ album.cover(false).to_str.should eq "spotify:image:3ad93423add99766e02d563605c6e76ed2b0e450"
43
43
  end
44
44
  end
45
45
 
@@ -1,22 +1,23 @@
1
1
  # coding: utf-8
2
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
3
+ let(:browse) do
4
+ artist = Hallon::Artist.new(mock_artist)
5
+ mock_session { Hallon::ArtistBrowse.new(artist) }
9
6
  end
10
7
 
8
+ subject { browse }
9
+
11
10
  it { should be_loaded }
12
- its(:error) { should eq :ok }
11
+ its(:status) { should eq :ok }
13
12
  its(:artist) { should eq Hallon::Artist.new(mock_artist) }
14
13
 
15
14
  its('portraits.size') { should eq 2 }
16
15
  its('portraits.to_a') do
17
- Hallon::Session.should_receive(:instance).exactly(2).times.and_return(session)
16
+ mock_session(2) { subject.map{ |img| img.id(true) }.should eq [mock_image_id, mock_image_id] }
17
+ end
18
18
 
19
- subject.map{ |img| img.id(true) }.should eq [mock_image_id, mock_image_id]
19
+ specify 'portraits(false)' do
20
+ browse.portraits(false)[0].should eq Hallon::Link.new(mock_image_link)
20
21
  end
21
22
 
22
23
  its('tracks.size') { should eq 2 }
@@ -26,4 +27,22 @@ describe Hallon::ArtistBrowse do
26
27
  its('similar_artists.size') { should eq 2 }
27
28
  its('similar_artists.to_a') { should eq [mock_artist, mock_artist_two].map{ |p| Hallon::Artist.new(p) } }
28
29
  its(:biography) { should eq 'grew up in DA BLOCK' }
30
+
31
+ describe "#request_duration" do
32
+ it "should return the request duration in seconds" do
33
+ browse.request_duration.should eq 2.751
34
+ end
35
+
36
+ it "should be nil if the request was fetched from local cache" do
37
+ Spotify.should_receive(:artistbrowse_backend_request_duration).and_return(-1)
38
+ browse.request_duration.should be_nil
39
+ end
40
+ end
41
+
42
+ describe '.types' do
43
+ subject { Hallon::ArtistBrowse.types }
44
+
45
+ it { should be_an Array }
46
+ it { should include :full }
47
+ end
29
48
  end
@@ -1,32 +1,48 @@
1
1
  # coding: utf-8
2
2
  describe Hallon::Artist do
3
- subject { Hallon::Artist.new(mock_artist) }
3
+ it_should_behave_like "a Linkable object" do
4
+ let(:spotify_uri) { "spotify:artist:3bftcFwl4vqRNNORRsqm1G" }
5
+ end
6
+
7
+ let(:artist) { Hallon::Artist.new(mock_artist) }
8
+ subject { artist }
4
9
 
5
10
  it { should be_loaded }
6
11
  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
12
 
11
- should eq Hallon::ArtistBrowse.new(mock_artist)
12
- end
13
+ describe "#browse" do
14
+ it "should return an artist browsing object" do
15
+ mock_session(2) { subject.browse.should eq Hallon::ArtistBrowse.new(mock_artist) }
16
+ end
13
17
 
14
- describe "#portrait" do
15
- let(:link) { Hallon::Link.new("spotify:image:c78f091482e555bd2ffacfcd9cbdc0714b221663") }
16
- let(:link_pointer) { FFI::Pointer.new(link.pointer.address) }
18
+ it "should default to full browsing" do
19
+ Hallon::ArtistBrowse.should_receive(:new).with(artist.pointer, :full)
20
+ artist.browse
21
+ end
17
22
 
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)
23
+ it "should pass the browsing type along when creating the artist browsing object" do
24
+ Hallon::ArtistBrowse.should_receive(:new).with(artist.pointer, :no_tracks)
25
+ artist.browse(:no_tracks)
21
26
  end
27
+ end
28
+
29
+ describe "#portrait" do
30
+ let(:link) { Hallon::Link.new(mock_image_uri) }
22
31
 
23
32
  specify "as an image" do
24
33
  Hallon::Session.should_receive(:instance).twice.and_return(session)
25
- subject.portrait.should eq Hallon::Image.new(link_pointer)
34
+
35
+ subject.portrait.should eq Hallon::Image.new(mock_image_id)
26
36
  end
27
37
 
28
38
  specify "as a link" do
29
- subject.portrait(false).should eq link
39
+ subject.portrait(false).should eq Hallon::Link.new(mock_image_uri)
40
+ end
41
+
42
+ it "should be nil if an image is not available" do
43
+ Spotify.should_receive(:artist_portrait).and_return(null_pointer)
44
+
45
+ subject.portrait.should be_nil
30
46
  end
31
47
  end
32
48
  end
@@ -19,7 +19,6 @@ describe Hallon::Enumerator do
19
19
 
20
20
  describe "#each" do
21
21
  it "should call the containing block" do
22
- calls = []
23
22
  enum = Hallon::Enumerator.new(4) { |i| item.get(i) }
24
23
  enum.each_with_index { |x, i| x.should eq alphabet[i] }
25
24
  end
@@ -1,10 +1,11 @@
1
+ # coding: utf-8
1
2
  describe Hallon do
2
3
  describe "VERSION" do
3
4
  specify { Hallon::VERSION.should be_a String }
4
5
  end
5
6
 
6
7
  describe "API_VERSION" do
7
- specify { Hallon::API_VERSION.should == 9 }
8
+ specify { Hallon::API_VERSION.should == 10 }
8
9
  end
9
10
 
10
11
  describe "API_BUILD" do
@@ -17,4 +18,22 @@ describe Hallon do
17
18
  it { should match uri }
18
19
  end
19
20
  end
21
+
22
+ describe "object callbacks" do
23
+ pending <<-REASON
24
+
25
+ Once callbacks are implemented in libmockspotify, we should also
26
+ test them on the following objects:
27
+
28
+ - Session
29
+ - Image
30
+ - AlbumBrowse
31
+ - ArtistBrowse
32
+ - Search
33
+ - Playlist
34
+ - PlaylistContainer
35
+ - Toplist
36
+ - Inbox
37
+ REASON
38
+ end
20
39
  end
@@ -1,8 +1,25 @@
1
1
  # coding: utf-8
2
+ require 'ostruct'
3
+
2
4
  describe Hallon::Image do
5
+ it_should_behave_like "a Linkable object" do
6
+ let(:spotify_uri) { "spotify:image:#{mock_image_hex}" }
7
+ let(:custom_object) { mock_image_hex }
8
+
9
+
10
+ let(:described_class) do
11
+ real_session = session
12
+ Hallon::Image.dup.tap do |klass|
13
+ klass.class_eval do
14
+ define_method(:session) { real_session }
15
+ end
16
+ end
17
+ end
18
+ end
19
+
3
20
  describe "an image instance" do
4
21
  subject { image }
5
- let(:image) { mock_session { Hallon::Image.new(mock_image) } }
22
+ let(:image) { Hallon::Image.new(mock_image) }
6
23
 
7
24
  it { should be_loaded }
8
25
  its(:status) { should be :ok }
@@ -21,7 +38,6 @@ describe Hallon::Image do
21
38
  end
22
39
 
23
40
  it "should have a binary encoding" do
24
- pending "ruby 1.8 does not support String#encoding" unless subject.respond_to?(:encoding)
25
41
  subject.encoding.name.should eq 'ASCII-8BIT'
26
42
  end
27
43
  end
@@ -43,43 +59,4 @@ describe Hallon::Image do
43
59
  end
44
60
  end
45
61
  end
46
-
47
- describe "instantiation" do
48
- subject do
49
- mock_session do
50
- Hallon::Image.new(image_uri)
51
- end
52
- end
53
-
54
- context "created from an url" do
55
- let(:image_uri) { "http://open.spotify.com/image/c78f091482e555bd2ffacfcd9cbdc0714b221663" }
56
- its(:id) { should eq "c78f091482e555bd2ffacfcd9cbdc0714b221663" }
57
- end
58
-
59
- context "created from an uri" do
60
- let(:image_uri) { "spotify:image:c78f091482e555bd2ffacfcd9cbdc0714b221663" }
61
- its(:id) { should eq "c78f091482e555bd2ffacfcd9cbdc0714b221663" }
62
- end
63
-
64
- context "created from an id" do
65
- let(:image_uri) { mock_image_id }
66
- its(:id) { should eq mock_image_hex }
67
- end
68
-
69
- context "created from a link" do
70
- let(:image_uri) { Hallon::Link.new("spotify:image:c78f091482e555bd2ffacfcd9cbdc0714b221663") }
71
- its(:id) { should eq "c78f091482e555bd2ffacfcd9cbdc0714b221663" }
72
- end
73
- end
74
-
75
- describe "callbacks" do
76
- it "should trigger :load when loaded", :pending => true do
77
- uri = "spotify:image:c78f091482e555bd2ffacfcd9cbdc0714b221663"
78
- image = Hallon::Image.new(uri)
79
- image.should_not be_loaded
80
- image.should_receive(:trigger).with(:load).once
81
-
82
- session.process_events_on { image.loaded? }
83
- end
84
- end
85
62
  end
@@ -2,35 +2,33 @@ describe Hallon::Link do
2
2
  subject { Hallon::Link.new("spotify:user:burgestrand") }
3
3
 
4
4
  context "class methods" do
5
- subject { described_class }
6
-
7
5
  describe "::new" do
8
6
  it "should raise an ArgumentError on an invalid link" do
9
- expect { subject.new("omgwtfbbq") }.to raise_error(ArgumentError, /omgwtfbbq/)
7
+ expect { Hallon::Link.new("omgwtfbbq") }.to raise_error(ArgumentError, /omgwtfbbq/)
10
8
  end
11
9
 
12
10
  it "should not raise error on valid links" do
13
- expect { subject.new("spotify:user:burgestrand") }.to_not raise_error
11
+ expect { Hallon::Link.new("spotify:user:burgestrand") }.to_not raise_error
14
12
  end
15
13
 
16
14
  it "should accept an FFI pointer" do
17
- expect { subject.new FFI::Pointer.new(0) }.to raise_error(ArgumentError, /is not a valid Spotify link/)
15
+ expect { Hallon::Link.new(Spotify::Pointer.new(null_pointer, :link, false)) }.to raise_error(ArgumentError, /is not a valid spotify link/)
18
16
  end
19
17
 
20
- it "should not initialize when given a Link" do
21
- link = subject.new('spotify:user:burgestrand')
22
- link.should_not_receive :to_str
23
- subject.new link
18
+ it "should raise an error when no session instance is about" do
19
+ # this is due to a bug in libspotify, it will segfault otherwise
20
+ Hallon::Session.stub(:instance?).and_return(false)
21
+ expect { Hallon::Link.new("spotify:user:burgestrand") }.to raise_error(/session/i)
24
22
  end
25
23
  end
26
24
 
27
25
  describe "::valid?" do
28
26
  it "should be true for a valid link" do
29
- subject.valid?("spotify:user:burgestrand").should be_true
27
+ Hallon::Link.valid?("spotify:user:burgestrand").should be_true
30
28
  end
31
29
 
32
30
  it "should be false for an invalid link" do
33
- subject.valid?("omgwtfbbq").should be_false
31
+ Hallon::Link.valid?("omgwtfbbq").should be_false
34
32
  end
35
33
  end
36
34
  end
@@ -84,7 +82,7 @@ describe Hallon::Link do
84
82
  end
85
83
 
86
84
  it "should compare underlying pointers if #to_str is unavailable" do
87
- object = Hallon::Link.new(FFI::Pointer.new(subject.pointer))
85
+ object = Hallon::Link.new(subject.pointer)
88
86
 
89
87
  def object.respond_to?(o)
90
88
  return false if o == :to_str
@@ -10,40 +10,59 @@ describe Hallon::Linkable do
10
10
  let(:object) { klass.new }
11
11
  let(:pointer) { FFI::Pointer.new(1) }
12
12
 
13
- before(:each) { Spotify.stub(:link_as_search) }
13
+ before(:each) { Spotify.stub(:link_as_search!) }
14
14
 
15
15
  it "should define the #from_link method" do
16
- object.should_not respond_to :from_link
17
- klass.from_link(:as_search)
18
- object.should respond_to :from_link
16
+ object.respond_to?(:from_link, true).should be_false
17
+
18
+ klass.instance_eval do
19
+ from_link(:as_search)
20
+ end
21
+
22
+ object.respond_to?(:from_link, true).should be_true
19
23
  end
20
24
 
21
25
  describe "#from_link" do
22
26
  it "should call the appropriate Spotify function" do
23
- Spotify.should_receive(:link_as_search).and_return(pointer)
27
+ Spotify.should_receive(:link_as_search!).and_return(pointer)
28
+
29
+ klass.instance_eval do
30
+ from_link(:as_search)
31
+ end
24
32
 
25
- klass.from_link(:as_search)
26
- object.from_link 'spotify:search:moo'
33
+ object.send(:from_link, 'spotify:search:moo')
27
34
  end
28
35
 
29
36
  it "should call the given block if necessary" do
30
- Spotify.should_not_receive(:link_as_search)
37
+ Spotify.should_not_receive(:link_as_search!)
38
+
39
+ called = false
40
+ pointer = double(:null? => false)
31
41
 
32
- called = false
33
- klass.from_link(:as_search) { called = true and pointer }
34
- expect { object.from_link 'spotify:search:whatever' }.to change { called }
42
+ klass.instance_eval do
43
+ from_link(:as_search) do
44
+ called = true
45
+ pointer
46
+ end
47
+ end
48
+
49
+ expect { object.send(:from_link, 'spotify:search:whatever') }.to change { called }
35
50
  end
36
51
 
37
52
  it "should pass extra parameters to the defining block" do
38
53
  passed_args = nil
39
- klass.from_link(:search) { |link, *args| passed_args = args and pointer }
40
- object.from_link("spotify:search:burgestrand", :cool, 5)
41
- passed_args.should eq [:cool, 5]
42
- end
43
54
 
44
- it "should fail, given a null pointer" do
45
- klass.from_link(:as_search)
46
- expect { object.from_link FFI::Pointer.new(0) }.to raise_error(Hallon::Error)
55
+ pointer = double(:null? => false)
56
+
57
+ klass.instance_eval do
58
+ from_link(:search) do |link, *args|
59
+ passed_args = args
60
+ pointer
61
+ end
62
+ end
63
+
64
+ object.send(:from_link, "spotify:search:burgestrand", :cool, 5)
65
+ passed_args.should eq [:cool, 5]
47
66
  end
48
67
  end
49
68
  end
@@ -70,4 +70,12 @@ describe Hallon::Player do
70
70
  player.seek(1)
71
71
  end
72
72
  end
73
+
74
+ describe "#volume_normalization" do
75
+ it "should be settable and gettable" do
76
+ player.volume_normalization?.should be_false
77
+ player.volume_normalization = true
78
+ player.volume_normalization?.should be_true
79
+ end
80
+ end
73
81
  end