using System.Net; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; using MusicBridge.Agent.Media; using MusicBridge.Agent.Network; using Windows.Media.Control; Console.OutputEncoding = Encoding.UTF8; Console.Title = "MusicBridge Agent"; var lan = false; var port = 8765; for (var i = 0; i < args.Length; i++) { if (args[i] == "--lan") lan = true; else if (args[i] == "--port" && i + 1 < args.Length && int.TryParse(args[++i], out var parsed) && parsed is > 0 and <= 65535) port = parsed; else { Console.WriteLine("Запуск: dotnet run -- [--lan] [--port 8765]"); Console.WriteLine("Без --lan соединения разрешены только с этого ПК. Веб-интерфейса нет."); return; } } using var shutdown = new CancellationTokenSource(); Console.CancelKeyPress += (_, e) => { e.Cancel = true; shutdown.Cancel(); }; try { using var identity = new IdentityStore(IdentityStore.DefaultDirectory); Console.WriteLine("Подключаемся к медиаплееру Windows..."); var manager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync(); using var media = new MediaService(manager); var pairing = new PairingService(store: identity); if (pairing.Devices.Count == 0) pairing.Open(); var certificate = identity.Certificate; await using var server = new RemoteServer(media, pairing, certificate, lan, port); await server.StartAsync(shutdown.Token); using var hostStopping = server.Stopping.Register(() => shutdown.Cancel()); var fingerprint = certificate.GetCertHashString(HashAlgorithmName.SHA256); var addresses = new List { $"https://127.0.0.1:{port}" }; if (lan) { try { addresses.AddRange(Dns.GetHostAddresses(Dns.GetHostName()) .Where(ip => ip.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(ip)) .Select(ip => $"https://{ip}:{port}")); } catch (SocketException) { } } var message = "Готов к управлению."; IReadOnlyList displayedDevices = []; var nextRefresh = DateTimeOffset.MinValue; while (!shutdown.IsCancellationRequested) { if (!Console.IsInputRedirected && Console.KeyAvailable) { var key = Console.ReadKey(intercept: true).Key; if (key == ConsoleKey.Q) break; if (key is >= ConsoleKey.D1 and <= ConsoleKey.D8) { var index = (int)key - (int)ConsoleKey.D1; if (index < displayedDevices.Count) { try { var device = displayedDevices[index]; message = pairing.Revoke(device.Id) ? $"Доступ отозван: {device.Name}." : "Устройство уже отключено."; } catch (Exception ex) { message = $"Не удалось сохранить отзыв доступа: {ex.Message}"; } } } else if (key == ConsoleKey.I) { var invitationTime = DateTimeOffset.UtcNow; var window = pairing.Current; var endpoints = addresses.Select(a => new Uri(a)).Where(ConnectionInvitation.IsLocalEndpoint).ToArray(); message = window is null ? "Сначала нажми B, чтобы открыть сопряжение." : endpoints.Length == 0 ? "Для данных подключения запусти агент с --lan в домашней сети." : string.Join(Environment.NewLine, endpoints.Select(endpoint => $"Данные для будущего QR ({endpoint.Host}): {ConnectionInvitation.Create(endpoint, fingerprint, window, invitationTime).ToQrPayload()}")); } else if (key == ConsoleKey.B) { pairing.Open(); message = "Новый код сопряжения действует 5 минут."; } else { var command = key switch { ConsoleKey.Spacebar => new MediaCommand("toggle"), ConsoleKey.N => new MediaCommand("next"), ConsoleKey.P => new MediaCommand("previous"), ConsoleKey.LeftArrow => new MediaCommand("seekRelative", -10), ConsoleKey.RightArrow => new MediaCommand("seekRelative", 10), ConsoleKey.UpArrow => new MediaCommand("volumeRelative", 0.05), ConsoleKey.DownArrow => new MediaCommand("volumeRelative", -0.05), ConsoleKey.M => new MediaCommand("toggleMute"), _ => null }; if (command is not null) message = (await media.ExecuteAsync(command, shutdown.Token)).Message; } nextRefresh = DateTimeOffset.MinValue; } if (DateTimeOffset.UtcNow >= nextRefresh) { if (!Console.IsOutputRedirected) Console.Clear(); var state = server.Latest; Console.WriteLine("MUSICBRIDGE\n"); if (state.HasSession) { Console.WriteLine($"Трек: {state.Title}"); Console.WriteLine($"Исполнитель: {state.Artist}"); Console.WriteLine($"Состояние: {state.PlaybackStatus}"); Console.WriteLine($"Позиция: {FormatTime(state.PositionSeconds)} / {FormatTime(state.EndSeconds)}"); Console.WriteLine($"Источник: {state.Source}"); Console.WriteLine($"Обложка: {(state.ArtworkId is not null ? "доступна" : state.ArtworkError ?? "нет у плеера")}"); } else Console.WriteLine("Медиаплеер не найден. Включи музыку в браузере или приложении."); if (state.MediaError is not null) Console.WriteLine($"Ошибка плеера: {state.MediaError}"); if (state.Volume is float volume) Console.WriteLine($"Громкость: {volume:P0}{(state.Muted == true ? " (звук выключен)" : "")}"); else Console.WriteLine($"Громкость недоступна: {state.VolumeError}"); Console.WriteLine($"\nПодключение: {string.Join(" | ", addresses)}"); Console.WriteLine(lan ? "Режим локальной сети (IPv4)." : "Режим только этого ПК; для Wi-Fi запусти с --lan."); Console.WriteLine($"SHA-256 сертификата: {fingerprint}"); var window = pairing.Current; Console.WriteLine(window is null ? "Сопряжение закрыто. B — открыть заново." : $"Код сопряжения: {window.Code} (до {window.ExpiresAt.ToLocalTime():HH:mm:ss})"); displayedDevices = pairing.Devices; Console.WriteLine($"Доверенные устройства: {displayedDevices.Count}/8 (клавиша с номером — отозвать доступ)"); for (var i = 0; i < displayedDevices.Count; i++) Console.WriteLine($" {i + 1}. {displayedDevices[i].Name}"); Console.WriteLine("\nПробел: play/pause | N/P: следующий/предыдущий трек"); Console.WriteLine("←/→: ±10 с | ↑/↓: громкость ±5% | M: звук | B: сопряжение"); Console.WriteLine("I: данные для будущего QR | Выход: Q или Ctrl+C"); Console.WriteLine($"\n{message}"); nextRefresh = DateTimeOffset.UtcNow.AddSeconds(1); } await Task.Delay(50, shutdown.Token); } } catch (OperationCanceledException) when (shutdown.IsCancellationRequested) { } catch (Exception ex) { Console.Error.WriteLine($"Не удалось запустить или продолжить работу агента: {ex.Message}"); Environment.ExitCode = 1; } static string FormatTime(double seconds) { var time = TimeSpan.FromSeconds(seconds); return time.TotalHours >= 1 ? $"{(int)time.TotalHours}:{time.Minutes:00}:{time.Seconds:00}" : $"{(int)time.TotalMinutes}:{time.Seconds:00}"; }