78 lines
2.5 KiB
Swift
78 lines
2.5 KiB
Swift
|
|
//
|
||
|
|
// SpotifyAPIClient.swift
|
||
|
|
// iris
|
||
|
|
//
|
||
|
|
// Spotify Web API client for fetching currently playing track.
|
||
|
|
//
|
||
|
|
|
||
|
|
import Foundation
|
||
|
|
import os
|
||
|
|
|
||
|
|
@available(iOS 16.0, *)
|
||
|
|
@MainActor
|
||
|
|
final class SpotifyAPIClient {
|
||
|
|
private let authManager: SpotifyAuthManager
|
||
|
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "iris", category: "SpotifyAPI")
|
||
|
|
private let baseUrl = "https://api.spotify.com/v1"
|
||
|
|
|
||
|
|
init(authManager: SpotifyAuthManager) {
|
||
|
|
self.authManager = authManager
|
||
|
|
}
|
||
|
|
|
||
|
|
func getCurrentPlayback() async throws -> SpotifyPlaybackState? {
|
||
|
|
try await getCurrentPlayback(isRetry: false)
|
||
|
|
}
|
||
|
|
|
||
|
|
private func getCurrentPlayback(isRetry: Bool) async throws -> SpotifyPlaybackState? {
|
||
|
|
let refreshed = await authManager.refreshTokenIfNeeded()
|
||
|
|
guard refreshed else {
|
||
|
|
throw SpotifyAPIError.notAuthenticated
|
||
|
|
}
|
||
|
|
|
||
|
|
guard let token = authManager.accessToken else {
|
||
|
|
throw SpotifyAPIError.notAuthenticated
|
||
|
|
}
|
||
|
|
|
||
|
|
var request = URLRequest(url: URL(string: "\(baseUrl)/me/player/currently-playing")!)
|
||
|
|
request.httpMethod = "GET"
|
||
|
|
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||
|
|
|
||
|
|
let (data, response) = try await URLSession.shared.data(for: request)
|
||
|
|
|
||
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
||
|
|
throw SpotifyAPIError.invalidResponse
|
||
|
|
}
|
||
|
|
|
||
|
|
switch httpResponse.statusCode {
|
||
|
|
case 200:
|
||
|
|
return try JSONDecoder().decode(SpotifyPlaybackState.self, from: data)
|
||
|
|
case 204:
|
||
|
|
return nil
|
||
|
|
case 401:
|
||
|
|
guard !isRetry else {
|
||
|
|
throw SpotifyAPIError.tokenExpired
|
||
|
|
}
|
||
|
|
let refreshed = await authManager.refreshTokenIfNeeded()
|
||
|
|
if refreshed {
|
||
|
|
return try await getCurrentPlayback(isRetry: true)
|
||
|
|
}
|
||
|
|
throw SpotifyAPIError.tokenExpired
|
||
|
|
case 429:
|
||
|
|
let retryAfter = httpResponse.value(forHTTPHeaderField: "Retry-After")
|
||
|
|
logger.warning("Rate limited, retry after: \(retryAfter ?? "unknown")")
|
||
|
|
throw SpotifyAPIError.rateLimited
|
||
|
|
default:
|
||
|
|
logger.error("API error: \(httpResponse.statusCode)")
|
||
|
|
throw SpotifyAPIError.apiError(httpResponse.statusCode)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
enum SpotifyAPIError: Error {
|
||
|
|
case notAuthenticated
|
||
|
|
case tokenExpired
|
||
|
|
case invalidResponse
|
||
|
|
case rateLimited
|
||
|
|
case apiError(Int)
|
||
|
|
}
|
||
|
|
}
|