-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathSpotifyRemoteManager.swift
282 lines (238 loc) · 9.88 KB
/
SpotifyRemoteManager.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
//
// SpotifyRemoteManager.swift
// Boost
//
// Created by Dan Appel on 8/17/18.
// Copyright © 2018 Dan Appel. All rights reserved.
//
import SafariServices
import UserNotifications
import SpotifyiOS
enum SpotifyError: Error {
case notAuthenticated
case networkError
case apiError(code: Int)
case decodingError(DecodingError)
static var successCodes: [Int] = [200, 204]
}
enum SpotifyResult<T> {
case success(value: T, code: Int)
case error(SpotifyError)
var value: (value: T, code: Int)? {
guard case let .success(value, code) = self else {
return nil
}
return (value: value, code: code)
}
var error: SpotifyError? {
guard case let .error(error) = self else {
return nil
}
return error
}
}
public class SpotifyRemoteManager: NSObject {
public static let shared = SpotifyRemoteManager()
private static let apiRootURL = "https://api.spotify.com/v1"
private static let apiPlayerURL = apiRootURL + "/me/player"
private static let apiStatusURL = URL(string: apiPlayerURL + "/currently-playing")!
private static let apiPlayURL = URL(string: apiPlayerURL + "/play")!
private static let apiPauseURL = URL(string: apiPlayerURL + "/pause")!
private static let apiNextURL = URL(string: apiPlayerURL + "/next")!
private static let apiPrevURL = URL(string: apiPlayerURL + "/previous")!
private static let apiSeekURL = URL(string: apiPlayerURL + "/seek")!
private static let apiToggleURL = URL(string: "https://boost-spotify.danapp.app/toggle")!
private let scope: SPTScope = [.userReadPlaybackState, .userModifyPlaybackState]
private let configuration: SPTConfiguration = {
let auth = SPTConfiguration(clientID: "9a23191fb3f24dda88b0be476bb32a5e", redirectURL: URL(string: "appdanappboost://")!)
auth.tokenSwapURL = URL(string: "https://boost-spotify.danapp.app/swap")
auth.tokenRefreshURL = URL(string: "https://boost-spotify.danapp.app/refresh")
return auth
}()
private lazy var sessionManager: SPTSessionManager = SPTSessionManager(configuration: configuration, delegate: self)
// callback true if was authenticated or is now authenticated
// callback false if failed or requires app open to authenticate
public func authenticateIfNeeded(callback: ((_ authenticated: Bool) -> ())? = nil) {
if sessionManager.session == nil {
// attempt to restore from cache
sessionManager.session = cachedSession()
}
guard let session = sessionManager.session else {
beginAuthenticationFlow()
callback?(false)
return
}
guard !session.isExpired else {
print("Attempting to renew session")
Notifications.send(.spotifyRenewingSession)
sessionManager.renewSession()
callback?(false)
return
}
print("Access token still valid")
callback?(true)
}
private func beginAuthenticationFlow() {
print("Authenticating user through spotify sdk")
// ensure in foreground
guard UIApplication.shared.applicationState == .active else {
print("App in background, send notification alerting user!")
return Notifications.send(.foregroundRequiredToAuthenticateSpotify)
}
sessionManager.initiateSession(with: scope, options: .clientOnly)
}
public func authCallback(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) {
sessionManager.application(application, open: url, options: options)
}
private func api(request: URLRequest, callback: @escaping (SpotifyResult<Data>) -> ()) {
authenticateIfNeeded { authenticated in
guard authenticated else {
print("Spotify not yet authenticated")
return callback(.error(.notAuthenticated))
}
let request = self.authorized(request: request)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("Network error! \(error)")
return callback(.error(.networkError))
}
// if there was no error then there is definitely an http response
let response = response as! HTTPURLResponse
if SpotifyError.successCodes.contains(response.statusCode) {
return callback(.success(value: data ?? Data(), code: response.statusCode))
} else {
return callback(.error(.apiError(code: response.statusCode)))
}
}
task.resume()
}
}
private func api<T: Decodable>(request: URLRequest, callback: @escaping (SpotifyResult<T>) -> ()) {
api(request: request) { result in
switch result {
case let .error(error):
Notifications.error(message: "\(error)")
return callback(.error(error))
case let .success(value: data, code: code):
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let result = try decoder.decode(T.self, from: data)
callback(.success(value: result, code: code))
} catch let error as DecodingError {
callback(.error(.decodingError(error)))
} catch {
// cannot happen
fatalError("JSONDecoder.decode can only throw a DecodingError")
}
}
}
}
private func api(request: URLRequest, callback: @escaping () -> ()) {
api(request: request) { data in
if case let .error(error) = data {
Notifications.error(message: "\(error)")
}
callback()
}
}
private func authorized(request: URLRequest) -> URLRequest {
var request = request
request.setValue("Bearer \(sessionManager.session!.accessToken)", forHTTPHeaderField: "Authorization")
return request
}
public func next() {
Notifications.send(.spotifySkip)
var nextRequest = URLRequest(url: SpotifyRemoteManager.apiNextURL)
nextRequest.httpMethod = "POST"
api(request: nextRequest) {
print("Skipped to next song")
}
}
public func jumpToBeginning() {
Notifications.send(.spotifyJumpToBeginning)
var components = URLComponents(url: SpotifyRemoteManager.apiSeekURL, resolvingAgainstBaseURL: false)!
components.queryItems = [URLQueryItem(name: "position_ms", value: "0")]
let url = components.url!
var seekRequest = URLRequest(url: url)
seekRequest.httpMethod = "PUT"
api(request: seekRequest) {
print("Jumped to beginning of song")
}
}
public func previous() {
Notifications.send(.spotifyPrevious)
var prevRequest = URLRequest(url: SpotifyRemoteManager.apiPrevURL)
prevRequest.httpMethod = "POST"
api(request: prevRequest) {
print("Skipped to previous song")
}
}
public func play() {
var playRequest = URLRequest(url: SpotifyRemoteManager.apiPlayURL)
playRequest.httpMethod = "PUT"
self.api(request: playRequest) {
print("Sent play request")
}
}
public func pause() {
var pauseRequest = URLRequest(url: SpotifyRemoteManager.apiPauseURL)
pauseRequest.httpMethod = "PUT"
api(request: pauseRequest) {
print("Sent pause request")
}
}
public func togglePlaying() {
Notifications.send(.spotifyPlayPause)
struct Status: Codable {
let isPlaying: Bool
}
var toggleRequest = URLRequest(url: SpotifyRemoteManager.apiToggleURL)
toggleRequest.httpMethod = "PUT"
api(request: toggleRequest) { (result: SpotifyResult<Status?>) in
guard let (newStatus, statusCode) = result.value else {
return print(result.error!)
}
if statusCode == 200, let isPlaying = newStatus?.isPlaying {
print("Toggled playing, new state: [playing: \(isPlaying)]")
} else if statusCode == 204 {
print("Failed to toggle as nothing is playing")
} else {
fatalError("Result should not be a success if status code is non-[200,204]")
}
}
}
}
extension SpotifyRemoteManager: SPTSessionManagerDelegate {
public func sessionManager(manager: SPTSessionManager, didInitiate session: SPTSession) {
Notifications.send(.spotifyInitiatedSession)
cache(session: session)
}
public func sessionManager(manager: SPTSessionManager, didRenew session: SPTSession) {
Notifications.send(.spotifyRenewedSession)
cache(session: session)
}
public func sessionManager(manager: SPTSessionManager, didFailWith error: Error) {
Notifications.error(message: "\(error)")
}
}
extension SpotifyRemoteManager {
private static let sessionCacheKey = "app.danapp.boost.spotify.session"
private func cache(session: SPTSession) {
do {
let sessionData = try NSKeyedArchiver.archivedData(withRootObject: session, requiringSecureCoding: false)
UserDefaults.standard.set(sessionData, forKey: Self.sessionCacheKey)
} catch {
Notifications.error(message: "Failed to archive session")
}
}
private func cachedSession() -> SPTSession? {
guard let sessionData = UserDefaults.standard.data(forKey: Self.sessionCacheKey) else {
return nil
}
guard let session = try? NSKeyedUnarchiver.unarchivedObject(ofClass: SPTSession.self, from: sessionData) else {
return nil
}
return session
}
}