2021-09-27 12:42:46 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2022-06-09 20:54:41 +00:00
|
|
|
|
using System.IO;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Net.Http;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using HtmlAgilityPack;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
2022-08-21 20:23:11 +00:00
|
|
|
|
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.Validation;
|
|
|
|
|
using Wabbajack.Hashing.xxHash64;
|
2022-06-09 20:54:41 +00:00
|
|
|
|
using Wabbajack.Networking.Http;
|
2021-09-27 12:42:46 +00:00
|
|
|
|
using Wabbajack.Networking.Http.Interfaces;
|
|
|
|
|
using Wabbajack.Paths;
|
|
|
|
|
using Wabbajack.RateLimiter;
|
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
namespace Wabbajack.Downloaders.ModDB;
|
|
|
|
|
|
2022-06-09 20:54:41 +00:00
|
|
|
|
public class ModDBDownloader : ADownloader<DTOs.DownloadStates.ModDB>, IUrlDownloader, IProxyable
|
2021-09-27 12:42:46 +00:00
|
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
|
private readonly IHttpDownloader _downloader;
|
|
|
|
|
private readonly HttpClient _httpClient;
|
|
|
|
|
private readonly ILogger<ModDBDownloader> _logger;
|
|
|
|
|
|
|
|
|
|
public ModDBDownloader(ILogger<ModDBDownloader> logger, HttpClient httpClient, IHttpDownloader downloader)
|
2021-09-27 12:42:46 +00:00
|
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
|
_logger = logger;
|
|
|
|
|
_httpClient = httpClient;
|
|
|
|
|
_downloader = downloader;
|
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public override Task<bool> Prepare()
|
|
|
|
|
{
|
|
|
|
|
return Task.FromResult(true);
|
|
|
|
|
}
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override IDownloadState? Resolve(IReadOnlyDictionary<string, string> iniData)
|
|
|
|
|
{
|
|
|
|
|
if (iniData.ContainsKey("directURL") &&
|
2022-08-21 20:23:11 +00:00
|
|
|
|
iniData["directURL"].CleanIniString().StartsWith("https://www.moddb.com/downloads/start") &&
|
|
|
|
|
Uri.TryCreate(iniData["directURL"].CleanIniString().CleanIniString(), UriKind.Absolute, out var uri))
|
2021-09-27 12:42:46 +00:00
|
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
|
var state = new DTOs.DownloadStates.ModDB
|
2021-09-27 12:42:46 +00:00
|
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
|
Url = uri
|
|
|
|
|
};
|
|
|
|
|
return state;
|
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 IDownloadState? Parse(Uri uri)
|
|
|
|
|
{
|
|
|
|
|
if (!uri.ToString().StartsWith("https://www.moddb.com/downloads/start"))
|
|
|
|
|
return null;
|
|
|
|
|
return new DTOs.DownloadStates.ModDB {Url = uri};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Uri UnParse(IDownloadState state)
|
|
|
|
|
{
|
|
|
|
|
return ((DTOs.DownloadStates.ModDB) state).Url;
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-09 20:54:41 +00:00
|
|
|
|
public async Task<T> DownloadStream<T>(Archive archive, Func<Stream, Task<T>> fn, CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
var state = archive.State as DTOs.DownloadStates.ModDB;
|
2022-06-09 21:05:26 +00:00
|
|
|
|
foreach (var url in await GetDownloadUrls(state!))
|
2022-06-09 20:54:41 +00:00
|
|
|
|
{
|
2022-06-09 21:05:26 +00:00
|
|
|
|
try
|
2022-06-09 20:54:41 +00:00
|
|
|
|
{
|
2022-06-09 21:05:26 +00:00
|
|
|
|
var msg = new HttpRequestMessage
|
|
|
|
|
{
|
|
|
|
|
Method = HttpMethod.Get,
|
|
|
|
|
RequestUri = new Uri(url)
|
|
|
|
|
};
|
|
|
|
|
using var response = await _httpClient.SendAsync(msg, HttpCompletionOption.ResponseHeadersRead, token);
|
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
|
|
|
continue;
|
|
|
|
|
HttpException.ThrowOnFailure(response);
|
|
|
|
|
await using var stream = await response.Content.ReadAsStreamAsync(token);
|
|
|
|
|
return await fn(stream);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogError(ex, "While downloading from ModDB");
|
|
|
|
|
throw;
|
|
|
|
|
}
|
2022-06-09 20:54:41 +00:00
|
|
|
|
}
|
2022-06-09 21:05:26 +00:00
|
|
|
|
_logger.LogError("All servers were invalid downloading from ModDB {Uri}", state.Url);
|
|
|
|
|
return default;
|
2022-06-09 20:54:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public override async Task<Hash> Download(Archive archive, DTOs.DownloadStates.ModDB state,
|
|
|
|
|
AbsolutePath destination, IJob job, CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
var urls = await GetDownloadUrls(state);
|
|
|
|
|
foreach (var (url, idx) in urls.Zip(Enumerable.Range(0, urls.Length), (s, i) => (s, i)))
|
|
|
|
|
try
|
2021-09-27 12:42:46 +00:00
|
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
|
var msg = new HttpRequestMessage
|
2021-09-27 12:42:46 +00:00
|
|
|
|
{
|
2021-10-23 16:51:17 +00:00
|
|
|
|
Method = HttpMethod.Get,
|
|
|
|
|
RequestUri = new Uri(url)
|
2021-09-27 12:42:46 +00:00
|
|
|
|
};
|
2021-10-23 16:51:17 +00:00
|
|
|
|
return await _downloader.Download(msg, destination, job, token);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception)
|
|
|
|
|
{
|
|
|
|
|
if (idx == urls.Length - 1)
|
|
|
|
|
throw;
|
|
|
|
|
_logger.LogInformation("Download from {url} failed, trying next mirror", url);
|
2021-09-27 12:42:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
return default;
|
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
private async Task<string[]> GetDownloadUrls(DTOs.DownloadStates.ModDB state, CancellationToken? token = null)
|
|
|
|
|
{
|
|
|
|
|
var modId = state.Url.AbsolutePath.Split('/').Reverse().FirstOrDefault(f => int.TryParse(f, out _));
|
|
|
|
|
if (modId == default)
|
|
|
|
|
return Array.Empty<string>();
|
2021-09-27 12:42:46 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
var data = await _httpClient.GetStringAsync($"https://www.moddb.com/downloads/start/{modId}/all",
|
|
|
|
|
token ?? CancellationToken.None);
|
|
|
|
|
var doc = new HtmlDocument();
|
|
|
|
|
doc.LoadHtml(data);
|
|
|
|
|
var mirrors = doc.DocumentNode.Descendants().Where(d => d.NodeType == HtmlNodeType.Element && d.HasClass("row"))
|
|
|
|
|
.Select(d => new
|
|
|
|
|
{
|
|
|
|
|
Link = "https://www.moddb.com" +
|
|
|
|
|
d.Descendants().Where(s => s.Id == "downloadon")
|
|
|
|
|
.Select(i => i.GetAttributeValue("href", ""))
|
|
|
|
|
.FirstOrDefault(),
|
|
|
|
|
Load = d.Descendants().Where(s => s.HasClass("subheading"))
|
|
|
|
|
.Select(i => i.InnerHtml.Split(',')
|
|
|
|
|
.Last()
|
|
|
|
|
.Split('%')
|
|
|
|
|
.Select(v => double.TryParse(v, out var dr) ? dr : double.MaxValue)
|
|
|
|
|
.First())
|
|
|
|
|
.FirstOrDefault()
|
|
|
|
|
})
|
|
|
|
|
.OrderBy(d => d.Load)
|
|
|
|
|
.ToList();
|
2021-09-27 12:42:46 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
return mirrors.Select(d => d.Link).ToArray();
|
|
|
|
|
}
|
2021-09-27 12:42:46 +00:00
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public override async Task<bool> Verify(Archive archive, DTOs.DownloadStates.ModDB archiveState, IJob job,
|
|
|
|
|
CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
var urls = await GetDownloadUrls(archiveState, token);
|
|
|
|
|
return urls.Any();
|
2021-09-27 12:42:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-10-23 16:51:17 +00:00
|
|
|
|
public override IEnumerable<string> MetaIni(Archive a, DTOs.DownloadStates.ModDB state)
|
|
|
|
|
{
|
|
|
|
|
return new[] {$"directURL={state.Url}"};
|
|
|
|
|
}
|
|
|
|
|
}
|