Last active
August 4, 2022 20:12
-
-
Save TerjeLon/8513732e66a184812bc70954dffd0233 to your computer and use it in GitHub Desktop.
Spotify managing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SpotifyAPIManager.swift | |
// Musock | |
// | |
// Created by Terje Lønøy on 02/08/2022. | |
// | |
import Foundation | |
class SpotifyAPIManager { | |
enum Endpoint { | |
case myPlaylists | |
case playlistTracks(playlistId: String) | |
var httpBody: Data? { | |
switch self { | |
default: return nil | |
} | |
} | |
var httpMethod: String { | |
switch self { | |
default: return "GET" | |
} | |
} | |
var path: String { | |
switch self { | |
case .myPlaylists: return "v1/me/playlists" | |
case .playlistTracks(let playlistId): return "v1/playlists/\(playlistId)/tracks" | |
} | |
} | |
var queryParams: [URLQueryItem]? { | |
switch self { | |
case .myPlaylists, .playlistTracks: | |
return [URLQueryItem(name: "limit", value: "50"), URLQueryItem(name: "offset", value: "0")] | |
} | |
} | |
private func getDefaultHttpBody(params: [String: Any]) -> Data? { | |
return try? JSONSerialization.data(withJSONObject: params, options: []) | |
} | |
} | |
func authorize(bearer: String, refreshToken: String, clientID: String) async throws -> (Data, URLResponse) { | |
let params = ["grant_type": "refresh_token", | |
"refresh_token": refreshToken, | |
"client_id": clientID] | |
var data = [String]() | |
for(key, value) in params { | |
data.append(key + "=\(value)") | |
} | |
let url = URL(string: "https://accounts.spotify.com/api/token")! | |
var request = URLRequest(url: url) | |
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") | |
request.setValue("Basic \(bearer)", forHTTPHeaderField: "Authorization") | |
request.httpMethod = "POST" | |
request.httpBody = data.map { String($0) }.joined(separator: "&").data(using: .utf8) | |
return try await URLSession.shared.data(for: request) | |
} | |
func call(_ endpoint: Endpoint, accessToken: String) async throws -> (Data, URLResponse) { | |
var url = URL(string: "https://api.spotify.com/\(endpoint.path)")! | |
url.addQueryParams(endpoint.queryParams) | |
var request = URLRequest(url: url) | |
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") | |
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") | |
request.httpMethod = endpoint.httpMethod | |
request.httpBody = endpoint.httpBody | |
return try await URLSession.shared.data(for: request) | |
} | |
func callNext(_ url: URL, accessToken: String) async throws -> (Data, URLResponse) { | |
var request = URLRequest(url: url) | |
request.httpMethod = "GET" | |
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") | |
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") | |
return try await URLSession.shared.data(for: request) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class SpotifyManager: NSObject, ObservableObject, SPTSessionManagerDelegate, SPTAppRemoteDelegate, SPTAppRemotePlayerStateDelegate { | |
public static let shared = SpotifyManager() | |
private let clientId = "***" // Get from spotify developer dashboard | |
private let clientSecret = "***" // Get from spotify developer dashboard | |
private let redirectURL = URL(string: "***")! // Setup in spotify developer dashboard | |
private let apiManager: SpotifyAPIManager | |
var accessToken: String? | |
var refreshToken: String? | |
lazy var bearer: String = Data("\(clientId):\(clientSecret)".utf8).base64EncodedString() | |
lazy var configuration = SPTConfiguration( | |
clientID: clientId, | |
redirectURL: redirectURL | |
) | |
lazy var sessionManager = SPTSessionManager(configuration: configuration, delegate: self) | |
lazy var appRemote: SPTAppRemote = { | |
let appRemote = SPTAppRemote(configuration: self.configuration, logLevel: .debug) | |
appRemote.delegate = self | |
return appRemote | |
}() | |
var session: SPTSession? | |
let hasPlaylistsSelected = !(UserDefaults.standard.stringArray(forKey: "selectedPlaylistIds") ?? []).isEmpty | |
@Published var attemptedSignIn = false | |
@Published var isSignedIn = false | |
@Published var playlists: [SpotifyPlaylistResponseItem] = [] | |
@Published var tracks: [SpotifyTrackResponse] = [] | |
@Published var selectedPlaylistIds: [String] = UserDefaults.standard.stringArray(forKey: "selectedPlaylistIds") ?? [] | |
override init() { | |
self.apiManager = SpotifyAPIManager() | |
super.init() | |
if let refreshTokenData = KeyChainManager.load(key: .refreshToken), | |
let refreshToken = String(data: refreshTokenData, encoding: .utf8) { | |
self.refreshToken = refreshToken | |
} | |
attemptAuthorizeInBackground() | |
} | |
func attemptAuthorizeInBackground() { | |
guard let refreshToken = refreshToken else { | |
attemptedSignIn = true | |
return | |
} | |
Task { | |
do { | |
let (data, _) = try await apiManager.authorize(bearer: bearer, refreshToken: refreshToken, clientID: clientId) | |
if let authResponse = SpotifyAuthResponse.fromData(data) { | |
KeyChainManager.save(key: .refreshToken, data: authResponse.refreshToken.data(using: .utf8)!) | |
self.accessToken = authResponse.accessToken | |
self.refreshToken = authResponse.refreshToken | |
await MainActor.run { | |
self.objectWillChange.send() | |
self.isSignedIn = true | |
self.appRemote.connectionParameters.accessToken = authResponse.accessToken | |
self.appRemote.connect() | |
} | |
} | |
await MainActor.run { | |
self.attemptedSignIn = true | |
} | |
} catch let error { | |
print(error) | |
} | |
} | |
} | |
func authorize() { | |
let scopes: SPTScope = [ | |
.userReadEmail, | |
.userLibraryRead, | |
.userModifyPlaybackState, | |
.userReadCurrentlyPlaying, | |
.streaming, | |
.playlistReadPrivate, | |
.userReadPlaybackState | |
] | |
sessionManager.initiateSession(with: scopes, options: .default) | |
} | |
func fetchAllPlaylists() async { | |
guard let accessToken = accessToken else { | |
#warning("Handle error etc") | |
return | |
} | |
do { | |
let (data, _) = try await apiManager.call(.myPlaylists, accessToken: accessToken) | |
if let playlistData = SpotifyPlaylistResponse.fromData(data)?.items { | |
await MainActor.run { | |
self.playlists = playlistData | |
} | |
} | |
} catch let error { | |
print(error) | |
} | |
} | |
func fetchAvailableSongs() async { | |
guard let accessToken = accessToken else { | |
#warning("Handle error etc") | |
return | |
} | |
await selectedPlaylistIds.asyncForEach { playlistId in | |
do { | |
let (data, _) = try await apiManager.call(.playlistTracks(playlistId: playlistId), accessToken: accessToken) | |
if let trackData = SpotifyPlaylistTracksResponse.fromData(data) { | |
await MainActor.run { | |
#warning("Shite way of doing tihs") | |
self.tracks.append(contentsOf: trackData.items.map { $0.track }) | |
self.tracks = Array(Set(self.tracks)) | |
} | |
await fetchNextSongs(urlString: trackData.next) | |
} | |
} catch let error { | |
print(error) | |
} | |
} | |
} | |
func fetchNextSongs(urlString: String?) async { | |
guard | |
let accessToken = accessToken, | |
let urlString = urlString | |
else { | |
#warning("Handle error etc") | |
return | |
} | |
do { | |
let (data, _) = try await apiManager.callNext(URL(string: urlString)!, accessToken: accessToken) | |
if let trackData = SpotifyPlaylistTracksResponse.fromData(data) { | |
await MainActor.run { | |
self.tracks.append(contentsOf: trackData.items.map { $0.track }) | |
self.tracks = Array(Set(self.tracks)) | |
} | |
await fetchNextSongs(urlString: trackData.next) | |
} | |
} catch let error { | |
print(error) | |
} | |
} | |
// MARK: Session Delegate | |
func sessionManager(manager: SPTSessionManager, didInitiate session: SPTSession) { | |
self.session = session | |
self.accessToken = session.accessToken | |
self.refreshToken = session.refreshToken | |
self.appRemote.connectionParameters.accessToken = session.accessToken | |
self.appRemote.connect() | |
#warning("Implement disconnect and reconnect on app backgrounded / foregrounded") | |
KeyChainManager.save(key: .refreshToken, data: session.refreshToken.data(using: .utf8)!) | |
DispatchQueue.main.async { | |
self.objectWillChange.send() | |
self.isSignedIn = true | |
} | |
} | |
func sessionManager(manager: SPTSessionManager, didFailWith error: Error) { | |
print(error) | |
} | |
// MARK: Remote Delegate | |
func appRemoteDidEstablishConnection(_ appRemote: SPTAppRemote) { | |
appRemote.playerAPI?.delegate = self | |
appRemote.playerAPI?.subscribe(toPlayerState: { (result, error) in | |
if let error = error { | |
debugPrint(error.localizedDescription) | |
} | |
}) | |
} | |
func appRemote(_ appRemote: SPTAppRemote, didFailConnectionAttemptWithError error: Error?) { | |
#warning("This hits when the spotify app is not open and / or playing any song. Alert user etc") | |
print(error.debugDescription) | |
} | |
func appRemote(_ appRemote: SPTAppRemote, didDisconnectWithError error: Error?) { | |
print(error.debugDescription) | |
} | |
// MARK: Player Delegate | |
func playerStateDidChange(_ playerState: SPTAppRemotePlayerState) { | |
print("STATE CHANGED") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment