You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
193 lines
6.3 KiB
193 lines
6.3 KiB
import Foundation
|
|
|
|
// MARK: - Protocol Version
|
|
|
|
/// Current version of the remote control wire protocol.
|
|
public nonisolated let RemoteProtocolVersion: Int = 1
|
|
|
|
// MARK: - Supporting Types
|
|
|
|
/// Snapshot of the host's playback state, sent to remote clients.
|
|
public nonisolated struct PlaybackStatePayload: Codable, Equatable, Sendable {
|
|
public var trackId: Int64?
|
|
public var isPlaying: Bool
|
|
public var currentTime: Double
|
|
public var duration: Double
|
|
public var volume: Float
|
|
public var isShuffled: Bool
|
|
|
|
public init(
|
|
trackId: Int64? = nil,
|
|
isPlaying: Bool,
|
|
currentTime: Double,
|
|
duration: Double,
|
|
volume: Float,
|
|
isShuffled: Bool
|
|
) {
|
|
self.trackId = trackId
|
|
self.isPlaying = isPlaying
|
|
self.currentTime = currentTime
|
|
self.duration = duration
|
|
self.volume = volume
|
|
self.isShuffled = isShuffled
|
|
}
|
|
}
|
|
|
|
/// Exchanged during connection setup to agree on protocol version.
|
|
public nonisolated struct HandshakeMessage: Codable, Equatable, Sendable {
|
|
public var protocolVersion: Int
|
|
public var appVersion: String
|
|
|
|
public init(protocolVersion: Int, appVersion: String) {
|
|
self.protocolVersion = protocolVersion
|
|
self.appVersion = appVersion
|
|
}
|
|
}
|
|
|
|
// MARK: - RemoteCommand
|
|
|
|
/// Commands sent from a remote client to the host.
|
|
/// Wire format: `{"type":"<case>","payload":{...}}` (payload omitted for cases with no associated values).
|
|
public nonisolated enum RemoteCommand: Equatable, Sendable {
|
|
case play(trackId: Int64, queueIds: [Int64])
|
|
case pause
|
|
case resume
|
|
case next
|
|
case previous
|
|
case seek(position: Double)
|
|
case setVolume(level: Float)
|
|
case toggleShuffle
|
|
case refreshDB
|
|
}
|
|
|
|
extension RemoteCommand: Codable {
|
|
private enum TypeKey: String, Codable {
|
|
case play, pause, resume, next, previous, seek, setVolume, toggleShuffle, refreshDB
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case type, payload
|
|
}
|
|
|
|
// Payload structs for cases with associated values
|
|
private struct PlayPayload: Codable {
|
|
var trackId: Int64
|
|
var queueIds: [Int64]
|
|
}
|
|
|
|
private struct SeekPayload: Codable {
|
|
var position: Double
|
|
}
|
|
|
|
private struct VolumePayload: Codable {
|
|
var level: Float
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
switch self {
|
|
case .play(let trackId, let queueIds):
|
|
try container.encode(TypeKey.play, forKey: .type)
|
|
try container.encode(PlayPayload(trackId: trackId, queueIds: queueIds), forKey: .payload)
|
|
case .pause:
|
|
try container.encode(TypeKey.pause, forKey: .type)
|
|
case .resume:
|
|
try container.encode(TypeKey.resume, forKey: .type)
|
|
case .next:
|
|
try container.encode(TypeKey.next, forKey: .type)
|
|
case .previous:
|
|
try container.encode(TypeKey.previous, forKey: .type)
|
|
case .seek(let position):
|
|
try container.encode(TypeKey.seek, forKey: .type)
|
|
try container.encode(SeekPayload(position: position), forKey: .payload)
|
|
case .setVolume(let level):
|
|
try container.encode(TypeKey.setVolume, forKey: .type)
|
|
try container.encode(VolumePayload(level: level), forKey: .payload)
|
|
case .toggleShuffle:
|
|
try container.encode(TypeKey.toggleShuffle, forKey: .type)
|
|
case .refreshDB:
|
|
try container.encode(TypeKey.refreshDB, forKey: .type)
|
|
}
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
let type = try container.decode(TypeKey.self, forKey: .type)
|
|
switch type {
|
|
case .play:
|
|
let payload = try container.decode(PlayPayload.self, forKey: .payload)
|
|
self = .play(trackId: payload.trackId, queueIds: payload.queueIds)
|
|
case .pause:
|
|
self = .pause
|
|
case .resume:
|
|
self = .resume
|
|
case .next:
|
|
self = .next
|
|
case .previous:
|
|
self = .previous
|
|
case .seek:
|
|
let payload = try container.decode(SeekPayload.self, forKey: .payload)
|
|
self = .seek(position: payload.position)
|
|
case .setVolume:
|
|
let payload = try container.decode(VolumePayload.self, forKey: .payload)
|
|
self = .setVolume(level: payload.level)
|
|
case .toggleShuffle:
|
|
self = .toggleShuffle
|
|
case .refreshDB:
|
|
self = .refreshDB
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - HostEvent
|
|
|
|
/// Events sent from the host to remote clients.
|
|
/// Wire format: `{"type":"<case>","payload":{...}}` (payload omitted for cases with no associated values).
|
|
public nonisolated enum HostEvent: Equatable, Sendable {
|
|
case playbackState(PlaybackStatePayload)
|
|
case dbReady
|
|
case error(message: String)
|
|
}
|
|
|
|
extension HostEvent: Codable {
|
|
private enum TypeKey: String, Codable {
|
|
case playbackState, dbReady, error
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case type, payload
|
|
}
|
|
|
|
private struct ErrorPayload: Codable {
|
|
var message: String
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
switch self {
|
|
case .playbackState(let payload):
|
|
try container.encode(TypeKey.playbackState, forKey: .type)
|
|
try container.encode(payload, forKey: .payload)
|
|
case .dbReady:
|
|
try container.encode(TypeKey.dbReady, forKey: .type)
|
|
case .error(let message):
|
|
try container.encode(TypeKey.error, forKey: .type)
|
|
try container.encode(ErrorPayload(message: message), forKey: .payload)
|
|
}
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
let type = try container.decode(TypeKey.self, forKey: .type)
|
|
switch type {
|
|
case .playbackState:
|
|
let payload = try container.decode(PlaybackStatePayload.self, forKey: .payload)
|
|
self = .playbackState(payload)
|
|
case .dbReady:
|
|
self = .dbReady
|
|
case .error:
|
|
let payload = try container.decode(ErrorPayload.self, forKey: .payload)
|
|
self = .error(message: payload.message)
|
|
}
|
|
}
|
|
}
|
|
|