270 lines
8.7 KiB
Swift
270 lines
8.7 KiB
Swift
//
|
|
// MAPlayerManager.swift
|
|
// Mobile Music Assistant
|
|
//
|
|
// Created by Sven Hanold on 26.03.26.
|
|
//
|
|
|
|
import Foundation
|
|
import OSLog
|
|
|
|
private let logger = Logger(subsystem: "com.musicassistant.mobile", category: "PlayerManager")
|
|
|
|
/// Manages player state and real-time updates
|
|
@Observable
|
|
final class MAPlayerManager {
|
|
// MARK: - Properties
|
|
|
|
private(set) var players: [String: MAPlayer] = [:]
|
|
private(set) var playerQueues: [String: MAPlayerQueue] = [:]
|
|
private(set) var queues: [String: [MAQueueItem]] = [:]
|
|
|
|
private weak var service: MAService?
|
|
private var eventTask: Task<Void, Never>?
|
|
|
|
// MARK: - Initialization
|
|
|
|
init(service: MAService?) {
|
|
self.service = service
|
|
}
|
|
|
|
func setService(_ service: MAService) {
|
|
self.service = service
|
|
}
|
|
|
|
deinit {
|
|
stopListening()
|
|
}
|
|
|
|
// MARK: - Event Listening
|
|
|
|
/// Start listening to player events
|
|
func startListening() {
|
|
guard eventTask == nil, let service else { return }
|
|
|
|
logger.info("Starting event listener")
|
|
|
|
eventTask = Task {
|
|
for await event in service.webSocketClient.eventStream {
|
|
await handleEvent(event)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Stop listening to events
|
|
func stopListening() {
|
|
logger.info("Stopping event listener")
|
|
eventTask?.cancel()
|
|
eventTask = nil
|
|
}
|
|
|
|
private func handleEvent(_ event: MAEvent) async {
|
|
logger.debug("Handling event: \(event.event)")
|
|
|
|
switch event.event {
|
|
case "player_updated":
|
|
await handlePlayerUpdated(event)
|
|
|
|
case "queue_updated":
|
|
await handleQueueUpdated(event)
|
|
|
|
case "queue_items_updated":
|
|
await handleQueueItemsUpdated(event)
|
|
|
|
default:
|
|
logger.debug("Unhandled event: \(event.event)")
|
|
}
|
|
}
|
|
|
|
private func handlePlayerUpdated(_ event: MAEvent) async {
|
|
guard let data = event.data else { return }
|
|
|
|
do {
|
|
let player = try data.decode(as: MAPlayer.self)
|
|
await MainActor.run {
|
|
players[player.playerId] = player
|
|
logger.debug("Updated player: \(player.name) state=\(player.state.rawValue) item=\(player.currentItem?.name ?? "nil")")
|
|
}
|
|
} catch {
|
|
logger.error("Failed to decode player_updated event: \(error)")
|
|
}
|
|
}
|
|
|
|
private func handleQueueUpdated(_ event: MAEvent) async {
|
|
guard let data = event.data else { return }
|
|
|
|
// The event data IS the PlayerQueue object — decode it directly for current_item
|
|
if let queue = try? data.decode(as: MAPlayerQueue.self), !queue.queueId.isEmpty {
|
|
await MainActor.run {
|
|
playerQueues[queue.queueId] = queue
|
|
logger.debug("Updated queue state for player \(queue.queueId), current: \(queue.currentItem?.name ?? "nil")")
|
|
}
|
|
return
|
|
}
|
|
|
|
// Fallback: extract queue_id and fetch from API
|
|
guard let dict = data.value as? [String: Any],
|
|
let queueId = dict["queue_id"] as? String,
|
|
let service else { return }
|
|
|
|
do {
|
|
let queue = try await service.getPlayerQueue(playerId: queueId)
|
|
await MainActor.run {
|
|
playerQueues[queueId] = queue
|
|
logger.debug("Fetched queue state for player \(queueId), current: \(queue.currentItem?.name ?? "nil")")
|
|
}
|
|
} catch {
|
|
logger.error("Failed to reload queue state: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
private func handleQueueItemsUpdated(_ event: MAEvent) async {
|
|
// Update queue state (current item, current index)
|
|
await handleQueueUpdated(event)
|
|
|
|
// Reload the items list if we already have it cached (i.e., queue view was opened)
|
|
guard let data = event.data,
|
|
let dict = data.value as? [String: Any],
|
|
let queueId = dict["queue_id"] as? String,
|
|
queues[queueId] != nil,
|
|
let service else { return }
|
|
|
|
do {
|
|
let items = try await service.getQueue(playerId: queueId)
|
|
await MainActor.run {
|
|
queues[queueId] = items
|
|
logger.debug("Reloaded queue items for player \(queueId): \(items.count) items")
|
|
}
|
|
} catch {
|
|
logger.error("Failed to reload queue items: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
// MARK: - Data Loading
|
|
|
|
/// Load all players and their queue states
|
|
func loadPlayers() async throws {
|
|
guard let service else {
|
|
throw MAWebSocketClient.ClientError.notConnected
|
|
}
|
|
|
|
logger.info("Loading players")
|
|
let playerList = try await service.getPlayers()
|
|
|
|
await MainActor.run {
|
|
players = Dictionary(uniqueKeysWithValues: playerList.map { ($0.playerId, $0) })
|
|
}
|
|
|
|
// Concurrently fetch queue state for each player to get current_item
|
|
var queueResults: [String: MAPlayerQueue] = [:]
|
|
await withTaskGroup(of: (String, MAPlayerQueue?).self) { group in
|
|
for player in playerList {
|
|
let pid = player.playerId
|
|
group.addTask {
|
|
let queue = try? await service.getPlayerQueue(playerId: pid)
|
|
return (pid, queue)
|
|
}
|
|
}
|
|
for await (pid, queue) in group {
|
|
if let queue { queueResults[pid] = queue }
|
|
}
|
|
}
|
|
|
|
await MainActor.run {
|
|
for (pid, queue) in queueResults {
|
|
playerQueues[pid] = queue
|
|
}
|
|
logger.info("Loaded queue states for \(queueResults.count) players")
|
|
}
|
|
}
|
|
|
|
/// Load queue for specific player
|
|
func loadQueue(playerId: String) async throws {
|
|
guard let service else {
|
|
throw MAWebSocketClient.ClientError.notConnected
|
|
}
|
|
|
|
logger.info("Loading queue for player \(playerId)")
|
|
let items = try await service.getQueue(playerId: playerId)
|
|
|
|
await MainActor.run {
|
|
queues[playerId] = items
|
|
}
|
|
}
|
|
|
|
// MARK: - Player Control
|
|
|
|
func play(playerId: String) async throws {
|
|
guard let service else {
|
|
throw MAWebSocketClient.ClientError.notConnected
|
|
}
|
|
try await service.play(playerId: playerId)
|
|
}
|
|
|
|
func pause(playerId: String) async throws {
|
|
guard let service else {
|
|
throw MAWebSocketClient.ClientError.notConnected
|
|
}
|
|
try await service.pause(playerId: playerId)
|
|
}
|
|
|
|
func stop(playerId: String) async throws {
|
|
guard let service else {
|
|
throw MAWebSocketClient.ClientError.notConnected
|
|
}
|
|
try await service.stop(playerId: playerId)
|
|
}
|
|
|
|
func nextTrack(playerId: String) async throws {
|
|
guard let service else {
|
|
throw MAWebSocketClient.ClientError.notConnected
|
|
}
|
|
try await service.nextTrack(playerId: playerId)
|
|
}
|
|
|
|
func previousTrack(playerId: String) async throws {
|
|
guard let service else {
|
|
throw MAWebSocketClient.ClientError.notConnected
|
|
}
|
|
try await service.previousTrack(playerId: playerId)
|
|
}
|
|
|
|
func setVolume(playerId: String, level: Int) async throws {
|
|
guard let service else {
|
|
throw MAWebSocketClient.ClientError.notConnected
|
|
}
|
|
try await service.setVolume(playerId: playerId, level: level)
|
|
}
|
|
|
|
func syncPlayer(playerId: String, targetPlayerId: String) async throws {
|
|
guard let service else { throw MAWebSocketClient.ClientError.notConnected }
|
|
try await service.syncPlayer(playerId: playerId, targetPlayerId: targetPlayerId)
|
|
}
|
|
|
|
func unsyncPlayer(playerId: String) async throws {
|
|
guard let service else { throw MAWebSocketClient.ClientError.notConnected }
|
|
try await service.unsyncPlayer(playerId: playerId)
|
|
}
|
|
|
|
func playMedia(playerId: String, uri: String) async throws {
|
|
guard let service else {
|
|
throw MAWebSocketClient.ClientError.notConnected
|
|
}
|
|
try await service.playMedia(playerId: playerId, uri: uri)
|
|
}
|
|
|
|
func enqueueMedia(playerId: String, uri: String) async throws {
|
|
guard let service else {
|
|
throw MAWebSocketClient.ClientError.notConnected
|
|
}
|
|
try await service.enqueueMedia(playerId: playerId, uri: uri)
|
|
}
|
|
|
|
func playIndex(playerId: String, index: Int) async throws {
|
|
guard let service else {
|
|
throw MAWebSocketClient.ClientError.notConnected
|
|
}
|
|
try await service.playIndex(playerId: playerId, index: index)
|
|
}
|
|
}
|