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 string MO2ArchiveName { get; internal set; }
public Game Game { get; internal set; } public Game Game { get; internal set; }
public string NexusName { 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 MO2Name { get; internal set; }
public string GameLocationRegistryKey { get; internal set; } public string GameLocationRegistryKey { get; internal set; }
// to get steam ids: https://steamdb.info // to get steam ids: https://steamdb.info
@ -211,6 +213,7 @@ namespace Wabbajack.Common
SupportedModManager = ModManager.MO2, SupportedModManager = ModManager.MO2,
Game = Game.SkyrimSpecialEdition, Game = Game.SkyrimSpecialEdition,
NexusName = "skyrimspecialedition", NexusName = "skyrimspecialedition",
NexusGameId = 1704,
MO2Name = "Skyrim Special Edition", MO2Name = "Skyrim Special Edition",
MO2ArchiveName = "skyrimse", MO2ArchiveName = "skyrimse",
GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\Skyrim Special Edition", GameLocationRegistryKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Bethesda Softworks\Skyrim Special Edition",

View File

@ -236,7 +236,7 @@ namespace Wabbajack.Lib
var dispatchers = missing.Select(m => m.State.GetDownloader()).Distinct(); var dispatchers = missing.Select(m => m.State.GetDownloader()).Distinct();
await Task.WhenAll(dispatchers.Select(d => d.Prepare())); await Task.WhenAll(dispatchers.Select(d => d.Prepare()));
await DownloadMissingArchives(missing); await DownloadMissingArchives(missing);
} }

View File

@ -17,7 +17,7 @@ namespace Wabbajack.Lib.Downloaders
public class NexusDownloader : IDownloader, INeedsLogin public class NexusDownloader : IDownloader, INeedsLogin
{ {
private bool _prepared; private bool _prepared;
private SemaphoreSlim _lock = new SemaphoreSlim(1); private AsyncLock _lock = new AsyncLock();
private UserStatus _status; private UserStatus _status;
private NexusApiClient _client; private NexusApiClient _client;
@ -94,32 +94,33 @@ namespace Wabbajack.Lib.Downloaders
{ {
if (!_prepared) if (!_prepared)
{ {
await _lock.WaitAsync(); using var _ = await _lock.Wait();
try // Could have become prepared while we waited for the lock
if (!_prepared)
{ {
// Could have become prepared while we waited for the lock _client = await NexusApiClient.Get();
if (!_prepared) _status = await _client.GetUserStatus();
if (!_client.IsAuthenticated)
{ {
_client = await NexusApiClient.Get(); Utils.ErrorThrow(new UnconvertedError(
_status = await _client.GetUserStatus(); $"Authenticating for the Nexus failed. A nexus account is required to automatically download mods."));
if (!_client.IsAuthenticated) 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( Utils.ErrorThrow(new UnconvertedError($"Aborting at the request of the user"));
$"Authenticating for the Nexus failed. A nexus account is required to automatically download mods."));
return;
} }
_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 public class State : AbstractDownloadState

View File

@ -37,7 +37,7 @@ namespace Wabbajack.Lib.LibCefHelpers
return container; return container;
} }
public static async Task<Cookie[]> GetCookies(string domainEnding) public static async Task<Cookie[]> GetCookies(string domainEnding = "")
{ {
var manager = Cef.GetGlobalCookieManager(); var manager = Cef.GetGlobalCookieManager();
var visitor = new CookieVisitor(); var visitor = new CookieVisitor();
@ -85,8 +85,14 @@ namespace Wabbajack.Lib.LibCefHelpers
public static void Init() 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 public static class ModuleInitializer

View File

@ -14,6 +14,7 @@ using Wabbajack.Lib.Downloaders;
using WebSocketSharp; using WebSocketSharp;
using static Wabbajack.Lib.NexusApi.NexusApiUtils; using static Wabbajack.Lib.NexusApi.NexusApiUtils;
using System.Threading; using System.Threading;
using Wabbajack.Lib.Exceptions;
using Wabbajack.Lib.WebAutomation; using Wabbajack.Lib.WebAutomation;
namespace Wabbajack.Lib.NexusApi namespace Wabbajack.Lib.NexusApi
@ -261,7 +262,24 @@ namespace Wabbajack.Lib.NexusApi
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
var url = $"https://api.nexusmods.com/v1/games/{ConvertGameName(archive.GameName)}/mods/{archive.ModID}/files/{archive.FileID}/download_link.json"; 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) public async Task<NexusFileInfo> GetFileInfo(NexusDownloader.State mod)
@ -328,5 +346,10 @@ namespace Wabbajack.Lib.NexusApi
} }
set => _localCacheDir = value; 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using CefSharp; using CefSharp;
using Wabbajack.Common;
using Wabbajack.Lib.LibCefHelpers; using Wabbajack.Lib.LibCefHelpers;
namespace Wabbajack.Lib.WebAutomation namespace Wabbajack.Lib.WebAutomation
@ -11,7 +13,7 @@ namespace Wabbajack.Lib.WebAutomation
public class CefSharpWrapper : IWebDriver public class CefSharpWrapper : IWebDriver
{ {
private IWebBrowser _browser; private IWebBrowser _browser;
public Action<Uri> DownloadHandler { get; set; }
public CefSharpWrapper(IWebBrowser browser) public CefSharpWrapper(IWebBrowser browser)
{ {
_browser = browser; _browser = browser;
@ -33,6 +35,7 @@ namespace Wabbajack.Lib.WebAutomation
_browser.LoadingStateChanged += handler; _browser.LoadingStateChanged += handler;
_browser.Load(uri.ToString()); _browser.Load(uri.ToString());
_browser.DownloadHandler = new DownloadHandler(this);
return tcs.Task; return tcs.Task;
} }
@ -50,10 +53,34 @@ namespace Wabbajack.Lib.WebAutomation
return Helpers.GetCookies(domainPrefix); return Helpers.GetCookies(domainPrefix);
} }
private const string CefStateName = "cef-state";
public async Task WaitForInitialized() public async Task WaitForInitialized()
{ {
while (!_browser.IsBrowserInitialized) while (!_browser.IsBrowserInitialized)
await Task.Delay(100); 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 NavigateTo(Uri uri);
Task<string> EvaluateJavaScript(string text); Task<string> EvaluateJavaScript(string text);
Task<Helpers.Cookie[]> GetCookies(string domainPrefix); Task<Helpers.Cookie[]> GetCookies(string domainPrefix);
public Action<Uri> DownloadHandler { get; set; }
} }
} }

View File

@ -74,18 +74,21 @@ namespace Wabbajack
.Bind(Log) .Bind(Log)
.Subscribe() .Subscribe()
.DisposeWith(CompositeDisposable); .DisposeWith(CompositeDisposable);
var singleton_lock = new AsyncLock();
Utils.LogMessages Utils.LogMessages
.OfType<IUserIntervention>() .OfType<IUserIntervention>()
.ObserveOnGuiThread() .ObserveOnGuiThread()
.SelectTask(async msg => .SelectTask(async msg =>
{ {
using var _ = await singleton_lock.Wait();
try try
{ {
await UserInterventionHandlers.Handle(msg); await UserInterventionHandlers.Handle(msg);
} }
catch (Exception ex) 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()}"); Utils.Error(ex, $"Error while handling user intervention of type {msg?.GetType()}");
try try

View File

@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Threading; using System.Windows.Threading;
using CefSharp;
using ReactiveUI; using ReactiveUI;
using Wabbajack.Common; using Wabbajack.Common;
using Wabbajack.Lib; using Wabbajack.Lib;
@ -66,6 +67,9 @@ namespace Wabbajack
c.Resume(key); c.Resume(key);
}); });
break; break;
case ManuallyDownloadNexusFile c:
await WrapBrowserJob(msg, (vm, cancel) => HandleManualNexusDownload(vm, cancel, c));
break;
case RequestBethesdaNetLogin c: case RequestBethesdaNetLogin c:
var data = await BethesdaNetDownloader.Login(); var data = await BethesdaNetDownloader.Login();
c.Resume(data); c.Resume(data);
@ -78,14 +82,6 @@ namespace Wabbajack
c.Resume(data); c.Resume(data);
}); });
break; 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: case CriticalFailureIntervention c:
MessageBox.Show(c.ExtendedDescription, c.ShortDescription, MessageBoxButton.OK, MessageBox.Show(c.ExtendedDescription, c.ShortDescription, MessageBoxButton.OK,
MessageBoxImage.Error); 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);
}
}
} }
} }