69 lines
3.7 KiB
Swift
69 lines
3.7 KiB
Swift
|
|
import ActivityKit
|
||
|
|
import Foundation
|
||
|
|
|
||
|
|
@MainActor
|
||
|
|
enum LiveActivityCommands {
|
||
|
|
private static var busy = false
|
||
|
|
static let bindingKey = "MusicBridge.liveActivityPC"
|
||
|
|
|
||
|
|
static func perform(_ action: MusicRemoteAction, activityID: String) async {
|
||
|
|
guard !busy, let activity = Activity<MusicActivityAttributes>.activities.first(where: { $0.id == activityID }),
|
||
|
|
activity.activityState == .active || activity.activityState == .stale else { return }
|
||
|
|
busy = true
|
||
|
|
defer { busy = false }
|
||
|
|
do {
|
||
|
|
guard let saved = try KeychainStore.load(),
|
||
|
|
UserDefaults.standard.string(forKey: bindingKey + activityID) == saved.fingerprint else {
|
||
|
|
throw BridgeError.message("Откройте приложение и включите карточку заново.")
|
||
|
|
}
|
||
|
|
let client = BridgeClient(saved, timeout: 5)
|
||
|
|
defer { client.close() }
|
||
|
|
let fresh = try await execute(action, client: client) {
|
||
|
|
(activity.activityState == .active || activity.activityState == .stale) &&
|
||
|
|
UserDefaults.standard.string(forKey: bindingKey + activityID) == saved.fingerprint
|
||
|
|
}
|
||
|
|
guard fresh.hasSession else { await activity.end(nil, dismissalPolicy: .immediate); return }
|
||
|
|
await activity.update(ActivityContent(state: LiveActivityManager.content(fresh), staleDate: Date().addingTimeInterval(15)))
|
||
|
|
} catch {
|
||
|
|
if case BridgeError.unauthorized = error {
|
||
|
|
await activity.end(nil, dismissalPolicy: .immediate)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var value = activity.content.state
|
||
|
|
value.message = MusicActivityAttributes.ContentState.bounded(BridgeError.describe(error))
|
||
|
|
await activity.update(ActivityContent(state: value, staleDate: Date()))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// A fresh capability check, one command at most, then a fresh snapshot. No command retries.
|
||
|
|
static func execute(_ action: MusicRemoteAction, client: BridgeClient,
|
||
|
|
stillValid: () -> Bool = { true }) async throws -> MediaState {
|
||
|
|
let before = try await client.state()
|
||
|
|
guard stillValid() else { throw BridgeError.message("Карточка закрыта. Откройте MusicBridge.") }
|
||
|
|
if action == .refresh { return before }
|
||
|
|
guard before.hasSession else { return before }
|
||
|
|
let command = try command(action, state: before)
|
||
|
|
do { try await client.command(Command(type: command, value: nil)) }
|
||
|
|
catch {
|
||
|
|
if error is URLError { throw BridgeError.message("Результат команды неизвестен. Нажмите обновление перед повтором.") }
|
||
|
|
throw error
|
||
|
|
}
|
||
|
|
do { return try await client.state() }
|
||
|
|
catch {
|
||
|
|
if case BridgeError.unauthorized = error { throw error }
|
||
|
|
throw BridgeError.message("Команда выполнена. Нажмите обновление, чтобы получить новый трек.")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static func command(_ action: MusicRemoteAction, state: MediaState) throws -> String {
|
||
|
|
switch action {
|
||
|
|
case .previous where state.capabilities.previous: return "previous"
|
||
|
|
case .next where state.capabilities.next: return "next"
|
||
|
|
case .toggle where state.capabilities.toggle: return "toggle"
|
||
|
|
case .toggle where state.playbackStatus == "Playing" && state.capabilities.pause: return "pause"
|
||
|
|
case .toggle where state.playbackStatus != "Playing" && state.capabilities.play: return "play"
|
||
|
|
default: throw BridgeError.message("Плеер на ПК не поддерживает эту команду.")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|