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.
- data/.travis.yml +2 -0
- data/CHANGELOG +43 -0
- data/Gemfile +2 -0
- data/README.markdown +21 -13
- data/Rakefile +84 -23
- data/dev/login.rb +16 -0
- data/examples/adding_tracks_to_playlist.rb +49 -0
- data/examples/logging_in.rb +1 -6
- data/examples/show_published_playlists_of_user.rb +9 -19
- data/hallon.gemspec +1 -1
- data/lib/hallon.rb +3 -2
- data/lib/hallon/album.rb +55 -41
- data/lib/hallon/album_browse.rb +41 -37
- data/lib/hallon/artist.rb +30 -21
- data/lib/hallon/artist_browse.rb +59 -41
- data/lib/hallon/base.rb +68 -5
- data/lib/hallon/enumerator.rb +1 -0
- data/lib/hallon/error.rb +3 -0
- data/lib/hallon/ext/spotify.rb +169 -36
- data/lib/hallon/image.rb +30 -44
- data/lib/hallon/link.rb +29 -43
- data/lib/hallon/linkable.rb +68 -20
- data/lib/hallon/observable.rb +0 -1
- data/lib/hallon/player.rb +21 -7
- data/lib/hallon/playlist.rb +291 -0
- data/lib/hallon/playlist_container.rb +27 -0
- data/lib/hallon/search.rb +52 -45
- data/lib/hallon/session.rb +129 -81
- data/lib/hallon/toplist.rb +37 -19
- data/lib/hallon/track.rb +68 -45
- data/lib/hallon/user.rb +69 -33
- data/lib/hallon/version.rb +1 -1
- data/spec/hallon/album_browse_spec.rb +15 -9
- data/spec/hallon/album_spec.rb +15 -15
- data/spec/hallon/artist_browse_spec.rb +28 -9
- data/spec/hallon/artist_spec.rb +30 -14
- data/spec/hallon/enumerator_spec.rb +0 -1
- data/spec/hallon/hallon_spec.rb +20 -1
- data/spec/hallon/image_spec.rb +18 -41
- data/spec/hallon/link_spec.rb +10 -12
- data/spec/hallon/linkable_spec.rb +37 -18
- data/spec/hallon/player_spec.rb +8 -0
- data/spec/hallon/playlist_container_spec.rb +75 -0
- data/spec/hallon/playlist_spec.rb +204 -0
- data/spec/hallon/search_spec.rb +19 -16
- data/spec/hallon/session_spec.rb +61 -29
- data/spec/hallon/spotify_spec.rb +30 -0
- data/spec/hallon/toplist_spec.rb +22 -14
- data/spec/hallon/track_spec.rb +62 -21
- data/spec/hallon/user_spec.rb +47 -36
- data/spec/mockspotify.rb +35 -10
- data/spec/mockspotify/mockspotify_spec.rb +22 -0
- data/spec/spec_helper.rb +7 -3
- data/spec/support/common_objects.rb +91 -16
- data/spec/support/shared_for_linkable_objects.rb +39 -0
- metadata +30 -20
- data/Termfile +0 -7
- data/lib/hallon/synchronizable.rb +0 -32
- data/spec/hallon/synchronizable_spec.rb +0 -19
data/lib/hallon/link.rb
CHANGED
@@ -6,92 +6,78 @@ module Hallon
|
|
6
6
|
class Link < Base
|
7
7
|
# True if the given Spotify URI is valid (parsable by libspotify).
|
8
8
|
#
|
9
|
-
# @param
|
9
|
+
# @param [#to_s] spotify_uri
|
10
10
|
# @return [Boolean]
|
11
11
|
def self.valid?(spotify_uri)
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
end
|
12
|
+
if spotify_uri.is_a?(Link)
|
13
|
+
return true
|
14
|
+
end
|
16
15
|
|
17
|
-
|
18
|
-
|
19
|
-
# @return [Hallon::Link]
|
20
|
-
def self.new(uri)
|
21
|
-
uri.is_a?(Link) ? uri : super
|
16
|
+
link = Spotify.link_create_from_string!(spotify_uri.to_s)
|
17
|
+
not link.null?
|
22
18
|
end
|
23
19
|
|
24
20
|
# Parse the given Spotify URI into a Link.
|
25
21
|
#
|
26
|
-
# @note
|
22
|
+
# @note You must initialize a Session before you call this method.
|
27
23
|
# @param [#to_str] uri
|
28
24
|
# @raise [ArgumentError] link could not be parsed
|
29
25
|
def initialize(uri)
|
30
|
-
if
|
31
|
-
|
26
|
+
# if no session instance exists, libspotify segfaults, so assert that we have one
|
27
|
+
unless Session.instance?
|
28
|
+
raise "Link.new requires an existing Session instance"
|
32
29
|
end
|
33
30
|
|
34
|
-
@pointer =
|
35
|
-
|
36
|
-
|
31
|
+
@pointer = to_pointer(uri, :link) do
|
32
|
+
Spotify.link_create_from_string!(uri.to_str)
|
33
|
+
end
|
37
34
|
end
|
38
35
|
|
39
|
-
#
|
40
|
-
#
|
41
|
-
# @return [Symbol]
|
36
|
+
# @return [Symbol] link type as a symbol (e.g. `:playlist`).
|
42
37
|
def type
|
43
|
-
Spotify.link_type(
|
38
|
+
Spotify.link_type(pointer)
|
44
39
|
end
|
45
40
|
|
46
|
-
#
|
47
|
-
#
|
48
|
-
# @return [Fixnum]
|
41
|
+
# @return [Fixnum] spotify URI length.
|
49
42
|
def length
|
50
|
-
Spotify.link_as_string(
|
43
|
+
Spotify.link_as_string(pointer, nil, 0)
|
51
44
|
end
|
52
45
|
|
53
|
-
# Get the Spotify URI this Link represents.
|
54
|
-
#
|
55
46
|
# @see #length
|
56
47
|
# @param [Fixnum] length truncate to this size
|
57
|
-
# @return [String]
|
48
|
+
# @return [String] spotify URI representation of this Link.
|
58
49
|
def to_str(length = length)
|
59
50
|
FFI::Buffer.alloc_out(length + 1) do |b|
|
60
|
-
Spotify.link_as_string(
|
51
|
+
Spotify.link_as_string(pointer, b, b.size)
|
61
52
|
return b.get_string(0)
|
62
53
|
end
|
63
54
|
end
|
64
55
|
|
65
|
-
|
66
|
-
|
67
|
-
# @return [String]
|
56
|
+
alias_method :to_uri, :to_str
|
57
|
+
|
58
|
+
# @return [String] full Spotify HTTP URL.
|
68
59
|
def to_url
|
69
60
|
"http://open.spotify.com/%s" % to_str[8..-1].gsub(':', '/')
|
70
61
|
end
|
71
62
|
|
72
|
-
#
|
73
|
-
#
|
74
|
-
# @param [#to_str] other
|
75
|
-
# @return [Boolean]
|
63
|
+
# @param [Object] other
|
64
|
+
# @return [Boolean] true if this link equals `other.to_str`.
|
76
65
|
def ==(other)
|
77
|
-
return super unless other.respond_to?(:to_str)
|
78
66
|
to_str == other.to_str
|
67
|
+
rescue NoMethodError
|
68
|
+
super
|
79
69
|
end
|
80
70
|
|
81
|
-
# String representation of the
|
82
|
-
#
|
83
|
-
# @return [String]
|
71
|
+
# @return [String] string representation of the Link.
|
84
72
|
def to_s
|
85
73
|
"<#{self.class.name} #{to_str}>"
|
86
74
|
end
|
87
75
|
|
88
|
-
# Retrieve the underlying pointer. Used by {Linkable}.
|
89
|
-
#
|
90
76
|
# @param [Symbol] expected_type if given, makes sure the link is of this type
|
91
|
-
# @return [
|
77
|
+
# @return [Spotify::Pointer] the underlying Spotify::Pointer.
|
92
78
|
# @raise ArgumentError if `type` is given and does not match link {#type}
|
93
79
|
def pointer(expected_type = nil)
|
94
|
-
unless type == expected_type
|
80
|
+
unless type == expected_type or (expected_type == :playlist and type == :starred)
|
95
81
|
raise ArgumentError, "expected #{expected_type} link, but it is of type #{type}"
|
96
82
|
end if expected_type
|
97
83
|
super()
|
data/lib/hallon/linkable.rb
CHANGED
@@ -3,58 +3,106 @@ module Hallon
|
|
3
3
|
# Methods shared between objects that can be created from Spotify URIs,
|
4
4
|
# or can be turned into Spotify URIs.
|
5
5
|
#
|
6
|
-
# @note Linkable is
|
6
|
+
# @note Linkable is part of Hallons’ private API. You probably do not
|
7
|
+
# not need to care about these methods.
|
8
|
+
#
|
7
9
|
# @private
|
8
10
|
module Linkable
|
9
|
-
# Defines `#from_link`, used in converting a link to a pointer.
|
11
|
+
# Defines `#from_link`, used in converting a link to a pointer. You
|
12
|
+
# can either pass it a `method_name`, or a `type` and a block.
|
10
13
|
#
|
11
|
-
# @overload from_link(
|
12
|
-
#
|
14
|
+
# @overload from_link(method_name)
|
15
|
+
# Define `#from_link` simply by giving the name of the method,
|
16
|
+
# minus the `link_` prefix.
|
13
17
|
#
|
14
18
|
# @example
|
15
|
-
#
|
19
|
+
# class Album
|
20
|
+
# extend Linkable
|
21
|
+
#
|
22
|
+
# from_link :as_album # => Spotify.link_as_album(pointer, *args)
|
23
|
+
# # ^ is roughly equivalent to:
|
24
|
+
# def from_link(link, *args)
|
25
|
+
# unless Spotify::Pointer.typechecks?(link, :link)
|
26
|
+
# link = Link.new(link).pointer(:album)
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# Spotify.link_as_album!(link)
|
30
|
+
# end
|
31
|
+
# end
|
16
32
|
#
|
17
|
-
# @param [Symbol]
|
33
|
+
# @param [Symbol] method_name
|
18
34
|
#
|
19
35
|
# @overload from_link(type) { |*args| … }
|
20
|
-
#
|
36
|
+
# Define `#from_link` to use the given block to convert an object
|
37
|
+
# from a link. The link is converted to a pointer and typechecked
|
38
|
+
# to be of the same type as `type` before given to the block.
|
21
39
|
#
|
22
40
|
# @example
|
23
|
-
#
|
24
|
-
#
|
41
|
+
# class User
|
42
|
+
# extend Linkable
|
43
|
+
#
|
44
|
+
# from_link :profile do |pointer|
|
45
|
+
# Spotify.link_as_user!(pointer)
|
46
|
+
# end
|
47
|
+
# # ^ is roughly equivalent to:
|
48
|
+
# def from_link(link, *args)
|
49
|
+
# unless Spotify::Pointer.typechecks?(link, :link)
|
50
|
+
# link = Link.new(link).pointer(:profile)
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# Spotify.link_as_user!(link)
|
54
|
+
# end
|
25
55
|
# end
|
26
56
|
#
|
27
57
|
# @param [#to_s] type link type
|
28
58
|
# @yield [link, *args] called when conversion is needed from Link pointer
|
29
|
-
# @yieldparam [
|
59
|
+
# @yieldparam [Spotify::Pointer] link
|
30
60
|
# @yieldparam *args any extra arguments given to `#from_link`
|
31
61
|
#
|
32
|
-
# @
|
62
|
+
# @note Private API. You probably do not need to care about this method.
|
33
63
|
def from_link(as_object, &block)
|
34
|
-
block ||= Spotify.method(:"link_#{as_object}")
|
64
|
+
block ||= Spotify.method(:"link_#{as_object}!")
|
35
65
|
type = as_object.to_s[/^(as_)?([^_]+)/, 2].to_sym
|
36
66
|
|
37
67
|
define_method(:from_link) do |link, *args|
|
38
|
-
|
39
|
-
|
40
|
-
|
68
|
+
if link.is_a?(FFI::Pointer) and not link.is_a?(Spotify::Pointer)
|
69
|
+
link
|
70
|
+
else
|
71
|
+
unless Spotify::Pointer.typechecks?(link, :link)
|
72
|
+
link = Link.new(link).pointer(type)
|
73
|
+
end
|
41
74
|
|
42
|
-
|
75
|
+
instance_exec(link, *args, &block)
|
76
|
+
end
|
43
77
|
end
|
78
|
+
|
79
|
+
private :from_link
|
44
80
|
end
|
45
81
|
|
46
|
-
# Defines `#to_link` method,
|
82
|
+
# Defines `#to_link` method, used in converting the object to a {Link}.
|
47
83
|
#
|
48
84
|
# @example
|
49
|
-
#
|
85
|
+
# class Artist
|
86
|
+
# extend Linkable
|
50
87
|
#
|
51
|
-
#
|
88
|
+
# to_link :from_artist
|
89
|
+
# # ^ is the same as:
|
90
|
+
# def to_link(*args)
|
91
|
+
# link = Spotify.link_create_from_artist!(pointer, *args)
|
92
|
+
# Link.new(link)
|
93
|
+
# end
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# @param [Symbol] cmethod name of the C method, say `from_artist` in `Spotify.link_create_from_artist`.
|
52
97
|
# @return [Link]
|
53
98
|
def to_link(cmethod)
|
54
99
|
define_method(:to_link) do |*args|
|
55
|
-
link = Spotify.__send__(:"link_create_#{cmethod}",
|
100
|
+
link = Spotify.__send__(:"link_create_#{cmethod}!", pointer, *args)
|
56
101
|
Link.new(link)
|
57
102
|
end
|
58
103
|
end
|
104
|
+
|
105
|
+
private :from_link
|
106
|
+
private :to_link
|
59
107
|
end
|
60
108
|
end
|
data/lib/hallon/observable.rb
CHANGED
@@ -26,7 +26,6 @@ module Hallon
|
|
26
26
|
# of the event that called it
|
27
27
|
# @param [#to_sym] event name of event to handle
|
28
28
|
# @yield (*args) event handler block
|
29
|
-
# @see #initialize
|
30
29
|
def on(*events, &block)
|
31
30
|
raise ArgumentError, "no block given" unless block
|
32
31
|
wrap = events.length > 1
|
data/lib/hallon/player.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# coding: utf-8
|
1
2
|
module Hallon
|
2
3
|
# A wrapper around Session for playing, stopping and otherwise
|
3
4
|
# controlling the playback features of libspotify.
|
@@ -9,6 +10,9 @@ module Hallon
|
|
9
10
|
class Player
|
10
11
|
include Observable
|
11
12
|
|
13
|
+
# @return [Spotify::Pointer<Session>] session pointer
|
14
|
+
attr_reader :pointer
|
15
|
+
|
12
16
|
# @return [Array<Symbol>] a list of available playback bitrates.
|
13
17
|
def self.bitrates
|
14
18
|
Spotify.enum_type(:bitrate).symbols.sort_by do |sym|
|
@@ -74,7 +78,7 @@ module Hallon
|
|
74
78
|
# @param [Symbol] bitrate one of :96k, :160k, :320k
|
75
79
|
# @return [Symbol]
|
76
80
|
def bitrate=(bitrate)
|
77
|
-
Spotify.session_preferred_bitrate(
|
81
|
+
Spotify.session_preferred_bitrate(pointer, bitrate)
|
78
82
|
end
|
79
83
|
|
80
84
|
# Loads a Track for playing.
|
@@ -83,7 +87,7 @@ module Hallon
|
|
83
87
|
# @return [Player]
|
84
88
|
# @raise [Error] if the track could not be loaded
|
85
89
|
def load(track)
|
86
|
-
error = Spotify.session_player_load(
|
90
|
+
error = Spotify.session_player_load(pointer, track.pointer)
|
87
91
|
tap { Error.maybe_raise(error) }
|
88
92
|
end
|
89
93
|
|
@@ -93,7 +97,7 @@ module Hallon
|
|
93
97
|
# @param [Track] track
|
94
98
|
# @return [Player]
|
95
99
|
def prefetch(track)
|
96
|
-
error = Spotify.session_player_prefetch(
|
100
|
+
error = Spotify.session_player_prefetch(pointer, track.pointer)
|
97
101
|
tap { Error.maybe_raise(error) }
|
98
102
|
end
|
99
103
|
|
@@ -102,21 +106,21 @@ module Hallon
|
|
102
106
|
# @return [Player]
|
103
107
|
def play(track = nil)
|
104
108
|
load(track) unless track.nil?
|
105
|
-
tap { Spotify.session_player_play(
|
109
|
+
tap { Spotify.session_player_play(pointer, true) }
|
106
110
|
end
|
107
111
|
|
108
112
|
# Pause playback of a Track.
|
109
113
|
#
|
110
114
|
# @return [Player]
|
111
115
|
def pause
|
112
|
-
tap { Spotify.session_player_play(
|
116
|
+
tap { Spotify.session_player_play(pointer, false) }
|
113
117
|
end
|
114
118
|
|
115
119
|
# Stop playing current track and unload it.
|
116
120
|
#
|
117
121
|
# @return [Player]
|
118
122
|
def stop
|
119
|
-
tap { Spotify.session_player_unload(
|
123
|
+
tap { Spotify.session_player_unload(pointer) }
|
120
124
|
end
|
121
125
|
|
122
126
|
# Seek to the desired position of the currently loaded Track.
|
@@ -124,7 +128,17 @@ module Hallon
|
|
124
128
|
# @param [Numeric] seconds offset position in seconds
|
125
129
|
# @return [Player]
|
126
130
|
def seek(seconds)
|
127
|
-
tap { Spotify.session_player_seek(
|
131
|
+
tap { Spotify.session_player_seek(pointer, seconds * 1000) }
|
132
|
+
end
|
133
|
+
|
134
|
+
# @return [Boolean] true if libspotify is set to normalize audio volume.
|
135
|
+
def volume_normalization?
|
136
|
+
Spotify.session_get_volume_normalization(pointer)
|
137
|
+
end
|
138
|
+
|
139
|
+
# @param [Boolean] normalize_volume true if libspotify should normalize audio volume.
|
140
|
+
def volume_normalization=(normalize_volume)
|
141
|
+
Spotify.session_set_volume_normalization(pointer, !! normalize_volume)
|
128
142
|
end
|
129
143
|
end
|
130
144
|
end
|
@@ -0,0 +1,291 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module Hallon
|
3
|
+
# Playlists are playlists. They contain tracks and track information
|
4
|
+
# such as when tracks were added or by whom. They also contain some
|
5
|
+
# metadata such as their own name.
|
6
|
+
#
|
7
|
+
# @see http://developer.spotify.com/en/libspotify/docs/group__playlist.html
|
8
|
+
class Playlist < Base
|
9
|
+
include Observable
|
10
|
+
extend Linkable
|
11
|
+
|
12
|
+
# Playlist::Track is a {Track} with additional information attached to it,
|
13
|
+
# that is specific to the playlist it was created from. The returned track
|
14
|
+
# is a snapshot of the information, so even if the underlying track moves,
|
15
|
+
# this Playlist::Track will still contain the same information.
|
16
|
+
#
|
17
|
+
# There is no way to refresh the information. You’ll have to retrieve the
|
18
|
+
# track again.
|
19
|
+
class Track < Hallon::Track
|
20
|
+
def initialize(playlist, index)
|
21
|
+
super Spotify.playlist_track!(playlist, index)
|
22
|
+
|
23
|
+
@index = index
|
24
|
+
@create_time = Time.at Spotify.playlist_track_create_time(playlist, index)
|
25
|
+
@message = Spotify.playlist_track_message(playlist, index)
|
26
|
+
@seen = Spotify.playlist_track_seen(playlist, index)
|
27
|
+
@creator = begin
|
28
|
+
creator = Spotify.playlist_track_creator!(playlist, index)
|
29
|
+
User.new(creator) unless creator.null?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# @note this value never changes, even if the original track is moved/removed
|
34
|
+
# @return [Integer] index this track was created with.
|
35
|
+
attr_reader :index
|
36
|
+
|
37
|
+
# @return [Time] time when track at {#index} was added to playlist.
|
38
|
+
def create_time
|
39
|
+
@create_time
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [User, nil] person who added track at {#index} to this playlist.
|
43
|
+
def creator
|
44
|
+
@creator
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [String] message attached to this track at {#index}.
|
48
|
+
def message
|
49
|
+
@message
|
50
|
+
end
|
51
|
+
|
52
|
+
# @see Playlist#seen
|
53
|
+
# @return [Boolean] true if track at {#index} has been seen.
|
54
|
+
def seen?
|
55
|
+
@seen
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
from_link :playlist do |pointer|
|
60
|
+
Spotify.playlist_create!(session.pointer, pointer)
|
61
|
+
end
|
62
|
+
|
63
|
+
to_link :from_playlist
|
64
|
+
|
65
|
+
# Construct a new Playlist, given a pointer.
|
66
|
+
#
|
67
|
+
# @param [String, Link, FFI::Pointer] link
|
68
|
+
def initialize(link)
|
69
|
+
callbacks = Spotify::PlaylistCallbacks.new(self, @sp_callbacks = {})
|
70
|
+
@pointer = to_pointer(link, :playlist)
|
71
|
+
Spotify.playlist_add_callbacks(pointer, callbacks, nil)
|
72
|
+
end
|
73
|
+
|
74
|
+
# @return [Boolean] true if the playlist is loaded
|
75
|
+
def loaded?
|
76
|
+
Spotify.playlist_is_loaded(pointer)
|
77
|
+
end
|
78
|
+
|
79
|
+
# @return [Boolean] true if the playlist is collaborative
|
80
|
+
def collaborative?
|
81
|
+
Spotify.playlist_is_collaborative(pointer)
|
82
|
+
end
|
83
|
+
|
84
|
+
# @param [Boolean] collaborative true to set the playlist to collaborative
|
85
|
+
def collaborative=(collaborative)
|
86
|
+
Spotify.playlist_set_collaborative(pointer, !!collaborative)
|
87
|
+
end
|
88
|
+
|
89
|
+
# @return [Boolean] true if playlist has pending changes
|
90
|
+
def pending?
|
91
|
+
Spotify.playlist_has_pending_changes(pointer)
|
92
|
+
end
|
93
|
+
|
94
|
+
# @return [Boolean] true if the playlist is in RAM
|
95
|
+
def in_ram?
|
96
|
+
Spotify.playlist_is_in_ram(session.pointer, pointer)
|
97
|
+
end
|
98
|
+
|
99
|
+
# @param [Boolean] in_ram true if you want to store the playlist in RAM
|
100
|
+
def in_ram=(in_ram)
|
101
|
+
Spotify.playlist_set_in_ram(session.pointer, pointer, !! in_ram)
|
102
|
+
end
|
103
|
+
|
104
|
+
# @return [Boolean] true if playlist is available offline (fully synced)
|
105
|
+
def available_offline?
|
106
|
+
offline_status == :yes
|
107
|
+
end
|
108
|
+
|
109
|
+
# @return [Boolean] true if playlist is currently syncing
|
110
|
+
def syncing?
|
111
|
+
offline_status == :downloading
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return [Boolean] true if playlist is queued for offline syncing
|
115
|
+
def waiting?
|
116
|
+
offline_status == :waiting
|
117
|
+
end
|
118
|
+
|
119
|
+
# @return [Boolean] true if playlist is requested to be available offline
|
120
|
+
def offline_mode?
|
121
|
+
offline_status != :no
|
122
|
+
end
|
123
|
+
|
124
|
+
# @return [Symbol] one of :no, :yes, :downloading, :waiting
|
125
|
+
def offline_status
|
126
|
+
Spotify.playlist_get_offline_status(session.pointer, pointer)
|
127
|
+
end
|
128
|
+
|
129
|
+
# @param [Boolean] available_offline true if you want this playlist available offline
|
130
|
+
def offline_mode=(available_offline)
|
131
|
+
Spotify.playlist_set_offline_mode(session.pointer, pointer, !! available_offline)
|
132
|
+
end
|
133
|
+
|
134
|
+
# @return [String]
|
135
|
+
def name
|
136
|
+
Spotify.playlist_name(pointer)
|
137
|
+
end
|
138
|
+
|
139
|
+
# @note The name must not consist of only spaces and it must be shorter than 256 characters.
|
140
|
+
# @param [#to_s] name new name for playlist
|
141
|
+
# @raise [Error] if name could not be changed
|
142
|
+
def name=(name)
|
143
|
+
name = name.to_s.encode('UTF-8')
|
144
|
+
|
145
|
+
unless name.length < 256
|
146
|
+
raise ArgumentError, "name must be shorter than 256 characters (UTF-8)"
|
147
|
+
end
|
148
|
+
|
149
|
+
unless name =~ /[^ ]/u
|
150
|
+
raise ArgumentError, "name must not consist of only spaces"
|
151
|
+
end unless name.empty?
|
152
|
+
|
153
|
+
Error.maybe_raise(Spotify.playlist_rename(pointer, name))
|
154
|
+
end
|
155
|
+
|
156
|
+
# @return [User, nil]
|
157
|
+
def owner
|
158
|
+
user = Spotify.playlist_owner!(pointer)
|
159
|
+
User.new(user) unless user.null?
|
160
|
+
end
|
161
|
+
|
162
|
+
# @return [String]
|
163
|
+
def description
|
164
|
+
Spotify.playlist_get_description(pointer)
|
165
|
+
end
|
166
|
+
|
167
|
+
# @return [Image, nil]
|
168
|
+
def image
|
169
|
+
buffer = FFI::Buffer.alloc_out(20)
|
170
|
+
if Spotify.playlist_get_image(pointer, buffer)
|
171
|
+
Image.new buffer.read_bytes(20)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# @note this list might be shorter than {#total_subscribers}, as
|
176
|
+
# libspotify does not store more than 500 subscriber names
|
177
|
+
# @return [Array<String>] list of canonical usernames
|
178
|
+
def subscribers
|
179
|
+
ptr = Spotify.playlist_subscribers(pointer)
|
180
|
+
struct = Spotify::Subscribers.new(ptr)
|
181
|
+
|
182
|
+
if struct[:count].zero?
|
183
|
+
[]
|
184
|
+
else
|
185
|
+
struct[:subscribers].map(&:read_string)
|
186
|
+
end
|
187
|
+
ensure
|
188
|
+
Spotify.playlist_subscribers_free(ptr)
|
189
|
+
end
|
190
|
+
|
191
|
+
# @return [Integer] total number of subscribers.
|
192
|
+
def total_subscribers
|
193
|
+
Spotify.playlist_num_subscribers(pointer)
|
194
|
+
end
|
195
|
+
|
196
|
+
# Ask libspotify to update subscriber information
|
197
|
+
#
|
198
|
+
# @return [Playlist]
|
199
|
+
def update_subscribers
|
200
|
+
Spotify.playlist_update_subscribers(session.pointer, pointer)
|
201
|
+
end
|
202
|
+
|
203
|
+
# @note only applicable if {#offline_status} is `:downloading`
|
204
|
+
# @return [Integer] percentage done of playlist offline sync
|
205
|
+
def sync_progress
|
206
|
+
Spotify.playlist_get_offline_download_completed(session.pointer, pointer)
|
207
|
+
end
|
208
|
+
|
209
|
+
# @param [Boolean] autolink_tracks if you want unplayable tracks to be linked to playable tracks (if possible)
|
210
|
+
def autolink_tracks=(autolink_tracks)
|
211
|
+
Spotify.playlist_set_autolink_tracks(pointer, !! autolink_tracks)
|
212
|
+
end
|
213
|
+
|
214
|
+
# @note Will be 0 unless {#loaded?}.
|
215
|
+
# @return [Integer] number of tracks in playlist
|
216
|
+
def size
|
217
|
+
Spotify.playlist_num_tracks(pointer)
|
218
|
+
end
|
219
|
+
|
220
|
+
# @example retrieve track at index 3
|
221
|
+
# track = playlist.tracks[3]
|
222
|
+
# puts track.name
|
223
|
+
#
|
224
|
+
# @return [Enumerable<Playlist::Track>] a list of playlist tracks.
|
225
|
+
def tracks
|
226
|
+
Enumerator.new(size) { |i| Playlist::Track.new(pointer, i) }
|
227
|
+
end
|
228
|
+
|
229
|
+
# Set seen status of the Playlist::Track at the given index.
|
230
|
+
#
|
231
|
+
# @see #tracks
|
232
|
+
# @raise [Error] if the operation could not be completed
|
233
|
+
# @param [Integer] index
|
234
|
+
# @param [Boolean] seen true if the track is now seen
|
235
|
+
# @return [Playlist::Track] track at the given index
|
236
|
+
def seen(index, seen)
|
237
|
+
error = Spotify.playlist_track_set_seen(pointer, index, !! seen)
|
238
|
+
Error.maybe_raise(error)
|
239
|
+
tracks[index]
|
240
|
+
end
|
241
|
+
|
242
|
+
# Add a list of tracks to the playlist starting at given position.
|
243
|
+
#
|
244
|
+
# @param [Integer] index starting index to add tracks from (between 0..{#size})
|
245
|
+
# @param [Track, Array<Track>] tracks
|
246
|
+
# @return [Playlist]
|
247
|
+
# @raise [Hallon::Error] if the operation failed
|
248
|
+
def insert(index = size, tracks)
|
249
|
+
tracks = Array(tracks).map(&:pointer)
|
250
|
+
tracks_ary = FFI::MemoryPointer.new(:pointer, tracks.size)
|
251
|
+
tracks_ary.write_array_of_pointer(tracks)
|
252
|
+
|
253
|
+
tap do
|
254
|
+
error = Spotify.playlist_add_tracks(pointer, tracks_ary, tracks.size, index, session.pointer)
|
255
|
+
Error.maybe_raise(error)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Remove tracks at given indices.
|
260
|
+
#
|
261
|
+
# @param [Integer, ...] indices
|
262
|
+
# @return [Playlist]
|
263
|
+
# @raise [Error] if the operation failed
|
264
|
+
def remove(*indices)
|
265
|
+
indices_ary = FFI::MemoryPointer.new(:int, indices.size)
|
266
|
+
indices_ary.write_array_of_int(indices)
|
267
|
+
|
268
|
+
tap do
|
269
|
+
error = Spotify.playlist_remove_tracks(pointer, indices_ary, indices.size)
|
270
|
+
Error.maybe_raise(error)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# Move tracks at given indices to given index.
|
275
|
+
#
|
276
|
+
# @param [Integer] destination index to move tracks to
|
277
|
+
# @param [Integer, Array<Integer>] indices
|
278
|
+
# @return [Playlist]
|
279
|
+
# @raise [Error] if the operation failed
|
280
|
+
def move(destination, indices)
|
281
|
+
indices = Array(indices)
|
282
|
+
indices_ary = FFI::MemoryPointer.new(:int, indices.size)
|
283
|
+
indices_ary.write_array_of_int(indices)
|
284
|
+
|
285
|
+
tap do
|
286
|
+
error = Spotify.playlist_reorder_tracks(pointer, indices_ary, indices.size, destination)
|
287
|
+
Error.maybe_raise(error)
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|