Files
windows/Program.cs
T

192 lines
9.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using MusicBridge.Agent;
using MusicBridge.Agent.Desktop;
using MusicBridge.Agent.Media;
using MusicBridge.Agent.Network;
using Windows.Media.Control;
Console.OutputEncoding = Encoding.UTF8;
Console.Title = "MusicBridge Agent";
var lan = false;
var tray = false;
var minimized = false;
var port = 8765;
for (var i = 0; i < args.Length; i++)
{
if (args[i] == "--lan") lan = true;
else if (args[i] == "--tray") tray = true;
else if (args[i] == "--minimized") { minimized = true; tray = 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 -- [--tray] [--minimized] [--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<string> { $"https://127.0.0.1:{port}" };
Uri[] qrEndpoints = [];
using var mdns = new MdnsPublisher(fingerprint, port);
using var qrWindow = new PairingWindowHost(pairing, fingerprint, () => Volatile.Read(ref qrEndpoints));
string networkStatus = lan ? "Подключение к домашней сети…" : "Локальный режим. Для iPhone запусти с --lan.";
using var desktop = tray ? new DesktopHost(() => server.Latest, media, pairing,
() => Volatile.Read(ref networkStatus), qrWindow.Open, shutdown.Cancel,
startup: new StartupSettings(Path.Combine(AppContext.BaseDirectory, "MusicBridge.Agent.exe"), lan, port), minimized: minimized) : null;
if (desktop is not null)
{
await desktop.StartAsync();
DesktopHost.FreeConsole();
}
var message = "Готов к управлению.";
IReadOnlyList<TrustedDevice> displayedDevices = [];
var nextRefresh = DateTimeOffset.MinValue;
while (!shutdown.IsCancellationRequested)
{
if (!tray && !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)
{
qrWindow.Open();
message = "Окно QR открыто. Для нового кода нажми B.";
}
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 (lan)
{
try
{
var ips = LocalAddresses.Read();
Volatile.Write(ref qrEndpoints, ips.Select(ip => new Uri($"https://{ip}:{port}")).ToArray());
addresses = new List<string> { $"https://127.0.0.1:{port}" };
addresses.AddRange(qrEndpoints.Select(u => u.GetLeftPart(UriPartial.Authority)));
mdns.Refresh(ips);
Volatile.Write(ref networkStatus, string.Join(" | ", qrEndpoints.Select(u => u.GetLeftPart(UriPartial.Authority))) + "\n" + mdns.Status);
}
catch (Exception ex)
{
Volatile.Write(ref qrEndpoints, []);
mdns.Refresh([]);
message = $"Не удалось обновить адреса: {ex.Message}";
Volatile.Write(ref networkStatus, message);
}
}
if (tray)
{
nextRefresh = DateTimeOffset.UtcNow.AddSeconds(1);
await Task.Delay(50, shutdown.Token);
continue;
}
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.");
if (lan) Console.WriteLine(mdns.Status);
if (qrWindow.Error is { } qrError) Console.WriteLine($"Окно QR: {qrError}");
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)
{
var error = $"Не удалось запустить или продолжить работу агента: {ex.Message}";
if (tray) MessageBox.Show(error, "MusicBridge", MessageBoxButtons.OK, MessageBoxIcon.Error);
else Console.Error.WriteLine(error);
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}";
}