├── 7zip └── 7za.exe ├── Assets └── icon.ico ├── AppMain.axaml.cs ├── Services ├── IPlatformService.cs ├── PlatformServiceFactory.cs ├── PatcherService.cs ├── BasePlatformService.cs ├── LinuxService.cs ├── WindowsService.cs ├── MacOsService.cs └── ArchLinuxService.cs ├── Program.cs ├── YandexMusicPatcherGui.sln ├── Main.axaml.cs ├── Utils.cs ├── Update.cs ├── YandexMusicPatcherGui.csproj ├── AppMain.axaml ├── MainViewModel.cs ├── Main.axaml ├── .gitignore ├── Patcher.cs ├── README.md └── AsarIntegrity.cs /7zip/7za.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkPlayOff/YandexMusicExtMod/HEAD/7zip/7za.exe -------------------------------------------------------------------------------- /Assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DarkPlayOff/YandexMusicExtMod/HEAD/Assets/icon.ico -------------------------------------------------------------------------------- /AppMain.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls.ApplicationLifetimes; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace YandexMusicPatcherGui; 6 | 7 | public class AppMain : Application 8 | { 9 | public override void Initialize() 10 | { 11 | AvaloniaXamlLoader.Load(this); 12 | } 13 | 14 | public override void OnFrameworkInitializationCompleted() 15 | { 16 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) desktop.MainWindow = new Main(); 17 | 18 | base.OnFrameworkInitializationCompleted(); 19 | } 20 | } -------------------------------------------------------------------------------- /Services/IPlatformService.cs: -------------------------------------------------------------------------------- 1 | namespace YandexMusicPatcherGui.Services; 2 | 3 | public interface IPlatformService 4 | { 5 | string GetModPath(); 6 | 7 | string GetAsarPath(); 8 | 9 | Task DownloadLatestMusic(string tempFolder); 10 | 11 | Task<(bool, string)> IsSupported(); 12 | Task InstallMod(string archivePath, string tempFolder); 13 | 14 | Task InstallModUnpacked(string archivePath, string tempFolder); 15 | 16 | Task FinishInstallation(string tempFolder); 17 | 18 | Task CreateDesktopShortcut(string linkName, string path); 19 | 20 | void RunApplication(); 21 | 22 | string GetApplicationExecutablePath(); 23 | } -------------------------------------------------------------------------------- /Services/PlatformServiceFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace YandexMusicPatcherGui.Services; 4 | 5 | public static class PlatformServiceFactory 6 | { 7 | public static IPlatformService Create() 8 | { 9 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 10 | { 11 | return new WindowsService(); 12 | } 13 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 14 | { 15 | return new MacOsService(); 16 | } 17 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 18 | { 19 | return IsArchBased() ? new ArchLinuxService() : new LinuxService(); 20 | } 21 | throw new PlatformNotSupportedException(); 22 | } 23 | 24 | private static bool IsArchBased() 25 | { 26 | return File.Exists("/usr/bin/pacman"); 27 | } 28 | } -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Avalonia; 3 | using YandexMusicPatcherGui.Services; 4 | 5 | namespace YandexMusicPatcherGui; 6 | 7 | internal static class Program 8 | { 9 | public static readonly string RepoUrl = "https://github.com/DarkPlayOff/YandexMusicExtMod"; 10 | 11 | public static readonly IPlatformService PlatformService = PlatformServiceFactory.Create(); 12 | 13 | public static readonly string TempPath = Path.Combine(Path.GetTempPath(), "YandexMusicPatcher"); 14 | 15 | public static readonly string ModPath = PlatformService.GetModPath(); 16 | public static readonly string Version = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0"; 17 | 18 | [STAThread] 19 | public static void Main(string[] args) 20 | { 21 | BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); 22 | } 23 | 24 | private static AppBuilder BuildAvaloniaApp() 25 | { 26 | return AppBuilder.Configure() 27 | .UsePlatformDetect(); 28 | } 29 | } -------------------------------------------------------------------------------- /YandexMusicPatcherGui.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.10.35122.118 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YandexMusicPatcherGui", "YandexMusicPatcherGui.csproj", "{8DFBAF2A-64F1-43FA-BA51-D51F012BD7A9}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {8DFBAF2A-64F1-43FA-BA51-D51F012BD7A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {8DFBAF2A-64F1-43FA-BA51-D51F012BD7A9}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {8DFBAF2A-64F1-43FA-BA51-D51F012BD7A9}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {8DFBAF2A-64F1-43FA-BA51-D51F012BD7A9}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {4B140F4F-828A-4AF3-9AAD-A881F4926265} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Services/PatcherService.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace YandexMusicPatcherGui.Services; 4 | 5 | public class PatcherService 6 | { 7 | private const string YandexMusicProcessName = "Яндекс Музыка"; 8 | private const string ShortcutName = "Яндекс Музыка"; 9 | 10 | public async Task Patch(IProgress<(int, string)> progress) 11 | { 12 | await KillYandexMusicProcess(); 13 | var tempFolder = Program.TempPath; 14 | await Patcher.DownloadLatestMusic(); 15 | await Patcher.DownloadModifiedAsar(); 16 | await Program.PlatformService.FinishInstallation(tempFolder); 17 | 18 | var latestVersion = await Update.GetLatestModVersion(); 19 | if (latestVersion != null) 20 | { 21 | Update.SetInstalledVersion(latestVersion); 22 | } 23 | 24 | await CreateDesktopShortcut(); 25 | 26 | } 27 | 28 | private static async Task CreateDesktopShortcut() 29 | { 30 | await Program.PlatformService.CreateDesktopShortcut(ShortcutName, Program.PlatformService.GetApplicationExecutablePath()); 31 | } 32 | 33 | private static async Task KillYandexMusicProcess() 34 | { 35 | await Task.Run(() => 36 | { 37 | foreach (var p in Process.GetProcessesByName(YandexMusicProcessName)) 38 | { 39 | try 40 | { 41 | p.Kill(); 42 | } 43 | catch (Exception e) 44 | { 45 | Trace.TraceWarning("Не удалось завершить процесс Яндекс Музыки: {0}", e.Message); 46 | } 47 | } 48 | }); 49 | } 50 | } -------------------------------------------------------------------------------- /Main.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Input; 3 | using Avalonia.Markup.Xaml; 4 | using Avalonia.Media; 5 | using Avalonia.Interactivity; 6 | 7 | namespace YandexMusicPatcherGui; 8 | 9 | public partial class Main : Window 10 | { 11 | public Main() 12 | { 13 | InitializeComponent(); 14 | DataContext = new MainViewModel(); 15 | 16 | if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)) 17 | { 18 | TransparencyLevelHint = new[] { WindowTransparencyLevel.Mica, WindowTransparencyLevel.None }; 19 | } 20 | else if (OperatingSystem.IsWindows()) 21 | { 22 | TransparencyLevelHint = new[] { WindowTransparencyLevel.AcrylicBlur, WindowTransparencyLevel.None }; 23 | Background = new SolidColorBrush(Color.Parse("#CC000000")); 24 | } 25 | else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) 26 | { 27 | SystemDecorations = SystemDecorations.None; 28 | TransparencyLevelHint = new[] { WindowTransparencyLevel.Blur, WindowTransparencyLevel.None }; 29 | Background = new SolidColorBrush(Color.Parse("#CC000000")); 30 | } 31 | } 32 | 33 | private void InitializeComponent() 34 | { 35 | AvaloniaXamlLoader.Load(this); 36 | } 37 | 38 | private void Window_MouseDown(object sender, PointerPressedEventArgs e) 39 | { 40 | if (e.Source is Button) 41 | { 42 | return; 43 | } 44 | 45 | if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) 46 | { 47 | BeginMoveDrag(e); 48 | } 49 | } 50 | 51 | private void CloseButton_Click(object sender, RoutedEventArgs e) 52 | { 53 | Close(); 54 | } 55 | } -------------------------------------------------------------------------------- /Services/BasePlatformService.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Compression; 2 | 3 | namespace YandexMusicPatcherGui.Services; 4 | 5 | public abstract class BasePlatformService : IPlatformService 6 | { 7 | public abstract string GetModPath(); 8 | public abstract string GetAsarPath(); 9 | public abstract Task<(bool, string)> IsSupported(); 10 | public abstract Task CreateDesktopShortcut(string linkName, string path); 11 | public abstract void RunApplication(); 12 | public abstract string GetApplicationExecutablePath(); 13 | 14 | public virtual Task DownloadLatestMusic(string tempFolder) 15 | { 16 | throw new NotImplementedException(); 17 | } 18 | 19 | public virtual async Task InstallMod(string archivePath, string tempFolder) 20 | { 21 | var asarPath = GetAsarPath(); 22 | await using var sourceStream = File.OpenRead(archivePath); 23 | await using var destinationStream = File.Create(asarPath); 24 | await using var decompressionStream = new ZstdSharp.DecompressionStream(sourceStream); 25 | await decompressionStream.CopyToAsync(destinationStream); 26 | } 27 | 28 | public virtual Task InstallModUnpacked(string archivePath, string tempFolder) 29 | { 30 | var asarPath = GetAsarPath(); 31 | var resourcesPath = Path.GetDirectoryName(asarPath)!; 32 | var unpackedAsarPath = Path.Combine(resourcesPath, "app.asar.unpacked"); 33 | 34 | if (Directory.Exists(unpackedAsarPath)) 35 | { 36 | Directory.Delete(unpackedAsarPath, true); 37 | } 38 | 39 | ZipFile.ExtractToDirectory(archivePath, unpackedAsarPath); 40 | return Task.CompletedTask; 41 | } 42 | 43 | public virtual Task FinishInstallation(string tempFolder) 44 | { 45 | return Task.CompletedTask; 46 | } 47 | } -------------------------------------------------------------------------------- /Utils.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Net; 3 | using Avalonia.Data.Converters; 4 | 5 | namespace YandexMusicPatcherGui; 6 | public class ProgressToWidthConverter : IValueConverter 7 | { 8 | public static readonly ProgressToWidthConverter Instance = new(); 9 | 10 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 11 | { 12 | if (value is not (double progress and >= 0 and <= 100) || parameter is not double width) return 0.0; 13 | 14 | return progress / 100.0 * width; 15 | } 16 | 17 | public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 18 | { 19 | throw new NotSupportedException(); 20 | } 21 | } 22 | public static class Utils 23 | { 24 | private const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"; 25 | public static readonly HttpClient HttpClient; 26 | public static readonly HttpClient NoRedirectHttpClient; 27 | 28 | static Utils() 29 | { 30 | HttpClient = CreateHttpClient(true); 31 | NoRedirectHttpClient = CreateHttpClient(false); 32 | } 33 | 34 | private static HttpClient CreateHttpClient(bool allowAutoRedirect) 35 | { 36 | var handler = new HttpClientHandler 37 | { 38 | AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, 39 | AllowAutoRedirect = allowAutoRedirect 40 | }; 41 | 42 | var client = new HttpClient(handler) 43 | { 44 | Timeout = TimeSpan.FromSeconds(45) 45 | }; 46 | client.DefaultRequestHeaders.UserAgent.ParseAdd(UserAgent); 47 | 48 | return client; 49 | } 50 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Update.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace YandexMusicPatcherGui; 4 | 5 | public static class Update 6 | { 7 | private static readonly string VersionFilePath = Path.Combine(Program.ModPath, "version.txt"); 8 | 9 | public static Task GetLatestAppVersion() 10 | { 11 | return GetLatestVersionFromRepo(Program.RepoUrl); 12 | } 13 | 14 | public static Version? GetInstalledVersion() 15 | { 16 | if (!File.Exists(VersionFilePath)) 17 | { 18 | return null; 19 | } 20 | 21 | var versionString = File.ReadAllText(VersionFilePath); 22 | Version.TryParse(versionString, out var version); 23 | return version; 24 | } 25 | 26 | public static void SetInstalledVersion(Version version) 27 | { 28 | Directory.CreateDirectory(Program.ModPath); 29 | File.WriteAllText(VersionFilePath, version.ToString()); 30 | } 31 | 32 | public static Task GetLatestModVersion() 33 | { 34 | return GetLatestVersionFromRepo("https://github.com/DarkPlayOff/YandexMusicAsar"); 35 | } 36 | 37 | private static async Task GetLatestVersionFromRepo(string repoUrl) 38 | { 39 | var request = new HttpRequestMessage(HttpMethod.Get, $"{repoUrl}/releases/latest"); 40 | var response = await Utils.NoRedirectHttpClient.SendAsync(request); 41 | 42 | if (response.StatusCode is not (HttpStatusCode.Redirect or HttpStatusCode.Found)) 43 | { 44 | return null; 45 | } 46 | 47 | var location = response.Headers.Location?.ToString(); 48 | if (location == null) 49 | { 50 | return null; 51 | } 52 | 53 | var tagName = location.Split('/').LastOrDefault(); 54 | if (tagName == null) 55 | { 56 | return null; 57 | } 58 | 59 | var versionString = tagName.Split('@').LastOrDefault(); 60 | 61 | return versionString != null && Version.TryParse(versionString, out var version) ? version : null; 62 | } 63 | } -------------------------------------------------------------------------------- /YandexMusicPatcherGui.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | WinExe 4 | net10.0 5 | win-x64 6 | enable 7 | AnyCPU 8 | False 9 | 3.3.0 10 | 3.3.0 11 | true 12 | https://github.com/DarkPlayOff/YandexMusicExtMod 13 | Установщик мода для Яндекс Музыки 14 | true 15 | Assets\icon.ico 16 | true 17 | enable 18 | 19 | 20 | 21 | portable 22 | 23 | 24 | 25 | none 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | %(Filename) 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Services/LinuxService.cs: -------------------------------------------------------------------------------- 1 | namespace YandexMusicPatcherGui.Services; 2 | 3 | public class LinuxService : BasePlatformService 4 | { 5 | private const string AppInstallPath = "/opt/Яндекс Музыка"; 6 | private const string AppExecutableName = "Яндекс Музыка"; 7 | private const string DebName = "Yandex_Music.deb"; 8 | 9 | public override string GetModPath() 10 | { 11 | var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); 12 | return Path.Combine(home, ".local", "share", "YandexMusicPatcherGui"); 13 | } 14 | 15 | public override string GetAsarPath() 16 | { 17 | return Path.Combine(AppInstallPath, "resources", "app.asar"); 18 | } 19 | 20 | public override async Task DownloadLatestMusic(string tempFolder) 21 | { 22 | const string latestUrl = "https://music-desktop-application.s3.yandex.net/stable/Yandex_Music.deb"; 23 | var debPath = Path.Combine(tempFolder, DebName); 24 | 25 | await Patcher.DownloadFileWithProgress(latestUrl, debPath, "Загрузка клиента"); 26 | 27 | Patcher.ReportProgress(50, "Установка .deb пакета..."); 28 | var installCommand = $"apt-get install --allow-downgrades -y ./{Path.GetFileName(debPath)}"; 29 | await Patcher.RunProcess("/bin/bash", $"-c \"{installCommand.Replace("\"", "\\\"")}\"", "Установка .deb пакета...", tempFolder); 30 | Patcher.ReportProgress(100, "Пакет установлен"); 31 | } 32 | 33 | public override Task<(bool, string)> IsSupported() 34 | { 35 | if (Environment.UserName != "root") 36 | { 37 | return Task.FromResult((false, "Для установки требуются права root.")); 38 | } 39 | return Task.FromResult((true, string.Empty)); 40 | } 41 | 42 | public override Task CreateDesktopShortcut(string linkName, string path) 43 | { 44 | return Task.CompletedTask; 45 | } 46 | 47 | public override void RunApplication() 48 | { 49 | Patcher.RunProcess(GetApplicationExecutablePath(), "", "запуска приложения").GetAwaiter().GetResult(); 50 | } 51 | public override string GetApplicationExecutablePath() 52 | { 53 | return Path.Combine(AppInstallPath, AppExecutableName); 54 | } 55 | 56 | public override Task InstallModUnpacked(string archivePath, string tempFolder) 57 | { 58 | return Task.CompletedTask; 59 | } 60 | } -------------------------------------------------------------------------------- /Services/WindowsService.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Runtime.Versioning; 3 | using WindowsShortcutFactory; 4 | 5 | namespace YandexMusicPatcherGui.Services; 6 | 7 | public class WindowsService : BasePlatformService 8 | { 9 | private const string YandexMusicAppName = "YandexMusic"; 10 | private const string YandexMusicExeName = "Яндекс Музыка.exe"; 11 | private const string ResourcesDirName = "resources"; 12 | private const string AppAsarName = "app.asar"; 13 | private const string AppAsarUnpackedName = "app.asar.unpacked"; 14 | private const string ProgramsDirName = "Programs"; 15 | 16 | public override string GetModPath() 17 | { 18 | return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ProgramsDirName, YandexMusicAppName); 19 | } 20 | 21 | public override string GetAsarPath() 22 | { 23 | return Path.Combine(GetResourcesPath(), AppAsarName); 24 | } 25 | 26 | public override async Task DownloadLatestMusic(string tempFolder) 27 | { 28 | const string latestUrl = "https://music-desktop-application.s3.yandex.net/stable/Yandex_Music.exe"; 29 | var stableExePath = Path.Combine(tempFolder, "stable.exe"); 30 | await Patcher.DownloadFileWithProgress(latestUrl, stableExePath, "Загрузка клиента"); 31 | 32 | Patcher.ReportProgress(100, "Распаковка..."); 33 | await Patcher.ExtractArchive(stableExePath, GetModPath(), tempFolder, "распаковки архива"); 34 | Patcher.ReportProgress(100, "Распаковка завершена"); 35 | } 36 | 37 | public override Task<(bool, string)> IsSupported() 38 | { 39 | return Task.FromResult((true, string.Empty)); 40 | } 41 | 42 | public override Task CreateDesktopShortcut(string linkName, string path) 43 | { 44 | #if WINDOWS 45 | return Task.Run(() => CreateShortcut(linkName, path)); 46 | #else 47 | return Task.CompletedTask; 48 | #endif 49 | } 50 | 51 | [SupportedOSPlatform("windows")] 52 | private static void CreateShortcut(string linkName, string path) 53 | { 54 | var desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); 55 | var shortcutPath = Path.Combine(desktopPath, $"{linkName}.lnk"); 56 | var targetDirectory = Path.GetDirectoryName(path) ?? ""; 57 | 58 | using var shortcut = new WindowsShortcut 59 | { 60 | Path = path, 61 | WorkingDirectory = targetDirectory 62 | }; 63 | shortcut.Save(shortcutPath); 64 | } 65 | 66 | public override void RunApplication() 67 | { 68 | Process.Start(new ProcessStartInfo 69 | { 70 | FileName = GetApplicationExecutablePath(), 71 | UseShellExecute = true 72 | }); 73 | } 74 | 75 | public override string GetApplicationExecutablePath() 76 | { 77 | return Path.Combine(GetModPath(), YandexMusicExeName); 78 | } 79 | public override Task InstallMod(string archivePath, string tempFolder) 80 | { 81 | return Patcher.ExtractArchive(archivePath, GetResourcesPath(), tempFolder, "распаковки asar.zst"); 82 | } 83 | 84 | public override Task InstallModUnpacked(string archivePath, string tempFolder) 85 | { 86 | var unpackedAsarPath = Path.Combine(GetResourcesPath(), AppAsarUnpackedName); 87 | 88 | if (Directory.Exists(unpackedAsarPath)) 89 | { 90 | Directory.Delete(unpackedAsarPath, true); 91 | } 92 | 93 | return Patcher.ExtractArchive(archivePath, unpackedAsarPath, tempFolder, "распаковки app.asar.unpacked"); 94 | } 95 | 96 | private string GetResourcesPath() 97 | { 98 | return Path.Combine(GetModPath(), ResourcesDirName); 99 | } 100 | } -------------------------------------------------------------------------------- /Services/MacOsService.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Runtime.Versioning; 3 | 4 | namespace YandexMusicPatcherGui.Services; 5 | 6 | public class MacOsService : BasePlatformService 7 | { 8 | private const string YandexMusicAppName = "Яндекс Музыка.app"; 9 | private const string DmgName = "Yandex_Music.dmg"; 10 | 11 | public override string GetModPath() 12 | { 13 | return Path.Combine("/Applications", YandexMusicAppName); 14 | } 15 | 16 | public override string GetAsarPath() 17 | { 18 | return Path.Combine(GetModPath(), "Contents", "Resources", "app.asar"); 19 | } 20 | 21 | public override async Task DownloadLatestMusic(string tempFolder) 22 | { 23 | const string latestUrl = "https://music-desktop-application.s3.yandex.net/stable/Yandex_Music.dmg"; 24 | var dmgPath = Path.Combine(tempFolder, DmgName); 25 | var mountPath = Path.Combine(tempFolder, "mount"); 26 | 27 | Directory.CreateDirectory(mountPath); 28 | 29 | await Patcher.DownloadFileWithProgress(latestUrl, dmgPath, "Загрузка клиента"); 30 | 31 | Patcher.ReportProgress(100, "Распаковка DMG..."); 32 | var attachResult = await Patcher.RunProcess("hdiutil", $"attach -mountpoint \"{mountPath}\" \"{dmgPath}\"", "Монтирования DMG..."); 33 | if (attachResult.ExitCode != 0) 34 | { 35 | throw new Exception($"Ошибка монтирования DMG: {attachResult.Error}"); 36 | } 37 | 38 | var appPath = Path.Combine(mountPath, YandexMusicAppName); 39 | var targetAppPath = GetModPath(); 40 | 41 | if (Directory.Exists(targetAppPath)) 42 | { 43 | Directory.Delete(targetAppPath, true); 44 | } 45 | 46 | var copyResult = await Patcher.RunProcess("cp", $"-R \"{appPath}\" \"/Applications/\"", "Копирования приложения..."); 47 | if (copyResult.ExitCode != 0) 48 | { 49 | throw new Exception($"Ошибка копирования приложения: {copyResult.Error}"); 50 | } 51 | 52 | var detachResult = await Patcher.RunProcess("hdiutil", $"detach \"{mountPath}\"", "Размонтирование DMG..."); 53 | if (detachResult.ExitCode != 0) 54 | { 55 | throw new Exception($"Ошибка размонтирования DMG: {detachResult.Error}"); 56 | } 57 | 58 | Patcher.ReportProgress(100, "Распаковка завершена."); 59 | } 60 | 61 | public override async Task<(bool, string)> IsSupported() 62 | { 63 | #if MACOS 64 | if (await IsSipEnabled()) 65 | { 66 | return (false, "Отключите SIP для установки."); 67 | } 68 | #endif 69 | return (true, string.Empty); 70 | } 71 | 72 | [SupportedOSPlatform("macos")] 73 | private static async Task IsSipEnabled() 74 | { 75 | try 76 | { 77 | var process = new Process 78 | { 79 | StartInfo = new ProcessStartInfo 80 | { 81 | FileName = "csrutil", 82 | Arguments = "status", 83 | RedirectStandardOutput = true, 84 | UseShellExecute = false, 85 | CreateNoWindow = true 86 | } 87 | }; 88 | process.Start(); 89 | var output = await process.StandardOutput.ReadToEndAsync(); 90 | await process.WaitForExitAsync(); 91 | return output.Contains("System Integrity Protection status: enabled."); 92 | } 93 | catch (Exception e) 94 | { 95 | Console.WriteLine($"Error checking SIP status: {e.Message}"); 96 | return true; 97 | } 98 | } 99 | 100 | public override Task CreateDesktopShortcut(string linkName, string path) 101 | { 102 | return Task.CompletedTask; 103 | } 104 | 105 | public override void RunApplication() 106 | { 107 | Process.Start("open", $"-a \"{YandexMusicAppName}\""); 108 | } 109 | public override string GetApplicationExecutablePath() 110 | { 111 | return Path.Combine(GetModPath(), YandexMusicAppName); 112 | } 113 | 114 | public override Task InstallModUnpacked(string archivePath, string tempFolder) 115 | { 116 | return Task.CompletedTask; 117 | } 118 | 119 | 120 | } -------------------------------------------------------------------------------- /AppMain.axaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | 30 | 33 | 36 | 39 | 40 | 44 | 45 | 51 | 52 | 55 | 56 | 63 | 64 | 72 | 73 | 91 | 92 | 95 | 96 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /MainViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Avalonia.Threading; 3 | using CommunityToolkit.Mvvm.ComponentModel; 4 | using CommunityToolkit.Mvvm.Input; 5 | using YandexMusicPatcherGui.Services; 6 | 7 | namespace YandexMusicPatcherGui; 8 | 9 | public partial class MainViewModel : ObservableObject 10 | { 11 | private readonly PatcherService _patcherService = new(); 12 | 13 | [ObservableProperty] 14 | private string? _statusText; 15 | 16 | [ObservableProperty] 17 | private double _progressValue; 18 | 19 | [ObservableProperty] 20 | private bool _isProgressIndeterminate; 21 | 22 | [ObservableProperty] 23 | private bool _isProgressBarVisible; 24 | 25 | [ObservableProperty] 26 | private bool _isPatchButtonEnabled = true; 27 | 28 | [ObservableProperty] 29 | private bool _isRunButtonEnabled; 30 | 31 | [ObservableProperty] 32 | private bool _isRunButtonVisible = !OperatingSystem.IsLinux(); 33 | 34 | [ObservableProperty] 35 | private bool _isUpdateButtonVisible; 36 | 37 | [ObservableProperty] 38 | private bool _areButtonsVisible = true; 39 | 40 | [ObservableProperty] 41 | private string? _versionText; 42 | 43 | [ObservableProperty] 44 | private bool _isVersionTextVisible; 45 | 46 | [ObservableProperty] 47 | private string? _patchButtonContent = "Установить мод"; 48 | 49 | public MainViewModel() 50 | { 51 | Patcher.OnDownloadProgress += OnPatcherOnDownloadProgress; 52 | Dispatcher.UIThread.InvokeAsync(Initialize); 53 | } 54 | 55 | private void OnPatcherOnDownloadProgress(object? sender, (int Progress, string Status) e) 56 | { 57 | Dispatcher.UIThread.InvokeAsync(() => 58 | { 59 | if (e.Progress < 0) 60 | { 61 | IsProgressIndeterminate = true; 62 | } 63 | else 64 | { 65 | IsProgressIndeterminate = false; 66 | ProgressValue = e.Progress; 67 | } 68 | StatusText = e.Status; 69 | IsProgressBarVisible = true; 70 | }); 71 | } 72 | 73 | private async Task Initialize() 74 | { 75 | try 76 | { 77 | var (isSupported, message) = await Program.PlatformService.IsSupported(); 78 | if (!isSupported) 79 | { 80 | IsPatchButtonEnabled = false; 81 | StatusText = message; 82 | AreButtonsVisible = false; 83 | IsProgressBarVisible = true; 84 | return; 85 | } 86 | 87 | await CheckForUpdates(); 88 | await UpdateVersionInfo(); 89 | } 90 | catch (Exception ex) 91 | { 92 | StatusText = "Ошибка инициализации: " + ex.Message; 93 | AreButtonsVisible = false; 94 | IsProgressBarVisible = true; 95 | } 96 | } 97 | 98 | private async Task CheckForUpdates() 99 | { 100 | var githubVersion = await Update.GetLatestAppVersion(); 101 | var hasUpdate = false; 102 | if (githubVersion != null && Version.TryParse(Program.Version, out var currentVersion)) 103 | { 104 | hasUpdate = githubVersion > currentVersion; 105 | } 106 | IsUpdateButtonVisible = hasUpdate; 107 | } 108 | 109 | [RelayCommand] 110 | public async Task Patch() 111 | { 112 | AreButtonsVisible = false; 113 | IsProgressBarVisible = true; 114 | IsPatchButtonEnabled = false; 115 | 116 | try 117 | { 118 | var progress = new Progress<(int, string)>(e => OnPatcherOnDownloadProgress(this, e)); 119 | await _patcherService.Patch(progress); 120 | 121 | await UpdateVersionInfo(); 122 | StatusText = "Установка завершена!"; 123 | AreButtonsVisible = true; 124 | IsPatchButtonEnabled = true; 125 | } 126 | catch (Exception ex) 127 | { 128 | StatusText = $"Ошибка: {ex.Message}"; 129 | ProgressValue = 0; 130 | IsProgressIndeterminate = false; 131 | Trace.TraceError("Ошибка патча: {0}", ex.ToString()); 132 | } 133 | finally 134 | { 135 | Patcher.CleanupTempFiles(); 136 | } 137 | } 138 | 139 | [RelayCommand] 140 | public void Run() 141 | { 142 | if (!IsRunButtonEnabled) return; 143 | 144 | try 145 | { 146 | Program.PlatformService.RunApplication(); 147 | } 148 | catch (Exception ex) 149 | { 150 | Console.WriteLine($"Ошибка запуска Яндекс Музыки: {ex}"); 151 | } 152 | 153 | Environment.Exit(0); 154 | } 155 | 156 | [RelayCommand] 157 | public void OpenUpdateUrl() 158 | { 159 | OpenUrlSafely(Program.RepoUrl + "/releases/latest"); 160 | } 161 | 162 | private async Task UpdateVersionInfo() 163 | { 164 | var installedVersion = Update.GetInstalledVersion(); 165 | var latestVersion = await Update.GetLatestModVersion(); 166 | 167 | IsRunButtonEnabled = installedVersion != null; 168 | IsVersionTextVisible = installedVersion != null; 169 | 170 | if (installedVersion == null) 171 | { 172 | PatchButtonContent = "Установить мод"; 173 | return; 174 | } 175 | 176 | VersionText = $"Версия мода: {installedVersion}"; 177 | 178 | if (latestVersion != null && installedVersion < latestVersion) 179 | { 180 | VersionText = $"Доступно обновление мода: {installedVersion} -> {latestVersion}"; 181 | PatchButtonContent = "Обновить мод"; 182 | } 183 | else 184 | { 185 | PatchButtonContent = "Переустановить"; 186 | } 187 | } 188 | 189 | private static void OpenUrlSafely(string url) 190 | { 191 | try 192 | { 193 | Process.Start(new ProcessStartInfo 194 | { 195 | FileName = url, 196 | UseShellExecute = true 197 | }); 198 | } 199 | catch (Exception ex) 200 | { 201 | Console.WriteLine($"Не удалось открыть ссылку {url}: {ex}"); 202 | } 203 | } 204 | 205 | } -------------------------------------------------------------------------------- /Main.axaml: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 52 | 55 | 61 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |