Skip to content

Instantly share code, notes, and snippets.

@TerjeLon
Last active August 4, 2022 20:12
Show Gist options
  • Save TerjeLon/8513732e66a184812bc70954dffd0233 to your computer and use it in GitHub Desktop.
Save TerjeLon/8513732e66a184812bc70954dffd0233 to your computer and use it in GitHub Desktop.
Spotify managing
//
// 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)
}
}
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