spotify-ruby-kev 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|