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

Commit 7ab6b0f

Browse files
authored
Add Transfer a User's Playback (#23)
* wip * fixed devices * added transfer_playback docs * cleanup * added model class * added Spotify::SDK::Model * bug fix * migrated codebase to spotify::sdk::model * fixed tests * polished stub_spotify_api_request * fixup * fixup * added tests * sdk -> _sdk_opts
1 parent 0a03df7 commit 7ab6b0f

File tree

10 files changed

+238
-34
lines changed

10 files changed

+238
-34
lines changed

lib/spotify/sdk.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
require "spotify/sdk/initialization/query_string"
99
require "spotify/sdk/initialization/url_string"
1010
require "spotify/sdk/base"
11+
require "spotify/sdk/model"
1112
require "spotify/sdk/connect"
1213
require "spotify/sdk/connect/device"
1314

lib/spotify/sdk/base.rb

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class Base
1717
# @auth = Spotify::SDK::Base.new(@sdk)
1818
#
1919
# @sdk = Spotify::SDK.new("access_token_here")
20-
# @sdk.to_hash # => { access_token: ..., expires_in: ... }
20+
# @sdk.to_hash # => { access_token: ..., expires_at: ... }
2121
#
2222
# @param [Spotify::SDK] sdk An instance of Spotify::SDK as a reference point.
2323
#
@@ -34,14 +34,22 @@ def initialize(sdk)
3434
# Handle HTTParty responses.
3535
#
3636
# @example
37-
# handle_response self.class.get("/v1/me/player/devices", @options)
37+
# # Return the Hash from the JSON response.
38+
# send_http_request(:get, "/v1/me/player/devices", @options)
3839
#
39-
# @param [HTTParty::Response] response_obj The response object when a HTTParty request is made.
40+
# # Return the raw HTTParty::Response object.
41+
# send_http_request(:get, "/v1/me/player/devices", @options.merge(raw: true))
42+
#
43+
# @param [Hash,HTTParty::Response] response The response from the HTTP request.
4044
# @return
4145
#
42-
def handle_response(response_obj, &_block)
43-
response = block_given? ? yield : response_obj
44-
response.parsed_response.deep_symbolize_keys
46+
def send_http_request(method, endpoint, opts={}, &_block)
47+
sdk_opts = opts[:_sdk_opts].presence || {}
48+
opts_sdk = {raw: false, expect_nil: false}.merge(sdk_opts)
49+
response = self.class.send(method, endpoint, @options.merge(opts))
50+
response = response.parsed_response.try(:deep_symbolize_keys) if opts_sdk[:raw] == false
51+
response = true if opts_sdk[:expect_nil] == true && response.nil?
52+
response
4553
end
4654

4755
attr_reader :sdk

lib/spotify/sdk/connect.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ class Connect < Base
1616
# @return
1717
#
1818
def devices(override_opts={})
19-
resp = self.class.get("/v1/me/player/devices", @options.merge(override_opts))
20-
handle_response(resp)[:devices].map do |device|
21-
Spotify::SDK::Connect::Device.new(device)
19+
response = send_http_request(:get, "/v1/me/player/devices", override_opts)
20+
response[:devices].map do |device|
21+
Spotify::SDK::Connect::Device.new(device, self)
2222
end
2323
end
2424
end

lib/spotify/sdk/connect/device.rb

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,52 @@
33
module Spotify
44
class SDK
55
class Connect
6-
class Device < OpenStruct
6+
class Device < Model
7+
##
8+
# Transfer a user's playback to another device, and continue playing.
9+
# PUT /v1/me/player
10+
#
11+
# @example
12+
# device = @sdk.connect.transfer_playback!
13+
#
14+
# @see https://developer.spotify.com/web-api/transfer-a-users-playback/
15+
#
16+
# @return [Spotify::SDK::Connect::Device] self Return itself, so chained methods can be supported.
17+
#
18+
def transfer_playback!
19+
transfer_playback_method(playing: true)
20+
self
21+
end
22+
23+
##
24+
# Transfer a user's playback to another device, and pause.
25+
# PUT /v1/me/player
26+
#
27+
# @example
28+
# device = @sdk.connect.transfer_state!
29+
#
30+
# @see https://developer.spotify.com/web-api/transfer-a-users-playback/
31+
#
32+
# @return [Spotify::SDK::Connect::Device] self Return itself, so chained methods can be supported.
33+
#
34+
def transfer_state!
35+
transfer_playback_method(playing: false)
36+
self
37+
end
38+
39+
private
40+
41+
def transfer_playback_method(playing:)
42+
override_opts = {
43+
_sdk_opts: {expect_nil: true},
44+
body: {
45+
device_ids: [id],
46+
play: playing
47+
}.to_json
48+
}
49+
50+
parent.send_http_request(:put, "/v1/me/player", override_opts)
51+
end
752
end
853
end
954
end

lib/spotify/sdk/initialization.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ def detect(subject)
5252
when 1
5353
matches.first.perform
5454
when 0
55-
raise Spotify::Errors::InitializationObjectInvalidError
55+
raise Spotify::Errors::InitializationObjectInvalidError.new
5656
else
57-
raise Spotify::Errors::InitializationObjectDuplicationError
57+
raise Spotify::Errors::InitializationObjectDuplicationError.new
5858
end
5959
end
6060
end
@@ -63,12 +63,12 @@ def detect(subject)
6363

6464
class Errors
6565
##
66-
# A Error class for when the initialization subjectect is not valid (see `initialize(subject)` for more info).
66+
# A Error class for when the initialization subject is not valid (see `initialize(subject)` for more info).
6767
#
6868
class InitializationObjectInvalidError < StandardError; end
6969

7070
##
71-
# A Error class for when the initialization subjectect matches against multiple selectors.
71+
# A Error class for when the initialization subject matches against multiple selectors.
7272
# When this Error occurs, this becomes an internal bug. It should be filed on the GitHub issue tracker.
7373
#
7474
class InitializationObjectDuplicationError < StandardError; end

lib/spotify/sdk/model.rb

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# frozen_string_literal: true
2+
3+
module Spotify
4+
class SDK
5+
##
6+
# For each SDK response object (i.e. Device), we have a Model class. We're using OpenStruct.
7+
#
8+
class Model < OpenStruct
9+
##
10+
# Initialize a new Model instance.
11+
#
12+
# @param [Hash] hash The response payload.
13+
# @param [Spotify::SDK] parent The SDK object for context.
14+
#
15+
def initialize(payload, parent)
16+
@payload = payload
17+
validate_payload
18+
19+
@parent = parent
20+
validate_parent
21+
22+
super(payload)
23+
end
24+
25+
##
26+
# A reference to Spotify::SDK::Connect, so we can also do stuff.
27+
#
28+
attr_reader :parent
29+
30+
private
31+
32+
def validate_payload
33+
raise Spotify::Errors::ModelPayloadExpectedToBeHashError.new unless @payload.instance_of? Hash
34+
end
35+
36+
def validate_parent
37+
raise Spotify::Errors::ModelParentInvalidSDKBaseObjectError.new unless @parent.is_a? Spotify::SDK::Base
38+
end
39+
end
40+
end
41+
42+
class Errors
43+
##
44+
# A Error class for when the payload is not a Hash instance.
45+
#
46+
class ModelPayloadExpectedToBeHashError < StandardError; end
47+
48+
##
49+
# A Error class for when the parent is not a Spotify::SDK::Base instance.
50+
#
51+
class ModelParentInvalidSDKBaseObjectError < StandardError; end
52+
end
53+
end

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

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,38 @@
1313
volume_percent: 100
1414
}
1515
end
16-
subject { Spotify::SDK::Connect::Device.new(raw_data) }
16+
let(:sdk) { Spotify::SDK.new("access_token") }
17+
let(:connect_sdk) { Spotify::SDK::Connect.new(sdk) }
18+
subject { Spotify::SDK::Connect::Device.new(raw_data, connect_sdk) }
1719

18-
describe "#raw_data" do
20+
describe "#to_h" do
1921
it "returns the correct value" do
2022
expect(subject.to_h).to eq raw_data
2123
end
2224
end
2325

26+
describe "#transfer_playback!" do
27+
it "should make an api call" do
28+
stub = stub_request(:put, "https://api.spotify.com/v1/me/player")
29+
.with(body: {device_ids: [raw_data[:id]], play: true}.to_json,
30+
headers: {Authorization: "Bearer access_token"})
31+
32+
subject.transfer_playback!
33+
expect(stub).to have_been_requested
34+
end
35+
end
36+
37+
describe "#transfer_state!" do
38+
it "should make an api call" do
39+
stub = stub_request(:put, "https://api.spotify.com/v1/me/player")
40+
.with(body: {device_ids: [raw_data[:id]], play: false}.to_json,
41+
headers: {Authorization: "Bearer access_token"})
42+
43+
subject.transfer_state!
44+
expect(stub).to have_been_requested
45+
end
46+
end
47+
2448
context "Method Missing" do
2549
it "connects method calls to raw_data" do
2650
raw_data.each do |key, value|

spec/lib/spotify/sdk/connect_spec.rb

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
let(:connect_sdk) { Spotify::SDK.new("access_token").connect }
77

88
describe "#devices" do
9-
before(:each) do
10-
stub_spotify_api_request(:get, "/v1/me/player/devices")
11-
end
9+
before(:each) { stub_spotify_api_request(:get, "/v1/me/player/devices") }
1210
let(:devices) { connect_sdk.devices }
1311

1412
it "should return an list of devices" do
@@ -26,19 +24,4 @@
2624
expect(device.volume_percent).to eq 100
2725
end
2826
end
29-
30-
private
31-
32-
def stub_spotify_api_request(method, endpoint)
33-
fixture_filename = "%s%s.json" % [method.to_s, endpoint.tr("/", "-")]
34-
spec_path = File.expand_path("../../../../", __FILE__)
35-
fixture_path = spec_path + "/support/fixtures/%s" % fixture_filename
36-
37-
request_headers = {Authorization: "Bearer access_token"}
38-
response_headers = {"Content-Type": "application/json; charset=utf-8"}
39-
40-
stub_request(method, "https://api.spotify.com%s" % endpoint)
41-
.with(headers: request_headers)
42-
.to_return(status: 200, body: File.read(fixture_path), headers: response_headers)
43-
end
4427
end

spec/lib/spotify/sdk/model_spec.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
5+
RSpec.describe Spotify::SDK::Model do
6+
let(:payload) { {id: 123_456, username: "example", country_code: "GB"} }
7+
let(:sdk) { Spotify::SDK.new("access_token") }
8+
let(:connect_sdk) { Spotify::SDK::Connect.new(sdk) }
9+
10+
context "good initialization" do
11+
it "should not raise an error" do
12+
expect {
13+
Spotify::SDK::Model.new(payload, connect_sdk)
14+
}.not_to raise_error
15+
end
16+
end
17+
18+
context "bad initialization" do
19+
context "parameters count" do
20+
it "should raise an error if zero parameters are given" do
21+
expect {
22+
Spotify::SDK::Model.new
23+
}.to raise_error ArgumentError
24+
end
25+
26+
it "should raise an error if only one parameter is given" do
27+
expect {
28+
Spotify::SDK::Model.new(payload)
29+
}.to raise_error ArgumentError
30+
end
31+
end
32+
33+
context "first parameter" do
34+
it "should raise an error if not a hash" do
35+
expect {
36+
Spotify::SDK::Model.new("Hi world", connect_sdk)
37+
}.to raise_error Spotify::Errors::ModelPayloadExpectedToBeHashError
38+
end
39+
end
40+
41+
context "second parameter" do
42+
it "should raise an error if not a hash" do
43+
expect {
44+
Spotify::SDK::Model.new(payload, "Hi world")
45+
}.to raise_error Spotify::Errors::ModelParentInvalidSDKBaseObjectError
46+
end
47+
end
48+
end
49+
end

spec/spec_helper.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,58 @@
55
require "coveralls"
66
require "webmock/rspec"
77

8+
# Code coverage.
89
Coveralls.wear!
910

11+
# Capture all API calls.
1012
WebMock.disable_net_connect!(allow_localhost: true)
1113

14+
module Helpers
15+
##
16+
# Mock Spotify API requests.
17+
#
18+
def stub_spotify_api_request(method, endpoint)
19+
StubSpotifyAPIRequestHelper.new(method, endpoint).perform
20+
end
21+
22+
class StubSpotifyAPIRequestHelper < OpenStruct
23+
REQUEST_HEADERS = {Authorization: "Bearer access_token"}.freeze
24+
RESPONSE_HEADERS = {"Content-Type": "application/json; charset=utf-8"}.freeze
25+
26+
def initialize(method, endpoint)
27+
@method = method
28+
@endpoint = endpoint
29+
end
30+
31+
def perform
32+
WebMock::API.stub_request(@method, "https://api.spotify.com%s" % @endpoint)
33+
.with(headers: REQUEST_HEADERS)
34+
.to_return(status: 200, body: File.read(fixture_path), headers: RESPONSE_HEADERS)
35+
end
36+
37+
private
38+
39+
def fixture_filename
40+
"%s%s.json" % [@method.to_s, @endpoint.tr("/", "-")]
41+
end
42+
43+
def fixture_path
44+
File.expand_path("../", __FILE__) + "/support/fixtures/%s" % fixture_filename
45+
end
46+
end
47+
end
48+
1249
RSpec.configure do |config|
1350
# Enable flags like --only-failures and --next-failure
1451
config.example_status_persistence_file_path = ".rspec_status"
1552

1653
# Disable RSpec exposing methods globally on `Module` and `main`
1754
config.disable_monkey_patching!
1855

56+
# Include custom helper methods.
57+
config.include Helpers
58+
59+
# Use expect syntax.
1960
config.expect_with :rspec do |c|
2061
c.syntax = :expect
2162
end

0 commit comments

Comments
 (0)