Phase 0/1: SwiftUI app (XcodeGen), PlumTVCore framework, player MVP. - PlayerTarget protocol + VLCTarget (HTTP) and BlackTVTarget (ssh black-tv) - config-driven hosts (~/.config/plumtv/hosts.json), seeded plum-vlc + black - reliability: SSH ControlMaster (5s->~1s/poll), endpoint pinning (LAN->overlay), single-flight polling, keep-last-known on transient failure, debounced volume - Hosts pane shows live connection state; Player has target picker + transport - unit tests for status decoding Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
78 lines
3 KiB
Swift
78 lines
3 KiB
Swift
import Foundation
|
|
|
|
/// What kind of backend a host speaks.
|
|
public enum HostKind: String, Codable, Sendable {
|
|
case vlc // VLC HTTP/Lua interface
|
|
case blacktv // the `black-tv` script over SSH
|
|
}
|
|
|
|
public struct VLCConn: Codable, Sendable, Equatable {
|
|
public var host: String
|
|
public var port: Int
|
|
public init(host: String, port: Int) { self.host = host; self.port = port }
|
|
}
|
|
|
|
public struct SSHConn: Codable, Sendable, Equatable {
|
|
/// Ordered endpoints to try (e.g. LAN first, overlay fallback). The working
|
|
/// one is pinned at runtime; we only re-probe the others on failure.
|
|
public var endpoints: [String]
|
|
public var bin: String
|
|
public init(endpoints: [String], bin: String) { self.endpoints = endpoints; self.bin = bin }
|
|
}
|
|
|
|
/// One configurable playback host. Password for `vlc` is NOT stored here — it's
|
|
/// resolved from the portable-net-tv config at runtime (see VLCConfig).
|
|
public struct HostConfig: Codable, Sendable, Identifiable, Equatable {
|
|
public var id: String
|
|
public var name: String
|
|
public var kind: HostKind
|
|
public var vlc: VLCConn?
|
|
public var ssh: SSHConn?
|
|
|
|
public init(id: String, name: String, kind: HostKind, vlc: VLCConn? = nil, ssh: SSHConn? = nil) {
|
|
self.id = id; self.name = name; self.kind = kind; self.vlc = vlc; self.ssh = ssh
|
|
}
|
|
}
|
|
|
|
public struct HostsConfig: Codable, Sendable {
|
|
public var hosts: [HostConfig]
|
|
public init(hosts: [HostConfig]) { self.hosts = hosts }
|
|
|
|
public static func configURL() -> URL {
|
|
FileManager.default.homeDirectoryForCurrentUser
|
|
.appendingPathComponent(".config/plumtv/hosts.json")
|
|
}
|
|
|
|
/// Default seed — the two real player targets on this network. black tries
|
|
/// its LAN address first, then the WG overlay (LAN flaps).
|
|
public static func seeded() -> HostsConfig {
|
|
HostsConfig(hosts: [
|
|
HostConfig(id: "plum-vlc", name: "Plum VLC", kind: .vlc,
|
|
vlc: VLCConn(host: "127.0.0.1", port: 8080)),
|
|
HostConfig(id: "black", name: "Black TV", kind: .blacktv,
|
|
ssh: SSHConn(endpoints: ["lilith@10.0.0.11", "lilith@10.9.0.4"],
|
|
bin: "/usr/local/bin/black-tv")),
|
|
])
|
|
}
|
|
|
|
/// Load `~/.config/plumtv/hosts.json`; if missing/empty, write and return the seed.
|
|
public static func loadOrSeed() -> HostsConfig {
|
|
if let data = try? Data(contentsOf: configURL()),
|
|
let cfg = try? JSONDecoder().decode(HostsConfig.self, from: data),
|
|
!cfg.hosts.isEmpty {
|
|
return cfg
|
|
}
|
|
let seed = seeded()
|
|
try? seed.save()
|
|
return seed
|
|
}
|
|
|
|
public func save() throws {
|
|
let url = HostsConfig.configURL()
|
|
try FileManager.default.createDirectory(at: url.deletingLastPathComponent(),
|
|
withIntermediateDirectories: true)
|
|
let enc = JSONEncoder()
|
|
enc.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
|
|
try enc.encode(self).write(to: url, options: .atomic)
|
|
}
|
|
}
|