Files
MobileMusicAssistant/Mobile Music Assistant/ServicesMAPlayerManager.swift
T

277 lines
8.9 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 playMedia(playerId: String, uris: [String]) async throws {
guard let service else {
throw MAWebSocketClient.ClientError.notConnected
}
try await service.playMedia(playerId: playerId, uris: uris)
}
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)
}
}