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,3 +1,4 @@
1
+ # coding: utf-8
1
2
  module Hallon
2
3
  # Toplists are what they sound like. They’re collections of
3
4
  # artists, albums or tracks popular in a certain area either
@@ -13,6 +14,15 @@ module Hallon
13
14
  # @overload initialize(type, country)
14
15
  # @overload initialize(type)
15
16
  #
17
+ # @example with a given username
18
+ # toplist = Hallon::Toplist.new(:artists, "burgestrand")
19
+ #
20
+ # @example with a given country
21
+ # toplist = Hallon::Toplist.new(:tracks, :se)
22
+ #
23
+ # @example everywhere
24
+ # toplist = Hallon::Toplist.new(:albums)
25
+ #
16
26
  # @param [Symbol] type one of :artists, :albums or :tracks
17
27
  # @param [String, Symbol, nil] region username, 2-letter country code or nil
18
28
  def initialize(type, region = nil)
@@ -21,53 +31,61 @@ module Hallon
21
31
  user = region
22
32
  region = :user
23
33
  when NilClass
24
- region = :anywhere
34
+ region = :everywhere
25
35
  when Symbol
26
36
  region = to_country(region)
27
37
  end
28
38
 
29
39
  @callback = proc { trigger(:load) }
30
- pointer = Spotify.toplistbrowse_create(session.pointer, type, region, user, @callback, nil)
31
- @pointer = Spotify::Pointer.new(pointer, :toplistbrowse, false)
40
+ @pointer = Spotify.toplistbrowse_create!(session.pointer, type, region, user, @callback, nil)
32
41
  end
33
42
 
34
- # @return [Boolean] true if the toplist is loaded
43
+ # @return [Boolean] true if the toplist is loaded.
35
44
  def loaded?
36
- Spotify.toplistbrowse_is_loaded(@pointer)
45
+ Spotify.toplistbrowse_is_loaded(pointer)
37
46
  end
38
47
 
39
- # @return [Symbol] toplist error status
40
- def error
41
- Spotify.toplistbrowse_error(@pointer)
48
+ # @see Error.explain
49
+ # @return [Symbol] toplist error status.
50
+ def status
51
+ Spotify.toplistbrowse_error(pointer)
42
52
  end
43
53
 
44
- # @return [Enumerator<Artist>]
54
+ # @return [Enumerator<Artist>] a list of artists.
45
55
  def artists
46
- size = Spotify.toplistbrowse_num_artists(@pointer)
56
+ size = Spotify.toplistbrowse_num_artists(pointer)
47
57
  Enumerator.new(size) do |i|
48
- artist = Spotify.toplistbrowse_artist(@pointer, i)
58
+ artist = Spotify.toplistbrowse_artist!(pointer, i)
49
59
  Artist.new(artist)
50
60
  end
51
61
  end
52
62
 
53
- # @return [Enumerator<Album>]
63
+ # @return [Enumerator<Album>] a list of albums.
54
64
  def albums
55
- size = Spotify.toplistbrowse_num_albums(@pointer)
65
+ size = Spotify.toplistbrowse_num_albums(pointer)
56
66
  Enumerator.new(size) do |i|
57
- album = Spotify.toplistbrowse_album(@pointer, i)
58
- Artist.new(album)
67
+ album = Spotify.toplistbrowse_album!(pointer, i)
68
+ Album.new(album)
59
69
  end
60
70
  end
61
71
 
62
- # @return [Enumerator<Track>]
72
+ # @return [Enumerator<Track>] a list of tracks.
63
73
  def tracks
64
- size = Spotify.toplistbrowse_num_tracks(@pointer)
74
+ size = Spotify.toplistbrowse_num_tracks(pointer)
65
75
  Enumerator.new(size) do |i|
66
- track = Spotify.toplistbrowse_track(@pointer, i)
67
- Artist.new(track)
76
+ track = Spotify.toplistbrowse_track!(pointer, i)
77
+ Track.new(track)
68
78
  end
69
79
  end
70
80
 
81
+ # @note If the object is not loaded, the result is undefined.
82
+ # @note Returns nil if the request was served from the local libspotify cache.
83
+ # @return [Rational, nil] time it took for the toplistbrowse request to complete (in seconds).
84
+ def request_duration
85
+ duration = Spotify.toplistbrowse_backend_request_duration(pointer)
86
+ Rational(duration, 1000) if duration > 0
87
+ end
88
+
71
89
  private
72
90
  # Convert a given two-character region to a Spotify
73
91
  # compliant region (encoded in a 16bit integer).
data/lib/hallon/track.rb CHANGED
@@ -13,12 +13,13 @@ module Hallon
13
13
  # Overriden to use default parameter.
14
14
  # @see #to_link
15
15
  alias_method :_to_link, :to_link
16
+
16
17
  # Create a Link to the current track and offset in seconds.
17
18
  #
18
19
  # @param [Float] offset offset into track in seconds
19
20
  # @return [Hallon::Link]
20
21
  def to_link(offset = offset)
21
- _to_link (offset * 1000).to_i
22
+ _to_link((offset * 1000).to_i)
22
23
  end
23
24
 
24
25
  # Offset into track in seconds this track was created with.
@@ -31,7 +32,7 @@ module Hallon
31
32
  # @param [String, Link, FFI::Pointer] link
32
33
  def initialize(link)
33
34
  FFI::MemoryPointer.new(:int) do |ptr|
34
- @pointer = Spotify::Pointer.new from_link(link, ptr), :track, true
35
+ @pointer = to_pointer(link, :track, ptr)
35
36
  @offset = Rational(ptr.read_int, 1000)
36
37
  end
37
38
  end
@@ -44,14 +45,14 @@ module Hallon
44
45
  # @param [Integer] length
45
46
  # @return [Track]
46
47
  def self.local(title, artist, album = nil, length = nil)
47
- track = Spotify.localtrack_create(artist, title, album || "", length || -1)
48
+ track = Spotify.localtrack_create!(artist, title, album || "", length || -1)
48
49
  new(track)
49
50
  end
50
51
 
51
52
  # @note This’ll be an empty string unless the track is loaded.
52
53
  # @return [String]
53
54
  def name
54
- Spotify.track_name(@pointer)
55
+ Spotify.track_name(pointer)
55
56
  end
56
57
 
57
58
  # Duration of the track in seconds.
@@ -59,7 +60,7 @@ module Hallon
59
60
  # @note This’ll be `0` unless the track is loaded.
60
61
  # @return [Rational]
61
62
  def duration
62
- Rational(Spotify.track_duration(@pointer), 1000)
63
+ Rational(Spotify.track_duration(pointer), 1000)
63
64
  end
64
65
 
65
66
  # Track popularity, between 0 and 1.
@@ -67,97 +68,119 @@ module Hallon
67
68
  # @note This’ll be `0` unless the track is loaded.
68
69
  # @return [Rational]
69
70
  def popularity
70
- Rational(Spotify.track_popularity(@pointer), 100)
71
+ Rational(Spotify.track_popularity(pointer), 100)
71
72
  end
72
73
 
73
74
  # Disc number this track appears in.
74
75
  #
75
76
  # @note This function is a bit special. See libspotify docs for details.
76
77
  def disc
77
- Spotify.track_disc(@pointer)
78
+ Spotify.track_disc(pointer)
78
79
  end
79
80
 
80
- # Position of track on its’ disc.
81
- #
82
81
  # @note This function is a bit special. See libspotify docs for details.
82
+ # @return [Integer] position of track on its’ disc.
83
83
  def index
84
- Spotify.track_index(@pointer)
84
+ Spotify.track_index(pointer)
85
85
  end
86
86
 
87
- # Retrieve track error status.
88
- #
89
- # @return [Symbol]
87
+ # @see Error.explain
88
+ # @return [Symbol] track error status.
90
89
  def status
91
- Spotify.track_error(@pointer)
90
+ Spotify.track_error(pointer)
92
91
  end
93
92
 
94
- # True if the track is loaded
95
- #
96
- # @return [Boolean]
93
+ # @return [Boolean] true if track is loaded.
97
94
  def loaded?
98
- Spotify.track_is_loaded(@pointer)
95
+ Spotify.track_is_loaded(pointer)
99
96
  end
100
97
 
101
- # Album this track belongs to.
98
+ # @note Track does not have to be loaded for this to return a useful value.
99
+ #
100
+ # @note Placeholder tracks are not really tracks, but merely containers
101
+ # for other objects to allow storing them in playlists such as the inbox.
102
102
  #
103
+ # @return [Boolean] true if the track is a placeholder.
104
+ # @see unwrap
105
+ def placeholder?
106
+ Spotify.track_is_placeholder(pointer)
107
+ end
108
+
109
+ # Unwraps a {#placeholder?} Track into its’ real object.
110
+ #
111
+ # @see placeholder?
112
+ # @return [Track, Artist, Album, Playlist]
113
+ def unwrap
114
+ return self unless placeholder?
115
+
116
+ case (link = to_link).type
117
+ when :playlist
118
+ Playlist.new(link)
119
+ when :album
120
+ Album.new(link)
121
+ when :artist
122
+ Artist.new(link)
123
+ end
124
+ end
125
+
126
+ # @return [Symbol] track offline status.
127
+ def offline_status
128
+ Spotify.track_offline_get_status(pointer)
129
+ end
130
+
103
131
  # @note This’ll be `nil` unless the track is loaded.
104
- # @return [Hallon::Album]
132
+ # @return [Hallon::Album] album this track belongs to.
105
133
  def album
106
- album = Spotify.track_album(@pointer)
134
+ album = Spotify.track_album!(pointer)
107
135
  Album.new(album) unless album.null?
108
136
  end
109
137
 
110
- # Artist who performed this Track.
111
- #
112
138
  # @note There may be more than one artist, see {#artists} for retrieving them all!
113
139
  # @see #artists
114
- # @return [Hallon::Artist, nil]
140
+ # @return [Hallon::Artist, nil] artist who performed this track.
115
141
  def artist
116
142
  artists.first
117
143
  end
118
144
 
119
- # All {Artist}s who performed this Track.
120
- #
121
145
  # @note Track must be loaded, or you’ll get zero artists.
122
- # @return [Hallon::Enumerator<Artist>]
146
+ # @return [Hallon::Enumerator<Artist>] all {Artist}s who performed this Track.
123
147
  def artists
124
- size = Spotify.track_num_artists(@pointer)
148
+ size = Spotify.track_num_artists(pointer)
125
149
  Enumerator.new(size) do |i|
126
- artist = Spotify.track_artist(@pointer, i)
127
- Artist.new(artist) unless artist.null?
150
+ artist = Spotify.track_artist!(pointer, i)
151
+ Artist.new(artist)
128
152
  end
129
153
  end
130
154
 
131
- # True if the Track is available.
132
- #
133
155
  # @note This’ll always return false unless the track is loaded.
134
- # @return [Boolean]
156
+ # @return [Boolean] true if {#availability} is available.
135
157
  def available?
136
- Spotify.track_is_available(session.pointer, @pointer)
158
+ availability == :available
137
159
  end
138
160
 
139
- # True if the Track is a local track.
161
+ # Track availability.
140
162
  #
163
+ # @return [Symbol] :unavailable, :available, :not_streamable, :banned_by_artist
164
+ def availability
165
+ Spotify.track_get_availability(session.pointer, pointer)
166
+ end
167
+
141
168
  # @note This’ll always return false unless the track is loaded.
142
- # @return [Boolean]
169
+ # @return [Boolean] true if the track is a local track.
143
170
  def local?
144
- Spotify.track_is_local(session.pointer, @pointer)
171
+ Spotify.track_is_local(session.pointer, pointer)
145
172
  end
146
173
 
147
- # True if the Track is autolinked.
148
- #
149
174
  # @note This’ll always return false unless the track is loaded.
150
- # @return [Boolean]
175
+ # @return [Boolean] true if the track is autolinked.
151
176
  def autolinked?
152
- Spotify.track_is_autolinked(session.pointer, @pointer)
177
+ Spotify.track_is_autolinked(session.pointer, pointer)
153
178
  end
154
179
 
155
- # True if the track is starred.
156
- #
157
180
  # @note This’ll always return false unless the track is loaded.
158
- # @return [Boolean]
181
+ # @return [Boolean] true if the track is starred.
159
182
  def starred?
160
- Spotify.track_is_starred(session.pointer, @pointer)
183
+ Spotify.track_is_starred(session.pointer, pointer)
161
184
  end
162
185
 
163
186
  # Set {#starred?} status of current track.
data/lib/hallon/user.rb CHANGED
@@ -9,60 +9,96 @@ module Hallon
9
9
  class User < Base
10
10
  extend Linkable
11
11
 
12
- # @macro [attach] from_link
13
- # Given a Link, get its’ underlying pointer.
12
+ # A Post is created upon sending tracks (with an optional message) to a user.
14
13
  #
15
- # @method to_link
16
- # @scope instance
17
- # @param [String, Hallon::Link, FFI::Pointer] link
18
- # @return [FFI::Pointer]
14
+ # @see http://developer.spotify.com/en/libspotify/docs/group__inbox.html
15
+ class Post < Base
16
+ include Observable
17
+
18
+ # @param [Spotify::Pointer<inbox>]
19
+ def initialize(username, message, tracks, &block)
20
+ @callback = proc { trigger(:load) }
21
+
22
+ FFI::MemoryPointer.new(:pointer, tracks.length) do |ary|
23
+ ary.write_array_of_pointer tracks.map(&:pointer)
24
+ @pointer = Spotify.inbox_post_tracks!(session.pointer, username, ary, tracks.length, message, @callback, nil)
25
+ end
26
+ end
27
+
28
+ # @see Error.explain
29
+ # @return [Symbol] error status of inbox post
30
+ def status
31
+ Spotify.inbox_error(pointer)
32
+ end
33
+ end
34
+
19
35
  from_link :profile do |link|
20
- Spotify.link_as_user(link)
36
+ Spotify.link_as_user!(link)
21
37
  end
22
38
 
23
- # @macro [attach] to_link
24
- # Create a Link to the current object.
25
- #
26
- # @method to_link
27
- # @scope instance
28
- # @return [Hallon::Link]
29
39
  to_link :from_user
30
40
 
31
41
  # Construct a new instance of User.
32
42
  #
33
- # @param [String, Link, FFI::Pointer] link
43
+ # @example from a canonical username
44
+ # Hallon::User.new("burgestrand")
45
+ #
46
+ # @example from a spotify URI
47
+ # Hallon::User.new("spotify:user:burgestrand")
48
+ #
49
+ # @note You can also instantiate User with a canonical username
50
+ # @param [String, Link, Spotify::Pointer] link
34
51
  def initialize(link)
35
- @pointer = Spotify::Pointer.new from_link(link), :user, true
52
+ @pointer = to_pointer(link, :user) do
53
+ if link.is_a?(String) and link !~ /\Aspotify:user:/
54
+ to_pointer("spotify:user:#{link}", :user)
55
+ end
56
+ end
36
57
  end
37
58
 
38
59
  # @return [Boolean] true if the user is loaded
39
60
  def loaded?
40
- Spotify.user_is_loaded(@pointer)
61
+ Spotify.user_is_loaded(pointer)
41
62
  end
42
63
 
43
- # Retrieve the name of the current user.
64
+ # Retrieve the canonical name of the User.
44
65
  #
45
- # @note Unless the user is {User#loaded?} only the canonical name is accessible
46
- # @param [Symbol] type one of :canonical, :display, :full
47
66
  # @return [String]
48
- def name(type = :canonical)
49
- case type
50
- when :display
51
- Spotify.user_display_name(@pointer)
52
- when :full
53
- Spotify.user_full_name(@pointer)
54
- when :canonical
55
- Spotify.user_canonical_name(@pointer)
56
- else
57
- raise ArgumentError, "expected type to be :display, :full or :canonical, but was #{type}"
58
- end.to_s
67
+ def name
68
+ Spotify.user_canonical_name(pointer)
59
69
  end
60
70
 
61
- # Retrieve the URL to the users’ profile picture.
71
+ # Retrieve the dispaly name of the User.
62
72
  #
73
+ # @note Unless {#loaded?} is true, this will return the same thing as {#name}.
63
74
  # @return [String]
64
- def picture
65
- Spotify.user_picture(@pointer).to_s
75
+ def display_name
76
+ Spotify.user_display_name(pointer)
77
+ end
78
+
79
+ # Retrieve the users’ starred playlist.
80
+ #
81
+ # @note Returns nil unless {User#loaded?}
82
+ # @return [Playlist, nil]
83
+ def starred
84
+ playlist = Spotify.session_starred_for_user_create!(session.pointer, name)
85
+ Playlist.new(playlist) unless playlist.null?
86
+ end
87
+
88
+ # Send tracks to this users’ inbox, with an optional message.
89
+ #
90
+ # @overload post(message, tracks)
91
+ # @param [#to_s] message
92
+ # @param [Array<Track>] tracks
93
+ #
94
+ # @overload post(tracks)
95
+ # @param [Array<Track>] tracks
96
+ #
97
+ # @return [Post, nil]
98
+ def post(message = nil, tracks)
99
+ message &&= message.encode('UTF-8')
100
+ post = Post.new(name, message, tracks)
101
+ post unless post.pointer.null?
66
102
  end
67
103
  end
68
104
  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, 8, 0].join('.')
6
+ VERSION = [0, 9, 0].join('.')
7
7
  end
@@ -1,20 +1,26 @@
1
1
  # coding: utf-8
2
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
3
+ let(:browse) { mock_session { Hallon::AlbumBrowse.new(mock_album) } }
4
+ subject { browse }
10
5
 
11
6
  it { should be_loaded }
12
- its(:error) { should eq :ok }
7
+ its(:status) { should eq :ok }
13
8
  its(:album) { should eq Hallon::Album.new(mock_album) }
14
9
  its(:artist) { should eq Hallon::Artist.new(mock_artist) }
15
10
  its('copyrights.size') { should eq 2 }
16
11
  its('copyrights.to_a') { should eq %w[Kim Elin] }
17
12
  its('tracks.size') { should eq 2 }
18
- its('tracks.to_a') { should eq [mock_track, mock_track_two].map{ |p| Hallon::Track.new(p) } }
13
+ its('tracks.to_a') { should eq instantiate(Hallon::Track, mock_track, mock_track_two) }
19
14
  its(:review) { should eq "This album is AWESOME" }
15
+
16
+ describe "#request_duration" do
17
+ it "should return the request duration in seconds" do
18
+ browse.request_duration.should eq 2.751
19
+ end
20
+
21
+ it "should be nil if the request was fetched from local cache" do
22
+ Spotify.should_receive(:albumbrowse_backend_request_duration).and_return(-1)
23
+ browse.request_duration.should be_nil
24
+ end
25
+ end
20
26
  end