Skip to content

Commit

Permalink
refactor(api): improve/cleanup LastFMAPI
Browse files Browse the repository at this point in the history
  • Loading branch information
angristan committed Dec 1, 2024
1 parent d61d51e commit 8f1935b
Showing 1 changed file with 59 additions and 63 deletions.
122 changes: 59 additions & 63 deletions firstfm/Service/LastFMAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,87 +4,83 @@ import os

class LastFMAPI {
private static let logger = Logger(
subsystem: Bundle.main.bundleIdentifier!,
category: String(describing: LastFMAPI.self)
subsystem: Bundle.main.bundleIdentifier!,
category: String(describing: LastFMAPI.self)
)
// swiftlint:disable force_cast
static let lastFMAPIKey = Bundle.main.object(forInfoDictionaryKey: "LASTFM_API_KEY") as! String
// swiftlint:disable force_cast
static let lastFMSharedSecret = Bundle.main.object(forInfoDictionaryKey: "LASTFM_API_SHARED_SECRET") as! String

static func request<T: Decodable>(method: String = "POST", lastFMMethod: String, args: [String: String] = [:], callback: @escaping (_ Data: T?, Error?) -> Void) {
var request = URLRequest(url: URL(string: "https://ws.audioscrobbler.com/2.0/?format=json")!)
static let lastFMAPIKey: String = {
guard let apiKey = Bundle.main.object(forInfoDictionaryKey: "LASTFM_API_KEY") as? String else {
fatalError("LASTFM_API_KEY not found in Info.plist")
}
return apiKey
}()

static let lastFMSharedSecret: String = {
guard let sharedSecret = Bundle.main.object(forInfoDictionaryKey: "LASTFM_API_SHARED_SECRET") as? String else {
fatalError("LASTFM_API_SHARED_SECRET not found in Info.plist")
}
return sharedSecret
}()

// We need to add these into the dict to compute the signature
static func request<T: Decodable>(method: String = "POST", lastFMMethod: String, args: [String: String] = [:], callback: @escaping (_ data: T?, _ error: Error?) -> Void) {
var fullArgs = args
fullArgs["method"] = lastFMMethod
fullArgs["api_key"] = lastFMAPIKey

var argsString = ""

// Final format will be md5([param1][value1]..[paramN][valueN][shared_secret])
// Format of signature is md5([param1][value1]..[paramN][valueN][shared_secret])
// https://www.last.fm/api/mobileauth
var toSign = ""
let signature = fullArgs.sorted(by: { $0.key < $1.key }).reduce("") { $0 + $1.key + $1.value } + lastFMSharedSecret
fullArgs["api_sig"] = signature.md5()

for key in Array(fullArgs.keys).sorted(by: <) {
if argsString != "" {
// Not first param
argsString += "&"
}
argsString += "\(key)=\(fullArgs[key] ?? "")"
var components = URLComponents(string: "https://ws.audioscrobbler.com/2.0/")!
components.queryItems = fullArgs.map { URLQueryItem(name: $0.key, value: $0.value) }
components.queryItems?.append(URLQueryItem(name: "format", value: "json"))

toSign += key
toSign += fullArgs[key] ?? ""
guard let url = components.url else {
logger.error("Failed to create URL")
callback(nil, NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to create URL"]))
return
}

toSign += lastFMSharedSecret

let data: Data = "\(argsString)&api_sig=\(toSign.md5())".data(using: .utf8)!
logger.info("Sending request with args: \(fullArgs)")

var request = URLRequest(url: url)
request.httpMethod = method
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpBody = data

URLSession.shared.dataTask(with: request) { (data, response, error) -> Void in
var callbackData: T?
var callbackError: Error?

do {
if let response = response {
let nsHTTPResponse = response as? HTTPURLResponse

if let statusCode = nsHTTPResponse?.statusCode {
logger.info("status code of request \(lastFMMethod): \(statusCode)")
if statusCode != 200 {
if lastFMMethod == "user.getFriends" && statusCode == 400 {
// The API returns a 400 when the user has no friends 🤨
callback((FriendsResponse(friends: Friends(user: [])) as! T), nil)
return
}
let error = NSError(domain: "", code: statusCode, userInfo: [NSLocalizedDescriptionKey: "Invalid API response 😢. Please try again"])
callback(callbackData, error as Error)
return
}
}
}

if let error = error {
logger.error("error for \(lastFMMethod): \(String(describing: error))")
callbackError = error
}
logger.info("Sending request with args: \(fullArgs)")

if let data = data {
let jsonResponse = try JSONDecoder().decode(T.self, from: data)
callbackData = jsonResponse
}
} catch {
logger.error("error for \(lastFMMethod): \(String(describing: error))")
callbackError = error
URLSession.shared.dataTask(with: request) { data, response, error in
var callbackData: T?
var callbackError: Error?

if let error = error {
logger.error("Error for \(lastFMMethod): \(String(describing: error))")
callbackError = error
} else if let response = response as? HTTPURLResponse, response.statusCode != 200 {
logger.info("Status code of request \(lastFMMethod): \(response.statusCode)")
if lastFMMethod == "user.getFriends" && response.statusCode == 400 {
// The API returns a 400 when the user has no friends 🤨
if let friendsResponse = FriendsResponse(friends: Friends(user: [])) as? T {
callback(friendsResponse, nil)
} else {
let error = NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to cast FriendsResponse to expected type"])
callback(nil, error)
}
return
}

callback(callbackData, callbackError)
let error = NSError(domain: "", code: response.statusCode, userInfo: [NSLocalizedDescriptionKey: "Invalid last.fm API response 😢. Please try again"])
callback(nil, error)
return
} else if let data = data {
do {
callbackData = try JSONDecoder().decode(T.self, from: data)
} catch {
logger.error("Error decoding response for \(lastFMMethod): \(String(describing: error))")
callbackError = error
}
.resume()
}

callback(callbackData, callbackError)
}.resume()
}
}

0 comments on commit 8f1935b

Please sign in to comment.