From 6255ec224fbbe9d472633ea5e3acdfdaea6f98dc Mon Sep 17 00:00:00 2001 From: Timothy Baldridge Date: Wed, 5 Feb 2020 22:30:31 -0700 Subject: [PATCH] Allow manual Nexus downloads --- Wabbajack.Common/GameMetaData.cs | 3 ++ Wabbajack.Lib/AInstaller.cs | 2 +- Wabbajack.Lib/Downloaders/NexusDownloader.cs | 41 ++++++++--------- Wabbajack.Lib/LibCefHelpers/Init.cs | 10 ++++- Wabbajack.Lib/NexusApi/NexusApi.cs | 25 ++++++++++- .../ManuallyDownloadNexusFile.cs | 38 ++++++++++++++++ .../WebAutomation/CefSharpWrapper.cs | 29 +++++++++++- Wabbajack.Lib/WebAutomation/IWebDriver.cs | 1 + Wabbajack/View Models/MainWindowVM.cs | 5 ++- .../View Models/UserInterventionHandlers.cs | 44 +++++++++++++++---- 10 files changed, 164 insertions(+), 34 deletions(-) create mode 100644 Wabbajack.Lib/StatusMessages/ManuallyDownloadNexusFile.cs diff --git a/Wabbajack.Common/GameMetaData.cs b/Wabbajack.Common/GameMetaData.cs index c0e446bd..756c2ad1 100644 --- a/Wabbajack.Common/GameMetaData.cs +++ b/Wabbajack.Common/GameMetaData.cs @@ -60,6 +60,8 @@ namespace Wabbajack.Common public string MO2ArchiveName { get; internal set; } public Game Game { get; internal set; } public string NexusName { get; internal set; } + // Nexus DB id for the game, used in some specific situations + public long NexusGameId { get; internal set; } public string MO2Name { get; internal set; } public string GameLocationRegistryKey { get; internal set; } // to get steam ids: https://steamdb.info @@ -211,6 +213,7 @@ namespace Wabbajack.Common SupportedModManager = ModManager.MO2, Game = Game.SkyrimSpecialEdition, NexusName = "skyrimspecialedition", + NexusGameId = 1704, MO2Name = "Skyrim Special Edition", MO2ArchiveName = "skyrimse", GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\Skyrim Special Edition", diff --git a/Wabbajack.Lib/AInstaller.cs b/Wabbajack.Lib/AInstaller.cs index 918f553a..3ddebb13 100644 --- a/Wabbajack.Lib/AInstaller.cs +++ b/Wabbajack.Lib/AInstaller.cs @@ -236,7 +236,7 @@ namespace Wabbajack.Lib var dispatchers = missing.Select(m => m.State.GetDownloader()).Distinct(); await Task.WhenAll(dispatchers.Select(d => d.Prepare())); - + await DownloadMissingArchives(missing); } diff --git a/Wabbajack.Lib/Downloaders/NexusDownloader.cs b/Wabbajack.Lib/Downloaders/NexusDownloader.cs index ff0c71a1..60e8f0e7 100644 --- a/Wabbajack.Lib/Downloaders/NexusDownloader.cs +++ b/Wabbajack.Lib/Downloaders/NexusDownloader.cs @@ -17,7 +17,7 @@ namespace Wabbajack.Lib.Downloaders public class NexusDownloader : IDownloader, INeedsLogin { private bool _prepared; - private SemaphoreSlim _lock = new SemaphoreSlim(1); + private AsyncLock _lock = new AsyncLock(); private UserStatus _status; private NexusApiClient _client; @@ -94,32 +94,33 @@ namespace Wabbajack.Lib.Downloaders { if (!_prepared) { - await _lock.WaitAsync(); - try + using var _ = await _lock.Wait(); + // Could have become prepared while we waited for the lock + if (!_prepared) { - // Could have become prepared while we waited for the lock - if (!_prepared) + _client = await NexusApiClient.Get(); + _status = await _client.GetUserStatus(); + if (!_client.IsAuthenticated) { - _client = await NexusApiClient.Get(); - _status = await _client.GetUserStatus(); - if (!_client.IsAuthenticated) + Utils.ErrorThrow(new UnconvertedError( + $"Authenticating for the Nexus failed. A nexus account is required to automatically download mods.")); + return; + } + + + if (!await _client.IsPremium()) + { + var result = await Utils.Log(new YesNoIntervention( + "Wabbajack can operate without a premium account, but downloads will be slower and the install process will require more user interactions (you will have to start each download by hand). Are you sure you wish to continue?", + "Continue without Premium?")).Task; + if (result == ConfirmationIntervention.Choice.Abort) { - Utils.ErrorThrow(new UnconvertedError( - $"Authenticating for the Nexus failed. A nexus account is required to automatically download mods.")); - return; + Utils.ErrorThrow(new UnconvertedError($"Aborting at the request of the user")); } + _prepared = true; } } - finally - { - _lock.Release(); - } } - - _prepared = true; - - if (_status.is_premium) return; - Utils.ErrorThrow(new UnconvertedError($"Automated installs with Wabbajack requires a premium nexus account. {await _client.Username()} is not a premium account.")); } public class State : AbstractDownloadState diff --git a/Wabbajack.Lib/LibCefHelpers/Init.cs b/Wabbajack.Lib/LibCefHelpers/Init.cs index 0fdc681c..cef0c039 100644 --- a/Wabbajack.Lib/LibCefHelpers/Init.cs +++ b/Wabbajack.Lib/LibCefHelpers/Init.cs @@ -37,7 +37,7 @@ namespace Wabbajack.Lib.LibCefHelpers return container; } - public static async Task GetCookies(string domainEnding) + public static async Task GetCookies(string domainEnding = "") { var manager = Cef.GetGlobalCookieManager(); var visitor = new CookieVisitor(); @@ -85,8 +85,14 @@ namespace Wabbajack.Lib.LibCefHelpers public static void Init() { - // does nothing, but kicks off the static constructor + if (Inited) return; + Inited = true; + CefSettings settings = new CefSettings(); + settings.CachePath = Path.Combine(Directory.GetCurrentDirectory() + @"\CEF"); + Cef.Initialize(settings); } + + public static bool Inited { get; set; } } public static class ModuleInitializer diff --git a/Wabbajack.Lib/NexusApi/NexusApi.cs b/Wabbajack.Lib/NexusApi/NexusApi.cs index 51834da4..74245fc9 100644 --- a/Wabbajack.Lib/NexusApi/NexusApi.cs +++ b/Wabbajack.Lib/NexusApi/NexusApi.cs @@ -14,6 +14,7 @@ using Wabbajack.Lib.Downloaders; using WebSocketSharp; using static Wabbajack.Lib.NexusApi.NexusApiUtils; using System.Threading; +using Wabbajack.Lib.Exceptions; using Wabbajack.Lib.WebAutomation; namespace Wabbajack.Lib.NexusApi @@ -261,7 +262,24 @@ namespace Wabbajack.Lib.NexusApi ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; var url = $"https://api.nexusmods.com/v1/games/{ConvertGameName(archive.GameName)}/mods/{archive.ModID}/files/{archive.FileID}/download_link.json"; - return (await Get>(url)).First().URI; + try + { + return (await Get>(url)).First().URI; + } + catch (HttpRequestException) + { + } + + try + { + Utils.Log($"Requesting manual download for {archive.ModName}"); + return (await Utils.Log(await ManuallyDownloadNexusFile.Create(archive)).Task).ToString(); + } + catch (TaskCanceledException ex) + { + Utils.Error(ex, "Manual cancellation of download"); + throw; + } } public async Task GetFileInfo(NexusDownloader.State mod) @@ -328,5 +346,10 @@ namespace Wabbajack.Lib.NexusApi } set => _localCacheDir = value; } + + public static Uri ManualDownloadUrl(NexusDownloader.State state) + { + return new Uri($"https://www.nexusmods.com/{GameRegistry.GetByMO2ArchiveName(state.GameName).NexusName}/mods/{state.ModID}?tab=files"); + } } } diff --git a/Wabbajack.Lib/StatusMessages/ManuallyDownloadNexusFile.cs b/Wabbajack.Lib/StatusMessages/ManuallyDownloadNexusFile.cs new file mode 100644 index 00000000..e403ab73 --- /dev/null +++ b/Wabbajack.Lib/StatusMessages/ManuallyDownloadNexusFile.cs @@ -0,0 +1,38 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Wabbajack.Common; +using Wabbajack.Lib.Downloaders; + +namespace Wabbajack.Lib +{ + public class ManuallyDownloadNexusFile : AUserIntervention + { + public NexusDownloader.State State { get; } + public override string ShortDescription { get; } + public override string ExtendedDescription { get; } + + private TaskCompletionSource _tcs = new TaskCompletionSource(); + public Task Task => _tcs.Task; + + private ManuallyDownloadNexusFile(NexusDownloader.State state) + { + State = state; + } + + public static async Task Create(NexusDownloader.State state) + { + var result = new ManuallyDownloadNexusFile(state); + return result; + } + public override void Cancel() + { + _tcs.SetCanceled(); + } + + public void Resume(Uri s) + { + _tcs.SetResult(s); + } + } +} diff --git a/Wabbajack.Lib/WebAutomation/CefSharpWrapper.cs b/Wabbajack.Lib/WebAutomation/CefSharpWrapper.cs index fcb9e919..c13254d2 100644 --- a/Wabbajack.Lib/WebAutomation/CefSharpWrapper.cs +++ b/Wabbajack.Lib/WebAutomation/CefSharpWrapper.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Text; using System.Threading.Tasks; using CefSharp; +using Wabbajack.Common; using Wabbajack.Lib.LibCefHelpers; namespace Wabbajack.Lib.WebAutomation @@ -11,7 +13,7 @@ namespace Wabbajack.Lib.WebAutomation public class CefSharpWrapper : IWebDriver { private IWebBrowser _browser; - + public Action DownloadHandler { get; set; } public CefSharpWrapper(IWebBrowser browser) { _browser = browser; @@ -33,6 +35,7 @@ namespace Wabbajack.Lib.WebAutomation _browser.LoadingStateChanged += handler; _browser.Load(uri.ToString()); + _browser.DownloadHandler = new DownloadHandler(this); return tcs.Task; } @@ -50,10 +53,34 @@ namespace Wabbajack.Lib.WebAutomation return Helpers.GetCookies(domainPrefix); } + private const string CefStateName = "cef-state"; + public async Task WaitForInitialized() { while (!_browser.IsBrowserInitialized) await Task.Delay(100); } } + + public class DownloadHandler : IDownloadHandler + { + private CefSharpWrapper _wrapper; + + public DownloadHandler(CefSharpWrapper wrapper) + { + _wrapper = wrapper; + } + + public void OnBeforeDownload(IWebBrowser chromiumWebBrowser, IBrowser browser, DownloadItem downloadItem, + IBeforeDownloadCallback callback) + { + _wrapper.DownloadHandler(new Uri(downloadItem.Url)); + } + + public void OnDownloadUpdated(IWebBrowser chromiumWebBrowser, IBrowser browser, DownloadItem downloadItem, + IDownloadItemCallback callback) + { + callback.Cancel(); + } + } } diff --git a/Wabbajack.Lib/WebAutomation/IWebDriver.cs b/Wabbajack.Lib/WebAutomation/IWebDriver.cs index 7fd35f72..c0ea960c 100644 --- a/Wabbajack.Lib/WebAutomation/IWebDriver.cs +++ b/Wabbajack.Lib/WebAutomation/IWebDriver.cs @@ -12,5 +12,6 @@ namespace Wabbajack.Lib.WebAutomation Task NavigateTo(Uri uri); Task EvaluateJavaScript(string text); Task GetCookies(string domainPrefix); + public Action DownloadHandler { get; set; } } } diff --git a/Wabbajack/View Models/MainWindowVM.cs b/Wabbajack/View Models/MainWindowVM.cs index 9c92851a..425f1bb6 100644 --- a/Wabbajack/View Models/MainWindowVM.cs +++ b/Wabbajack/View Models/MainWindowVM.cs @@ -74,18 +74,21 @@ namespace Wabbajack .Bind(Log) .Subscribe() .DisposeWith(CompositeDisposable); + + var singleton_lock = new AsyncLock(); Utils.LogMessages .OfType() .ObserveOnGuiThread() .SelectTask(async msg => { + using var _ = await singleton_lock.Wait(); try { await UserInterventionHandlers.Handle(msg); } catch (Exception ex) - when (ex.GetType() != typeof(TaskCanceledException)) + when (ex.GetType() != typeof(TaskCanceledException)) { Utils.Error(ex, $"Error while handling user intervention of type {msg?.GetType()}"); try diff --git a/Wabbajack/View Models/UserInterventionHandlers.cs b/Wabbajack/View Models/UserInterventionHandlers.cs index bc61899c..a7402b24 100644 --- a/Wabbajack/View Models/UserInterventionHandlers.cs +++ b/Wabbajack/View Models/UserInterventionHandlers.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Threading; +using CefSharp; using ReactiveUI; using Wabbajack.Common; using Wabbajack.Lib; @@ -66,6 +67,9 @@ namespace Wabbajack c.Resume(key); }); break; + case ManuallyDownloadNexusFile c: + await WrapBrowserJob(msg, (vm, cancel) => HandleManualNexusDownload(vm, cancel, c)); + break; case RequestBethesdaNetLogin c: var data = await BethesdaNetDownloader.Login(); c.Resume(data); @@ -78,14 +82,6 @@ namespace Wabbajack c.Resume(data); }); break; - case YesNoIntervention c: - var result = MessageBox.Show(c.ExtendedDescription, c.ShortDescription, MessageBoxButton.YesNo, - MessageBoxImage.Question); - if (result == MessageBoxResult.Yes) - c.Confirm(); - else - c.Cancel(); - break; case CriticalFailureIntervention c: MessageBox.Show(c.ExtendedDescription, c.ShortDescription, MessageBoxButton.OK, MessageBoxImage.Error); @@ -98,6 +94,38 @@ namespace Wabbajack } } + private async Task HandleManualNexusDownload(WebBrowserVM vm, CancellationTokenSource cancel, ManuallyDownloadNexusFile manuallyDownloadNexusFile) + { + var state = manuallyDownloadNexusFile.State; + var game = GameRegistry.GetByMO2ArchiveName(state.GameName); + var hrefs = new[] + { + $"/Core/Libs/Common/Widgets/DownloadPopUp?id={state.FileID}&game_id={game.NexusGameId}", + $"https://www.nexusmods.com/{game.NexusName}/mods/{state.ModID}?tab=files&file_id={state.FileID}", + $"/Core/Libs/Common/Widgets/ModRequirementsPopUp?id={state.FileID}&game_id={game.NexusGameId}" + }; + await vm.Driver.WaitForInitialized(); + IWebDriver browser = new CefSharpWrapper(vm.Browser); + vm.Instructions = $"Please Download {state.ModName} - {state.ModID} - {state.FileID}"; + browser.DownloadHandler = uri => + { + manuallyDownloadNexusFile.Resume(uri); + }; + await browser.NavigateTo(NexusApiClient.ManualDownloadUrl(manuallyDownloadNexusFile.State)); + var buttin_href = $"/Core/Libs/Common/Widgets/DownloadPopUp?id={manuallyDownloadNexusFile.State.FileID}&game_id={Game.SkyrimSpecialEdition}"; + + while (!cancel.IsCancellationRequested && !manuallyDownloadNexusFile.Task.IsCompleted) { + await browser.EvaluateJavaScript( + @"Array.from(document.getElementsByClassName('accordion')).forEach(e => Array.from(e.children).forEach(c => c.style=''))"); + foreach (var href in hrefs) + { + const string style = "border-thickness: thick; border-color: #ff0000;border-width: medium;border-style: dashed;background-color: teal;padding: 7px"; + await browser.EvaluateJavaScript($"Array.from(document.querySelectorAll('.accordion a[href=\"{href}\"]')).forEach(e => {{e.scrollIntoView({{behavior: 'smooth', block: 'center', inline: 'nearest'}}); e.setAttribute('style', '{style}');}});"); + } + await Task.Delay(250); + + } + } } }