spotify-ruby-kev 0.2.5
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.
- checksums.yaml +7 -0
- data/.github/CONTRIBUTING.md +3 -0
- data/.github/ISSUE_TEMPLATE.md +27 -0
- data/.gitignore +24 -0
- data/.rspec +3 -0
- data/.rubocop.yml +161 -0
- data/.ruby-version +1 -0
- data/.rvm-version +1 -0
- data/.travis.yml +17 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/COVERAGE.md +148 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.md +388 -0
- data/Rakefile +23 -0
- data/lib/spotify.rb +17 -0
- data/lib/spotify/accounts.rb +133 -0
- data/lib/spotify/accounts/session.rb +177 -0
- data/lib/spotify/sdk.rb +93 -0
- data/lib/spotify/sdk/.keep +0 -0
- data/lib/spotify/sdk/album.rb +84 -0
- data/lib/spotify/sdk/artist.rb +163 -0
- data/lib/spotify/sdk/base.rb +77 -0
- data/lib/spotify/sdk/connect.rb +75 -0
- data/lib/spotify/sdk/connect/device.rb +362 -0
- data/lib/spotify/sdk/connect/playback_state.rb +143 -0
- data/lib/spotify/sdk/image.rb +44 -0
- data/lib/spotify/sdk/item.rb +157 -0
- data/lib/spotify/sdk/me.rb +155 -0
- data/lib/spotify/sdk/me/info.rb +108 -0
- data/lib/spotify/sdk/model.rb +70 -0
- data/lib/spotify/version.rb +16 -0
- data/spotify-ruby-kev.gemspec +56 -0
- metadata +291 -0
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spotify
|
4
|
+
class SDK
|
5
|
+
class Connect
|
6
|
+
class PlaybackState < Model
|
7
|
+
##
|
8
|
+
# Get the device the current playback is on.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# device = @sdk.connect.devices[0]
|
12
|
+
# device.playback.device
|
13
|
+
#
|
14
|
+
# @return [Spotify::SDK::Connect::Device] self Return the device object.
|
15
|
+
#
|
16
|
+
def device
|
17
|
+
Spotify::SDK::Connect::Device.new(super, parent)
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Is the current user playing a track?
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# playback = @sdk.connect.playback
|
25
|
+
# playback.playing?
|
26
|
+
#
|
27
|
+
# @return [FalseClass,TrueClass] is_playing True if user is currently performing playback.
|
28
|
+
#
|
29
|
+
alias_attribute :playing?, :is_playing
|
30
|
+
|
31
|
+
##
|
32
|
+
# Is the current playback set to shuffle?
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# playback = @sdk.connect.playback
|
36
|
+
# playback.shuffling?
|
37
|
+
#
|
38
|
+
# @return [FalseClass,TrueClass] is_shuffling True if shuffle is set.
|
39
|
+
#
|
40
|
+
alias_attribute :shuffling?, :shuffle_state
|
41
|
+
|
42
|
+
##
|
43
|
+
# What repeat mode is the current playback set to?
|
44
|
+
#
|
45
|
+
# Options:
|
46
|
+
# :off => This means no repeat is set.
|
47
|
+
# :context => This means it will repeat within the same context.
|
48
|
+
# :track => This will repeat the same track.
|
49
|
+
#
|
50
|
+
# @example
|
51
|
+
# playback = @sdk.connect.playback
|
52
|
+
# playback.repeat # :off, :context, or :track
|
53
|
+
#
|
54
|
+
# @return [Symbol] repeat_mode Either :off, :context, or :track
|
55
|
+
#
|
56
|
+
def repeat_mode
|
57
|
+
repeat_state.to_sym
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# The current timestamp of the playback state
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# playback = @sdk.connect.playback
|
65
|
+
# playback.time
|
66
|
+
#
|
67
|
+
# @return [Time] time The accuracy time of the playback state.
|
68
|
+
#
|
69
|
+
def time
|
70
|
+
Time.at(timestamp / 1000)
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# What is the current position of the track?
|
75
|
+
#
|
76
|
+
# @example
|
77
|
+
# playback = @sdk.connect.playback
|
78
|
+
# playback.position
|
79
|
+
#
|
80
|
+
# @return [Integer] position_ms In milliseconds, the position of the track.
|
81
|
+
#
|
82
|
+
alias_attribute :position, :progress_ms
|
83
|
+
|
84
|
+
##
|
85
|
+
# How much percentage of the track is the position currently in?
|
86
|
+
#
|
87
|
+
# @example
|
88
|
+
# playback = @sdk.connect.playback
|
89
|
+
# playback.position_percentage # => 7.30
|
90
|
+
# playback.position_percentage(4) # => 7.3039
|
91
|
+
#
|
92
|
+
# @param [Integer] decimal_points How many decimal points to return
|
93
|
+
# @return [Float] percentage Completion percentage. Rounded to 2 decimal places.
|
94
|
+
#
|
95
|
+
def position_percentage(decimal_points=2)
|
96
|
+
return nil if position.nil?
|
97
|
+
|
98
|
+
((position.to_f / item.duration.to_f) * 100).ceil(decimal_points)
|
99
|
+
end
|
100
|
+
|
101
|
+
##
|
102
|
+
# Get the artists for the currently playing track.
|
103
|
+
#
|
104
|
+
# @example
|
105
|
+
# @sdk.connect.playback.artists
|
106
|
+
#
|
107
|
+
# @return [Array] artists An array of artists wrapped in Spotify::SDK::Artist
|
108
|
+
#
|
109
|
+
def artists
|
110
|
+
item[:artists].map do |artist|
|
111
|
+
Spotify::SDK::Artist.new(artist, parent)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Get the main artist for the currently playing track.
|
117
|
+
#
|
118
|
+
# @example
|
119
|
+
# @sdk.connect.playback.artist
|
120
|
+
#
|
121
|
+
# @return [Spotify::SDK::Artist] artist The main artist of the track.
|
122
|
+
#
|
123
|
+
def artist
|
124
|
+
artists.first
|
125
|
+
end
|
126
|
+
|
127
|
+
##
|
128
|
+
# Get the item for the currently playing track.
|
129
|
+
#
|
130
|
+
# @example
|
131
|
+
# @sdk.connect.playback.item
|
132
|
+
#
|
133
|
+
# @return [Spotify::SDK::Item] item The currently playing track, wrapped in Spotify::SDK::Item
|
134
|
+
#
|
135
|
+
def item
|
136
|
+
raise "Playback information is not available if user has a private session enabled" if device.private_session?
|
137
|
+
|
138
|
+
Spotify::SDK::Item.new(to_h, parent)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spotify
|
4
|
+
class SDK
|
5
|
+
class Image < Model
|
6
|
+
##
|
7
|
+
# Get the ID of the image.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# artist = @sdk.connect.playback.artist
|
11
|
+
# artist.images[0].id # => "941223d904f006c4d998598272d43d94"
|
12
|
+
#
|
13
|
+
# @return [String] image_id The image ID generated from Spotify.
|
14
|
+
#
|
15
|
+
def id
|
16
|
+
url.match(/[a-z0-9]+$/i)[0]
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Get the mobile-related link for the image. Designed for offline mobile apps.
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# artist = @sdk.connect.playback.artist
|
24
|
+
# artist.images[0].spotify_uri # => "spoitfy:image:..."
|
25
|
+
#
|
26
|
+
# @return [String] spotify_uri The mobile-embeddable image for the item.
|
27
|
+
#
|
28
|
+
def spotify_uri
|
29
|
+
"spotify:image:%s" % id
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Get the HTTP link for the image. Designed for web apps.
|
34
|
+
#
|
35
|
+
# @example
|
36
|
+
# artist = @sdk.connect.playback.artist
|
37
|
+
# artist.images[0].spotify_url # => "https://i.scdn.co/image/..."
|
38
|
+
#
|
39
|
+
# @return [String] spotify_url The web-embeddable HTTP image for the item.
|
40
|
+
#
|
41
|
+
alias_attribute :spotify_url, :url
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spotify
|
4
|
+
class SDK
|
5
|
+
class Item < Model
|
6
|
+
##
|
7
|
+
# Let's transform the item object into better for us.
|
8
|
+
# Before: { track: ..., played_at: ..., context: ... }
|
9
|
+
# After: { track_properties..., played_at: ..., context: ... }
|
10
|
+
#
|
11
|
+
# :nodoc:
|
12
|
+
def initialize(payload, parent)
|
13
|
+
track = payload.delete(:track) || payload.delete(:item)
|
14
|
+
properties = payload.except(:parent, :device, :repeat_state, :shuffle_state)
|
15
|
+
super(track.merge(properties: properties), parent)
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# Get the album for this item.
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# @sdk.connect.playback.item.album
|
23
|
+
#
|
24
|
+
# @return [Spotify::SDK::Album] album The album object, wrapped in Spotify::SDK::Album
|
25
|
+
#
|
26
|
+
def album
|
27
|
+
Spotify::SDK::Album.new(super, parent)
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Get the artists/creators for this item.
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# @sdk.connect.playback.item.artists
|
35
|
+
#
|
36
|
+
# @return [Array] artists A list of artists, wrapped in Spotify::SDK::Artist
|
37
|
+
#
|
38
|
+
def artists
|
39
|
+
super.map do |artist|
|
40
|
+
Spotify::SDK::Artist.new(artist, parent)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Get the primary artist/creator for this item.
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
# @sdk.connect.playback.item.artist
|
49
|
+
#
|
50
|
+
# @return [Spotify::SDK::Artist] artist The primary artist, wrapped in Spotify::SDK::Artist
|
51
|
+
#
|
52
|
+
def artist
|
53
|
+
artists.first
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Get the context.
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
# @sdk.connect.playback.item.context
|
61
|
+
# @sdk.me.history[0].context
|
62
|
+
#
|
63
|
+
# @return [Hash] context Information about the user's context.
|
64
|
+
#
|
65
|
+
alias_attribute :context, "properties.context"
|
66
|
+
|
67
|
+
##
|
68
|
+
# Get the duration.
|
69
|
+
# Alias to self.duration_ms
|
70
|
+
#
|
71
|
+
# @example
|
72
|
+
# @sdk.connect.playback.item.duration # => 10331
|
73
|
+
#
|
74
|
+
# @return [Integer] duration_ms In milliseconds, how long the item is.
|
75
|
+
#
|
76
|
+
alias_attribute :duration, :duration_ms
|
77
|
+
|
78
|
+
##
|
79
|
+
# Is this track explicit?
|
80
|
+
# Alias to self.explicit
|
81
|
+
#
|
82
|
+
# @example
|
83
|
+
# @sdk.connect.playback.item.explicit? # => true
|
84
|
+
#
|
85
|
+
# @return [TrueClass,FalseClass] is_explicit Returns true if item contains explicit content.
|
86
|
+
#
|
87
|
+
alias_attribute :explicit?, :explicit
|
88
|
+
|
89
|
+
##
|
90
|
+
# Is this a local track, not a Spotify track?
|
91
|
+
# Alias to self.is_local
|
92
|
+
#
|
93
|
+
# @example
|
94
|
+
# @sdk.connect.playback.item.local? # => false
|
95
|
+
#
|
96
|
+
# @return [TrueClass,FalseClass] is_local Returns true if item is local to the user.
|
97
|
+
#
|
98
|
+
alias_attribute :local?, :is_local
|
99
|
+
|
100
|
+
##
|
101
|
+
# Is this a playable track?
|
102
|
+
# Alias to self.is_playable
|
103
|
+
#
|
104
|
+
# @example
|
105
|
+
# @sdk.connect.playback.item.playable? # => false
|
106
|
+
#
|
107
|
+
# @return [TrueClass,FalseClass] is_playable Returns true if item is playable.
|
108
|
+
#
|
109
|
+
alias_attribute :playable?, :is_playable
|
110
|
+
|
111
|
+
##
|
112
|
+
# Is this a track?
|
113
|
+
# Alias to self.type == "track"
|
114
|
+
#
|
115
|
+
# @example
|
116
|
+
# @sdk.connect.playback.item.track? # => true
|
117
|
+
#
|
118
|
+
# @return [TrueClass,FalseClass] is_track Returns true if item is an music track.
|
119
|
+
#
|
120
|
+
def track?
|
121
|
+
type == "track"
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# Get the Spotify URI for this item.
|
126
|
+
# Alias to self.uri
|
127
|
+
#
|
128
|
+
# @example
|
129
|
+
# @sdk.connect.playback.item.spotify_uri # => "spotify:track:..."
|
130
|
+
#
|
131
|
+
# @return [String] spotify_uri The direct URI to this Spotify resource.
|
132
|
+
#
|
133
|
+
alias_attribute :spotify_uri, :uri
|
134
|
+
|
135
|
+
##
|
136
|
+
# Get the Spotify HTTP URL for this item.
|
137
|
+
# Alias to self.external_urls[:spotify]
|
138
|
+
#
|
139
|
+
# @example
|
140
|
+
# @sdk.connect.playback.item.spotify_url # => "https://open.spotify.com/..."
|
141
|
+
#
|
142
|
+
# @return [String] spotify_url The direct HTTP URL to this Spotify resource.
|
143
|
+
#
|
144
|
+
alias_attribute :spotify_url, "external_urls.spotify"
|
145
|
+
|
146
|
+
##
|
147
|
+
# Get the ISRC for this track.
|
148
|
+
#
|
149
|
+
# @example
|
150
|
+
# @sdk.connect.playback.item.isrc # => "USUM00000000"
|
151
|
+
#
|
152
|
+
# @return [String] isrc The ISRC string for this track.
|
153
|
+
#
|
154
|
+
alias_attribute :isrc, "external_ids.isrc"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Spotify
|
4
|
+
class SDK
|
5
|
+
class Me < Base
|
6
|
+
##
|
7
|
+
# Get the current user's information.
|
8
|
+
# Respective information requires the `user-read-private user-read-email user-read-birthdate` scopes.
|
9
|
+
# GET /v1/me
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# me = @sdk.me.info
|
13
|
+
#
|
14
|
+
# @see https://developer.spotify.com/console/get-current-user/
|
15
|
+
# @see https://developer.spotify.com/documentation/web-api/reference/users-profile/get-current-users-profile/
|
16
|
+
#
|
17
|
+
# @param [Hash] override_opts Custom options for HTTParty.
|
18
|
+
# @return [Spotify::SDK::Me::Info] user_info Return the user's information.
|
19
|
+
#
|
20
|
+
def info(override_opts={})
|
21
|
+
me_info = send_http_request(:get, "/v1/me", override_opts)
|
22
|
+
Spotify::SDK::Me::Info.new(me_info, self)
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# Check what tracks a user has recently played.
|
27
|
+
#
|
28
|
+
# @example
|
29
|
+
# @sdk.me.history
|
30
|
+
# @sdk.me.history(20)
|
31
|
+
#
|
32
|
+
# @param [Integer] limit How many results to request. Defaults to 10.
|
33
|
+
# @param [Hash] override_opts Custom options for HTTParty.
|
34
|
+
# @return [Array] response List of recently played tracked, in chronological order.
|
35
|
+
#
|
36
|
+
def history(limit=10, override_opts={})
|
37
|
+
request = {
|
38
|
+
method: :get,
|
39
|
+
http_path: "/v1/me/player/recently-played",
|
40
|
+
keys: %i[items],
|
41
|
+
limit: limit
|
42
|
+
}
|
43
|
+
|
44
|
+
send_multiple_http_requests(request, override_opts).map do |item|
|
45
|
+
Spotify::SDK::Item.new(item, self)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Check if the current user is following N users.
|
51
|
+
#
|
52
|
+
# @example
|
53
|
+
# artists = %w(3q7HBObVc0L8jNeTe5Gofh 0NbfKEOTQCcwd6o7wSDOHI 3TVXtAsR1Inumwj472S9r4)
|
54
|
+
# @sdk.me.following?(artists, :artist)
|
55
|
+
# # => {"3q7HBObVc0L8jNeTe5Gofh" => false, "0NbfKEOTQCcwd6o7wSDOHI" => false, ...}
|
56
|
+
#
|
57
|
+
# users = %w(3q7HBObVc0L8jNeTe5Gofh 0NbfKEOTQCcwd6o7wSDOHI 3TVXtAsR1Inumwj472S9r4)
|
58
|
+
# @sdk.me.following?(users, :user)
|
59
|
+
# # => {"3q7HBObVc0L8jNeTe5Gofh" => false, "0NbfKEOTQCcwd6o7wSDOHI" => false, ...}
|
60
|
+
#
|
61
|
+
# @param [Array] list List of Spotify user/artist IDs. Cannot mix user and artist IDs in single request.
|
62
|
+
# @param [Symbol] type Either :user or :artist. Checks if follows respective type of account.
|
63
|
+
# @param [Hash] override_opts Custom options for HTTParty.
|
64
|
+
# @return [Hash] hash A hash containing a key with the ID, and a value that equals is_following (boolean).
|
65
|
+
#
|
66
|
+
def following?(list, type=:artist, override_opts={})
|
67
|
+
raise "Must contain an array" unless list.is_a?(Array)
|
68
|
+
raise "Must contain an array of String or Spotify::SDK::Artist" if any_of?(list, [String, Spotify::SDK::Artist])
|
69
|
+
raise "type must be either 'artist' or 'user'" unless %i[artist user].include?(type)
|
70
|
+
|
71
|
+
send_is_following_http_requests(list.map {|id| id.try(:id) || id }, type, override_opts)
|
72
|
+
end
|
73
|
+
|
74
|
+
def following_artists?(list, override_opts={})
|
75
|
+
following?(list, :artist, override_opts)
|
76
|
+
end
|
77
|
+
|
78
|
+
def following_users?(list, override_opts={})
|
79
|
+
following?(list, :user, override_opts)
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# Get the current user's followed artists. Requires the `user-read-follow` scope.
|
84
|
+
# GET /v1/me/following
|
85
|
+
#
|
86
|
+
# @example
|
87
|
+
# @sdk.me.following
|
88
|
+
#
|
89
|
+
# @param [Integer] n Number of results to return.
|
90
|
+
# @param [Hash] override_opts Custom options for HTTParty.
|
91
|
+
# @return [Array] artists A list of followed artists, wrapped in Spotify::SDK::Artist
|
92
|
+
#
|
93
|
+
def following(limit=50, override_opts={})
|
94
|
+
request = {
|
95
|
+
method: :get,
|
96
|
+
# TODO: Spotify API bug - `limit={n}` returns n-1 artists.
|
97
|
+
# ^ Example: `limit=5` returns 4 artists.
|
98
|
+
# TODO: Support `type=users` as well as `type=artists`.
|
99
|
+
http_path: "/v1/me/following?type=artist&limit=#{[limit, 50].min}",
|
100
|
+
keys: %i[artists items],
|
101
|
+
limit: limit
|
102
|
+
}
|
103
|
+
|
104
|
+
send_multiple_http_requests(request, override_opts).map do |artist|
|
105
|
+
Spotify::SDK::Artist.new(artist, self)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def any_of?(array, klasses) # :nodoc:
|
112
|
+
(array.map(&:class) - klasses).any?
|
113
|
+
end
|
114
|
+
|
115
|
+
def send_multiple_http_requests(opts, override_opts) # :nodoc:
|
116
|
+
response = send_http_request(opts[:method], opts[:http_path], override_opts)
|
117
|
+
responses, next_request = hash_deep_lookup(response, opts[:keys].dup)
|
118
|
+
if next_request && responses.size < opts[:limit]
|
119
|
+
responses += send_multiple_http_requests(opts.merge(http_path: next_request), override_opts)
|
120
|
+
end
|
121
|
+
responses.first(opts[:limit])
|
122
|
+
end
|
123
|
+
|
124
|
+
def hash_deep_lookup(response, keys) # :nodoc:
|
125
|
+
error_message = "Cannot find '%s' key in Spotify::SDK::Me#hash_deep_lookup"
|
126
|
+
while keys.any?
|
127
|
+
next_request ||= response[:next]
|
128
|
+
next_key = keys.shift
|
129
|
+
response = next_key ? response[next_key] : raise(error_message % next_key)
|
130
|
+
end
|
131
|
+
[response, next_request ? next_request[23..-1] : nil]
|
132
|
+
end
|
133
|
+
|
134
|
+
# TODO: Migrate this into the abstracted send_multiple_http_requests
|
135
|
+
def send_is_following_http_requests(list, type, override_opts) # :nodoc:
|
136
|
+
max_ids = list.first(50)
|
137
|
+
remaining_ids = list - max_ids
|
138
|
+
|
139
|
+
ids = max_ids.map {|id| {id.strip => nil} }.inject(&:merge)
|
140
|
+
following = send_http_request(
|
141
|
+
:get,
|
142
|
+
"/v1/me/following/contains?type=%s&ids=%s" % [type, ids.keys.join(",")],
|
143
|
+
override_opts
|
144
|
+
)
|
145
|
+
ids.each_key {|id| ids[id] = following.shift }
|
146
|
+
|
147
|
+
# rubocop:disable Style/IfUnlessModifier
|
148
|
+
if remaining_ids.any?
|
149
|
+
ids.merge(send_is_following_http_requests(remaining_ids, type, override_opts))
|
150
|
+
end || ids
|
151
|
+
# rubocop:enable Style/IfUnlessModifier
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|