- Add OAuth 2.0 PKCE authentication for Spotify Web API - Create SpotifyNowPlayingMonitor for polling current track - Add Settings tab with music source toggle (Apple Music/Spotify) - Store tokens securely in Keychain - Display current track on Glass as NOW_PLAYING card 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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)
|
|
}
|
|
}
|