mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Fix: Migrations
This commit is contained in:
parent
471fb6723e
commit
be152c1531
41 changed files with 2373 additions and 1321 deletions
|
|
@ -3,6 +3,7 @@
|
|||
// DynamicIsland
|
||||
//
|
||||
// Created By Alexander on 2025-03-30.
|
||||
// Modified by Pranav on 2025-06-16.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
|
@ -12,143 +13,129 @@ import SwiftUI
|
|||
class YouTubeMusicController: MediaControllerProtocol {
|
||||
// MARK: - Properties
|
||||
@Published var playbackState: PlaybackState = .init(
|
||||
bundleIdentifier: "com.github.th-ch.youtube-music",
|
||||
isPlaying: true
|
||||
bundleIdentifier: "com.github.th-ch.youtube-music"
|
||||
)
|
||||
|
||||
var playbackStatePublisher: Published<PlaybackState>.Publisher { $playbackState }
|
||||
var playbackStatePublisher: AnyPublisher<PlaybackState, Never> {
|
||||
$playbackState.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
var isWorking: Bool {
|
||||
return true // YouTube Music controller always works
|
||||
}
|
||||
private let baseURL = "http://localhost:26538"
|
||||
private var accessToken: String?
|
||||
private var refreshTimer: Timer?
|
||||
private var periodicUpdateTask: Task<Void, Never>?
|
||||
private var notificationTask: Task<Void, Never>?
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var playbackInfoCancellable: AnyCancellable?
|
||||
private var isAuthenticating = false
|
||||
private let authQueue = DispatchQueue(label: "com.dynamicisland.youtubemusicauth", qos: .background)
|
||||
|
||||
init() {
|
||||
setupAppStateObservers()
|
||||
authQueue.async { [weak self] in
|
||||
self?.authenticateAndSetup()
|
||||
}
|
||||
Task {await authenticateAndSetup()}
|
||||
}
|
||||
|
||||
deinit {
|
||||
stopPeriodicUpdates()
|
||||
notificationTask?.cancel()
|
||||
cancellables.forEach { $0.cancel() }
|
||||
NSWorkspace.shared.notificationCenter.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: - Authentication
|
||||
|
||||
private func authenticateAndSetup() {
|
||||
private func authenticateAndSetup() async {
|
||||
// Prevent multiple concurrent authentication attempts
|
||||
guard !isAuthenticating else { return }
|
||||
|
||||
isAuthenticating = true
|
||||
getAccessToken { [weak self] success in
|
||||
guard let self = self else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.isAuthenticating = false
|
||||
if success {
|
||||
self.startPeriodicUpdates()
|
||||
self.updatePlaybackInfo()
|
||||
} else {
|
||||
// Retry authentication after a delay if it fails
|
||||
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 5) { [weak self] in
|
||||
self?.authenticateAndSetup()
|
||||
}
|
||||
}
|
||||
}
|
||||
let success = await getAccessToken()
|
||||
|
||||
self.isAuthenticating = false
|
||||
if success {
|
||||
await self.startPeriodicUpdates()
|
||||
await updatePlaybackInfo()
|
||||
} else {
|
||||
try? await Task.sleep(for: .seconds(5))
|
||||
await authenticateAndSetup()
|
||||
}
|
||||
}
|
||||
|
||||
private func getAccessToken(completion: @escaping (Bool) -> Void) {
|
||||
guard let url = URL(string: "\(baseURL)/auth/DynamicIsland") else {
|
||||
completion(false)
|
||||
return
|
||||
private func getAccessToken() async -> Bool {
|
||||
guard let url = URL(string: "\(baseURL)/auth/boringNotch") else {
|
||||
return false
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
|
||||
URLSession.shared.dataTaskPublisher(for: request)
|
||||
.map { data, response -> Data in
|
||||
return data
|
||||
}
|
||||
.decode(type: AuthResponse.self, decoder: JSONDecoder())
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { completionStatus in
|
||||
switch completionStatus {
|
||||
case .failure(_):
|
||||
completion(false)
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
}, receiveValue: { [weak self] response in
|
||||
self?.accessToken = response.accessToken
|
||||
completion(true)
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
let response = try JSONDecoder().decode(AuthResponse.self, from: data)
|
||||
accessToken = response.accessToken
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Protocol Implementation
|
||||
|
||||
func play() {
|
||||
sendCommand(endpoint: "/play", method: "POST")
|
||||
updatePlaybackInfo()
|
||||
func play() async {
|
||||
await sendCommand(endpoint: "/play", method: "POST")
|
||||
await updatePlaybackInfo()
|
||||
}
|
||||
|
||||
func pause() {
|
||||
sendCommand(endpoint: "/pause", method: "POST")
|
||||
updatePlaybackInfo()
|
||||
func pause() async {
|
||||
await sendCommand(endpoint: "/pause", method: "POST")
|
||||
await updatePlaybackInfo()
|
||||
}
|
||||
|
||||
func togglePlay() {
|
||||
func togglePlay() async {
|
||||
if !isActive() {
|
||||
if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: playbackState.bundleIdentifier) {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
sendCommand(endpoint: "/toggle-play", method: "POST")
|
||||
updatePlaybackInfo()
|
||||
await sendCommand(endpoint: "/toggle-play", method: "POST")
|
||||
await updatePlaybackInfo()
|
||||
}
|
||||
|
||||
func nextTrack() {
|
||||
sendCommand(endpoint: "/next", method: "POST")
|
||||
updatePlaybackInfo()
|
||||
func nextTrack() async {
|
||||
await sendCommand(endpoint: "/next", method: "POST")
|
||||
await updatePlaybackInfo()
|
||||
}
|
||||
|
||||
func previousTrack() {
|
||||
sendCommand(endpoint: "/previous", method: "POST")
|
||||
updatePlaybackInfo()
|
||||
func previousTrack() async {
|
||||
await sendCommand(endpoint: "/previous", method: "POST")
|
||||
await updatePlaybackInfo()
|
||||
}
|
||||
|
||||
func seek(to time: Double) {
|
||||
guard let url = URL(string: "\(baseURL)/seek-to") else { return }
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
if let token = accessToken {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
} else {
|
||||
authenticateAndSetup()
|
||||
return
|
||||
}
|
||||
|
||||
// Create the request body with the seconds value
|
||||
func seek(to time: Double) async {
|
||||
// Format the seek data payload according to the API schema
|
||||
let seekData = ["seconds": time]
|
||||
|
||||
do {
|
||||
request.httpBody = try JSONEncoder().encode(seekData)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
URLSession.shared.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response -> Data in
|
||||
// Convert seek data to JSON
|
||||
let jsonData = try JSONEncoder().encode(seekData)
|
||||
|
||||
guard let url = URL(string: "\(baseURL)/api/v1/seek-to") else { return }
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
if let token = accessToken {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
} else {
|
||||
await authenticateAndSetup()
|
||||
return
|
||||
}
|
||||
|
||||
request.httpBody = jsonData
|
||||
|
||||
do {
|
||||
let (_, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
|
|
@ -159,22 +146,44 @@ class YouTubeMusicController: MediaControllerProtocol {
|
|||
throw URLError(.badServerResponse)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { [weak self] completion in
|
||||
if case .failure(let error) = completion {
|
||||
if let urlError = error as? URLError, urlError.code == .userAuthenticationRequired {
|
||||
self?.accessToken = nil
|
||||
self?.authenticateAndSetup()
|
||||
}
|
||||
// Update playback info after seeking
|
||||
try? await Task.sleep(for: .milliseconds(300))
|
||||
await updatePlaybackInfo()
|
||||
|
||||
} catch {
|
||||
print("Seek error: \(error)")
|
||||
if let urlError = error as? URLError, urlError.code == .userAuthenticationRequired {
|
||||
self.accessToken = nil
|
||||
await self.authenticateAndSetup()
|
||||
}
|
||||
}, receiveValue: { [weak self] _ in
|
||||
self?.updatePlaybackInfo()
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
} catch {
|
||||
print("Error encoding seek data: \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func fetchShuffleState() async {
|
||||
await sendCommand(endpoint: "/shuffle", method: "GET")
|
||||
}
|
||||
|
||||
func toggleShuffle() async {
|
||||
await sendCommand(endpoint: "/shuffle", method: "POST")
|
||||
try? await Task.sleep(for: .milliseconds(200))
|
||||
await fetchShuffleState()
|
||||
}
|
||||
|
||||
func toggleRepeat() async {
|
||||
await sendCommand(endpoint: "/switch-repeat", method: "POST")
|
||||
try? await Task.sleep(for: .milliseconds(200))
|
||||
await fetchRepeatMode()
|
||||
}
|
||||
|
||||
func fetchRepeatMode() async {
|
||||
await sendCommand(endpoint: "/repeat-mode", method: "GET")
|
||||
}
|
||||
|
||||
func isActive() -> Bool {
|
||||
let runningApps = NSWorkspace.shared.runningApplications
|
||||
return runningApps.contains { $0.bundleIdentifier == playbackState.bundleIdentifier }
|
||||
|
|
@ -183,131 +192,170 @@ class YouTubeMusicController: MediaControllerProtocol {
|
|||
// MARK: - Private Methods
|
||||
|
||||
private func setupAppStateObservers() {
|
||||
// Register for app launch notifications
|
||||
NSWorkspace.shared.notificationCenter.addObserver(
|
||||
self,
|
||||
selector: #selector(handleAppStateChange),
|
||||
name: NSWorkspace.didLaunchApplicationNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
// Register for app termination notifications
|
||||
NSWorkspace.shared.notificationCenter.addObserver(
|
||||
self,
|
||||
selector: #selector(handleAppStateChange),
|
||||
name: NSWorkspace.didTerminateApplicationNotification,
|
||||
object: nil
|
||||
)
|
||||
notificationTask = Task { @Sendable [weak self] in
|
||||
await withTaskGroup(of: Void.self) { group in
|
||||
// Handle app launch notifications
|
||||
group.addTask {
|
||||
let launchNotifications = NSWorkspace.shared.notificationCenter.notifications(
|
||||
named: NSWorkspace.didLaunchApplicationNotification
|
||||
)
|
||||
|
||||
for await notification in launchNotifications {
|
||||
await self?.handleAppStateChange(notification: notification)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle app termination notifications
|
||||
group.addTask {
|
||||
let terminateNotifications = NSWorkspace.shared.notificationCenter.notifications(
|
||||
named: NSWorkspace.didTerminateApplicationNotification
|
||||
)
|
||||
|
||||
for await notification in terminateNotifications {
|
||||
await self?.handleAppStateChange(notification: notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleAppStateChange(notification: Notification) {
|
||||
private func handleAppStateChange(notification: Notification) async {
|
||||
guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication,
|
||||
app.bundleIdentifier == playbackState.bundleIdentifier else {
|
||||
return
|
||||
}
|
||||
|
||||
if notification.name == NSWorkspace.didLaunchApplicationNotification {
|
||||
startPeriodicUpdates()
|
||||
await startPeriodicUpdates()
|
||||
} else if notification.name == NSWorkspace.didTerminateApplicationNotification {
|
||||
// App has terminated - update state and stop timer
|
||||
stopPeriodicUpdates()
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.playbackState = PlaybackState(
|
||||
bundleIdentifier: self.playbackState.bundleIdentifier,
|
||||
isPlaying: false
|
||||
)
|
||||
}
|
||||
self.playbackState = PlaybackState(
|
||||
bundleIdentifier: self.playbackState.bundleIdentifier,
|
||||
isPlaying: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func startPeriodicUpdates() {
|
||||
private func startPeriodicUpdates() async {
|
||||
stopPeriodicUpdates() // Ensure we don't create multiple timers
|
||||
|
||||
// Only start the timer if the app is actually running
|
||||
guard isActive() else { return }
|
||||
|
||||
refreshTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in
|
||||
self?.updatePlaybackInfo()
|
||||
periodicUpdateTask = Task { [weak self] in
|
||||
while !Task.isCancelled {
|
||||
await self?.updatePlaybackInfo()
|
||||
try? await Task.sleep(for: .seconds(2))
|
||||
}
|
||||
}
|
||||
refreshTimer?.tolerance = 0.5
|
||||
|
||||
// Initial update when timer starts
|
||||
updatePlaybackInfo()
|
||||
await updatePlaybackInfo()
|
||||
await fetchRepeatMode()
|
||||
await fetchShuffleState()
|
||||
}
|
||||
|
||||
private func stopPeriodicUpdates() {
|
||||
refreshTimer?.invalidate()
|
||||
refreshTimer = nil
|
||||
periodicUpdateTask?.cancel()
|
||||
periodicUpdateTask = nil
|
||||
}
|
||||
|
||||
private func updateRepeatMode(_ modeString: String?) {
|
||||
guard let modeString = modeString else { return }
|
||||
|
||||
let repeatMode: RepeatMode
|
||||
switch modeString {
|
||||
case "NONE":
|
||||
repeatMode = .off
|
||||
case "ALL":
|
||||
repeatMode = .all
|
||||
case "ONE":
|
||||
repeatMode = .one
|
||||
default:
|
||||
repeatMode = .off
|
||||
}
|
||||
|
||||
if repeatMode != playbackState.repeatMode {
|
||||
playbackState.repeatMode = repeatMode
|
||||
}
|
||||
}
|
||||
|
||||
func updatePlaybackInfo() {
|
||||
func updatePlaybackInfo() async {
|
||||
if !isActive() {
|
||||
playbackState = PlaybackState(bundleIdentifier: playbackState.bundleIdentifier, isPlaying: false)
|
||||
stopPeriodicUpdates()
|
||||
return
|
||||
}
|
||||
guard let request = createAuthenticatedRequest(for: "/api/v1/song") else { return }
|
||||
|
||||
guard let request = await createAuthenticatedRequest(for: "/api/v1/song") else { return }
|
||||
|
||||
let backgroundQueue = DispatchQueue.global(qos: .utility)
|
||||
|
||||
backgroundQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
do {
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
playbackInfoCancellable?.cancel()
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
|
||||
playbackInfoCancellable = URLSession.shared.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response -> Data in
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
|
||||
// Check for authentication errors
|
||||
if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 {
|
||||
// Re-authenticate and retry later
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.accessToken = nil
|
||||
self?.authenticateAndSetup()
|
||||
}
|
||||
throw URLError(.userAuthenticationRequired)
|
||||
} else if httpResponse.statusCode < 200 || httpResponse.statusCode >= 300 {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
.decode(type: PlaybackResponse.self, decoder: JSONDecoder())
|
||||
.subscribe(on: backgroundQueue)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { _ in }, receiveValue: { [weak self] response in
|
||||
self?.updatePlaybackState(with: response)
|
||||
})
|
||||
// Check for authentication errors
|
||||
if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 {
|
||||
// Re-authenticate and retry later
|
||||
self.accessToken = nil
|
||||
await authenticateAndSetup()
|
||||
throw URLError(.userAuthenticationRequired)
|
||||
} else if httpResponse.statusCode < 200 || httpResponse.statusCode >= 300 {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
|
||||
let playbackResponse = try JSONDecoder().decode(PlaybackResponse.self, from: data)
|
||||
await updatePlaybackState(with: playbackResponse)
|
||||
|
||||
} catch {
|
||||
// Handle error silently for now, as this is called periodically
|
||||
}
|
||||
}
|
||||
|
||||
private func updatePlaybackState(with response: PlaybackResponse) {
|
||||
playbackState.isPlaying = !response.isPaused
|
||||
playbackState.title = response.title
|
||||
playbackState.artist = response.artist
|
||||
playbackState.album = response.album ?? ""
|
||||
playbackState.currentTime = response.elapsedSeconds
|
||||
playbackState.duration = response.songDuration
|
||||
playbackState.lastUpdated = Date()
|
||||
private func updatePlaybackState(with response: PlaybackResponse) async {
|
||||
var updatedState = self.playbackState
|
||||
updatedState.isPlaying = !response.isPaused
|
||||
updatedState.title = response.title
|
||||
updatedState.artist = response.artist
|
||||
updatedState.album = response.album ?? ""
|
||||
updatedState.currentTime = response.elapsedSeconds
|
||||
updatedState.duration = response.songDuration
|
||||
updatedState.lastUpdated = Date()
|
||||
|
||||
if let isShuffled = response.isShuffled {
|
||||
updatedState.isShuffled = isShuffled
|
||||
}
|
||||
|
||||
// Load artwork if available
|
||||
if let artworkURL = response.imageSrc, let url = URL(string: artworkURL) {
|
||||
DispatchQueue.global(qos: .background).async { [weak self] in
|
||||
do {
|
||||
let artworkData = try Data(contentsOf: url)
|
||||
DispatchQueue.main.async {
|
||||
self?.playbackState.artwork = artworkData
|
||||
}
|
||||
} catch { return }
|
||||
do {
|
||||
let artworkData = try Data(contentsOf: url)
|
||||
playbackState.artwork = artworkData
|
||||
|
||||
} catch {
|
||||
NSLog("Failed to load artwork: \(error)")
|
||||
updatedState.artwork = nil
|
||||
self.playbackState = updatedState
|
||||
return
|
||||
}
|
||||
}
|
||||
self.playbackState = updatedState
|
||||
}
|
||||
|
||||
private func sendCommand(endpoint: String, method: String = "GET") {
|
||||
func pollPlaybackState() async {
|
||||
if !isActive() {
|
||||
return
|
||||
}
|
||||
|
||||
await fetchRepeatMode()
|
||||
await fetchShuffleState()
|
||||
await updatePlaybackInfo()
|
||||
}
|
||||
|
||||
private func sendCommand(endpoint: String, method: String = "GET") async {
|
||||
guard let url = URL(string: "\(baseURL)/api/v1\(endpoint)") else { return }
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
|
|
@ -316,49 +364,65 @@ class YouTubeMusicController: MediaControllerProtocol {
|
|||
if let token = accessToken {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
} else {
|
||||
authenticateAndSetup()
|
||||
await authenticateAndSetup()
|
||||
return
|
||||
}
|
||||
|
||||
URLSession.shared.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response -> Data in
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
|
||||
if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 {
|
||||
throw URLError(.userAuthenticationRequired)
|
||||
} else if httpResponse.statusCode < 200 || httpResponse.statusCode >= 300 {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
|
||||
return data
|
||||
do {
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveCompletion: { [weak self] completion in
|
||||
if case .failure(let error) = completion {
|
||||
if let urlError = error as? URLError, urlError.code == .userAuthenticationRequired {
|
||||
// Authentication error - token might be expired
|
||||
self?.accessToken = nil
|
||||
self?.authenticateAndSetup()
|
||||
|
||||
// Try the command again after authentication
|
||||
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 2) { [weak self] in
|
||||
self?.sendCommand(endpoint: endpoint, method: method)
|
||||
}
|
||||
|
||||
if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 {
|
||||
throw URLError(.userAuthenticationRequired)
|
||||
} else if httpResponse.statusCode < 200 || httpResponse.statusCode >= 300 {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
|
||||
// Handle data based on endpoint
|
||||
if endpoint == "/shuffle" || endpoint == "/shuffle-status" {
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let shuffleState = json["state"] as? Bool {
|
||||
playbackState.isShuffled = shuffleState
|
||||
}
|
||||
} catch {
|
||||
print("Error parsing shuffle state: \(error)")
|
||||
}
|
||||
}, receiveValue: { [weak self] _ in
|
||||
self?.updatePlaybackInfo()
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
} else if endpoint == "/repeat-mode" {
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let mode = json["mode"] as? String {
|
||||
updateRepeatMode(mode)
|
||||
}
|
||||
} catch {
|
||||
print("Error parsing repeat mode: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// Update playback info for relevant commands
|
||||
await updatePlaybackInfo()
|
||||
|
||||
} catch {
|
||||
if let urlError = error as? URLError, urlError.code == .userAuthenticationRequired {
|
||||
// Authentication error - token might be expired
|
||||
accessToken = nil
|
||||
await authenticateAndSetup()
|
||||
|
||||
// Try the command again after authentication
|
||||
try? await Task.sleep(for: .seconds(2))
|
||||
await sendCommand(endpoint: endpoint, method: method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createAuthenticatedRequest(for endpoint: String) -> URLRequest? {
|
||||
private func createAuthenticatedRequest(for endpoint: String) async -> URLRequest? {
|
||||
guard let token = accessToken, let url = URL(string: "\(baseURL)\(endpoint)") else {
|
||||
if accessToken == nil {
|
||||
// Token is missing, try to authenticate again
|
||||
authenticateAndSetup()
|
||||
await authenticateAndSetup()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -383,4 +447,6 @@ struct PlaybackResponse: Codable {
|
|||
let elapsedSeconds: Double
|
||||
let songDuration: Double
|
||||
let imageSrc: String?
|
||||
let repeatMode: Int?
|
||||
let isShuffled: Bool?
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue