Allow manual Nexus downloads

This commit is contained in:
Timothy Baldridge 2020-02-05 22:30:31 -07:00
parent d8500fd618
commit 6255ec224f
10 changed files with 164 additions and 34 deletions

View File

@ -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",

View File

@ -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

View File

@ -37,7 +37,7 @@ namespace Wabbajack.Lib.LibCefHelpers
return container;
}
public static async Task<Cookie[]> GetCookies(string domainEnding)
public static async Task<Cookie[]> 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

View File

@ -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<List<DownloadLink>>(url)).First().URI;
try
{
return (await Get<List<DownloadLink>>(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<NexusFileInfo> 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");
}
}
}

View File

@ -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<Uri> _tcs = new TaskCompletionSource<Uri>();
public Task<Uri> Task => _tcs.Task;
private ManuallyDownloadNexusFile(NexusDownloader.State state)
{
State = state;
}
public static async Task<ManuallyDownloadNexusFile> 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);
}
}
}

View File

@ -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<Uri> 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();
}
}
}

View File

@ -12,5 +12,6 @@ namespace Wabbajack.Lib.WebAutomation
Task NavigateTo(Uri uri);
Task<string> EvaluateJavaScript(string text);
Task<Helpers.Cookie[]> GetCookies(string domainPrefix);
public Action<Uri> DownloadHandler { get; set; }
}
}

View File

@ -75,17 +75,20 @@ namespace Wabbajack
.Subscribe()
.DisposeWith(CompositeDisposable);
var singleton_lock = new AsyncLock();
Utils.LogMessages
.OfType<IUserIntervention>()
.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

View File

@ -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);
}
}
}
}