From 694832b9d8debbc3e519a432ab0abb759720b887 Mon Sep 17 00:00:00 2001 From: Thatcher Clough Date: Tue, 8 Jun 2021 20:55:15 -0400 Subject: [PATCH 1/6] Started work on the CoverFlow API --- .gitignore | 3 ++ CoverFlow/AppleMusicController.swift | 28 +++++++++++++++++++ api/api.py | 29 ++++++++++++++++++++ api/apple_music.py | 41 ++++++++++++++++++++++++++++ api/data.json | 7 +++++ 5 files changed, 108 insertions(+) create mode 100644 api/api.py create mode 100644 api/apple_music.py create mode 100644 api/data.json diff --git a/.gitignore b/.gitignore index bfa9520..55cdf1d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ /Pods *xcworkspace/xcuserdata/* /videos/ +/api/real_api.py +/api/real_data.json +/api/.vscode diff --git a/CoverFlow/AppleMusicController.swift b/CoverFlow/AppleMusicController.swift index bde8a62..9013ed6 100644 --- a/CoverFlow/AppleMusicController.swift +++ b/CoverFlow/AppleMusicController.swift @@ -22,6 +22,34 @@ class AppleMusicController { getCountryCode() } + func getApiKey(baseUrl: String, completion: @escaping (String?)->()) { + let url = URL(string: "\(baseUrl)/api/apple_music/key")! + var request = URLRequest(url: url) + request.httpMethod = "GET" + + let task = URLSession.shared.dataTask(with: request as URLRequest, completionHandler: { data, response, error in + guard error == nil else { + return completion(nil) + } + guard let data = data else { + return completion(nil) + } + + do { + if let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] { + if let key = json["key"] as? String { + return completion(key) + } else { + return completion(nil) + } + } + } catch { + return completion(nil) + } + }) + task.resume() + } + func getCountryCode() { DispatchQueue.global(qos: .background).async { SKCloudServiceController().requestStorefrontCountryCode { countryCode, error in diff --git a/api/api.py b/api/api.py new file mode 100644 index 0000000..7981afe --- /dev/null +++ b/api/api.py @@ -0,0 +1,29 @@ +import flask +from flask import jsonify +import apple_music + +data_file_path = "./data.json" + +app = flask.Flask(__name__) +app.config["DEBUG"] = True + + +@app.route('/api', methods=['GET']) +def api(): + ret = {"message": "CoverFlow API"} + return jsonify(ret) + + +@app.route('/api/apple_music/key', methods=['GET']) +def key(): + key = apple_music.generateKey(data_file_path) + + ret = None + if key == None: + ret = {"error": "Could not generate API key"} + else: + ret = {"key": key} + return jsonify(ret) + + +app.run(host="0.0.0.0") diff --git a/api/apple_music.py b/api/apple_music.py new file mode 100644 index 0000000..fcb7a81 --- /dev/null +++ b/api/apple_music.py @@ -0,0 +1,41 @@ +import datetime +import jwt +from flask import json + +alg = "ES256" +time_now = datetime.datetime.now() +time_expired = datetime.datetime.now() + datetime.timedelta(days=180) + + +def generateKey(data_file_path): + try: + with open(data_file_path) as file: + json_data = json.load(file) + + if "apple_music" in json_data: + apple_music_data = dict(json_data["apple_music"]) + + if ("private_key" in apple_music_data) & ("key_id" in apple_music_data) & ("team_id" in apple_music_data): + private_key = apple_music_data["private_key"] + headers = { + "alg": alg, + "kid": apple_music_data["key_id"] + } + payload = { + "iss": apple_music_data["team_id"], + "exp": int(time_expired.strftime("%s")), + "iat": int(time_now.strftime("%s")) + } + + try: + key = jwt.encode(payload, private_key, + algorithm=alg, headers=headers) + return key + except ValueError: + return None + else: + return None + else: + return None + except FileNotFoundError: + return None diff --git a/api/data.json b/api/data.json new file mode 100644 index 0000000..3193651 --- /dev/null +++ b/api/data.json @@ -0,0 +1,7 @@ +{ + "apple_music" : { + "private_key" : "-----BEGIN PRIVATE KEY-----\n0123456789012345678901234567890123456789012345678901234567890123\n0123456789012345678901234567890123456789012345678901234567890123\n0123456789012345678901234567890123456789012345678901234567890123\n01234567\n-----END PRIVATE KEY-----", + "key_id" : "XXXXXXXXXX", + "team_id" : "XXXXXXXXXX" + } +} \ No newline at end of file From 9e37186a2e52e4bf74e6a0b8e6a6deaaf9fcdd3f Mon Sep 17 00:00:00 2001 From: Thatcher Clough Date: Thu, 10 Jun 2021 00:36:54 -0400 Subject: [PATCH 2/6] Added basic Spotify support to the api --- CoverFlow/SpotifyController.swift | 75 +++++++++++++++------------- api/api.py | 45 ++++++++++++++++- api/spotify.py | 81 +++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+), 36 deletions(-) create mode 100644 api/spotify.py diff --git a/CoverFlow/SpotifyController.swift b/CoverFlow/SpotifyController.swift index 9b015c8..a35bcb6 100644 --- a/CoverFlow/SpotifyController.swift +++ b/CoverFlow/SpotifyController.swift @@ -11,6 +11,13 @@ class SpotifyController: UIResponder, SPTSessionManagerDelegate { // MARK: Variables and constructor + // TODO: + // Handle when api is not running + // Api errors and return codes (and handling them) + // Storring the api location + + let apiBaseURL = "http://192.168.86.31:5000" + var accessToken: String! var refreshToken: String! var codeVerifier: String! @@ -82,7 +89,7 @@ class SpotifyController: UIResponder, SPTSessionManagerDelegate { func getAccessAndRefreshTokens(accessCode: String) { if clientID != nil && clientSecret != nil && redirectURI != nil && codeVerifier != nil { - getAccessAndRefreshTokens(clientID: clientID, clientSecret: clientSecret, redirectURI: redirectURI, accessCode: accessCode, codeVerifier: codeVerifier) { (data, error) in + getAccessAndRefreshTokens(accessCode: accessCode, codeVerifier: codeVerifier) { (data, error) in if error != nil || data == nil { self.resetAccessAndRefreshTokens() return @@ -105,23 +112,15 @@ class SpotifyController: UIResponder, SPTSessionManagerDelegate { } } - private func getAccessAndRefreshTokens(clientID: String, clientSecret: String, redirectURI: URL, accessCode: String, codeVerifier: String, completion: @escaping ([String: Any]?, Error?) -> Void) { - let url = URL(string: "https://accounts.spotify.com/api/token")! - var request = URLRequest(url: url) - request.httpMethod = "POST" - let spotifyAuthKey = "Basic \((clientID + ":" + clientSecret).data(using: .utf8)!.base64EncodedString())" - request.allHTTPHeaderFields = ["Authorization": spotifyAuthKey, "Content-Type": "application/x-www-form-urlencoded"] - var requestBodyComponents = URLComponents() - - requestBodyComponents.queryItems = [ - URLQueryItem(name: "client_id", value: clientID), - URLQueryItem(name: "grant_type", value: "authorization_code"), - URLQueryItem(name: "code", value: accessCode), - URLQueryItem(name: "redirect_uri", value: redirectURI.absoluteString), - URLQueryItem(name: "code_verifier", value: codeVerifier), - URLQueryItem(name: "scope", value: "user-read-currently-playing") + private func getAccessAndRefreshTokens(accessCode: String, codeVerifier: String, completion: @escaping ([String:Any]?, Error?) -> Void) { + var urlComponents = URLComponents(string: "\(apiBaseURL)/api/spotify/swap")! + urlComponents.queryItems = [ + URLQueryItem(name: "access_code", value: accessCode), + URLQueryItem(name: "code_verifier", value: codeVerifier) ] - request.httpBody = requestBodyComponents.query?.data(using: .utf8) + + var request = URLRequest(url: urlComponents.url!) + request.httpMethod = "POST" let task = URLSession.shared.dataTask(with: request) { data, response, error in guard error == nil else { @@ -132,8 +131,15 @@ class SpotifyController: UIResponder, SPTSessionManagerDelegate { } do { - let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] - return completion(json, nil) + if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] { + if json["error"] != nil { + return completion(nil, nil) + } else { + return completion(json, nil) + } + } else { + return completion(nil, nil) + } } catch { return completion(nil, nil) } @@ -144,7 +150,7 @@ class SpotifyController: UIResponder, SPTSessionManagerDelegate { func refreshAccessToken(refreshToken: String) { self.refreshToken = refreshToken if clientID != nil && clientSecret != nil && redirectURI != nil { - refreshAccessToken(clientID: clientID, clientSecret: clientSecret, redirectURI: redirectURI, refreshToken: refreshToken) { (data, error) in + refreshAccessToken(refreshToken: refreshToken) { (data, error) in if error != nil || data == nil { self.resetAccessAndRefreshTokens() return @@ -167,20 +173,14 @@ class SpotifyController: UIResponder, SPTSessionManagerDelegate { } } - private func refreshAccessToken (clientID: String, clientSecret: String, redirectURI: URL, refreshToken: String, completion: @escaping ([String: Any]?, Error?) -> Void) { - let url = URL(string: "https://accounts.spotify.com/api/token")! - var request = URLRequest(url: url) - request.httpMethod = "POST" - let spotifyAuthKey = "Basic \((clientID + ":" + clientSecret).data(using: .utf8)!.base64EncodedString())" - request.allHTTPHeaderFields = ["Authorization": spotifyAuthKey, "Content-Type": "application/x-www-form-urlencoded"] - var requestBodyComponents = URLComponents() - - requestBodyComponents.queryItems = [ - URLQueryItem(name: "client_id", value: clientID), - URLQueryItem(name: "grant_type", value: "refresh_token"), + private func refreshAccessToken (refreshToken: String, completion: @escaping ([String: Any]?, Error?) -> Void) { + var urlComponents = URLComponents(string: "\(apiBaseURL)/api/spotify/refresh")! + urlComponents.queryItems = [ URLQueryItem(name: "refresh_token", value: refreshToken) ] - request.httpBody = requestBodyComponents.query?.data(using: .utf8) + + var request = URLRequest(url: urlComponents.url!) + request.httpMethod = "POST" let task = URLSession.shared.dataTask(with: request) { data, response, error in guard error == nil else { @@ -191,8 +191,15 @@ class SpotifyController: UIResponder, SPTSessionManagerDelegate { } do { - let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] - return completion(json, nil) + if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] { + if json["error"] != nil { + return completion(nil, nil) + } else { + return completion(json, nil) + } + } else { + return completion(nil, nil) + } } catch { return completion(nil, nil) } diff --git a/api/api.py b/api/api.py index 7981afe..68beb80 100644 --- a/api/api.py +++ b/api/api.py @@ -1,6 +1,8 @@ import flask from flask import jsonify +from flask import request import apple_music +import spotify data_file_path = "./data.json" @@ -8,13 +10,13 @@ app.config["DEBUG"] = True -@app.route('/api', methods=['GET']) +@app.route("/api", methods=["GET"]) def api(): ret = {"message": "CoverFlow API"} return jsonify(ret) -@app.route('/api/apple_music/key', methods=['GET']) +@app.route("/api/apple_music/key", methods=["GET"]) def key(): key = apple_music.generateKey(data_file_path) @@ -26,4 +28,43 @@ def key(): return jsonify(ret) +@app.route("/api/spotify/swap", methods=["POST"]) +def swap(): + if ("access_code" in request.args) & ("code_verifier" in request.args): + access_code = request.args["access_code"] + code_verifier = request.args["code_verifier"] + + swap = spotify.swap(data_file_path=data_file_path, + access_code=access_code, code_verifier=code_verifier) + + ret = None + if swap == None: + ret = {"error": "Could not get access and refresh tokens"} + else: + ret = swap + return jsonify(ret) + else: + ret = {"error": "Missing parameters"} + return jsonify(ret) + + +@app.route("/api/spotify/refresh", methods=["POST"]) +def refresh(): + if "refresh_token" in request.args: + refresh_token = request.args["refresh_token"] + + refresh = spotify.refresh( + data_file_path=data_file_path, refresh_token=refresh_token) + + ret = None + if refresh == None: + ret = {"error": "Could not refresh"} + else: + ret = refresh + return jsonify(ret) + else: + ret = {"error": "Missing parameters"} + return jsonify(ret) + + app.run(host="0.0.0.0") diff --git a/api/spotify.py b/api/spotify.py new file mode 100644 index 0000000..eb6c6e9 --- /dev/null +++ b/api/spotify.py @@ -0,0 +1,81 @@ +import requests +import base64 +import six +import json + + +def create_header(client_id, client_secret): + auth_header = base64.b64encode( + six.text_type(client_id + ":" + client_secret).encode("ascii") + ) + return {"Authorization": "Basic %s" % auth_header.decode("ascii")} + + +def swap(data_file_path, access_code, code_verifier): + try: + with open(data_file_path) as file: + json_data = json.load(file) + + if "spotify" in json_data: + spotify_data = dict(json_data["spotify"]) + + if ("client_id" in spotify_data) & ("client_secret" in spotify_data) & ("redirect_uri" in spotify_data): + client_id = spotify_data["client_id"] + client_secret = spotify_data["client_id"] + redirect_uri = spotify_data["redirect_uri"] + + url = "https://accounts.spotify.com/api/token" + header = create_header( + client_id=client_id, client_secret=client_secret) + body = { + "client_id": client_id, + "grant_type": "authorization_code", + "redirect_uri": redirect_uri, + "scope": "user-read-currently-playing", + "code_verifier": code_verifier, + "code": access_code + } + request = requests.post(url=url, headers=header, data=body) + try: + return request.json() + except json.decoder.JSONDecodeError: + return None + else: + return None + else: + return None + except FileNotFoundError: + return None + + +def refresh(data_file_path, refresh_token): + try: + with open(data_file_path) as file: + json_data = json.load(file) + + if "spotify" in json_data: + spotify_data = dict(json_data["spotify"]) + + if ("client_id" in spotify_data) & ("client_secret" in spotify_data): + client_id = spotify_data["client_id"] + client_secret = spotify_data["client_id"] + + url = "https://accounts.spotify.com/api/token" + header = create_header( + client_id=client_id, client_secret=client_secret) + body = { + "client_id": client_id, + "grant_type": "refresh_token", + "refresh_token": refresh_token + } + request = requests.post(url=url, headers=header, data=body) + try: + return request.json() + except json.decoder.JSONDecodeError: + return None + else: + return None + else: + return None + except FileNotFoundError: + return None From 286e82aac4ca6caff79c9f833f5aae628f9afcd8 Mon Sep 17 00:00:00 2001 From: Thatcher Clough Date: Fri, 11 Jun 2021 14:13:45 -0400 Subject: [PATCH 3/6] Updated API sample data and added return codes --- api/api.py | 10 +++++----- api/data.json | 5 +++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/api/api.py b/api/api.py index 68beb80..f88ae31 100644 --- a/api/api.py +++ b/api/api.py @@ -13,7 +13,7 @@ @app.route("/api", methods=["GET"]) def api(): ret = {"message": "CoverFlow API"} - return jsonify(ret) + return jsonify(ret), 200 @app.route("/api/apple_music/key", methods=["GET"]) @@ -22,10 +22,10 @@ def key(): ret = None if key == None: - ret = {"error": "Could not generate API key"} + ret = {"error": "Could not generate key"} else: ret = {"key": key} - return jsonify(ret) + return jsonify(ret), 200 @app.route("/api/spotify/swap", methods=["POST"]) @@ -45,7 +45,7 @@ def swap(): return jsonify(ret) else: ret = {"error": "Missing parameters"} - return jsonify(ret) + return jsonify(ret), 200 @app.route("/api/spotify/refresh", methods=["POST"]) @@ -64,7 +64,7 @@ def refresh(): return jsonify(ret) else: ret = {"error": "Missing parameters"} - return jsonify(ret) + return jsonify(ret), 200 app.run(host="0.0.0.0") diff --git a/api/data.json b/api/data.json index 3193651..95289e4 100644 --- a/api/data.json +++ b/api/data.json @@ -3,5 +3,10 @@ "private_key" : "-----BEGIN PRIVATE KEY-----\n0123456789012345678901234567890123456789012345678901234567890123\n0123456789012345678901234567890123456789012345678901234567890123\n0123456789012345678901234567890123456789012345678901234567890123\n01234567\n-----END PRIVATE KEY-----", "key_id" : "XXXXXXXXXX", "team_id" : "XXXXXXXXXX" + }, + "spotify" : { + "client_id" : "1101010x01010xxx0101010xx01010xx", + "client_secret" : "1101010x01010xxx0101010xx01010xx", + "redirect_uri": "coverflow://spotify-login-callback" } } \ No newline at end of file From e74c0573afd768e4e4d1bd5007a550df7fa0c269 Mon Sep 17 00:00:00 2001 From: Thatcher Clough Date: Tue, 15 Jun 2021 13:28:40 -0400 Subject: [PATCH 4/6] Updated API implementation --- CoverFlow.xcodeproj/project.pbxproj | 22 +++-- CoverFlow/AppleMusicController.swift | 18 +++- CoverFlow/SpotifyController.swift | 87 ++++++++++--------- .../BridgeDiscoveryViewController.swift | 0 .../LightSelectionViewController.swift | 0 .../MainViewController.swift | 50 +++++++++-- .../MusicProviderViewController.swift | 2 +- .../PushButtonViewController.swift | 0 .../SettingsViewController.swift | 0 9 files changed, 121 insertions(+), 58 deletions(-) rename CoverFlow/{ => ViewControllers}/BridgeDiscoveryViewController.swift (100%) rename CoverFlow/{ => ViewControllers}/LightSelectionViewController.swift (100%) rename CoverFlow/{ => ViewControllers}/MainViewController.swift (96%) rename CoverFlow/{ => ViewControllers}/MusicProviderViewController.swift (97%) rename CoverFlow/{ => ViewControllers}/PushButtonViewController.swift (100%) rename CoverFlow/{ => ViewControllers}/SettingsViewController.swift (100%) diff --git a/CoverFlow.xcodeproj/project.pbxproj b/CoverFlow.xcodeproj/project.pbxproj index 23d81bf..244c44e 100644 --- a/CoverFlow.xcodeproj/project.pbxproj +++ b/CoverFlow.xcodeproj/project.pbxproj @@ -137,20 +137,15 @@ isa = PBXGroup; children = ( DDDF442725866D380013CCC4 /* Storyboards */, + DDE41A8B2675189B002E8A33 /* ViewControllers */, DDAEDC97253CF53D002F3175 /* AppDelegate.swift */, DDAEDC99253CF53D002F3175 /* SceneDelegate.swift */, - DDAEDC9B253CF53D002F3175 /* SettingsViewController.swift */, DD439AD225609FE700082CEF /* SpotifyController.swift */, DD439AD025609FDC00082CEF /* AppleMusicController.swift */, - DDFD8BE0253E382C00BBE27A /* PushButtonViewController.swift */, - DD21BD622592D8CE0069304C /* BridgeDiscoveryViewController.swift */, DDFD8BDE253E323900BBE27A /* BridgeInfo.swift */, + DDB07A7B25799901003EA4AF /* LocalNetworkPermissionService.swift */, DDAEDCA0253CF541002F3175 /* Assets.xcassets */, DDAEDCA5253CF541002F3175 /* Info.plist */, - DDFD8BE2253E479400BBE27A /* LightSelectionViewController.swift */, - DDF07093256C375100353469 /* MusicProviderViewController.swift */, - DDB07A7B25799901003EA4AF /* LocalNetworkPermissionService.swift */, - DDB07A952579AD5C003EA4AF /* MainViewController.swift */, ); path = CoverFlow; sourceTree = ""; @@ -164,6 +159,19 @@ path = Storyboards; sourceTree = ""; }; + DDE41A8B2675189B002E8A33 /* ViewControllers */ = { + isa = PBXGroup; + children = ( + DDF07093256C375100353469 /* MusicProviderViewController.swift */, + DDB07A952579AD5C003EA4AF /* MainViewController.swift */, + DDAEDC9B253CF53D002F3175 /* SettingsViewController.swift */, + DD21BD622592D8CE0069304C /* BridgeDiscoveryViewController.swift */, + DDFD8BE0253E382C00BBE27A /* PushButtonViewController.swift */, + DDFD8BE2253E479400BBE27A /* LightSelectionViewController.swift */, + ); + path = ViewControllers; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ diff --git a/CoverFlow/AppleMusicController.swift b/CoverFlow/AppleMusicController.swift index 9013ed6..0cc1c14 100644 --- a/CoverFlow/AppleMusicController.swift +++ b/CoverFlow/AppleMusicController.swift @@ -13,17 +13,26 @@ class AppleMusicController { // MARK: Variables and constructor + let apiBaseURL = "http://192.168.86.31:5000" + var apiKey: String! var countryCode: String! let player = MPMusicPlayerController.systemMusicPlayer - init(apiKey: String) { - self.apiKey = apiKey + init() { getCountryCode() + + getApiKey() { (apiKey) in + if apiKey == nil { + // handle + } else { + self.apiKey = apiKey + } + } } - func getApiKey(baseUrl: String, completion: @escaping (String?)->()) { - let url = URL(string: "\(baseUrl)/api/apple_music/key")! + func getApiKey(completion: @escaping (String?) -> ()) { + let url = URL(string: "\(apiBaseURL)/api/apple_music/key")! var request = URLRequest(url: url) request.httpMethod = "GET" @@ -140,6 +149,7 @@ class AppleMusicController { } } } catch { + // handle (api key invalid) return completion(nil) } }) diff --git a/CoverFlow/SpotifyController.swift b/CoverFlow/SpotifyController.swift index a35bcb6..d4cd9c0 100644 --- a/CoverFlow/SpotifyController.swift +++ b/CoverFlow/SpotifyController.swift @@ -12,9 +12,10 @@ class SpotifyController: UIResponder, SPTSessionManagerDelegate { // MARK: Variables and constructor // TODO: - // Handle when api is not running - // Api errors and return codes (and handling them) // Storring the api location + // Hosting api + // API is off and connecting for the first time + // Apple Music implementation let apiBaseURL = "http://192.168.86.31:5000" @@ -24,19 +25,18 @@ class SpotifyController: UIResponder, SPTSessionManagerDelegate { var sessionManager: SPTSessionManager! private var clientID: String! - private var clientSecret: String! private var redirectURI: URL! init(clientID: String, clientSecret: String, redirectURI: URL) { super.init() self.clientID = clientID - self.clientSecret = clientSecret self.redirectURI = redirectURI if let refreshToken = UserDefaults.standard.string(forKey: "refreshToken") { refreshAccessToken(refreshToken: refreshToken) } else { + resetAccessAndRefreshTokens() initSessionManager() } } @@ -88,17 +88,17 @@ class SpotifyController: UIResponder, SPTSessionManagerDelegate { } func getAccessAndRefreshTokens(accessCode: String) { - if clientID != nil && clientSecret != nil && redirectURI != nil && codeVerifier != nil { - getAccessAndRefreshTokens(accessCode: accessCode, codeVerifier: codeVerifier) { (data, error) in - if error != nil || data == nil { + if codeVerifier != nil { + getAccessAndRefreshTokens(accessCode: accessCode, codeVerifier: codeVerifier) { data in + if data == nil { self.resetAccessAndRefreshTokens() return } else { if let accessToken = data!["access_token"] as? String, let refreshToken = data!["refresh_token"] as? String { + self.setUserDefault(key: "refreshToken", value: refreshToken) self.accessToken = accessToken self.refreshToken = refreshToken - UserDefaults.standard.set(refreshToken, forKey: "refreshToken") return } else { self.resetAccessAndRefreshTokens() @@ -112,7 +112,7 @@ class SpotifyController: UIResponder, SPTSessionManagerDelegate { } } - private func getAccessAndRefreshTokens(accessCode: String, codeVerifier: String, completion: @escaping ([String:Any]?, Error?) -> Void) { + private func getAccessAndRefreshTokens(accessCode: String, codeVerifier: String, completion: @escaping ([String:Any]?) -> Void) { var urlComponents = URLComponents(string: "\(apiBaseURL)/api/spotify/swap")! urlComponents.queryItems = [ URLQueryItem(name: "access_code", value: accessCode), @@ -124,24 +124,24 @@ class SpotifyController: UIResponder, SPTSessionManagerDelegate { let task = URLSession.shared.dataTask(with: request) { data, response, error in guard error == nil else { - return completion(nil, error) + return completion(nil) } guard let data = data else { - return completion(nil, nil) + return completion(nil) } do { if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] { if json["error"] != nil { - return completion(nil, nil) + return completion(nil) } else { - return completion(json, nil) + return completion(json) } } else { - return completion(nil, nil) + return completion(nil) } } catch { - return completion(nil, nil) + return completion(nil) } } task.resume() @@ -149,31 +149,40 @@ class SpotifyController: UIResponder, SPTSessionManagerDelegate { func refreshAccessToken(refreshToken: String) { self.refreshToken = refreshToken - if clientID != nil && clientSecret != nil && redirectURI != nil { - refreshAccessToken(refreshToken: refreshToken) { (data, error) in - if error != nil || data == nil { - self.resetAccessAndRefreshTokens() + + refreshAccessToken(refreshToken: refreshToken) { data in + if data == nil { + self.resetAccessAndRefreshTokens() + return + } else { + if let accessToken = data!["access_token"] as? String, + let refreshToken = data!["refresh_token"] as? String { + self.setUserDefault(key: "refreshToken", value: refreshToken) + self.accessToken = accessToken + self.refreshToken = refreshToken return } else { - if let accessToken = data!["access_token"] as? String, - let refreshToken = data!["refresh_token"] as? String { - self.accessToken = accessToken - self.refreshToken = refreshToken - UserDefaults.standard.set(refreshToken, forKey: "refreshToken") - return - } else { - self.resetAccessAndRefreshTokens() - return - } + self.resetAccessAndRefreshTokens() + return } } - } else { - resetAccessAndRefreshTokens() - return } } - private func refreshAccessToken (refreshToken: String, completion: @escaping ([String: Any]?, Error?) -> Void) { + func setUserDefault(key: String, value: String) { + setUserDefault(key: key, value: value) { + if UserDefaults.standard.string(forKey: key) != value { + self.setUserDefault(key: key, value: value) + } + } + } + + func setUserDefault(key: String, value: String?, completion: ()->()) { + UserDefaults.standard.setValue(value, forKey: key) + return completion() + } + + private func refreshAccessToken(refreshToken: String, completion: @escaping ([String: Any]?) -> Void) { var urlComponents = URLComponents(string: "\(apiBaseURL)/api/spotify/refresh")! urlComponents.queryItems = [ URLQueryItem(name: "refresh_token", value: refreshToken) @@ -184,24 +193,24 @@ class SpotifyController: UIResponder, SPTSessionManagerDelegate { let task = URLSession.shared.dataTask(with: request) { data, response, error in guard error == nil else { - return completion(nil, error) + return completion(nil) } guard let data = data else { - return completion(nil, nil) + return completion(nil) } do { if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] { if json["error"] != nil { - return completion(nil, nil) + return completion(nil) } else { - return completion(json, nil) + return completion(json) } } else { - return completion(nil, nil) + return completion(nil) } } catch { - return completion(nil, nil) + return completion(nil) } } task.resume() diff --git a/CoverFlow/BridgeDiscoveryViewController.swift b/CoverFlow/ViewControllers/BridgeDiscoveryViewController.swift similarity index 100% rename from CoverFlow/BridgeDiscoveryViewController.swift rename to CoverFlow/ViewControllers/BridgeDiscoveryViewController.swift diff --git a/CoverFlow/LightSelectionViewController.swift b/CoverFlow/ViewControllers/LightSelectionViewController.swift similarity index 100% rename from CoverFlow/LightSelectionViewController.swift rename to CoverFlow/ViewControllers/LightSelectionViewController.swift diff --git a/CoverFlow/MainViewController.swift b/CoverFlow/ViewControllers/MainViewController.swift similarity index 96% rename from CoverFlow/MainViewController.swift rename to CoverFlow/ViewControllers/MainViewController.swift index f0ee566..9ff066e 100644 --- a/CoverFlow/MainViewController.swift +++ b/CoverFlow/ViewControllers/MainViewController.swift @@ -17,6 +17,8 @@ class MainViewController: UIViewController { // MARK: Variables, IBOutlets, and IBActions + let apiBaseURL = "http://192.168.86.31:5000" + let keys = CoverFlowKeys() var canPushNotifications: Bool = false var appleMusicController: AppleMusicController! @@ -48,12 +50,23 @@ class MainViewController: UIViewController { } else { startButton.isEnabled = false if startButton.titleLabel?.text == "Start" { - startButton.setTitle("Starting...", alpha: 0.9) - - DispatchQueue.global(qos: .background).async { - self.getCurrentLightsStates() - self.start() - self.startBackgrounding() + checkAPI { (online) in + if online { + DispatchQueue.main.async { + self.startButton.setTitle("Starting...", alpha: 0.9) + } + + DispatchQueue.global(qos: .background).async { + self.getCurrentLightsStates() + self.start() + self.startBackgrounding() + } + } else { + DispatchQueue.main.async { + self.alert(title: "Error", body: "CoverFlow API is not online. Try again later.") + self.startButton.isEnabled = true + } + } } } else { stop() @@ -110,7 +123,7 @@ class MainViewController: UIViewController { NotificationCenter.default.addObserver(self, selector:#selector(appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil) if MainViewController.musicProvider == "appleMusic" && appleMusicController == nil { - appleMusicController = AppleMusicController(apiKey: keys.appleMusicAPIKey1) + appleMusicController = AppleMusicController() } else if MainViewController.musicProvider == "spotify" && spotifyController == nil { spotifyController = SpotifyController(clientID: keys.spotifyClientID, clientSecret: keys.spotifyClientSecret, redirectURI: URL(string: "coverflow://spotify-login-callback")!) } @@ -286,6 +299,29 @@ class MainViewController: UIViewController { } } + // MARK: API Related + + func checkAPI(completion: @escaping (Bool) -> Void) { + guard let url = URL(string: "\(apiBaseURL)/api") else { return } + + var request = URLRequest(url: url) + request.timeoutInterval = 1.0 + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + guard error == nil else { + return completion(false) + } + if let responseCode = response as? HTTPURLResponse { + if responseCode.statusCode == 200 { + return completion(true) + } else { + return completion(false) + } + } + } + task.resume() + } + // MARK: Bridge Related func buildBridge(info: BridgeInfo) -> PHSBridge { diff --git a/CoverFlow/MusicProviderViewController.swift b/CoverFlow/ViewControllers/MusicProviderViewController.swift similarity index 97% rename from CoverFlow/MusicProviderViewController.swift rename to CoverFlow/ViewControllers/MusicProviderViewController.swift index 93dc1fb..2b6a84d 100644 --- a/CoverFlow/MusicProviderViewController.swift +++ b/CoverFlow/ViewControllers/MusicProviderViewController.swift @@ -31,7 +31,7 @@ public class MusicProviderViewController: UIViewController { @IBAction func appleMusicButtonAction(_ sender: Any) { if appleMusicController == nil { - appleMusicController = AppleMusicController(apiKey: keys.appleMusicAPIKey1) + appleMusicController = AppleMusicController() } requestLibraryAccess() diff --git a/CoverFlow/PushButtonViewController.swift b/CoverFlow/ViewControllers/PushButtonViewController.swift similarity index 100% rename from CoverFlow/PushButtonViewController.swift rename to CoverFlow/ViewControllers/PushButtonViewController.swift diff --git a/CoverFlow/SettingsViewController.swift b/CoverFlow/ViewControllers/SettingsViewController.swift similarity index 100% rename from CoverFlow/SettingsViewController.swift rename to CoverFlow/ViewControllers/SettingsViewController.swift From aed09158fd34fe23706763db03622b13f913048b Mon Sep 17 00:00:00 2001 From: Thatcher Clough Date: Wed, 16 Jun 2021 11:05:25 -0400 Subject: [PATCH 5/6] Finished Apple Music API implementation --- CoverFlow.xcodeproj/project.pbxproj | 2 +- CoverFlow/AppleMusicController.swift | 50 ++++++++----- CoverFlow/SpotifyController.swift | 2 - .../Storyboards/Base.lproj/Main.storyboard | 5 +- .../ViewControllers/MainViewController.swift | 2 +- .../MusicProviderViewController.swift | 75 +++++++++++++++++-- 6 files changed, 101 insertions(+), 35 deletions(-) diff --git a/CoverFlow.xcodeproj/project.pbxproj b/CoverFlow.xcodeproj/project.pbxproj index 244c44e..afdfc7a 100644 --- a/CoverFlow.xcodeproj/project.pbxproj +++ b/CoverFlow.xcodeproj/project.pbxproj @@ -140,8 +140,8 @@ DDE41A8B2675189B002E8A33 /* ViewControllers */, DDAEDC97253CF53D002F3175 /* AppDelegate.swift */, DDAEDC99253CF53D002F3175 /* SceneDelegate.swift */, - DD439AD225609FE700082CEF /* SpotifyController.swift */, DD439AD025609FDC00082CEF /* AppleMusicController.swift */, + DD439AD225609FE700082CEF /* SpotifyController.swift */, DDFD8BDE253E323900BBE27A /* BridgeInfo.swift */, DDB07A7B25799901003EA4AF /* LocalNetworkPermissionService.swift */, DDAEDCA0253CF541002F3175 /* Assets.xcassets */, diff --git a/CoverFlow/AppleMusicController.swift b/CoverFlow/AppleMusicController.swift index 0cc1c14..28781ec 100644 --- a/CoverFlow/AppleMusicController.swift +++ b/CoverFlow/AppleMusicController.swift @@ -21,16 +21,27 @@ class AppleMusicController { init() { getCountryCode() + setApiKey() + } + + func getCountryCode() { + countryCode = "us" - getApiKey() { (apiKey) in - if apiKey == nil { - // handle - } else { - self.apiKey = apiKey + DispatchQueue.global(qos: .background).async { + SKCloudServiceController().requestStorefrontCountryCode { countryCode, error in + if countryCode != nil && error == nil { + self.countryCode = countryCode + } } } } + func setApiKey() { + getApiKey { (apiKey) in + self.apiKey = apiKey + } + } + func getApiKey(completion: @escaping (String?) -> ()) { let url = URL(string: "\(apiBaseURL)/api/apple_music/key")! var request = URLRequest(url: url) @@ -59,18 +70,6 @@ class AppleMusicController { task.resume() } - func getCountryCode() { - DispatchQueue.global(qos: .background).async { - SKCloudServiceController().requestStorefrontCountryCode { countryCode, error in - if countryCode == nil || error != nil { - self.countryCode = "us" - } else { - self.countryCode = countryCode - } - } - } - } - // MARK: Functions func getCurrentAlbumName() -> String { @@ -149,8 +148,21 @@ class AppleMusicController { } } } catch { - // handle (api key invalid) - return completion(nil) + if data.count > 0 { + return completion(nil) + } else { + self.getApiKey { (apiKey) in + if apiKey == nil { + return completion(nil) + } else { + self.apiKey = apiKey + + self.getCoverFromAPI(albumName: albumName, artistName: artistName) { (url) in + return completion(url) + } + } + } + } } }) task.resume() diff --git a/CoverFlow/SpotifyController.swift b/CoverFlow/SpotifyController.swift index d4cd9c0..7429500 100644 --- a/CoverFlow/SpotifyController.swift +++ b/CoverFlow/SpotifyController.swift @@ -14,8 +14,6 @@ class SpotifyController: UIResponder, SPTSessionManagerDelegate { // TODO: // Storring the api location // Hosting api - // API is off and connecting for the first time - // Apple Music implementation let apiBaseURL = "http://192.168.86.31:5000" diff --git a/CoverFlow/Storyboards/Base.lproj/Main.storyboard b/CoverFlow/Storyboards/Base.lproj/Main.storyboard index 91be764..fed7021 100644 --- a/CoverFlow/Storyboards/Base.lproj/Main.storyboard +++ b/CoverFlow/Storyboards/Base.lproj/Main.storyboard @@ -21,7 +21,7 @@ - + @@ -844,8 +844,5 @@ - - - diff --git a/CoverFlow/ViewControllers/MainViewController.swift b/CoverFlow/ViewControllers/MainViewController.swift index 9ff066e..ff78a86 100644 --- a/CoverFlow/ViewControllers/MainViewController.swift +++ b/CoverFlow/ViewControllers/MainViewController.swift @@ -63,7 +63,7 @@ class MainViewController: UIViewController { } } else { DispatchQueue.main.async { - self.alert(title: "Error", body: "CoverFlow API is not online. Try again later.") + self.alert(title: "Error", body: "The CoverFlow API is not online. Try again later.") self.startButton.isEnabled = true } } diff --git a/CoverFlow/ViewControllers/MusicProviderViewController.swift b/CoverFlow/ViewControllers/MusicProviderViewController.swift index 2b6a84d..ebd765f 100644 --- a/CoverFlow/ViewControllers/MusicProviderViewController.swift +++ b/CoverFlow/ViewControllers/MusicProviderViewController.swift @@ -20,6 +20,8 @@ public class MusicProviderViewController: UIViewController { // MARK: Variables, IBOutlets, and IBActions + let apiBaseURL = "http://192.168.86.31:5000" + var delegate: MusicProviderViewControllerDelegate? let keys = CoverFlowKeys() @@ -30,11 +32,23 @@ public class MusicProviderViewController: UIViewController { @IBOutlet var headerConstraint: NSLayoutConstraint! @IBAction func appleMusicButtonAction(_ sender: Any) { - if appleMusicController == nil { - appleMusicController = AppleMusicController() + self.checkAPI { (online) in + if !online { + DispatchQueue.main.async { + if self.presentedViewController == nil { + let alert = UIAlertController(title: "Error", message: "The CoverFlow API is not online. Try again later.", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Dismiss", style: .default, handler: nil)) + self.present(alert, animated: true, completion: nil) + } + } + } else { + if self.appleMusicController == nil { + self.appleMusicController = AppleMusicController() + } + + self.requestLibraryAccess() + } } - - requestLibraryAccess() } func requestLibraryAccess() { @@ -58,11 +72,23 @@ public class MusicProviderViewController: UIViewController { } @IBAction func spotifyButtonAction(_ sender: Any) { - if spotifyController == nil { - spotifyController = SpotifyController(clientID: keys.spotifyClientID, clientSecret: keys.spotifyClientSecret, redirectURI: URL(string: "coverflow://spotify-login-callback")!) + self.checkAPI { (online) in + if !online { + DispatchQueue.main.async { + if self.presentedViewController == nil { + let alert = UIAlertController(title: "Error", message: "The CoverFlow API is not online. Try again later.", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Dismiss", style: .default, handler: nil)) + self.present(alert, animated: true, completion: nil) + } + } + } else { + if self.spotifyController == nil { + self.spotifyController = SpotifyController(clientID: self.keys.spotifyClientID, clientSecret: self.keys.spotifyClientSecret, redirectURI: URL(string: "coverflow://spotify-login-callback")!) + } + + self.spotifyController.connect() + } } - - spotifyController.connect() } // MARK: View Related @@ -104,6 +130,39 @@ public class MusicProviderViewController: UIViewController { if error == nil { self.delegate?.didGetNotificationsSettings(canPushNotifications: granted) } + + self.checkAPI { (online) in + if !online { + DispatchQueue.main.async { + if self.presentedViewController == nil { + let alert = UIAlertController(title: "Error", message: "The CoverFlow API is not online. Try again later.", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Dismiss", style: .default, handler: nil)) + self.present(alert, animated: true, completion: nil) + } + } + } + } + } + } + + func checkAPI(completion: @escaping (Bool) -> Void) { + guard let url = URL(string: "\(apiBaseURL)/api") else { return } + + var request = URLRequest(url: url) + request.timeoutInterval = 1.0 + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + guard error == nil else { + return completion(false) + } + if let responseCode = response as? HTTPURLResponse { + if responseCode.statusCode == 200 { + return completion(true) + } else { + return completion(false) + } + } } + task.resume() } } From 87c3dec1f15e6f37b50584a80c1c039679b6b216 Mon Sep 17 00:00:00 2001 From: Thatcher Clough Date: Thu, 17 Jun 2021 13:45:38 -0400 Subject: [PATCH 6/6] Finished API work --- .gitignore | 1 + CoverFlow.xcodeproj/project.pbxproj | 4 +- .../xcshareddata/xcschemes/CoverFlow.xcscheme | 2 +- CoverFlow/AppleMusicController.swift | 6 +- CoverFlow/SpotifyController.swift | 40 +++-- .../ViewControllers/MainViewController.swift | 11 +- .../MusicProviderViewController.swift | 16 +- Podfile | 4 +- Podfile.lock | 4 +- README.md | 10 +- api/README.md | 149 ++++++++++++++++++ api/api.py | 33 ++-- api/apple_music.py | 2 +- api/libs.txt | 8 + 14 files changed, 221 insertions(+), 69 deletions(-) create mode 100644 api/README.md create mode 100644 api/libs.txt diff --git a/.gitignore b/.gitignore index 55cdf1d..b69f9be 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /api/real_api.py /api/real_data.json /api/.vscode +.vscode \ No newline at end of file diff --git a/CoverFlow.xcodeproj/project.pbxproj b/CoverFlow.xcodeproj/project.pbxproj index afdfc7a..3b1dea5 100644 --- a/CoverFlow.xcodeproj/project.pbxproj +++ b/CoverFlow.xcodeproj/project.pbxproj @@ -466,7 +466,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.4; + MARKETING_VERSION = 1.5.0; OTHER_LDFLAGS = ( "$(inherited)", "-framework", @@ -516,7 +516,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.4; + MARKETING_VERSION = 1.5.0; OTHER_LDFLAGS = ( "$(inherited)", "-framework", diff --git a/CoverFlow.xcodeproj/xcshareddata/xcschemes/CoverFlow.xcscheme b/CoverFlow.xcodeproj/xcshareddata/xcschemes/CoverFlow.xcscheme index 9ead48b..d2cafd6 100644 --- a/CoverFlow.xcodeproj/xcshareddata/xcschemes/CoverFlow.xcscheme +++ b/CoverFlow.xcodeproj/xcshareddata/xcschemes/CoverFlow.xcscheme @@ -1,6 +1,6 @@ ()) { - let url = URL(string: "\(apiBaseURL)/api/apple_music/key")! + let url = URL(string: "\(keys.apiBaseUrl)/api/apple_music/key")! var request = URLRequest(url: url) request.httpMethod = "GET" diff --git a/CoverFlow/SpotifyController.swift b/CoverFlow/SpotifyController.swift index 7429500..22a3898 100644 --- a/CoverFlow/SpotifyController.swift +++ b/CoverFlow/SpotifyController.swift @@ -6,17 +6,13 @@ // import Foundation +import Keys class SpotifyController: UIResponder, SPTSessionManagerDelegate { // MARK: Variables and constructor - // TODO: - // Storring the api location - // Hosting api - - let apiBaseURL = "http://192.168.86.31:5000" - + let keys = CoverFlowKeys() var accessToken: String! var refreshToken: String! var codeVerifier: String! @@ -25,7 +21,7 @@ class SpotifyController: UIResponder, SPTSessionManagerDelegate { private var clientID: String! private var redirectURI: URL! - init(clientID: String, clientSecret: String, redirectURI: URL) { + init(clientID: String, redirectURI: URL) { super.init() self.clientID = clientID @@ -111,7 +107,7 @@ class SpotifyController: UIResponder, SPTSessionManagerDelegate { } private func getAccessAndRefreshTokens(accessCode: String, codeVerifier: String, completion: @escaping ([String:Any]?) -> Void) { - var urlComponents = URLComponents(string: "\(apiBaseURL)/api/spotify/swap")! + var urlComponents = URLComponents(string: "\(keys.apiBaseUrl)/api/spotify/swap")! urlComponents.queryItems = [ URLQueryItem(name: "access_code", value: accessCode), URLQueryItem(name: "code_verifier", value: codeVerifier) @@ -167,21 +163,8 @@ class SpotifyController: UIResponder, SPTSessionManagerDelegate { } } - func setUserDefault(key: String, value: String) { - setUserDefault(key: key, value: value) { - if UserDefaults.standard.string(forKey: key) != value { - self.setUserDefault(key: key, value: value) - } - } - } - - func setUserDefault(key: String, value: String?, completion: ()->()) { - UserDefaults.standard.setValue(value, forKey: key) - return completion() - } - private func refreshAccessToken(refreshToken: String, completion: @escaping ([String: Any]?) -> Void) { - var urlComponents = URLComponents(string: "\(apiBaseURL)/api/spotify/refresh")! + var urlComponents = URLComponents(string: "\(keys.apiBaseUrl)/api/spotify/refresh")! urlComponents.queryItems = [ URLQueryItem(name: "refresh_token", value: refreshToken) ] @@ -214,6 +197,19 @@ class SpotifyController: UIResponder, SPTSessionManagerDelegate { task.resume() } + func setUserDefault(key: String, value: String) { + setUserDefault(key: key, value: value) { + if UserDefaults.standard.string(forKey: key) != value { + self.setUserDefault(key: key, value: value) + } + } + } + + func setUserDefault(key: String, value: String?, completion: ()->()) { + UserDefaults.standard.setValue(value, forKey: key) + return completion() + } + public func getCurrentAlbum(completion: @escaping ([String: Any])->()) { if accessToken == nil { return completion(["retry": "Access token not set"]) diff --git a/CoverFlow/ViewControllers/MainViewController.swift b/CoverFlow/ViewControllers/MainViewController.swift index ff78a86..42e073d 100644 --- a/CoverFlow/ViewControllers/MainViewController.swift +++ b/CoverFlow/ViewControllers/MainViewController.swift @@ -17,8 +17,6 @@ class MainViewController: UIViewController { // MARK: Variables, IBOutlets, and IBActions - let apiBaseURL = "http://192.168.86.31:5000" - let keys = CoverFlowKeys() var canPushNotifications: Bool = false var appleMusicController: AppleMusicController! @@ -49,6 +47,7 @@ class MainViewController: UIViewController { alert(title: "Error", body: "Please connect to a bridge in settings before continuing.") } else { startButton.isEnabled = false + if startButton.titleLabel?.text == "Start" { checkAPI { (online) in if online { @@ -63,8 +62,8 @@ class MainViewController: UIViewController { } } else { DispatchQueue.main.async { - self.alert(title: "Error", body: "The CoverFlow API is not online. Try again later.") - self.startButton.isEnabled = true + self.alert(title: "Error", body: "The CoverFlow API is not online. Try again later.") + self.startButton.isEnabled = true } } } @@ -125,7 +124,7 @@ class MainViewController: UIViewController { if MainViewController.musicProvider == "appleMusic" && appleMusicController == nil { appleMusicController = AppleMusicController() } else if MainViewController.musicProvider == "spotify" && spotifyController == nil { - spotifyController = SpotifyController(clientID: keys.spotifyClientID, clientSecret: keys.spotifyClientSecret, redirectURI: URL(string: "coverflow://spotify-login-callback")!) + spotifyController = SpotifyController(clientID: keys.spotifyClientID, redirectURI: URL(string: keys.spotifyRedirectUri)!) } checkPermissionsAndSetupHue() @@ -302,7 +301,7 @@ class MainViewController: UIViewController { // MARK: API Related func checkAPI(completion: @escaping (Bool) -> Void) { - guard let url = URL(string: "\(apiBaseURL)/api") else { return } + guard let url = URL(string: "\(keys.apiBaseUrl)/api") else { return } var request = URLRequest(url: url) request.timeoutInterval = 1.0 diff --git a/CoverFlow/ViewControllers/MusicProviderViewController.swift b/CoverFlow/ViewControllers/MusicProviderViewController.swift index ebd765f..4015200 100644 --- a/CoverFlow/ViewControllers/MusicProviderViewController.swift +++ b/CoverFlow/ViewControllers/MusicProviderViewController.swift @@ -20,8 +20,6 @@ public class MusicProviderViewController: UIViewController { // MARK: Variables, IBOutlets, and IBActions - let apiBaseURL = "http://192.168.86.31:5000" - var delegate: MusicProviderViewControllerDelegate? let keys = CoverFlowKeys() @@ -32,7 +30,7 @@ public class MusicProviderViewController: UIViewController { @IBOutlet var headerConstraint: NSLayoutConstraint! @IBAction func appleMusicButtonAction(_ sender: Any) { - self.checkAPI { (online) in + checkAPI { (online) in if !online { DispatchQueue.main.async { if self.presentedViewController == nil { @@ -82,11 +80,13 @@ public class MusicProviderViewController: UIViewController { } } } else { - if self.spotifyController == nil { - self.spotifyController = SpotifyController(clientID: self.keys.spotifyClientID, clientSecret: self.keys.spotifyClientSecret, redirectURI: URL(string: "coverflow://spotify-login-callback")!) + DispatchQueue.main.async { + if self.spotifyController == nil { + self.spotifyController = SpotifyController(clientID: self.keys.spotifyClientID, redirectURI: URL(string: self.keys.spotifyRedirectUri)!) + } + + self.spotifyController.connect() } - - self.spotifyController.connect() } } } @@ -146,7 +146,7 @@ public class MusicProviderViewController: UIViewController { } func checkAPI(completion: @escaping (Bool) -> Void) { - guard let url = URL(string: "\(apiBaseURL)/api") else { return } + guard let url = URL(string: "\(keys.apiBaseUrl)/api") else { return } var request = URLRequest(url: url) request.timeoutInterval = 1.0 diff --git a/Podfile b/Podfile index 12db17a..2553b41 100644 --- a/Podfile +++ b/Podfile @@ -4,9 +4,9 @@ platform :ios, '13.0' plugin 'cocoapods-keys', { :project => "CoverFlow", :keys => [ - "AppleMusicAPIKey1", + "ApiBaseUrl", "SpotifyClientID", - "SpotifyClientSecret" + "SpotifyRedirectUri" ] } diff --git a/Podfile.lock b/Podfile.lock index 7c7310d..6117bc7 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -26,6 +26,6 @@ SPEC CHECKSUMS: Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 -PODFILE CHECKSUM: 43bac3ebb130075c37f805748bd6173d4ebbbf89 +PODFILE CHECKSUM: 98d8d50e6c4d0c9d38c8bf5e4195240eec5c9951 -COCOAPODS: 1.9.3 +COCOAPODS: 1.10.1 diff --git a/README.md b/README.md index f00641c..93e8702 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,11 @@ CoverFlow uses [ColorThief](https://github.com/yamoridon/ColorThiefSwift) to fin CoverFlow is compatible with both Apple Music and Spotify. -Note: While this project will compile, it will not run properly because an Apple Music API key, a Spotify Client ID, and a Spotify Client ID secret are required when running ``pod install``. -Instruction for obtaining the Apple Music API key can be found [here](https://developer.apple.com/documentation/applemusicapi/getting_keys_and_creating_tokens). -In addition to following those instructions, I used [this python program](https://github.com/pelauimagineering/apple-music-token-generator) to generate the key after gathering the necessary information. +Further development information: + +CoverFlow uses a custom python API that can be found in the "api" folder. More information about this API can be found in its [documentation](api/README.md). +While the Xcode project will compile, it will not run properly because an API base url, a Spotify client ID, and a Spotify redirect uri are required when running ``pod install``. + ## Installation CoverFlow can be installed from the [App Store](https://apps.apple.com/us/app/coverflow/id1537471277). @@ -21,4 +23,4 @@ CoverFlow can be installed from the [App Store](https://apps.apple.com/us/app/co ## License [MIT](https://choosealicense.com/licenses/mit/) -Copyright 2020 © Thatcher Clough. +Copyright 2021 © Thatcher Clough. diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..1d38899 --- /dev/null +++ b/api/README.md @@ -0,0 +1,149 @@ +# CoverFlow API + +CoverFlow uses a custom python API to generate Apple Music JWT tokens, get Spotify refresh / access tokens, and refresh Spotify refresh / access tokens. +To run the API, replace the sample data in "data.json" and run ``python3 api.py``. + +## Open endpoints +Open endpoints require no authentication. + +- [Test API](#test-api): ``GET /api`` +- [Generate Apple Music JWT token](#generate-apple-music-jwt-token): ``GET /api/apple_music/key`` +- [Generate Spotify access and refresh tokens](#generate-spotify-access-and-refresh-tokens): ``POST /api/spotify/swap`` +- [Refresh Spotify access and refresh tokens](#refresh-spotify-access-and-refresh-tokens): ``POST /api/spotify/refresh`` + +## Test API +Used to test if the API is online. + +URL: ``/api`` + +Method: ``GET`` + +### Success response +Code: ``200`` + +Content: +``` +{ + "message": "CoverFlow API" +} +``` + +## Generate Apple Music JWT token +Used to generate an Apple Music JWT token. + +URL: ``/api/apple_music/key`` + +Method: ``GET`` + +### Success response +Code: ``200`` + +Content example: +``` +{ + "key": "eyJ0e...SGDv-BdkeQ" +} +``` + +### Error response +Code ``400`` + +Content: +``` +{ + "error": "Could not generate key" +} +``` + +## Generate Spotify access and refresh tokens +Used to generate Spotify access and refresh tokens from an access code and its code verifier. + +URL: ``/api/spotify/swap`` + +Method: ``POST`` + +### Parameters +``` +{ + "access_code": "[spotify access code]", + "code_verifier": "[corresponding access code verifier]" +} +``` + +### Success response +Code: ``200`` + +Content example: +``` +{ + "access_token": "NgAagA...Um_SHo", + "expires_in": "3600" + "refresh_token": "NgCXwK...MzYjw" +} +``` + +### Error response +#### Could not get tokens +Code: ``400`` + +Content: +``` +{ + "error": "Could not get access and refresh tokens" +} +``` +#### Missing parameters +Code: ``400`` + +Content: +``` +{ + "error": "Missing parameters" +} +``` + +## Refresh Spotify access and refresh tokens +Used to refresh Spotify access and refresh tokens from a refresh token. + +URL: ``/api/spotify/refresh`` + +Method: ``POST`` + +### Parameters +``` +{ + "refresh_token": "[refresh token]" +} +``` + +### Success response +Code: ``200`` + +Content example: +``` +{ + "access_token": "NgAagA...Um_SHo", + "expires_in": "3600" + "refresh_token": "NgCXwK...MzYjw" +} +``` + +### Error response +#### Could not refresh tokens +Code: ``400`` + +Content: +``` +{ + "error": "Could not refresh tokens" +} +``` +#### Missing parameters +Code: ``400`` + +Content: +``` +{ + "error": "Missing parameters" +} +``` \ No newline at end of file diff --git a/api/api.py b/api/api.py index f88ae31..91f0f18 100644 --- a/api/api.py +++ b/api/api.py @@ -7,7 +7,6 @@ data_file_path = "./data.json" app = flask.Flask(__name__) -app.config["DEBUG"] = True @app.route("/api", methods=["GET"]) @@ -20,12 +19,12 @@ def api(): def key(): key = apple_music.generateKey(data_file_path) - ret = None - if key == None: - ret = {"error": "Could not generate key"} - else: + if key != None: ret = {"key": key} - return jsonify(ret), 200 + return jsonify(ret), 200 + else: + ret = {"error": "Could not generate key"} + return jsonify(ret), 400 @app.route("/api/spotify/swap", methods=["POST"]) @@ -37,15 +36,14 @@ def swap(): swap = spotify.swap(data_file_path=data_file_path, access_code=access_code, code_verifier=code_verifier) - ret = None - if swap == None: - ret = {"error": "Could not get access and refresh tokens"} + if swap != None: + return jsonify(swap), 200 else: - ret = swap - return jsonify(ret) + ret = {"error": "Could not get access and refresh tokens"} + return jsonify(ret), 400 else: ret = {"error": "Missing parameters"} - return jsonify(ret), 200 + return jsonify(ret), 400 @app.route("/api/spotify/refresh", methods=["POST"]) @@ -56,15 +54,14 @@ def refresh(): refresh = spotify.refresh( data_file_path=data_file_path, refresh_token=refresh_token) - ret = None - if refresh == None: - ret = {"error": "Could not refresh"} + if refresh != None: + return jsonify(refresh), 200 else: - ret = refresh - return jsonify(ret) + ret = {"error": "Could not refresh tokens"} + return jsonify(ret), 400 else: ret = {"error": "Missing parameters"} - return jsonify(ret), 200 + return jsonify(ret), 400 app.run(host="0.0.0.0") diff --git a/api/apple_music.py b/api/apple_music.py index fcb7a81..6617866 100644 --- a/api/apple_music.py +++ b/api/apple_music.py @@ -4,7 +4,7 @@ alg = "ES256" time_now = datetime.datetime.now() -time_expired = datetime.datetime.now() + datetime.timedelta(days=180) +time_expired = datetime.datetime.now() + datetime.timedelta(hours=2) def generateKey(data_file_path): diff --git a/api/libs.txt b/api/libs.txt new file mode 100644 index 0000000..72ba354 --- /dev/null +++ b/api/libs.txt @@ -0,0 +1,8 @@ +List of required libraries: + +flask +jsonify +pyjwt +cryptography +six +requests \ No newline at end of file