wabbajack/Wabbajack.Downloaders.Nexus/NexusDownloader.cs

245 lines
8.5 KiB
C#
Raw Permalink Normal View History

2021-09-27 12:42:46 +00:00
using System;
using System.Collections.Generic;
using System.IO;
2021-09-27 12:42:46 +00:00
using System.Linq;
using System.Net;
2021-09-27 12:42:46 +00:00
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Microsoft.Extensions.Logging;
using Wabbajack.Common;
2021-09-27 12:42:46 +00:00
using Wabbajack.Downloaders.Interfaces;
using Wabbajack.DTOs;
using Wabbajack.DTOs.DownloadStates;
using Wabbajack.DTOs.Interventions;
2021-09-27 12:42:46 +00:00
using Wabbajack.DTOs.Validation;
using Wabbajack.Hashing.xxHash64;
using Wabbajack.Networking.Http;
using Wabbajack.Networking.Http.Interfaces;
using Wabbajack.Networking.NexusApi;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
2021-09-27 12:42:46 +00:00
using Wabbajack.RateLimiter;
2021-10-23 16:51:17 +00:00
namespace Wabbajack.Downloaders;
public class NexusDownloader : ADownloader<Nexus>, IUrlDownloader
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
private readonly NexusApi _api;
private readonly HttpClient _client;
private readonly IHttpDownloader _downloader;
private readonly ILogger<NexusDownloader> _logger;
private readonly IUserInterventionHandler _userInterventionHandler;
private readonly IResource<IUserInterventionHandler> _interventionLimiter;
private const bool IsManualDebugMode = false;
2021-10-23 16:51:17 +00:00
public NexusDownloader(ILogger<NexusDownloader> logger, HttpClient client, IHttpDownloader downloader,
NexusApi api, IUserInterventionHandler userInterventionHandler, IResource<IUserInterventionHandler> interventionLimiter)
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
_logger = logger;
_client = client;
_downloader = downloader;
_api = api;
_userInterventionHandler = userInterventionHandler;
_interventionLimiter = interventionLimiter;
2021-10-23 16:51:17 +00:00
}
2021-09-27 12:42:46 +00:00
2022-10-07 22:57:12 +00:00
public override Task<bool> Prepare()
2021-10-23 16:51:17 +00:00
{
return Task.FromResult(_api.AuthInfo.HaveToken());
2021-10-23 16:51:17 +00:00
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public override bool IsAllowed(ServerAllowList allowList, IDownloadState state)
{
return true;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public IDownloadState? Parse(Uri uri)
{
if (uri.Host != "www.nexusmods.com")
return null;
var relPath = (RelativePath) uri.AbsolutePath;
long modId, fileId;
if (relPath.Depth != 3)
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
_logger.LogWarning("Got www.nexusmods.com link but it didn't match a parsable pattern: {url}", uri);
return null;
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
if (!long.TryParse(relPath.FileName.ToString(), out modId))
return null;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var game = GameRegistry.ByNexusName[relPath.Parent.Parent.ToString()].FirstOrDefault();
if (game == null) return null;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var query = HttpUtility.ParseQueryString(uri.Query);
var fileIdStr = query.Get("file_id");
if (!long.TryParse(fileIdStr, out fileId))
return null;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
return new Nexus
{
Game = game.Game,
ModID = modId,
FileID = fileId
};
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public Uri UnParse(IDownloadState state)
{
var nstate = (Nexus) state;
return new Uri(
$"https://www.nexusmods.com/{nstate.Game.MetaData().NexusName}/mods/{nstate.ModID}/?tab=files&file_id={nstate.FileID}");
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public override IDownloadState? Resolve(IReadOnlyDictionary<string, string> iniData)
{
if (iniData.TryGetValue("gameName", out var gameName) &&
iniData.TryGetValue("modID", out var modId) &&
iniData.TryGetValue("fileID", out var fileId) &&
!string.IsNullOrWhiteSpace(gameName) &&
!string.IsNullOrWhiteSpace(modId) &&
!string.IsNullOrWhiteSpace(fileId))
2021-09-27 12:42:46 +00:00
return new Nexus
{
2021-10-23 16:51:17 +00:00
Game = GameRegistry.GetByMO2ArchiveName(gameName)!.Game,
ModID = long.Parse(modId),
FileID = long.Parse(fileId)
2021-09-27 12:42:46 +00:00
};
2021-10-23 16:51:17 +00:00
return null;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public override Priority Priority => Priority.Normal;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public override async Task<Hash> Download(Archive archive, Nexus state, AbsolutePath destination,
IJob job, CancellationToken token)
{
if (IsManualDebugMode || !(await _api.IsPremium(token)))
{
return await DownloadManually(archive, state, destination, job, token);
}
else
{
try
{
var urls = await _api.DownloadLink(state.Game.MetaData().NexusName!, state.ModID, state.FileID, token);
_logger.LogInformation("Downloading Nexus File: {game}|{modid}|{fileid}", state.Game, state.ModID,
state.FileID);
2022-10-24 04:56:57 +00:00
foreach (var link in urls.info)
{
if (token.IsCancellationRequested)
{
return new Hash();
}
2022-10-24 04:56:57 +00:00
try
{
var message = new HttpRequestMessage(HttpMethod.Get, link.URI);
return await _downloader.Download(message, destination, job, token);
}
catch (Exception ex)
{
if (link.URI == urls.info.Last().URI)
throw;
_logger.LogInformation(ex, "While downloading {URI}, trying another link", link.URI);
}
}
// Should never be hit
throw new NotImplementedException();
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "While downloading from the Nexus {Message}", ex.Message);
if (ex.StatusCode == HttpStatusCode.Forbidden)
{
return await DownloadManually(archive, state, destination, job, token);
}
throw;
}
}
}
private async Task<Hash> DownloadManually(Archive archive, Nexus state, AbsolutePath destination, IJob job, CancellationToken token)
{
var md = new ManualDownload(new Archive
{
Name = archive.Name,
Hash = archive.Hash,
Meta = archive.Meta,
Size = archive.Size,
State = new Manual
{
Prompt = "Click Download - Buy Nexus Premium to automate this process",
Url = new Uri($"https://www.nexusmods.com/{state.Game.MetaData().NexusName}/mods/{state.ModID}?tab=files&file_id={state.FileID}")
}
});
2022-05-20 04:12:16 +00:00
ManualDownload.BrowserDownloadState browserState;
using (var _ = await _interventionLimiter.Begin("Downloading file manually", 1, token))
{
_userInterventionHandler.Raise(md);
browserState = await md.Task;
}
var msg = browserState.ToHttpRequestMessage();
using var response = await _client.SendAsync(msg, HttpCompletionOption.ResponseHeadersRead, token);
if (!response.IsSuccessStatusCode)
throw new HttpRequestException(response.ReasonPhrase, null, statusCode:response.StatusCode);
await using var strm = await response.Content.ReadAsStreamAsync(token);
await using var os = destination.Open(FileMode.Create, FileAccess.Write, FileShare.None);
return await strm.HashingCopy(os, token, job);
2021-10-23 16:51:17 +00:00
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public override async Task<bool> Verify(Archive archive, Nexus state, IJob job, CancellationToken token)
{
try
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
var fileInfo = await _api.FileInfo(state.Game.MetaData().NexusName!, state.ModID, state.FileID, token);
2022-08-18 23:02:19 +00:00
var (modInfo, _) = await _api.ModInfo(state.Game.MetaData().NexusName!, state.ModID, token);
state.Description = FixupSummary(modInfo.Summary);
state.Version = modInfo.Version;
state.Author = modInfo.Author;
2022-08-19 23:59:29 +00:00
if (Uri.TryCreate(modInfo.PictureUrl, UriKind.Absolute, out var uri))
state.ImageURL = uri;
2022-08-18 23:02:19 +00:00
state.Name = modInfo.Name;
state.IsNSFW = modInfo.ContainsAdultContent;
2021-10-23 16:51:17 +00:00
return fileInfo.info.FileId == state.FileID;
2021-09-27 12:42:46 +00:00
}
catch (HttpException ex)
2021-09-27 12:42:46 +00:00
{
_logger.LogError($"HttpException: {ex} on {archive.Name}");
2021-10-23 16:51:17 +00:00
return false;
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
}
2022-08-18 23:02:19 +00:00
public static string FixupSummary(string? argSummary)
{
if (argSummary == null)
return "";
return argSummary.Replace("&#39;", "'")
.Replace("<br/>", "\n\n")
.Replace("<br />", "\n\n")
.Replace("&#33;", "!");
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public override IEnumerable<string> MetaIni(Archive a, Nexus state)
{
2022-09-27 03:02:39 +00:00
var meta = state.Game.MetaData();
2021-10-23 16:51:17 +00:00
return new[]
2021-09-27 12:42:46 +00:00
{
2022-09-27 03:02:39 +00:00
$"gameName={meta.MO2ArchiveName ?? meta.NexusName}", $"modID={state.ModID}", $"fileID={state.FileID}"
2021-10-23 16:51:17 +00:00
};
2021-09-27 12:42:46 +00:00
}
}