Initial Commit
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
//
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user