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.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) } }