hallon 0.8.0 → 0.9.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 (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