Files
ios/MusicBridge/DiscoveryStore.swift
T

113 lines
5.2 KiB
Swift
Raw Normal View History

import Foundation
import Combine
import Darwin
struct DiscoveredComputer: Identifiable, Equatable {
let name: String
let endpoint: String
let fingerprint: String
var id: String { fingerprint + endpoint }
static func parse(name: String, host: String, port: Int, txt: [String: Data]) -> Self? {
guard txt["v"] == Data("1".utf8), let data = txt["id"],
let pin = String(data: data, encoding: .utf8),
let connection = try? Connection(endpoint: "https://\(host):\(port)", fingerprint: pin) else { return nil }
return Self(name: String(name.prefix(80)), endpoint: connection.endpoint.absoluteString,
fingerprint: connection.fingerprint)
}
// An advertisement is a routing hint, never a replacement for saved trust.
func reconnect(_ saved: Connection) -> Connection? {
guard fingerprint == saved.fingerprint else { return nil }
return try? Connection(endpoint: endpoint, fingerprint: saved.fingerprint, token: saved.token)
}
}
// Started on the main run loop; Foundation delivers browser/service delegates there.
final class DiscoveryStore: NSObject, ObservableObject, NetServiceBrowserDelegate, NetServiceDelegate {
@Published private(set) var computers: [DiscoveredComputer] = []
@Published private(set) var message = "Поиск ПК не запущен"
private var browser: NetServiceBrowser?
private var services: [String: NetService] = [:]
private var records: [String: [DiscoveredComputer]] = [:]
private var refresh: Timer?
func start() {
guard browser == nil else { return }
message = "Ищем MusicBridge в локальной сети…"
let browser = NetServiceBrowser()
self.browser = browser
browser.delegate = self
browser.searchForServices(ofType: "_musicbridge._tcp.", inDomain: "local.")
refresh = Timer.scheduledTimer(withTimeInterval: 15, repeats: true) { [weak self] _ in
guard let self else { return }
for service in self.services.values {
service.stop()
service.resolve(withTimeout: 5)
}
}
}
func stop() {
refresh?.invalidate(); refresh = nil
browser?.delegate = nil; browser?.stop(); browser = nil
for service in services.values { service.delegate = nil; service.stop() }
services.removeAll(); records.removeAll(); computers = []
message = "Поиск ПК приостановлен"
}
func restart() { stop(); start() }
private func key(_ service: NetService) -> String { service.domain + service.type + service.name }
private func publish() {
computers = Array(Set(records.values.flatMap { $0 }.map(\.id))).sorted().compactMap { id in
records.values.lazy.flatMap { $0 }.first { $0.id == id }
}
message = computers.isEmpty ? "ПК пока не найден. Проверьте Wi-Fi и LAN-режим агента." : "Найдено адресов: \(computers.count)"
}
func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
guard self.browser === browser, services.count < 64, services[key(service)] == nil else { return }
services[key(service)] = service
service.delegate = self
service.resolve(withTimeout: 5)
}
func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) {
guard self.browser === browser else { return }
let old = services.removeValue(forKey: key(service))
old?.delegate = nil; old?.stop()
records.removeValue(forKey: key(service)); publish()
}
func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch errorDict: [String: NSNumber]) {
guard self.browser === browser else { return }
stop()
message = "Поиск недоступен. Разрешите MusicBridge доступ к локальной сети в Настройках и повторите поиск."
}
func netServiceDidResolveAddress(_ sender: NetService) {
guard services[key(sender)] === sender else { return }
let txt = sender.txtRecordData().map(NetService.dictionary(fromTXTRecord:)) ?? [:]
records[key(sender)] = (sender.addresses ?? []).prefix(16).compactMap { data in
guard let host = Self.ipv4(data) else { return nil }
return DiscoveredComputer.parse(name: sender.name, host: host, port: sender.port, txt: txt)
}
publish()
}
func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) {
guard services[key(sender)] === sender else { return }
records.removeValue(forKey: key(sender)); publish()
}
static func ipv4(_ data: Data) -> String? {
guard data.count >= MemoryLayout<sockaddr_in>.size else { return nil }
var address = data.withUnsafeBytes { $0.loadUnaligned(as: sockaddr_in.self) }
guard address.sin_family == sa_family_t(AF_INET) else { return nil }
var buffer = [CChar](repeating: 0, count: Int(INET_ADDRSTRLEN))
guard inet_ntop(AF_INET, &address.sin_addr, &buffer, socklen_t(INET_ADDRSTRLEN)) != nil else { return nil }
return String(cString: buffer)
}
}