Skip to content
This repository was archived by the owner on Mar 5, 2025. It is now read-only.

Commit d86fd5e

Browse files
authored
Recently played tracks: Implement @sdk.me.history (#57)
- Implemented `Spotify::SDK::Me#history` which calls `GET /v1/me/player/recently-played` - Made non-breaking changes to `Spotify::SDK::Item` which transforms payloads to insert other-relevant information in `properties` such as `context` and `played_at`. - Added a `Spotify::SDK::Item#context` alias to `Spotify::SDK::Item#properties[:context]`
1 parent 6d02f46 commit d86fd5e

File tree

10 files changed

+1667
-42
lines changed

10 files changed

+1667
-42
lines changed

COVERAGE.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,15 @@ This covers all the Spotify API endpoints that are covered.
8787

8888
### Follow Endpoints
8989

90-
| Endpoint | Description | Coverage Status |
91-
| ------------------------------------------------------------------ | ---------------------------------------------- | ---------------------------------------------------------------------- |
92-
| GET /v1/me/following | Get Followed Artists | [me.rb] |
93-
| GET /v1/me/following/contains | Check if Current User Follows Artists or Users | [me.rb] |
94-
| PUT /v1/me/following | Follow Artists or Users | [🔘 Partial Support][artist.rb] (Following multiple isn't supported) |
95-
| DELETE /v1/me/following | Unfollow Artists or Users | [🔘 Partial Support][artist.rb] (Unfollowing multiple isn't supported) |
96-
| GET /v1/users/{user_id}/playlists/{playlist_id}/followers/contains | Check if Users Follow a Playlist | × Not Started |
97-
| PUT /v1/users/{user_id}/playlists/{playlist_id}/followers | Follow a Playlist | × Not Started |
98-
| DELETE /v1/users/{user_id}/playlists/{playlist_id}/followers | Unfollow a Playlist | × Not Started |
90+
| Endpoint | Description | Coverage Status |
91+
| ------------------------------------------------------------------ | ---------------------------------------------- | -------------------------------------------------------------------- |
92+
| GET /v1/me/following | Get Followed Artists | [me.rb] |
93+
| GET /v1/me/following/contains | Check if Current User Follows Artists or Users | [me.rb] |
94+
| PUT /v1/me/following | Follow Artists or Users | [🔘 Partial Support][artist.rb] (Following multiple not supported) |
95+
| DELETE /v1/me/following | Unfollow Artists or Users | [🔘 Partial Support][artist.rb] (Unfollowing multiple not supported) |
96+
| GET /v1/users/{user_id}/playlists/{playlist_id}/followers/contains | Check if Users Follow a Playlist | × Not Started |
97+
| PUT /v1/users/{user_id}/playlists/{playlist_id}/followers | Follow a Playlist | × Not Started |
98+
| DELETE /v1/users/{user_id}/playlists/{playlist_id}/followers | Unfollow a Playlist | × Not Started |
9999

100100
### Playlists Endpoints
101101

@@ -113,10 +113,10 @@ This covers all the Spotify API endpoints that are covered.
113113

114114
### History Endpoints
115115

116-
| Endpoint | Description | Coverage Status |
117-
| --------------------------------- | --------------------------------------------- | --------------- |
118-
| GET /v1/me/top/{type} | Get User's Top Artists and Tracks | × Not Started |
119-
| GET /v1/me/player/recently-played | Get the Current User's Recently Played Tracks | × Not Started |
116+
| Endpoint | Description | Coverage Status |
117+
| --------------------------------- | --------------------------------------------- | ----------------------- |
118+
| GET /v1/me/top/{type} | Get User's Top Artists and Tracks | × Not Started |
119+
| GET /v1/me/player/recently-played | Get the Current User's Recently Played Tracks | [Full support ✔][me.rb] |
120120

121121
### Connect Endpoints
122122

README.md

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@ The developer-friendly, opinionated Ruby SDK for [Spotify]. Works on Ruby 2.4+
2323
- [Creating a Session](#creating-a-session)
2424
- [Recreating a Session](#recreating-a-session)
2525
- [Using the SDK](#using-the-sdk)
26-
- [Spotify Connect](#spotify-connect)
26+
- [Spotify Connect API](#spotify-connect-api)
27+
- [Me API](#me-api)
28+
- [Listening History API](#listening-history-api)
29+
- [Following API](#following-api)
2730
- [Contributing](#contributing)
2831
- [Community Guidelines](#community-guidelines)
2932
- [Code of Conduct](#code-of-conduct)
3033
- [Getting Started](#getting-started)
3134
- [Releasing a Change](#releasing-a-change)
32-
- [Changelog](#changelog)
3335
- [License](#license)
3436

3537
## Introduction
@@ -182,7 +184,7 @@ To create an instance of the Spotify SDK, you'll need the `@session` from above
182184
@sdk = Spotify::SDK.new(@session)
183185
```
184186

185-
### Spotify Connect
187+
### Spotify Connect API
186188

187189
With [Spotify Connect], you can take your music experience anywhere on over 300 devices. And you can read and control most devices programmatically through the SDK:
188190

@@ -227,6 +229,98 @@ With [Spotify Connect], you can take your music experience anywhere on over 300
227229
@sdk.connect.devices[0].repeat_mode = :context
228230
```
229231

232+
#### Transfer playback\*
233+
234+
This will transfer state, and start playback.
235+
236+
```ruby
237+
@sdk.connect.devices[0].transfer_playback!
238+
```
239+
240+
#### Transfer state\*
241+
242+
This will transfer state, and pause playback.
243+
244+
```ruby
245+
@sdk.connect.devices[0].transfer_state!
246+
```
247+
248+
### Me API
249+
250+
This allows you to perform specific actions on behalf of a user.
251+
252+
#### My information\*
253+
254+
```ruby
255+
@sdk.me.info
256+
@sdk.me.info.free? # => false
257+
@sdk.me.info.premium? # => true
258+
@sdk.me.info.birthdate # => 1980-01-01
259+
@sdk.me.info.display_name? # => true
260+
@sdk.me.info.display_name # => "ABC Smith"
261+
@sdk.me.info.images[0].url # => "https://profile-images.scdn.co/userprofile/default/..."
262+
@sdk.me.info.followers # => 4913313
263+
@sdk.me.info.spotify_uri # => "spotify:user:abcsmith"
264+
@sdk.me.info.spotify_url # => "https://open.spotify.com/user/abcsmith"
265+
```
266+
267+
### Listening History API
268+
269+
#### My recently played tracks (up to last 50)\*
270+
271+
```ruby
272+
@sdk.me.history(10) # => [#<Spotify::SDK::Item...>, ...]
273+
@sdk.me.history(10).size # => 10
274+
@sdk.me.history(50) # => [#<Spotify::SDK::Item...>, ...]
275+
@sdk.me.history(50).size # => 50
276+
```
277+
278+
### Following API
279+
280+
#### Follow an artist\*
281+
282+
```ruby
283+
@sdk.playback.item.artist.follow!
284+
```
285+
286+
#### Unfollow an artist\*
287+
288+
```ruby
289+
@sdk.playback.item.artist.unfollow!
290+
```
291+
292+
#### Check if following Spotify artists?\*
293+
294+
```ruby
295+
@sdk.me.following_artists?(%w(3TVXtAsR1Inumwj472S9r4 6LuN9FCkKOj5PcnpouEgny 69GGBxA162lTqCwzJG5jLp))
296+
# => {
297+
# "3TVXtAsR1Inumwj472S9r4" => false,
298+
# "6LuN9FCkKOj5PcnpouEgny" => true,
299+
# "69GGBxA162lTqCwzJG5jLp" => false
300+
# }
301+
```
302+
303+
#### Check if following Spotify users?\*
304+
305+
```ruby
306+
@sdk.me.following_users?(%w(3TVXtAsR1Inumwj472S9r4 6LuN9FCkKOj5PcnpouEgny 69GGBxA162lTqCwzJG5jLp))
307+
# => {
308+
# "3TVXtAsR1Inumwj472S9r4" => false,
309+
# "6LuN9FCkKOj5PcnpouEgny" => true,
310+
# "69GGBxA162lTqCwzJG5jLp" => false
311+
# }
312+
```
313+
314+
#### See all followed artists\*
315+
316+
```ruby
317+
@sdk.me.following(5) # => [#<Spotify::SDK::Artist...>, ...]
318+
@sdk.me.following(5).size # => 5
319+
@sdk.me.following(50) # => [#<Spotify::SDK::Artist...>, ...]
320+
@sdk.me.following(50).size # => 50
321+
```
322+
323+
230324
<small><i>\* Requires specific user permissions/scopes. See [Authorization Scopes] for more information.</i></small>
231325

232326
## Contributing
@@ -266,16 +360,6 @@ For local development, you can run `bin/console` for an interactive prompt for e
266360
- Push git commits and tags
267361
- Push the `.gem` file to [rubygems.org].
268362

269-
### Changelog
270-
271-
```
272-
[2018-07-21] (0.2.1) First major release.
273-
- Support for Connect and User API endpoints.
274-
- Transitioned to YARD for documentation.
275-
- Website built using Jekyll with Contributing guide.
276-
- Removed Coveralls in favour for CodeClimate
277-
```
278-
279363
## License
280364

281365
The gem is available as open source under the terms of the [MIT License].

lib/spotify/sdk/connect/playback_state.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def artist
133133
#
134134
def item
135135
raise "Playback information is not available if user has a private session enabled" if device.private_session?
136-
Spotify::SDK::Item.new(super, parent)
136+
Spotify::SDK::Item.new(to_h, parent)
137137
end
138138
end
139139
end

lib/spotify/sdk/item.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
module Spotify
44
class SDK
55
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+
618
##
719
# Get the album for this item.
820
#
@@ -41,6 +53,17 @@ def artist
4153
artists.first
4254
end
4355

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+
4467
##
4568
# Get the duration.
4669
# Alias to self.duration_ms

lib/spotify/sdk/me.rb

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,39 +22,115 @@ def info(override_opts={})
2222
Spotify::SDK::Me::Info.new(me_info, self)
2323
end
2424

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(n=10, override_opts={})
37+
request = {
38+
method: :get,
39+
http_path: "/v1/me/player/recently-played",
40+
keys: %i[items],
41+
limit: n
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+
2549
##
2650
# Check if the current user is following N users.
2751
#
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+
#
2866
def following?(list, type=:artist, override_opts={})
2967
raise "Must contain an array" unless list.is_a?(Array)
3068
raise "Must contain an array of String or Spotify::SDK::Artist" if any_of?(list, [String, Spotify::SDK::Artist])
3169
raise "type must be either 'artist' or 'user'" unless %i[artist user].include?(type)
3270
send_is_following_http_requests(list.map {|id| id.try(:id) || id }, type, override_opts)
3371
end
3472

73+
def following_artists?(list, override_opts={})
74+
following?(list, :artist, override_opts)
75+
end
76+
77+
def following_users?(list, override_opts={})
78+
following?(list, :user, override_opts)
79+
end
80+
3581
##
3682
# Get the current user's followed artists. Requires the `user-read-follow` scope.
3783
# GET /v1/me/following
3884
#
3985
# @example
4086
# @sdk.me.following
4187
#
88+
# @param [Integer] n Number of results to return.
4289
# @param [Hash] override_opts Custom options for HTTParty.
4390
# @return [Array] artists A list of followed artists, wrapped in Spotify::SDK::Artist
4491
#
45-
def following(override_opts={})
46-
artists = send_following_http_requests("/v1/me/following?type=artist&limit=50", override_opts)
47-
artists.map do |artist|
92+
def following(n=50, override_opts={})
93+
request = {
94+
method: :get,
95+
# TODO: Spotify API bug - `limit={n}` returns n-1 artists.
96+
# ^ Example: `limit=5` returns 4 artists.
97+
# TODO: Support `type=users` as well as `type=artists`.
98+
http_path: "/v1/me/following?type=artist&limit=#{[n, 50].min}",
99+
keys: %i[artists items],
100+
limit: n
101+
}
102+
103+
send_multiple_http_requests(request, override_opts).map do |artist|
48104
Spotify::SDK::Artist.new(artist, self)
49105
end
50106
end
51107

52108
private
53109

54-
def any_of?(array, klasses)
110+
def any_of?(array, klasses) # :nodoc:
55111
(array.map(&:class) - klasses).any?
56112
end
57113

114+
def send_multiple_http_requests(opts, override_opts) # :nodoc:
115+
response = send_http_request(opts[:method], opts[:http_path], override_opts)
116+
responses, next_request = hash_deep_lookup(response, opts[:keys].dup)
117+
if next_request && responses.size < opts[:limit]
118+
responses += send_multiple_http_requests(opts.merge(http_path: next_request), override_opts)
119+
end
120+
responses.first(opts[:limit])
121+
end
122+
123+
def hash_deep_lookup(response, keys) # :nodoc:
124+
error_message = "Cannot find '%s' key in Spotify::SDK::Me#hash_deep_lookup"
125+
while keys.any?
126+
next_request ||= response[:next]
127+
next_key = keys.shift
128+
response = next_key ? response[next_key] : raise(error_message % next_key)
129+
end
130+
[response, next_request ? next_request[23..-1] : nil]
131+
end
132+
133+
# TODO: Migrate this into the abstracted send_multiple_http_requests
58134
def send_is_following_http_requests(list, type, override_opts) # :nodoc:
59135
max_ids = list.first(50)
60136
remaining_ids = list - max_ids
@@ -71,13 +147,6 @@ def send_is_following_http_requests(list, type, override_opts) # :nodoc:
71147
ids.merge(send_is_following_http_requests(remaining_ids, type, override_opts))
72148
end || ids
73149
end
74-
75-
def send_following_http_requests(http_path, override_opts) # :nodoc:
76-
request = send_http_request(:get, http_path, override_opts)[:artists]
77-
artists = request[:items]
78-
artists << send_following_http_requests(request[:next][23..-1], override_opts) if request[:next]
79-
artists.flatten
80-
end
81150
end
82151
end
83152
end

spec/lib/spotify/sdk/connect/playback_state_spec.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,11 @@
131131
end
132132

133133
it "sends the correct information" do
134-
expect(subject.item.to_h).to eq raw_data[:item]
134+
item_data = raw_data.dup
135+
track = item_data.delete(:track) || item_data.delete(:item)
136+
properties = item_data.except(:parent, :device, :repeat_state, :shuffle_state)
137+
138+
expect(subject.item.to_h).to eq track.merge(properties: properties)
135139
end
136140
end
137141
end

0 commit comments

Comments
 (0)