Files
MobileMusicAssistant/Mobile Music Assistant/ServicesMAService.swift
T
2026-03-27 09:21:41 +01:00

332 lines
11 KiB
Swift

//
// MAService.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: "Service")
/// High-level service for Music Assistant API
@Observable
final class MAService {
// MARK: - Properties
let authManager: MAAuthManager
let webSocketClient: MAWebSocketClient
let playerManager: MAPlayerManager
let libraryManager: MALibraryManager
private(set) var isConnected = false
// MARK: - Initialization
init() {
// Initialize simple properties first
self.authManager = MAAuthManager()
self.webSocketClient = MAWebSocketClient()
// Create a temporary service reference
let tempPlayerManager = MAPlayerManager(service: nil)
let tempLibraryManager = MALibraryManager(service: nil)
self.playerManager = tempPlayerManager
self.libraryManager = tempLibraryManager
// Now set the service reference
tempPlayerManager.setService(self)
tempLibraryManager.setService(self)
}
// MARK: - Connection
/// Connect to Music Assistant server using saved credentials
func connectWithSavedCredentials() async throws {
guard authManager.isAuthenticated,
let serverURL = authManager.serverURL,
let token = authManager.currentToken else {
throw MAAuthManager.AuthError.noStoredCredentials
}
try await connect(serverURL: serverURL, token: token)
}
/// Connect to server with explicit credentials
func connect(serverURL: URL, token: String) async throws {
logger.info("Connecting to Music Assistant")
try await webSocketClient.connect(serverURL: serverURL, authToken: token)
isConnected = true
}
/// Disconnect from server
func disconnect() {
logger.info("Disconnecting from Music Assistant")
webSocketClient.disconnect()
isConnected = false
}
// MARK: - Players
/// Get all players
func getPlayers() async throws -> [MAPlayer] {
logger.debug("Fetching players")
return try await webSocketClient.sendCommand(
"players",
resultType: [MAPlayer].self
)
}
/// Play on a player
func play(playerId: String) async throws {
logger.debug("Playing on player \(playerId)")
_ = try await webSocketClient.sendCommand(
"players/cmd/play",
args: ["player_id": playerId]
)
}
/// Pause a player
func pause(playerId: String) async throws {
logger.debug("Pausing player \(playerId)")
_ = try await webSocketClient.sendCommand(
"players/cmd/pause",
args: ["player_id": playerId]
)
}
/// Stop a player
func stop(playerId: String) async throws {
logger.debug("Stopping player \(playerId)")
_ = try await webSocketClient.sendCommand(
"players/cmd/stop",
args: ["player_id": playerId]
)
}
/// Next track
func nextTrack(playerId: String) async throws {
logger.debug("Next track on player \(playerId)")
_ = try await webSocketClient.sendCommand(
"players/cmd/next",
args: ["player_id": playerId]
)
}
/// Previous track
func previousTrack(playerId: String) async throws {
logger.debug("Previous track on player \(playerId)")
_ = try await webSocketClient.sendCommand(
"players/cmd/previous",
args: ["player_id": playerId]
)
}
/// Set volume (0-100)
func setVolume(playerId: String, level: Int) async throws {
let clampedLevel = max(0, min(100, level))
logger.debug("Setting volume to \(clampedLevel) on player \(playerId)")
_ = try await webSocketClient.sendCommand(
"players/cmd/volume_set",
args: [
"player_id": playerId,
"volume_level": clampedLevel
]
)
}
// MARK: - Queue
/// Get player queue
func getQueue(playerId: String) async throws -> [MAQueueItem] {
logger.debug("Fetching queue for player \(playerId)")
return try await webSocketClient.sendCommand(
"player_queues/items",
args: ["queue_id": playerId],
resultType: [MAQueueItem].self
)
}
/// Play media item
func playMedia(playerId: String, uri: String) async throws {
logger.debug("Playing media \(uri) on player \(playerId)")
_ = try await webSocketClient.sendCommand(
"player_queues/cmd/play_media",
args: [
"queue_id": playerId,
"media": [uri]
]
)
}
/// Play from queue index
func playIndex(playerId: String, index: Int) async throws {
logger.debug("Playing index \(index) on player \(playerId)")
_ = try await webSocketClient.sendCommand(
"player_queues/cmd/play_index",
args: [
"queue_id": playerId,
"index": index
]
)
}
/// Move queue item
func moveQueueItem(playerId: String, fromIndex: Int, toIndex: Int) async throws {
logger.debug("Moving queue item from \(fromIndex) to \(toIndex)")
_ = try await webSocketClient.sendCommand(
"player_queues/cmd/move_item",
args: [
"queue_id": playerId,
"queue_item_id": fromIndex,
"pos_shift": toIndex - fromIndex
]
)
}
// MARK: - Library
/// Get artists (with pagination)
func getArtists(limit: Int = 50, offset: Int = 0) async throws -> [MAArtist] {
logger.debug("Fetching artists (limit: \(limit), offset: \(offset))")
return try await webSocketClient.sendCommand(
"music/artists",
args: [
"limit": limit,
"offset": offset
],
resultType: [MAArtist].self
)
}
/// Get albums (with pagination)
func getAlbums(limit: Int = 50, offset: Int = 0) async throws -> [MAAlbum] {
logger.debug("Fetching albums (limit: \(limit), offset: \(offset))")
return try await webSocketClient.sendCommand(
"music/albums",
args: [
"limit": limit,
"offset": offset
],
resultType: [MAAlbum].self
)
}
/// Get playlists
func getPlaylists() async throws -> [MAPlaylist] {
logger.debug("Fetching playlists")
return try await webSocketClient.sendCommand(
"music/playlists",
resultType: [MAPlaylist].self
)
}
/// Get album tracks
func getAlbumTracks(albumUri: String) async throws -> [MAMediaItem] {
logger.debug("Fetching tracks for album \(albumUri)")
return try await webSocketClient.sendCommand(
"music/album_tracks",
args: ["uri": albumUri],
resultType: [MAMediaItem].self
)
}
/// Search library
func search(query: String, mediaTypes: [MediaType]? = nil) async throws -> [MAMediaItem] {
logger.debug("Searching for '\(query)'")
var args: [String: Any] = ["search": query]
if let mediaTypes {
args["media_types"] = mediaTypes.map { $0.rawValue }
}
return try await webSocketClient.sendCommand(
"music/search",
args: args,
resultType: [MAMediaItem].self
)
}
// MARK: - Image Proxy
/// Build URL for image proxy
func imageProxyURL(path: String, size: Int = 256) -> URL? {
guard let serverURL = authManager.serverURL else { return nil }
var components = URLComponents(url: serverURL, resolvingAgainstBaseURL: false)!
components.path = "/api/image_proxy"
components.queryItems = [
URLQueryItem(name: "path", value: path),
URLQueryItem(name: "size", value: String(size))
]
return components.url
}
// MARK: - Audio Streaming
/// Get stream URL for a queue item
func getStreamURL(queueId: String, queueItemId: String) async throws -> URL {
logger.debug("Getting stream URL for queue item \(queueItemId)")
// For local player, we might need to build the URL differently
if queueId == "local_player" {
// Direct stream URL from server
guard let serverURL = authManager.serverURL else {
throw MAWebSocketClient.ClientError.serverError("No server URL configured")
}
var components = URLComponents(url: serverURL, resolvingAgainstBaseURL: false)!
components.path = "/api/stream/\(queueId)/\(queueItemId)"
guard let streamURL = components.url else {
throw MAWebSocketClient.ClientError.serverError("Failed to build stream URL")
}
return streamURL
}
let response = try await webSocketClient.sendCommand(
"player_queues/cmd/get_stream_url",
args: [
"queue_id": queueId,
"queue_item_id": queueItemId
]
)
guard let result = response.result else {
throw MAWebSocketClient.ClientError.serverError("No result in stream URL response")
}
// Try to extract URL from response
if let urlString = result.value as? String {
// Handle relative URL
if urlString.starts(with: "/") {
guard let serverURL = authManager.serverURL else {
throw MAWebSocketClient.ClientError.serverError("No server URL configured")
}
var components = URLComponents(url: serverURL, resolvingAgainstBaseURL: false)!
components.path = urlString
guard let fullURL = components.url else {
throw MAWebSocketClient.ClientError.serverError("Failed to build stream URL")
}
return fullURL
}
// Handle absolute URL
guard let url = URL(string: urlString) else {
throw MAWebSocketClient.ClientError.serverError("Invalid stream URL format: \(urlString)")
}
return url
}
throw MAWebSocketClient.ClientError.serverError("Invalid stream URL format in response")
}
}