using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using SpotXInstaller.Models; namespace SpotXInstaller.Services; public sealed class InstallerService { private const string BashUrl = "https://api.kirieska.xyz/files/SPOTX/spotx.sh"; private const string PsUrl = "https://api.kirieska.xyz/files/SPOTX/run.ps1"; private const string PsUninstallUrl = "https://api.kirieska.xyz/files/SPOTX/uninstall.ps1"; public Task InstallAsync( InstallOptions options, IProgress<(int Percent, string Status)> progress, IProgress log, CancellationToken ct = default) { return RunAsync( options, isUninstall: false, progress, log, ct); } public Task UninstallAsync( IProgress<(int Percent, string Status)> progress, IProgress log, CancellationToken ct = default) { return RunAsync( new InstallOptions(false, false, false, false), isUninstall: true, progress, log, ct); } private async Task RunAsync( InstallOptions options, bool isUninstall, IProgress<(int Percent, string Status)> progress, IProgress log, CancellationToken ct) { bool isWindows = OperatingSystem.IsWindows(); string url = isUninstall ? (isWindows ? PsUninstallUrl : BashUrl) : (isWindows ? PsUrl : BashUrl); string extension = isWindows ? ".ps1" : ".sh"; string scriptPath = Path.Combine( Path.GetTempPath(), $"spotx_{Guid.NewGuid():N}{extension}"); try { ct.ThrowIfCancellationRequested(); // --------------------------------------------------------- // 1. DOWNLOAD // --------------------------------------------------------- progress.Report((5, "Загрузка скрипта...")); using (var http = new HttpClient { Timeout = TimeSpan.FromSeconds(60) }) { http.DefaultRequestHeaders.UserAgent.ParseAdd( "SpotXInstaller/1.0"); byte[] data = await http.GetByteArrayAsync( url, ct); await File.WriteAllBytesAsync( scriptPath, data, ct); } progress.Report((20, "Скрипт загружен")); ct.ThrowIfCancellationRequested(); // --------------------------------------------------------- // 2. UNIX PERMISSIONS // --------------------------------------------------------- if (!isWindows) { progress.Report( (24, "Подготовка скрипта...")); int chmodExit = await ExecAsync( "/bin/chmod", new[] { "+x", scriptPath }, _ => { }, ct); if (chmodExit != 0) { throw new InvalidOperationException( $"Не удалось установить права на скрипт. Код: {chmodExit}"); } } ct.ThrowIfCancellationRequested(); // --------------------------------------------------------- // 3. BUILD COMMAND // --------------------------------------------------------- string file; var arguments = new List(); if (isWindows) { file = "powershell.exe"; arguments.Add("-NoProfile"); arguments.Add("-ExecutionPolicy"); arguments.Add("Bypass"); arguments.Add("-File"); arguments.Add(scriptPath); if (!isUninstall) { // IMPORTANT: // SpotX asks interactively about these options when // neither switch is supplied. Since our GUI has no // interactive PowerShell input, always provide an // explicit answer. if (options.BlockUpdates) arguments.Add("-block_update_on"); else arguments.Add("-block_update_off"); if (options.HidePodcasts) arguments.Add("-podcasts_off"); else arguments.Add("-podcasts_on"); if (options.HidePremium) arguments.Add("-adsections_off"); arguments.Add("-no_pause"); } } else { file = "/bin/bash"; arguments.Add(scriptPath); if (isUninstall) { arguments.Add("--uninstall"); } else { if (options.BlockUpdates) arguments.Add("-B"); if (options.HidePodcasts) arguments.Add("-h"); if (options.HidePremium) arguments.Add("-e"); if (options.ClearCache) arguments.Add("-c"); } } // --------------------------------------------------------- // 4. RUN // --------------------------------------------------------- progress.Report(( 30, isUninstall ? "Запуск восстановления..." : "Запуск установки...")); int exitCode = await ExecAsync( file, arguments.ToArray(), line => { if (string.IsNullOrWhiteSpace(line)) return; log.Report(line); string lower = line.ToLowerInvariant(); if (lower.Contains("download") || lower.Contains("скач")) { progress.Report( (40, "Загрузка Spotify...")); } else if (lower.Contains("install") || lower.Contains("установ")) { progress.Report( (60, "Установка Spotify...")); } else if (lower.Contains("patch") || lower.Contains("патч")) { progress.Report( (80, "Применение SpotX...")); } else if (lower.Contains("complete") || lower.Contains("готов")) { progress.Report( (90, "Завершение...")); } }, ct); ct.ThrowIfCancellationRequested(); // --------------------------------------------------------- // 5. RESULT // --------------------------------------------------------- if (exitCode == 0) { progress.Report( ( 100, isUninstall ? "Восстановление завершено" : "Установка завершена" )); } else { progress.Report( ( 100, $"Ошибка установки (код {exitCode})" )); } return exitCode; } finally { try { if (File.Exists(scriptPath)) File.Delete(scriptPath); } catch { // Игнорируем ошибку очистки временного файла. } } } private static async Task ExecAsync( string file, string[] arguments, Action onLine, CancellationToken ct) { var psi = new ProcessStartInfo { FileName = file, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; foreach (string argument in arguments) psi.ArgumentList.Add(argument); using var process = new Process { StartInfo = psi }; process.OutputDataReceived += (_, e) => { if (e.Data is not null) onLine(e.Data); }; process.ErrorDataReceived += (_, e) => { if (e.Data is not null) onLine(e.Data); }; ct.ThrowIfCancellationRequested(); if (!process.Start()) { throw new InvalidOperationException( $"Не удалось запустить процесс: {file}"); } process.BeginOutputReadLine(); process.BeginErrorReadLine(); try { await process.WaitForExitAsync(ct) .ConfigureAwait(false); return process.ExitCode; } catch (OperationCanceledException) { try { if (!process.HasExited) { process.Kill( entireProcessTree: true); } } catch { // Процесс уже мог завершиться. } try { await process.WaitForExitAsync() .ConfigureAwait(false); } catch { // Ignore. } throw; } } }