// // MAModels.swift // Mobile Music Assistant // // Created by Sven Hanold on 26.03.26. // import Foundation // MARK: - Player Models struct MAPlayer: Codable, Identifiable, Hashable { let playerId: String let name: String let state: PlayerState let currentItem: MAQueueItem? let volume: Int let powered: Bool let available: Bool var id: String { playerId } enum CodingKeys: String, CodingKey { case playerId = "player_id" case name case state case currentItem = "current_item" case volume = "volume_level" case powered case available } } enum PlayerState: String, Codable { case playing case paused case idle case off } // MARK: - Queue Models struct MAQueueItem: Codable, Identifiable, Hashable { let queueItemId: String let mediaItem: MAMediaItem? let name: String let duration: Int? let streamDetails: MAStreamDetails? var id: String { queueItemId } enum CodingKeys: String, CodingKey { case queueItemId = "queue_item_id" case mediaItem = "media_item" case name case duration case streamDetails = "stream_details" } } struct MAStreamDetails: Codable, Hashable { let providerId: String let itemId: String let audioFormat: MAAudioFormat? enum CodingKeys: String, CodingKey { case providerId = "provider" case itemId = "item_id" case audioFormat = "audio_format" } } struct MAAudioFormat: Codable, Hashable { let contentType: String let sampleRate: Int? let bitDepth: Int? enum CodingKeys: String, CodingKey { case contentType = "content_type" case sampleRate = "sample_rate" case bitDepth = "bit_depth" } } // MARK: - Media Models struct MAMediaItem: Codable, Identifiable, Hashable { let uri: String let name: String let mediaType: MediaType let artists: [MAArtist]? let album: MAAlbum? let imageUrl: String? let duration: Int? var id: String { uri } enum CodingKeys: String, CodingKey { case uri case name case mediaType = "media_type" case artists case album case imageUrl = "image" case duration } } enum MediaType: String, Codable { case track case album case artist case playlist case radio } struct MAArtist: Codable, Identifiable, Hashable { let uri: String let name: String let imageUrl: String? let sortName: String? let musicbrainzId: String? var id: String { uri } enum CodingKeys: String, CodingKey { case uri case name case imageUrl = "image" case sortName = "sort_name" case musicbrainzId = "musicbrainz_id" } } struct MAAlbum: Codable, Identifiable, Hashable { let uri: String let name: String let artists: [MAArtist]? let imageUrl: String? let year: Int? var id: String { uri } enum CodingKeys: String, CodingKey { case uri case name case artists case imageUrl = "image" case year } } struct MAPlaylist: Codable, Identifiable, Hashable { let uri: String let name: String let owner: String? let imageUrl: String? let isEditable: Bool var id: String { uri } enum CodingKeys: String, CodingKey { case uri case name case owner case imageUrl = "image" case isEditable = "is_editable" } } // MARK: - WebSocket Protocol Models struct MACommand: Encodable { let messageId: String let command: String let args: [String: AnyCodable]? enum CodingKeys: String, CodingKey { case messageId = "message_id" case command case args } } struct MAResponse: Decodable { let messageId: String? let result: AnyCodable? let errorCode: String? let errorMessage: String? enum CodingKeys: String, CodingKey { case messageId = "message_id" case result case errorCode = "error_code" case errorMessage = "error" } } struct MAEvent: Decodable { let event: String let data: AnyCodable? } // MARK: - Auth Models struct MALoginRequest: Encodable { let username: String let password: String } struct MALoginResponse: Decodable { let accessToken: String enum CodingKeys: String, CodingKey { case accessToken = "access_token" } } // MARK: - AnyCodable Helper /// Helper to handle dynamic JSON values struct AnyCodable: Codable, Hashable { let value: Any init(_ value: Any) { self.value = value } init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if container.decodeNil() { value = NSNull() } else if let bool = try? container.decode(Bool.self) { value = bool } else if let int = try? container.decode(Int.self) { value = int } else if let double = try? container.decode(Double.self) { value = double } else if let string = try? container.decode(String.self) { value = string } else if let array = try? container.decode([AnyCodable].self) { value = array.map { $0.value } } else if let dict = try? container.decode([String: AnyCodable].self) { value = dict.mapValues { $0.value } } else { throw DecodingError.dataCorruptedError( in: container, debugDescription: "Unable to decode value" ) } } func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch value { case is NSNull: try container.encodeNil() case let bool as Bool: try container.encode(bool) case let int as Int: try container.encode(int) case let double as Double: try container.encode(double) case let string as String: try container.encode(string) case let array as [Any]: try container.encode(array.map { AnyCodable($0) }) case let dict as [String: Any]: try container.encode(dict.mapValues { AnyCodable($0) }) default: throw EncodingError.invalidValue( value, EncodingError.Context( codingPath: container.codingPath, debugDescription: "Unable to encode value" ) ) } } static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool { // Simple comparison - extend as needed return String(describing: lhs.value) == String(describing: rhs.value) } func hash(into hasher: inout Hasher) { hasher.combine(String(describing: value)) } } extension AnyCodable { /// Decode the wrapped value to a specific type func decode(as type: T.Type) throws -> T { let data = try JSONEncoder().encode(self) return try JSONDecoder().decode(T.self, from: data) } }