From d353ce7f4b134c42fc71f467eca504c750c73caa Mon Sep 17 00:00:00 2001 From: Assistant Date: Tue, 19 May 2020 11:47:45 -0600 Subject: [PATCH 1/2] Big commits will make me hate myself eventually but I'll hate myself anyways so lets go --- ModAssistant/App.xaml.cs | 7 +- .../Classes/External Interfaces/BeatSaver.cs | 200 ++++++++++++------ .../Classes/External Interfaces/Playlists.cs | 5 +- .../Classes/External Interfaces/Utils.cs | 26 ++- ModAssistant/Classes/OneClickInstaller.cs | 4 + ModAssistant/Localisation/en-DEBUG.xaml | 4 + ModAssistant/Localisation/en.xaml | 4 + ModAssistant/ModAssistant.csproj | 7 + ModAssistant/OneClickStatus.xaml | 98 +++++++++ ModAssistant/OneClickStatus.xaml.cs | 64 ++++++ ModAssistant/Pages/Options.xaml.cs | 2 +- 11 files changed, 338 insertions(+), 83 deletions(-) create mode 100644 ModAssistant/OneClickStatus.xaml create mode 100644 ModAssistant/OneClickStatus.xaml.cs diff --git a/ModAssistant/App.xaml.cs b/ModAssistant/App.xaml.cs index 4bed457..14cf15a 100644 --- a/ModAssistant/App.xaml.cs +++ b/ModAssistant/App.xaml.cs @@ -23,6 +23,7 @@ namespace ModAssistant public static bool ReinstallInstalledMods; public static string Version = Assembly.GetExecutingAssembly().GetName().Version.ToString(); public static List SavedMods = ModAssistant.Properties.Settings.Default.SavedMods.Split(',').ToList(); + public static MainWindow window; public static bool Update = true; public static bool GUI = true; @@ -82,12 +83,12 @@ namespace ModAssistant if (GUI) { - MainWindow window = new MainWindow(); + window = new MainWindow(); window.Show(); } else { - Application.Current.Shutdown(); + //Application.Current.Shutdown(); } } @@ -107,8 +108,6 @@ namespace ModAssistant await OneClickInstaller.InstallAsset(args[1]); } - Current.Shutdown(); - Update = false; GUI = false; args = Shift(args, 2); diff --git a/ModAssistant/Classes/External Interfaces/BeatSaver.cs b/ModAssistant/Classes/External Interfaces/BeatSaver.cs index 8022bae..41fad31 100644 --- a/ModAssistant/Classes/External Interfaces/BeatSaver.cs +++ b/ModAssistant/Classes/External Interfaces/BeatSaver.cs @@ -4,6 +4,7 @@ using System.IO; using System.IO.Compression; using System.Net; using System.Net.Http.Headers; +using System.Runtime.InteropServices; using System.Threading.Tasks; using System.Windows; using static ModAssistant.Http; @@ -18,11 +19,13 @@ namespace ModAssistant.API public static async Task GetFromKey(string Key, bool showNotification = true) { + if (showNotification) OneClickInstaller.Status.Show(); return await GetMap(Key, "key", showNotification); } public static async Task GetFromHash(string Hash, bool showNotification = true) { + if (showNotification) OneClickInstaller.Status.Show(); return await GetMap(Hash, "hash", showNotification); } @@ -43,24 +46,34 @@ namespace ModAssistant.API BeatSaverMap map = new BeatSaverMap(); map.Success = false; + if (showNotification) Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:Installing"), id)}"); try { BeatSaverApiResponse beatsaver = await GetResponse(BeatSaverURLPrefix + urlSegment + id); if (beatsaver != null && beatsaver.map != null) { + map.response = beatsaver; map.Name = await InstallMap(beatsaver.map, showNotification); map.Success = true; } } catch (Exception e) { - ModAssistant.Utils.Log($"Failed downloading BeatSaver map: {id} | Error: {e}", "ERROR"); + ModAssistant.Utils.Log($"Failed downloading BeatSaver map: {id} | Error: {e.Message}", "ERROR"); + Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:Failed"), (map.Name ?? id))}"); } return map; } - private static async Task GetResponse(string url, bool showNotification = true) + private static async Task GetResponse(string url, bool showNotification = true, int retries = 3) { + if (retries == 0) + { + ModAssistant.Utils.Log($"Max tries reached: Skipping {url}", "ERROR"); + Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:RatelimitSkip"), url)}"); + throw new Exception("Max retries allowed"); + } + BeatSaverApiResponse response = new BeatSaverApiResponse(); try { @@ -69,22 +82,21 @@ namespace ModAssistant.API response.ratelimit = GetRatelimit(resp.Headers); string body = await resp.Content.ReadAsStringAsync(); + if ((int)resp.StatusCode == 429) + { + Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:RatelimitHit"), response.ratelimit.ResetTime)}"); + await response.ratelimit.Wait(); + return await GetResponse(url, showNotification, retries - 1); + } + if (response.statusCode == HttpStatusCode.OK) { - if (response.ratelimit.IsSafe) - { - response.map = JsonSerializer.Deserialize(body); - return response; - } - else - { - ModAssistant.Utils.Log($"Ratelimit: ({response.ratelimit.Remaining}/{response.ratelimit.Total}) {response.ratelimit.ResetTime}"); - return response; - } + response.map = JsonSerializer.Deserialize(body); + return response; } else { - ModAssistant.Utils.Log($"Ratelimit: [{response.statusCode}]({response.ratelimit.Remaining}/{response.ratelimit.Total}) {response.ratelimit.ResetTime} \n{body}", "ERROR"); + Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:Failed"), url)}"); return response; } } @@ -98,9 +110,96 @@ namespace ModAssistant.API } } - private static BeatSaverRatelimit GetRatelimit(HttpResponseHeaders headers) + [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)] + static extern int memcmp(byte[] b1, byte[] b2, long count); + + public static async Task InstallMap(BeatSaverApiResponseMap Map, bool showNotification = true) { - BeatSaverRatelimit ratelimit = new BeatSaverRatelimit(); + string zip = Path.Combine(Utils.BeatSaberPath, CustomSongsFolder, Map.hash) + ".zip"; + string mapName = string.Concat(($"{Map.key} ({Map.metadata.songName} - {Map.metadata.levelAuthorName})") + .Split(ModAssistant.Utils.Constants.IllegalCharacters)); + string directory = Path.Combine(Utils.BeatSaberPath, CustomSongsFolder, mapName); + +#pragma warning disable CS0162 // Unreachable code detected + if (BypassDownloadCounter) + { + await Utils.DownloadAsset(BeatSaverURLPrefix + Map.directDownload, CustomSongsFolder, Map.hash + ".zip", mapName, showNotification, true); + } + else + { + await Utils.DownloadAsset(BeatSaverURLPrefix + Map.downloadURL, CustomSongsFolder, Map.hash + ".zip", mapName, showNotification, true); + } +#pragma warning restore CS0162 // Unreachable code detected + + if (File.Exists(zip)) + { + byte[] zipMagicNumber = { 80, 75, 3, 4 }; + byte[] magicNumber = new byte[4]; + + try + { + using (FileStream fs = new FileStream(zip, FileMode.Open, FileAccess.Read)) + { + fs.Read(magicNumber, 0, magicNumber.Length); + fs.Close(); + } + } + catch + { + return null; + } + + if (!(magicNumber.Length == zipMagicNumber.Length && memcmp(magicNumber, zipMagicNumber, magicNumber.Length) == 0)) + { + ModAssistant.Utils.Log($"Failed extracting BeatSaver map: {zip} \n| Content: {string.Join("\n", File.ReadAllLines(zip))}", "ERROR"); + throw new Exception("File not a zip."); + } + + try + { + using (FileStream stream = new FileStream(zip, FileMode.Open)) + using (ZipArchive archive = new ZipArchive(stream)) + { + foreach (ZipArchiveEntry file in archive.Entries) + { + string fileDirectory = Path.GetDirectoryName(Path.Combine(directory, file.FullName)); + if (!Directory.Exists(fileDirectory)) + { + Directory.CreateDirectory(fileDirectory); + } + + if (!string.IsNullOrEmpty(file.Name)) + { + file.ExtractToFile(Path.Combine(directory, file.FullName), true); + } + } + } + } + catch (Exception e) + { + File.Delete(zip); + ModAssistant.Utils.Log($"Failed extracting BeatSaver map: {zip} | Error: {e} \n| Content: {string.Join("\n", File.ReadAllLines(zip))}", "ERROR"); + throw new Exception("File extraction failed."); + } + File.Delete(zip); + } + else + { + if (showNotification) + { + string line1 = (string)Application.Current.FindResource("OneClick:SongDownload:Failed"); + string line2 = (string)Application.Current.FindResource("OneClick:SongDownload:NetworkIssues"); + string title = (string)Application.Current.FindResource("OneClick:SongDownload:FailedTitle"); + MessageBox.Show($"{line1}\n{line2}", title); + } + throw new Exception("Zip file not found."); + } + return mapName; + } + + public static BeatSaver.BeatSaverRatelimit GetRatelimit(HttpResponseHeaders headers) + { + BeatSaver.BeatSaverRatelimit ratelimit = new BeatSaver.BeatSaverRatelimit(); if (headers.TryGetValues("Rate-Limit-Remaining", out IEnumerable _remaining)) @@ -138,58 +237,30 @@ namespace ModAssistant.API return new DateTime(unixStart.Ticks + unixTimeStampInTicks, System.DateTimeKind.Utc); } - public static async Task InstallMap(BeatSaverApiResponseMap Map, bool showNotification = true) + public static async Task Download(string url, string output, int retries = 3) { - string zip = Path.Combine(Utils.BeatSaberPath, CustomSongsFolder, Map.hash) + ".zip"; - string mapName = string.Concat(($"{Map.key} ({Map.metadata.songName} - {Map.metadata.levelAuthorName})") - .Split(ModAssistant.Utils.Constants.IllegalCharacters)); - string directory = Path.Combine(Utils.BeatSaberPath, CustomSongsFolder, mapName); - -#pragma warning disable CS0162 // Unreachable code detected - if (BypassDownloadCounter) + if (retries == 0) { - await Utils.DownloadAsset(BeatSaverURLPrefix + Map.directDownload, CustomSongsFolder, Map.hash + ".zip", mapName, showNotification); + ModAssistant.Utils.Log($"Max tries reached: Couldn't download {url}", "ERROR"); + throw new Exception("Max retries allowed"); } - else - { - await Utils.DownloadAsset(BeatSaverURLPrefix + Map.downloadURL, CustomSongsFolder, Map.hash + ".zip", mapName, showNotification); - } -#pragma warning restore CS0162 // Unreachable code detected - if (File.Exists(zip)) - { - using (FileStream stream = new FileStream(zip, FileMode.Open)) - using (ZipArchive archive = new ZipArchive(stream)) - { - foreach (ZipArchiveEntry file in archive.Entries) - { - string fileDirectory = Path.GetDirectoryName(Path.Combine(directory, file.FullName)); - if (!Directory.Exists(fileDirectory)) - { - Directory.CreateDirectory(fileDirectory); - } + var resp = await HttpClient.GetAsync(url); - if (!string.IsNullOrEmpty(file.Name)) - { - file.ExtractToFile(Path.Combine(directory, file.FullName), true); - } - } - } - - File.Delete(zip); - } - else + if ((int)resp.StatusCode == 429) { - if (showNotification) - { - string line1 = (string)Application.Current.FindResource("OneClick:SongDownload:Failed"); - string line2 = (string)Application.Current.FindResource("OneClick:SongDownload:NetworkIssues"); - string title = (string)Application.Current.FindResource("OneClick:SongDownload:FailedTitle"); - MessageBox.Show($"{line1}\n{line2}", title); - } - return null; + var ratelimit = new BeatSaver.BeatSaverRatelimit(); + ratelimit = GetRatelimit(resp.Headers); + Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:RatelimitHit"), ratelimit.ResetTime)}"); + await ratelimit.Wait(); + await Download(url, output, retries - 1); + } + + using (var stream = await resp.Content.ReadAsStreamAsync()) + using (var fs = new FileStream(output, FileMode.OpenOrCreate, FileAccess.Write)) + { + await stream.CopyToAsync(fs); } - return mapName; } public class BeatSaverMap @@ -212,15 +283,6 @@ namespace ModAssistant.API public int? Total { get; set; } public int? Reset { get; set; } public DateTime ResetTime { get; set; } - public bool IsSafe - { - get - { - if (Remaining > 3) return true; - else return false; - } - } - public async Task Wait() { await Task.Delay(new TimeSpan(ResetTime.Ticks - DateTime.Now.Ticks)); diff --git a/ModAssistant/Classes/External Interfaces/Playlists.cs b/ModAssistant/Classes/External Interfaces/Playlists.cs index 17f74a7..ce8476c 100644 --- a/ModAssistant/Classes/External Interfaces/Playlists.cs +++ b/ModAssistant/Classes/External Interfaces/Playlists.cs @@ -91,10 +91,7 @@ namespace ModAssistant.API } } } - if (gui) - { - MainWindow.Instance.MainText = $"{string.Format((string)Application.Current.FindResource("Options:FinishedPlaylist"), Errors, playlist.playlistTitle)}"; - } + Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("Options:FinishedPlaylist"), Errors, playlist.playlistTitle)}"); } private static string TextProgress(int min, int max, int value) diff --git a/ModAssistant/Classes/External Interfaces/Utils.cs b/ModAssistant/Classes/External Interfaces/Utils.cs index 4588a47..604cef4 100644 --- a/ModAssistant/Classes/External Interfaces/Utils.cs +++ b/ModAssistant/Classes/External Interfaces/Utils.cs @@ -1,9 +1,12 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; +using System.Net.Http.Headers; using System.Threading.Tasks; using System.Windows; +using static ModAssistant.Http; namespace ModAssistant.API { @@ -11,6 +14,18 @@ namespace ModAssistant.API { public static readonly string BeatSaberPath = App.BeatSaberInstallDirectory; + public static void SetMessage(string message) + { + if (App.window == null) + { + OneClickStatus.Instance.MainText = message; + } + else + { + MainWindow.Instance.MainText = message; + } + } + public static async Task DownloadAsset(string link, string folder, bool showNotifcation, string fileName = null) { await DownloadAsset(link, folder, fileName, null, showNotifcation); @@ -21,7 +36,7 @@ namespace ModAssistant.API await DownloadAsset(link, folder, fileName, displayName, true); } - public static async Task DownloadAsset(string link, string folder, string fileName, string displayName, bool showNotification) + public static async Task DownloadAsset(string link, string folder, string fileName, string displayName, bool showNotification, bool beatsaver = false) { if (string.IsNullOrEmpty(BeatSaberPath)) { @@ -43,17 +58,18 @@ namespace ModAssistant.API displayName = Path.GetFileNameWithoutExtension(fileName); } - await ModAssistant.Utils.Download(link, fileName); + if (beatsaver) await BeatSaver.Download(link, fileName); + else await ModAssistant.Utils.Download(link, fileName); + if (showNotification) { - ModAssistant.Utils.SendNotify(string.Format((string)Application.Current.FindResource("OneClick:InstalledAsset"), displayName)); + SetMessage(string.Format((string)Application.Current.FindResource("OneClick:InstalledAsset"), displayName)); } } catch { - ModAssistant.Utils.SendNotify((string)Application.Current.FindResource("OneClick:AssetInstallFailed")); + SetMessage((string)Application.Current.FindResource("OneClick:AssetInstallFailed")); } } - } } diff --git a/ModAssistant/Classes/OneClickInstaller.cs b/ModAssistant/Classes/OneClickInstaller.cs index 9d0206d..bcdb0a5 100644 --- a/ModAssistant/Classes/OneClickInstaller.cs +++ b/ModAssistant/Classes/OneClickInstaller.cs @@ -9,6 +9,7 @@ namespace ModAssistant class OneClickInstaller { private static readonly string[] Protocols = new[] { "modelsaber", "beatsaver", "bsplaylist" }; + public static OneClickStatus Status = new OneClickStatus(); public static async Task InstallAsset(string link) { @@ -37,11 +38,14 @@ namespace ModAssistant private static async Task ModelSaber(Uri uri) { + Status.Show(); + API.Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("OneClick:Installing"), System.Web.HttpUtility.UrlDecode(uri.Segments.Last()))}"); await API.ModelSaber.GetModel(uri); } private static async Task Playlist(Uri uri) { + Status.Show(); await API.Playlists.DownloadAll(uri); } diff --git a/ModAssistant/Localisation/en-DEBUG.xaml b/ModAssistant/Localisation/en-DEBUG.xaml index 1cc0be3..8562968 100644 --- a/ModAssistant/Localisation/en-DEBUG.xaml +++ b/ModAssistant/Localisation/en-DEBUG.xaml @@ -157,6 +157,10 @@ OneClick:AssetInstallFailed {0} OneClick:ProtocolHandler:Registered {0} OneClick:ProtocolHandler:Unregistered + {0} OneClick:Installing + {0} OneClick:RatelimitSkip + {0} OneClick:RatelimitHit + {0} OneClick:Failed Themes:ThemeNotFound diff --git a/ModAssistant/Localisation/en.xaml b/ModAssistant/Localisation/en.xaml index 4838fec..08f4332 100644 --- a/ModAssistant/Localisation/en.xaml +++ b/ModAssistant/Localisation/en.xaml @@ -216,6 +216,10 @@ Failed to install. {0} OneClick™ Install handlers registered! {0} OneClick™ Install handlers unregistered! + Installing: {0} + Max tries reached: Skipping {0} + Ratelimit hit. Resuming in {0} + Download failed: {0} Theme not found, reverting to default theme... diff --git a/ModAssistant/ModAssistant.csproj b/ModAssistant/ModAssistant.csproj index 87d58f4..9291398 100644 --- a/ModAssistant/ModAssistant.csproj +++ b/ModAssistant/ModAssistant.csproj @@ -78,6 +78,9 @@ + + OneClickStatus.xaml + Intro.xaml @@ -131,6 +134,10 @@ MSBuild:Compile Designer + + Designer + MSBuild:Compile + Designer MSBuild:Compile diff --git a/ModAssistant/OneClickStatus.xaml b/ModAssistant/OneClickStatus.xaml new file mode 100644 index 0000000..74d0bcd --- /dev/null +++ b/ModAssistant/OneClickStatus.xaml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ModAssistant/OneClickStatus.xaml.cs b/ModAssistant/OneClickStatus.xaml.cs new file mode 100644 index 0000000..6b5d446 --- /dev/null +++ b/ModAssistant/OneClickStatus.xaml.cs @@ -0,0 +1,64 @@ +using System; +using System.Windows; +using System.Windows.Data; + +namespace ModAssistant +{ + /// + /// Interaction logic for OneClickStatus.xaml + /// + public partial class OneClickStatus : Window + { + public static OneClickStatus Instance; + + public string HistoryText + { + get + { + return HistoryTextBlock.Text; + } + set + { + Dispatcher.Invoke(new Action(() => { OneClickStatus.Instance.HistoryTextBlock.Text = value; })); + } + } + public string MainText + { + get + { + return HistoryTextBlock.Text; + } + set + { + Dispatcher.Invoke(new Action(() => { + OneClickStatus.Instance.HistoryTextBlock.Text = string.IsNullOrEmpty(MainText) ? $"{value}" : $"{value}\n{MainText}"; + })); + } + } + + public OneClickStatus() + { + InitializeComponent(); + Instance = this; + } + } + + [ValueConversion(typeof(double), typeof(double))] + public class DivideDoubleByTwoConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + if (targetType != typeof(double)) + { + throw new InvalidOperationException("The target must be a double"); + } + double d = (double)value; + return ((double)d) / 2; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} diff --git a/ModAssistant/Pages/Options.xaml.cs b/ModAssistant/Pages/Options.xaml.cs index 809dba4..4bc58d2 100644 --- a/ModAssistant/Pages/Options.xaml.cs +++ b/ModAssistant/Pages/Options.xaml.cs @@ -314,7 +314,7 @@ namespace ModAssistant.Pages string playlistFile = Utils.GetManualFile(); if (File.Exists(playlistFile)) { - Task.Run(() => { API.Playlists.DownloadFrom(playlistFile, true).Wait(); }); + Task.Run(() => { API.Playlists.DownloadFrom(playlistFile).Wait(); }); } } } From a2dd0f71b194794c70af9091d65a6045c8dc390b Mon Sep 17 00:00:00 2001 From: Assistant Date: Tue, 19 May 2020 11:48:25 -0600 Subject: [PATCH 2/2] why did this not commit what the fuck --- .../Classes/External Interfaces/Playlists.cs | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/ModAssistant/Classes/External Interfaces/Playlists.cs b/ModAssistant/Classes/External Interfaces/Playlists.cs index ce8476c..ab6b09a 100644 --- a/ModAssistant/Classes/External Interfaces/Playlists.cs +++ b/ModAssistant/Classes/External Interfaces/Playlists.cs @@ -41,7 +41,7 @@ namespace ModAssistant.API } } - public static async Task DownloadFrom(string file, bool gui = false, System.Windows.Controls.ProgressBar progress = null) + public static async Task DownloadFrom(string file) { if (Path.Combine(BeatSaberPath, PlaylistsFolder) != Path.GetDirectoryName(file)) { @@ -52,15 +52,9 @@ namespace ModAssistant.API int Errors = 0; int Minimum = 0; int Value = 0; - if (progress != null) - { - progress.Minimum = 0; - progress.Maximum = 1; - progress.Value = 0; - } + Playlist playlist = JsonSerializer.Deserialize(File.ReadAllText(file)); int Maximum = playlist.songs.Length; - if (progress != null) progress.Maximum = playlist.songs.Length; foreach (Playlist.Song song in playlist.songs) { @@ -74,21 +68,17 @@ namespace ModAssistant.API response = await BeatSaver.GetFromKey(song.key, false); } Value++; - if (progress != null) progress.Value++; - if (gui) + if (response.Success) { - if (response.Success) - { - MainWindow.Instance.MainText = $"{string.Format((string)Application.Current.FindResource("Options:InstallingPlaylist"), TextProgress(Minimum, Maximum, Value))}"; - } - else - { - MainWindow.Instance.MainText = $"{string.Format((string)Application.Current.FindResource("Options:FailedPlaylistSong"), song.songName)}"; - ModAssistant.Utils.Log($"Failed installing BeatSaver map: {song.songName} | {song.key} | {song.hash}"); - await Task.Delay(3 * 1000); - Errors++; - } + Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("Options:InstallingPlaylist"), TextProgress(Minimum, Maximum, Value))} {response.Name}"); + } + else + { + Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("Options:FailedPlaylistSong"), song.songName)}"); + ModAssistant.Utils.Log($"Failed installing BeatSaver map: {song.songName} | {song.key} | {song.hash} | ({response?.response?.ratelimit?.Remaining})"); + await Task.Delay(3 * 1000); + Errors++; } } Utils.SetMessage($"{string.Format((string)Application.Current.FindResource("Options:FinishedPlaylist"), Errors, playlist.playlistTitle)}");