using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
#if NETCOREAPP
using System.Net.Http;
using System.Net.Http.Headers;
#endif
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using SabreTools.RedumpLib.Data;
using SabreTools.RedumpLib.Legacy.Data;
using SabreTools.RedumpLib.Web;
using Constants = SabreTools.RedumpLib.Legacy.Data.Constants;
using DiscSubpath = SabreTools.RedumpLib.Legacy.Data.DiscSubpath;
using Language = SabreTools.RedumpLib.Legacy.Data.Language;
using PackType = SabreTools.RedumpLib.Legacy.Data.PackType;
using PhysicalSystem = SabreTools.RedumpLib.Legacy.Data.PhysicalSystem;
using Region = SabreTools.RedumpLib.Legacy.Data.Region;
namespace SabreTools.RedumpLib.Legacy.Web
{
public class Client
{
#region Properties
///
/// Determines if debug outputs are printed
///
public bool Debug { get; set; } = false;
///
/// Maximum attempt count for any operation
///
/// Value has to be greater than 0
public int AttemptCount
{
get;
set { field = value < 0 ? 3 : value; }
} = 3;
///
/// The timespan to wait before the request times out.
///
public TimeSpan Timeout
{
get => _internalClient.Timeout;
set
{
// Ensure a positive timespan
if (value <= TimeSpan.Zero)
value = TimeSpan.FromSeconds(30);
_internalClient.Timeout = value;
}
}
///
/// Indicates if existing files will be overwritten
///
public bool Overwrite { get; set; } = false;
///
/// Indicates if download errors are ignored
///
public bool IgnoreErrors { get; set; } = false;
#endregion
#region Fields
///
/// Internal client for interaction
///
#if NETCOREAPP
private readonly HttpClient _internalClient;
#else
private readonly CookieWebClient _internalClient;
#endif
///
/// Indicates if user is logged into Redump
///
/// Modifying to set as true does not change actual logged-in status
private bool _loggedIn = false;
///
/// Indicates if the user is a staff member
///
/// Modifying to set as true does not change actual staff status
private bool _staffMember = false;
#endregion
#region Constants
///
/// Login page URL template
///
public const string LoginUrl = "http://forum.redump.org/login/";
#endregion
///
/// Constructor
///
public Client()
{
#if NETCOREAPP
_internalClient = new HttpClient(new HttpClientHandler { UseCookies = true });
#else
_internalClient = new CookieWebClient();
#endif
Timeout = TimeSpan.FromSeconds(30);
}
#region Credentials
///
/// Validate supplied credentials
///
/// redump.org username
/// redump.org password
/// True if the user could be logged in, false otherwise, null on error
public static async Task ValidateCredentials(string? username, string? password)
{
// If options are invalid or we're missing something key, just return
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
return false;
// Try logging in with the supplied credentials otherwise
var redumpClient = new Client();
return await redumpClient.Login(username, password);
}
///
/// Login to redump.org, if possible
///
/// redump.org username
/// redump.org password
/// True if the user could be logged in, false otherwise, null on error
public async Task Login(string? username, string? password)
{
// Check for already logged in
if (_loggedIn)
{
Console.WriteLine("Already logged in!");
return true;
}
// Credentials verification
if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password))
{
Console.WriteLine("Credentials entered, will attempt redump.org login...");
}
else if (!string.IsNullOrEmpty(username) && string.IsNullOrEmpty(password))
{
Console.Error.WriteLine("Only a username was specified, will not attempt redump.org login...");
return false;
}
else if (string.IsNullOrEmpty(username))
{
Console.WriteLine("No credentials entered, will not attempt redump.org login...");
return false;
}
// HTTP encode the password
#if NET20 || NET35 || NET40
password = Uri.EscapeUriString(password);
#else
password = WebUtility.UrlEncode(password);
#endif
// Attempt to login up as many times as the retry count allows
for (int i = 0; i < AttemptCount; i++)
{
try
{
Console.WriteLine($"Login attempt {i + 1} of {AttemptCount}");
// Get the current token from the login page
var loginPage = await DownloadString(LoginUrl);
string token = Constants.TokenRegex.Match(loginPage ?? string.Empty).Groups[1].Value;
#if NETCOREAPP
// Construct the login request
var postContent = new StringContent($"form_sent=1&redirect_url=&csrf_token={token}&req_username={username}&req_password={password}&save_pass=0", Encoding.UTF8);
postContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/x-www-form-urlencoded");
// Send the login request and get the result
var response = await _internalClient.PostAsync(LoginUrl, postContent);
string? responseContent = null;
if (response?.Content is not null)
responseContent = await response.Content.ReadAsStringAsync();
#else
// Construct the login request
_internalClient.Headers[HttpRequestHeader.ContentType] = "application/x-www-form-urlencoded";
_internalClient.Encoding = Encoding.UTF8;
// Send the login request and get the result
string? responseContent = _internalClient.UploadString(LoginUrl, $"form_sent=1&redirect_url=&csrf_token={token}&req_username={username}&req_password={password}&save_pass=0");
#endif
// An empty response indicates an error
if (string.IsNullOrEmpty(responseContent))
{
Console.Error.WriteLine($"An error occurred while trying to log in on attempt {i}: No response");
continue;
}
// Explcit confirmation the login was wrong
if (responseContent.Contains("Incorrect username and/or password."))
{
Console.Error.WriteLine("Invalid credentials entered, continuing without logging in...");
return false;
}
// The user was able to be logged in
Console.WriteLine("Credentials accepted! Logged into Redump...");
_loggedIn = true;
// If the user is a moderator or staff, set accordingly
if (responseContent.Contains("http://forum.redump.org/forum/9/staff/"))
_staffMember = true;
return true;
}
catch (Exception ex)
{
Console.Error.WriteLine($"An exception occurred while trying to log in on attempt {i}: {ex}");
}
}
Console.Error.WriteLine($"Could not login to Redump in {AttemptCount} attempts, continuing without logging in...");
return false;
}
#endregion
#region Generic Helpers
///
/// Download from a URI to a byte array
///
/// Remote URI to retrieve
/// Byte array from the URI, null on error
public async Task DownloadData(string uri)
{
// Only retry a positive number of times
if (AttemptCount <= 0)
{
Console.Error.WriteLine("Invalid number of attempts provided, must be at least 1");
return null;
}
for (int i = 0; i < AttemptCount; i++)
{
try
{
if (Debug) Console.WriteLine($"DEBUG: DownloadData(\"{uri}\"), Attempt {i + 1} of {AttemptCount}");
#if NETCOREAPP
return await _internalClient.GetByteArrayAsync(uri);
#elif NET40
return await Task.Factory.StartNew(() => _internalClient.DownloadData(uri));
#else
return await Task.Run(() => _internalClient.DownloadData(uri));
#endif
}
catch { }
// Intentional delay here so we don't flood the server
DelayHelper.DelayRandom();
}
Console.Error.WriteLine($"Could not download \"{uri}\" after {AttemptCount} attempts");
return null;
}
///
/// Download from a URI to a local file
///
/// Remote URI to retrieve
/// Filename to write to
/// The remote filename from the URI, null on error
public async Task DownloadFile(string uri, string fileName)
{
// Only retry a positive number of times
if (AttemptCount <= 0)
{
Console.Error.WriteLine("Invalid number of attempts provided, must be at least 1");
return null;
}
for (int i = 0; i < AttemptCount; i++)
{
try
{
if (Debug) Console.WriteLine($"DEBUG: DownloadFile(\"{uri}\", \"{fileName}\"), Attempt {i + 1} of {AttemptCount}");
#if NETCOREAPP
// Make the call to get the file
var response = await _internalClient.GetAsync(uri);
if (response?.Content?.Headers is null || !response.IsSuccessStatusCode)
{
if (Debug) Console.Error.WriteLine($"DEBUG: DownloadFile failed, continuing...");
continue;
}
// Copy the data to a local temp file
using (var responseStream = await response.Content.ReadAsStreamAsync())
using (var tempFileStream = File.OpenWrite(fileName))
{
responseStream.CopyTo(tempFileStream);
}
return response.Content.Headers.ContentDisposition?.FileName?.Replace("\"", "");
#elif NET40
await Task.Factory.StartNew(() => { _internalClient.DownloadFile(uri, fileName); return true; });
string? lastFilename = _internalClient.GetLastFilename();
if (lastFilename is null)
{
if (Debug) Console.Error.WriteLine($"DEBUG: DownloadFile failed, continuing...");
continue;
}
return lastFilename;
#else
await Task.Run(() => _internalClient.DownloadFile(uri, fileName));
string? lastFilename = _internalClient.GetLastFilename();
if (lastFilename is null)
{
if (Debug) Console.Error.WriteLine($"DEBUG: DownloadFile failed, continuing...");
continue;
}
return lastFilename;
#endif
}
catch { }
// Intentional delay here so we don't flood the server
DelayHelper.DelayRandom();
}
Console.Error.WriteLine($"Could not download \"{uri}\" after {AttemptCount} attempts");
return null;
}
///
/// Download from a URI to a string
///
/// Remote URI to retrieve
/// String from the URI, null on error
public async Task DownloadString(string uri)
{
// Only retry a positive number of times
if (AttemptCount <= 0)
{
Console.Error.WriteLine("Invalid number of attempts provided, must be at least 1");
return null;
}
for (int i = 0; i < AttemptCount; i++)
{
try
{
if (Debug) Console.WriteLine($"DEBUG: DownloadString(\"{uri}\"), Attempt {i + 1} of {AttemptCount}");
#if NETCOREAPP
return await _internalClient.GetStringAsync(uri);
#elif NET40
return await Task.Factory.StartNew(() => _internalClient.DownloadString(uri));
#else
return await Task.Run(() => _internalClient.DownloadString(uri));
#endif
}
catch { }
// Intentional delay here so we don't flood the server
DelayHelper.DelayRandom();
}
Console.Error.WriteLine($"Could not download \"{uri}\" after {AttemptCount} attempts");
return null;
}
#endregion
#region Single Page Helpers
///
/// Process a Redump discs page as a list of possible IDs or disc page
///
/// Anti-modchip status to filter, null to omit
/// Add no barcode search to filter, false to omit
/// Add category to filter, null to omit
/// Disc type extension to filter, null to omit
/// Add dumper name to filter, null to omit
/// EDC status to filter, null to omit
/// Add edition to filter, null to omit
/// Add error count or range, null to omit
/// Add language to filter, null to omit
/// Starts with letter or '~' for numbers, null to omit
/// LibCrypt status to filter, null to omit
/// Non-specific media type to filter, null to omit
/// Write offset to filter, null to omit
/// Generic text search to filter, null to omit
/// Add region to filter, null to omit
/// Add ringcode to filter, null to omit
/// Add sorting type, null to omit
/// Add sorting direction, null to omit
/// Add status to filter, null to omit
/// Add system to filter, null to omit
/// Track count up to 99, null to omit
/// Marks search as comments field only, false to omit; incompatible with or
/// Marks search as contents field only, false to omit; incompatible with or
/// Marks search as protection field only, false to omit; incompatible with or
/// Page number, null to omit
/// List of IDs from the page, empty on none, null on error
public async Task?> CheckSingleDiscsPage(bool? antimodchip = null,
bool barcode = false,
DiscCategory? category = null,
MediaType? discType = null,
string? dumper = null,
YesNo? edc = null,
string? edition = null,
string? errors = null,
Language? language = null,
char? letter = null,
bool? libcrypt = null,
PhysicalMediaType? media = null,
int? offset = null,
string? quicksearch = null,
Region? region = null,
string? ringcode = null,
SortCategory? sort = null,
SortDirection? sortDir = null,
DumpStatus? status = null,
PhysicalSystem? system = null,
int? tracks = null,
bool comments = false,
bool contents = false,
bool protection = false,
int? page = null)
{
// Normalize the search query, if needed
if (quicksearch is not null)
quicksearch = NormalizeQuery(quicksearch);
string url = UrlBuilder.BuildDiscsUrl(antimodchip,
barcode,
category,
discType,
dumper,
edc,
edition,
errors,
language,
letter,
libcrypt,
media,
offset,
quicksearch,
region,
ringcode,
sort,
sortDir,
status,
system,
tracks,
comments,
contents,
protection,
page);
List ids = [];
// Try to retrieve the data
string? dumpsPage = await DownloadString(url);
// If the web client failed, return null
if (dumpsPage is null)
{
if (Debug) Console.WriteLine($"DEBUG: CheckSingleDiscsPage(\"{url}\") - Client failure");
return null;
}
// If we have no dumps left
if (dumpsPage.Contains("No discs found."))
{
if (Debug) Console.WriteLine($"DEBUG: CheckSingleDiscsPage(\"{url}\") - No discs found");
return ids;
}
// If we have a single disc page already
if (dumpsPage.Contains("Download:"))
{
if (Debug) Console.WriteLine($"DEBUG: CheckSingleDiscsPage(\"{url}\") - Single disc page");
var value = Constants.SfvRegex.Match(dumpsPage).Groups[1].Value;
if (int.TryParse(value, out int id))
ids.Add(id);
return ids;
}
// Otherwise, traverse each dump on the page
var matches = Constants.DiscRegex.Matches(dumpsPage);
foreach (Match? match in matches)
{
if (match is null)
continue;
try
{
if (int.TryParse(match.Groups[1].Value, out int value))
ids.Add(value);
}
catch (Exception ex)
{
Console.Error.WriteLine($"An exception has occurred: {ex}");
continue;
}
}
return ids;
}
///
/// Process a Redump discs page as a list of possible IDs or disc page
///
/// Output directory to save data to
/// Anti-modchip status to filter, null to omit
/// Add no barcode search to filter, false to omit
/// Add category to filter, null to omit
/// Disc type extension to filter, null to omit
/// Add dumper name to filter, null to omit
/// EDC status to filter, null to omit
/// Add edition to filter, null to omit
/// Add error count or range, null to omit
/// Add language to filter, null to omit
/// Starts with letter or '~' for numbers, null to omit
/// LibCrypt status to filter, null to omit
/// Non-specific media type to filter, null to omit
/// Write offset to filter, null to omit
/// Generic text search to filter, null to omit
/// Add region to filter, null to omit
/// Add ringcode to filter, null to omit
/// Add sorting type, null to omit
/// Add sorting direction, null to omit
/// Add status to filter, null to omit
/// Add system to filter, null to omit
/// Track count up to 99, null to omit
/// Marks search as comments field only, false to omit; incompatible with or
/// Marks search as contents field only, false to omit; incompatible with or
/// Marks search as protection field only, false to omit; incompatible with or
/// Page number, null to omit
/// Set of subpaths to download if available, null for all
/// List of IDs from the page, empty on none, null on error
public async Task?> CheckSingleDiscsPage(string? outDir,
bool? antimodchip = null,
bool barcode = false,
DiscCategory? category = null,
MediaType? discType = null,
string? dumper = null,
YesNo? edc = null,
string? edition = null,
string? errors = null,
Language? language = null,
char? letter = null,
bool? libcrypt = null,
PhysicalMediaType? media = null,
int? offset = null,
string? quicksearch = null,
Region? region = null,
string? ringcode = null,
SortCategory? sort = null,
SortDirection? sortDir = null,
DumpStatus? status = null,
PhysicalSystem? system = null,
int? tracks = null,
bool comments = false,
bool contents = false,
bool protection = false,
int? page = null,
DiscSubpath[]? discSubpaths = null)
{
// Get all IDs from the page
List? ids = await CheckSingleDiscsPage(antimodchip,
barcode,
category,
discType,
dumper,
edc,
edition,
errors,
language,
letter,
libcrypt,
media,
offset,
quicksearch,
region,
ringcode,
sort,
sortDir,
status,
system,
tracks,
comments,
contents,
protection,
page);
if (ids is null)
{
if (Debug) Console.WriteLine($"DEBUG: CheckSingleDiscsPage(\"{outDir}\") - Client failure");
return null;
}
// Try to download all IDs
List processed = [];
foreach (int id in ids)
{
try
{
bool downloaded = await DownloadSingleDiscPage(id, outDir, rename: false, discSubpaths);
if (!downloaded && !IgnoreErrors)
return processed;
processed.Add(id);
}
catch (Exception ex)
{
Console.Error.WriteLine($"An exception has occurred: {ex}");
continue;
}
}
return processed;
}
///
/// Process a Redump WIP page as a list of possible IDs or disc page
///
/// List of IDs from the page, empty on none, null on error
/// Limited to moderators and staff
public async Task?> CheckSingleWIPPage()
{
List ids = [];
// If the user is not a moderator
if (!_loggedIn || !_staffMember)
{
Console.Error.WriteLine("WIP download functionality is only available to Redump moderators");
return null;
}
// Try to retrieve the data
string url = UrlBuilder.BuildDiscsWipUrl();
string? dumpsPage = await DownloadString(url);
// If the web client failed, return null
if (dumpsPage is null)
{
if (Debug) Console.WriteLine($"DEBUG: CheckSingleWIPPage() - Client failure");
return null;
}
// If we have no dumps left
if (dumpsPage.Contains("No discs found."))
{
if (Debug) Console.WriteLine($"DEBUG: CheckSingleWIPPage() - No discs found");
return ids;
}
// Otherwise, traverse each dump on the page
var matches = Constants.NewDiscRegex.Matches(dumpsPage);
foreach (Match? match in matches)
{
if (match is null)
continue;
try
{
if (int.TryParse(match.Groups[2].Value, out int value))
ids.Add(value);
}
catch (Exception ex)
{
Console.WriteLine($"An exception has occurred: {ex}");
continue;
}
}
return ids;
}
///
/// Process a Redump WIP page as a list of possible IDs or disc page
///
/// Output directory to save data to
/// List of IDs that were found on success, empty on error
/// Limited to moderators and staff
public async Task?> CheckSingleWIPPage(string? outDir)
{
// Get all IDs from the page
List? ids = await CheckSingleWIPPage();
if (ids is null)
{
if (Debug) Console.WriteLine($"DEBUG: CheckSingleWIPPage(\"{outDir}\") - Client failure");
return null;
}
// Try to download all IDs
List processed = [];
foreach (int id in ids)
{
try
{
bool downloaded = await DownloadSingleWIPID(id, outDir, rename: false);
if (!downloaded && !IgnoreErrors)
return processed;
processed.Add(id);
}
catch (Exception ex)
{
Console.Error.WriteLine($"An exception has occurred: {ex}");
continue;
}
}
return processed;
}
///
/// Normalize a URL query string
///
/// Query string to normalize
/// Normalized query
private static string NormalizeQuery(string query)
{
// Strip quotes
query = query!.Trim('"', '\'');
// Special characters become dashes
query = query.Replace(' ', '-');
query = query.Replace('\\', '-');
query = query.Replace('/', '-');
// Lowercase is defined per language
query = query.ToLowerInvariant();
return query;
}
#endregion
#region Download Helpers
///
/// Download an individual list, if possible
///
/// True to show "have" discs, false to show "miss" discs
/// Username to use
/// System for filtering, null to retrieve for all systems at once
/// String containing the page contents if successful, null on error
/// may have to match the logged-in user
public async Task DownloadSingleList(bool have, string username, PhysicalSystem? system)
{
string systemName = system.ShortName() ?? "all";
Console.WriteLine($"Processing {(have ? "have" : "miss")} list for {username} for {systemName}");
try
{
// Try to retrieve the data
string listUri = UrlBuilder.BuildListUrl(username, have, system);
string? listPage = await DownloadString(listUri);
if (listPage is null)
{
Console.Error.WriteLine($"An error occurred retrieving {(have ? "have" : "miss")} list!");
return null;
}
Console.WriteLine($"{(have ? "Have" : "Miss")} list has been successfully downloaded");
return listPage;
}
catch (Exception ex)
{
Console.Error.WriteLine($"An exception has occurred: {ex}");
return null;
}
}
///
/// Download an individual list, if possible
///
/// True to show "have" discs, false to show "miss" discs
/// Username to use
/// System for filtering, null to retrieve for all systems at once
/// Output directory to save data to
/// True if all data was downloaded, false otherwise
/// may have to match the logged-in user
public async Task DownloadSingleList(bool have, string username, PhysicalSystem? system, string? outDir)
{
// If no output directory is defined, use the current directory instead
if (string.IsNullOrEmpty(outDir))
outDir = Environment.CurrentDirectory;
string systemName = system.ShortName() ?? "all";
Console.WriteLine($"Processing {(have ? "have" : "miss")} list for {username} for {systemName}");
try
{
// Try to retrieve the data
string? listPage = await DownloadSingleList(have, username, system);
if (listPage is null)
{
Console.Error.WriteLine($"An error occurred retrieving {(have ? "have" : "miss")} list!");
return false;
}
// Write the list to the output directory
Directory.CreateDirectory(outDir);
using (var listStreamWriter = File.CreateText(Path.Combine(outDir, $"{systemName}-{(have ? "have" : "miss")}.lst")))
{
listStreamWriter.Write(listPage);
listStreamWriter.Flush();
}
Console.WriteLine($"{(have ? "Have" : "Miss")} list has been successfully downloaded");
return true;
}
catch (Exception ex)
{
Console.Error.WriteLine($"An exception has occurred: {ex}");
return false;
}
}
///
/// Download a single pack
///
/// Pack type to use to determine the download URL
/// System to download packs for
/// Byte array containing the downloaded pack, null on error
public async Task DownloadSinglePack(PackType packType, PhysicalSystem? system)
{
try
{
if (Debug) Console.WriteLine($"DEBUG: DownloadSinglePack(\"{packType}\", {system})");
// If the system is invalid, we can't do anything
if (system is null || !system.IsAvailable())
{
if (Debug) Console.WriteLine($"DEBUG: {system} is not marked as available on Redump, skipping...");
return null;
}
// If we didn't have credentials
if (!_loggedIn && system.IsBanned())
{
if (Debug) Console.WriteLine($"DEBUG: {system} requires a user login to access, skipping...");
return null;
}
// If the system is unknown, we can't do anything
string? shortName = system.ShortName();
if (string.IsNullOrEmpty(shortName))
{
if (Debug) Console.WriteLine($"DEBUG: {system} is not a recognized system, skipping...");
return null;
}
// If the pack is not supported for the system
if (!PackTypeToAvailable(packType, system.Value))
{
if (Debug) Console.WriteLine($"DEBUG: {packType} is not available for {system}, skipping...");
return null;
}
// Determine the pack URL
string packUri = UrlBuilder.BuildPackUrl(packType, system.Value);
return await DownloadData(packUri);
}
catch (Exception ex)
{
Console.Error.WriteLine($"An exception has occurred: {ex}");
return null;
}
}
///
/// Download a single pack
///
/// Pack type to use to determine the download URL
/// System to download packs for
/// Output directory to save data to
public async Task DownloadSinglePack(PackType packType, PhysicalSystem? system, string? outDir)
{
try
{
if (Debug) Console.WriteLine($"DEBUG: DownloadSinglePack(\"{packType}\", {system}, \"{outDir}\")");
// If the system is invalid, we can't do anything
if (system is null || !system.IsAvailable())
{
if (Debug) Console.WriteLine($"DEBUG: {system} is not marked as available on Redump, skipping...");
return false;
}
// If we didn't have credentials
if (!_loggedIn && system.IsBanned())
{
if (Debug) Console.WriteLine($"DEBUG: {system} requires a user login to access, skipping...");
return false;
}
// If the system is unknown, we can't do anything
string? shortName = system.ShortName();
if (string.IsNullOrEmpty(shortName))
{
if (Debug) Console.WriteLine($"DEBUG: {system} is not a recognized system, skipping...");
return false;
}
// If the pack is not supported for the system
if (!PackTypeToAvailable(packType, system.Value))
{
if (Debug) Console.WriteLine($"DEBUG: {packType} is not available for {system}, skipping...");
return false;
}
// Determine the pack URL
string packUri = UrlBuilder.BuildPackUrl(packType, system.Value);
// If no output directory is defined, use the current directory instead
if (string.IsNullOrEmpty(outDir))
{
if (Debug) Console.WriteLine("DEBUG: Output directory was not provided, setting to current directory");
outDir = Environment.CurrentDirectory;
}
// Make the call to get the pack
string tempfile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString());
string? remoteFileName = await DownloadFile(packUri, tempfile);
if (remoteFileName is null)
return false;
MoveOrDelete(tempfile, remoteFileName, outDir!);
return true;
}
catch (Exception ex)
{
Console.Error.WriteLine($"An exception has occurred: {ex}");
return false;
}
}
///
/// Download a single pack
///
/// Pack type to use to determine the download URL
/// System to download packs for
/// Output directory to save data to
/// Named subfolder for the pack, used optionally
public async Task DownloadSinglePack(PackType packType, PhysicalSystem? system, string? outDir, string? subfolder)
{
try
{
if (Debug) Console.WriteLine($"DEBUG: DownloadSinglePack(\"{packType}\", {system}, \"{outDir}\", \"{subfolder}\")");
// If the system is invalid, we can't do anything
if (system is null || !system.IsAvailable())
{
if (Debug) Console.WriteLine($"DEBUG: {system} is not marked as available on Redump, skipping...");
return false;
}
// If we didn't have credentials
if (!_loggedIn && system.IsBanned())
{
if (Debug) Console.WriteLine($"DEBUG: {system} requires a user login to access, skipping...");
return false;
}
// If the system is unknown, we can't do anything
string? shortName = system.ShortName();
if (string.IsNullOrEmpty(shortName))
{
if (Debug) Console.WriteLine($"DEBUG: {system} is not a recognized system, skipping...");
return false;
}
// If the pack is not supported for the system
if (!PackTypeToAvailable(packType, system.Value))
{
if (Debug) Console.WriteLine($"DEBUG: {packType} is not available for {system}, skipping...");
return false;
}
// Determine the pack URL
string packUri = UrlBuilder.BuildPackUrl(packType, system.Value);
// If no output directory is defined, use the current directory instead
if (string.IsNullOrEmpty(outDir))
{
if (Debug) Console.WriteLine("DEBUG: Output directory was not provided, setting to current directory");
outDir = Environment.CurrentDirectory;
}
// Make the call to get the pack
string tempfile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString());
string? remoteFileName = await DownloadFile(packUri, tempfile);
if (remoteFileName is null)
return false;
MoveOrDelete(tempfile, remoteFileName, outDir!, subfolder);
return true;
}
catch (Exception ex)
{
Console.Error.WriteLine($"An exception has occurred: {ex}");
return false;
}
}
///
/// Download an individual disc ID page, if possible
///
/// Redump disc ID to retrieve
/// String containing the page contents if successful, null on error
/// This only includes the site page itself
public async Task DownloadSingleDiscPage(int id)
{
string paddedId = id.ToString().PadLeft(6, '0');
Console.WriteLine($"Processing ID: {paddedId}");
try
{
// Try to retrieve the data
string discPageUri = UrlBuilder.BuildDiscUrl(id);
string? discPage = await DownloadString(discPageUri);
if (discPage is null)
{
Console.Error.WriteLine($"An error occurred retrieving ID {paddedId}!");
return null;
}
else if (discPage.Contains($"Disc with ID \"{id}\" doesn't exist"))
{
Console.Error.WriteLine($"ID {paddedId} could not be found!");
return null;
}
Console.WriteLine($"ID {paddedId} has been successfully downloaded");
return discPage;
}
catch (Exception ex)
{
Console.Error.WriteLine($"An exception has occurred: {ex}");
return null;
}
}
///
/// Download an individual disc ID data, if possible
///
/// Redump disc ID to retrieve
/// Output directory to save data to
/// True to rename deleted entries, false otherwise
/// Set of subpaths to download if available, null for all
/// True if all data was downloaded, false otherwise
/// This includes all subpages and attached files
public async Task DownloadSingleDiscPage(int id,
string? outDir,
bool rename,
DiscSubpath[]? discSubpaths = null)
{
// If no output directory is defined, use the current directory instead
if (string.IsNullOrEmpty(outDir))
outDir = Environment.CurrentDirectory;
string paddedId = id.ToString().PadLeft(6, '0');
string paddedIdDir = Path.Combine(outDir, paddedId);
Console.WriteLine($"Processing ID: {paddedId}");
try
{
// Try to retrieve the data
string discPageUri = UrlBuilder.BuildDiscUrl(id);
string? discPage = await DownloadString(discPageUri);
if (discPage is null)
{
Console.Error.WriteLine($"An error occurred retrieving ID {paddedId}!");
return false;
}
else if (discPage.Contains($"Disc with ID \"{id}\" doesn't exist"))
{
if (rename)
{
try
{
if (Directory.Exists(paddedIdDir) && rename)
Directory.Move(paddedIdDir, $"{paddedIdDir}-deleted");
else
Directory.CreateDirectory($"{paddedIdDir}-deleted");
}
catch { }
}
Console.Error.WriteLine($"ID {paddedId} could not be found!");
return false;
}
// Check if the page has been updated since the last time it was downloaded, if possible
if (!Overwrite && File.Exists(Path.Combine(paddedIdDir, "disc.html")))
{
// Read in the cached file
var oldDiscPage = File.ReadAllText(Path.Combine(paddedIdDir, "disc.html"));
// Check for the last modified date in both pages
var oldResult = Constants.LastModifiedRegex.Match(oldDiscPage);
var newResult = Constants.LastModifiedRegex.Match(discPage);
// If both pages contain the same modified date, skip it
if (oldResult.Success && newResult.Success && oldResult.Groups[1].Value == newResult.Groups[1].Value)
{
Console.WriteLine($"ID {paddedId} has not been changed since last download, skipping...");
return false;
}
// If neither page contains a modified date, skip it
else if (!oldResult.Success && !newResult.Success)
{
Console.WriteLine($"ID {paddedId} has not been changed since last download, skipping...");
return false;
}
}
// If the downloaded data is invalid or otherwise empty, skip it
var hasAddedDate = Constants.AddedRegex.Match(discPage);
var hasModifiedDate = Constants.LastModifiedRegex.Match(discPage);
if (!hasAddedDate.Success && !hasModifiedDate.Success)
{
Console.WriteLine($"ID {paddedId} retieved an empty page, skipping...");
return false;
}
// Create ID subdirectory
Directory.CreateDirectory(paddedIdDir);
#region Pages
// View Edit History
if ((discSubpaths is null || Array.Exists(discSubpaths, s => s is DiscSubpath.Changes)) && discPage.Contains($" s is DiscSubpath.Edit)) && discPage.Contains($" s is DiscSubpath.WIP)) && Constants.NewDiscRegex.IsMatch(discPage))
{
var match = Constants.NewDiscRegex.Match(discPage);
if (int.TryParse(match.Groups[2].Value, out int newDiscId))
{
string uri = UrlBuilder.BuildNewDiscUrl(newDiscId);
string? wipPage = await DownloadString(uri);
if (!IgnoreErrors && wipPage is null)
{
if (Debug) Console.Error.WriteLine($"DEBUG: Downloading WIP entry for {id} failed!");
return false;
}
using var sw = File.CreateText(Path.Combine(paddedIdDir, "newdisc.html"));
sw.Write(wipPage);
sw.Flush();
}
}
#endregion
#region Files
// CUE
if ((discSubpaths is null || Array.Exists(discSubpaths, s => s is DiscSubpath.Cuesheet)) && discPage.Contains($" s is DiscSubpath.GDI)) && discPage.Contains($" s is DiscSubpath.Key)) && discPage.Contains($" s is DiscSubpath.LSD)) && discPage.Contains($" s is DiscSubpath.MD5)) && discPage.Contains($" s is DiscSubpath.SBI)) && discPage.Contains($" s is DiscSubpath.SFV)) && discPage.Contains($" s is DiscSubpath.SHA1)) && discPage.Contains($"
/// Download an individual WIP ID data, if possible
///
/// Redump WIP disc ID to retrieve
/// String containing the page contents if successful, null on error
/// Limited to moderators and staff
public async Task DownloadSingleWIPID(int id)
{
// If the user is not a moderator
if (!_loggedIn || !_staffMember)
{
Console.Error.WriteLine("WIP download functionality is only available to Redump moderators");
return null;
}
string paddedId = id.ToString().PadLeft(6, '0');
Console.WriteLine($"Processing WIP ID: {paddedId}");
try
{
// Try to retrieve the data
string discPageUri = UrlBuilder.BuildNewDiscUrl(id);
string? discPage = await DownloadString(discPageUri);
if (discPage is null)
{
Console.Error.WriteLine($"An error occurred retrieving ID {paddedId}!");
return null;
}
else if (discPage.Contains($"WIP disc with ID \"{id}\" doesn't exist"))
{
Console.Error.WriteLine($"ID {paddedId} could not be found!");
return null;
}
Console.WriteLine($"ID {paddedId} has been successfully downloaded");
return discPage;
}
catch (Exception ex)
{
Console.Error.WriteLine($"An exception has occurred: {ex}");
return null;
}
}
///
/// Download an individual WIP ID data, if possible
///
/// Redump WIP disc ID to retrieve
/// Output directory to save data to
/// True to rename deleted entries, false otherwise
/// True if all data was downloaded, false otherwise
/// Limited to moderators and staff
public async Task DownloadSingleWIPID(int id, string? outDir, bool rename)
{
// If the user is not a moderator
if (!_loggedIn || !_staffMember)
{
Console.Error.WriteLine("WIP download functionality is only available to Redump moderators");
return false;
}
// If no output directory is defined, use the current directory instead
if (string.IsNullOrEmpty(outDir))
{
if (Debug) Console.WriteLine("DEBUG: Output directory was not provided, setting to current directory");
outDir = Environment.CurrentDirectory;
}
string paddedId = id.ToString().PadLeft(6, '0');
string paddedIdDir = Path.Combine(outDir, paddedId);
Console.WriteLine($"Processing WIP ID: {paddedId}");
try
{
// Try to retrieve the data
string discPageUri = UrlBuilder.BuildNewDiscUrl(id);
string? discPage = await DownloadString(discPageUri);
if (discPage is null)
{
Console.Error.WriteLine($"An error occurred retrieving ID {paddedId}!");
return false;
}
else if (discPage.Contains($"WIP disc with ID \"{id}\" doesn't exist"))
{
if (rename)
{
try
{
if (Directory.Exists(paddedIdDir) && rename)
Directory.Move(paddedIdDir, $"{paddedIdDir}-deleted");
else
Directory.CreateDirectory($"{paddedIdDir}-deleted");
}
catch { }
}
Console.Error.WriteLine($"ID {paddedId} could not be found!");
return false;
}
// Check if the page has been updated since the last time it was downloaded, if possible
if (!Overwrite && File.Exists(Path.Combine(paddedIdDir, "disc.html")))
{
// Read in the cached file
var oldDiscPage = File.ReadAllText(Path.Combine(paddedIdDir, "disc.html"));
// Check for the full match ID in both pages
var oldResult = Constants.FullMatchRegex.Match(oldDiscPage);
var newResult = Constants.FullMatchRegex.Match(discPage);
// If both pages contain the same ID, skip it
if (oldResult.Success && newResult.Success && oldResult.Groups[1].Value == newResult.Groups[1].Value)
{
Console.WriteLine($"ID {paddedId} has not been changed since last download, skipping...");
return false;
}
// If neither page contains an ID, skip it
else if (!oldResult.Success && !newResult.Success)
{
Console.WriteLine($"ID {paddedId} has not been changed since last download, skipping...");
return false;
}
// Check the added date as a backup
oldResult = Constants.AddedRegex.Match(oldDiscPage);
newResult = Constants.AddedRegex.Match(discPage);
// If the downloaded data is invalid or otherwise empty, skip it
if (oldResult.Success && !newResult.Success)
{
Console.WriteLine($"ID {paddedId} retieved an empty page, skipping...");
return false;
}
}
// Create ID subdirectory
Directory.CreateDirectory(paddedIdDir);
// HTML
using (var discStreamWriter = File.CreateText(Path.Combine(paddedIdDir, "disc.html")))
{
discStreamWriter.Write(discPage);
discStreamWriter.Flush();
}
Console.WriteLine($"ID {paddedId} has been successfully downloaded");
return true;
}
catch (Exception ex)
{
Console.Error.WriteLine($"An exception has occurred: {ex}");
return false;
}
}
///
/// Download the statistics page
///
/// String containing the page contents if successful, null on error
public async Task DownloadStatisticsPage()
{
// If the user is not logged in
if (!_loggedIn)
{
Console.Error.WriteLine("Statistics download functionality is only available to logged in users");
return null;
}
Console.WriteLine("Processing statistics page");
try
{
// Try to retrieve the data
string statisticsUrl = UrlBuilder.BuildStatisticsUrl();
string? listPage = await DownloadString(statisticsUrl);
if (listPage is null)
{
Console.Error.WriteLine("An error occurred retrieving statistics page!");
return null;
}
Console.WriteLine("Statistics page has been successfully downloaded");
return listPage;
}
catch (Exception ex)
{
Console.Error.WriteLine($"An exception has occurred: {ex}");
return null;
}
}
///
/// Download the statistics page
///
/// Output directory to save data to
/// True if all data was downloaded, false otherwise
/// Limited to logged in users
public async Task DownloadStatisticsPage(string? outDir)
{
// If the user is not logged in
if (!_loggedIn)
{
Console.Error.WriteLine("Statistics download functionality is only available to logged in users");
return false;
}
// If no output directory is defined, use the current directory instead
if (string.IsNullOrEmpty(outDir))
outDir = Environment.CurrentDirectory;
Console.WriteLine("Processing statistics page");
try
{
// Try to retrieve the data
string? statisticsPage = await DownloadStatisticsPage();
if (statisticsPage is null)
{
Console.Error.WriteLine("An error occurred retrieving statistics page!");
return false;
}
// Write the list to the output directory
Directory.CreateDirectory(outDir);
using (var listStreamWriter = File.CreateText(Path.Combine(outDir, "statistics.html")))
{
listStreamWriter.Write(statisticsPage);
listStreamWriter.Flush();
}
Console.WriteLine("Statistics page has been successfully downloaded");
return true;
}
catch (Exception ex)
{
Console.Error.WriteLine($"An exception has occurred: {ex}");
return false;
}
}
#endregion
#region Helpers
///
/// Download a set of packs
///
/// Pack type to use to determine the download URL
/// Systems to download packs for
public async Task> DownloadPacks(PackType packType, PhysicalSystem[] systems)
{
// Determine if the pack type is valid
if (!Enum.IsDefined(typeof(PackType), packType))
{
if (Debug) Console.Error.WriteLine($"DEBUG: {packType} is not a recognized pack type, skipping...");
return [];
}
var packsDictionary = new Dictionary();
foreach (var system in systems)
{
string longName = system.LongName() ?? $"UNKNOWN_{system}";
if (Debug)
Console.WriteLine(longName);
else
Console.Write($"\r{longName}{new string(' ', Console.BufferWidth - longName!.Length - 1)}");
byte[]? pack = await DownloadSinglePack(packType, system);
if (pack is not null)
packsDictionary.Add(system, pack);
}
if (Debug)
Console.WriteLine("Complete!");
else
Console.Write($"\rComplete!{new string(' ', Console.BufferWidth - 10)}");
Console.WriteLine();
return packsDictionary;
}
///
/// Download a set of packs
///
/// Pack type to use to determine the download URL
/// Systems to download packs for
/// Output directory to save data to
public async Task DownloadPacks(PackType packType, PhysicalSystem[] systems, string? outDir)
{
// Determine if the pack type is valid
if (!Enum.IsDefined(typeof(PackType), packType))
{
if (Debug) Console.Error.WriteLine($"DEBUG: {packType} is not a recognized pack type, skipping...");
return false;
}
foreach (var system in systems)
{
string longName = system.LongName() ?? $"UNKNOWN_{system}";
if (Debug)
Console.WriteLine(longName);
else
Console.Write($"\r{longName}{new string(' ', Console.BufferWidth - longName!.Length - 1)}");
await DownloadSinglePack(packType, system, outDir);
}
if (Debug)
Console.WriteLine("Complete!");
else
Console.Write($"\rComplete!{new string(' ', Console.BufferWidth - 10)}");
Console.WriteLine();
return true;
}
///
/// Download a set of packs
///
/// Pack type to use to determine the download URL
/// Systems to download packs for
/// Output directory to save data to
/// Named subfolder for the pack, used optionally
public async Task DownloadPacks(PackType packType, PhysicalSystem[] systems, string? outDir, string? subfolder)
{
// Determine if the pack type is valid
if (!Enum.IsDefined(typeof(PackType), packType))
{
if (Debug) Console.Error.WriteLine($"DEBUG: {packType} is not a recognized pack type, skipping...");
return false;
}
foreach (var system in systems)
{
// If the system is invalid, we can't do anything
if (!system.IsAvailable())
{
if (Debug) Console.WriteLine($"DEBUG: {system} is not marked as available on Redump, skipping...");
continue;
}
// If we didn't have credentials
if (!_loggedIn && system.IsBanned())
{
if (Debug) Console.WriteLine($"DEBUG: {system} requires a user login to access, skipping...");
continue;
}
// If the system is unknown, we can't do anything
string? longName = system.LongName();
if (string.IsNullOrEmpty(longName))
{
if (Debug) Console.WriteLine($"DEBUG: {system} is not a recognized system, skipping...");
continue;
}
// If the pack is not supported for the system
if (!PackTypeToAvailable(packType, system))
{
if (Debug) Console.WriteLine($"DEBUG: {packType} is not available for {system}, skipping...");
continue;
}
if (Debug)
Console.WriteLine(longName);
else
Console.Write($"\r{longName}{new string(' ', Console.BufferWidth - longName!.Length - 1)}");
await DownloadSinglePack(packType, system, outDir, subfolder);
}
if (Debug)
Console.WriteLine("Complete!");
else
Console.Write($"\rComplete!{new string(' ', Console.BufferWidth - 10)}");
Console.WriteLine();
return true;
}
///
/// Move a tempfile to a new name unless it aleady exists, in which case, delete the tempfile
///
/// Path to existing temporary file
/// Path to new output file
/// Output directory to save data to
private static void MoveOrDelete(string tempfile, string? newfile, string outDir)
{
// If we don't have a file to move to, just delete the temp file
if (string.IsNullOrEmpty(newfile))
{
File.Delete(tempfile);
return;
}
// If the file already exists, don't overwrite it
if (File.Exists(Path.Combine(outDir, newfile)))
File.Delete(tempfile);
else
File.Move(tempfile, Path.Combine(outDir, newfile));
}
///
/// Move a tempfile to a new name unless it aleady exists, in which case, delete the tempfile
///
/// Path to existing temporary file
/// Path to new output file
/// Output directory to save data to
/// Optional subfolder to append to the path
private static void MoveOrDelete(string tempfile, string? newfile, string outDir, string? subfolder)
{
// If we don't have a file to move to, just delete the temp file
if (string.IsNullOrEmpty(newfile))
{
File.Delete(tempfile);
return;
}
// If we have a subfolder, create it and update the newfile name
if (!string.IsNullOrEmpty(subfolder))
{
if (!Directory.Exists(Path.Combine(outDir, subfolder)))
Directory.CreateDirectory(Path.Combine(outDir, subfolder));
newfile = Path.Combine(subfolder, newfile);
}
// If the file already exists, don't overwrite it
if (File.Exists(Path.Combine(outDir, newfile)))
File.Delete(tempfile);
else
File.Move(tempfile, Path.Combine(outDir, newfile));
}
///
/// Determine if a pack is available for a given system
///
/// Pack type to use to determine the support status
/// Systems to determine pack availability for
/// True if the pack is available for a system, false otherwise
private static bool PackTypeToAvailable(PackType packType, PhysicalSystem system)
{
return packType switch
{
PackType.Cuesheets => system.HasCues(),
PackType.Datfile => system.HasDat(),
PackType.DecryptedKeys => system.HasDkeys(),
PackType.Gdis => system.HasGdi(),
PackType.Keys => system.HasKeys(),
PackType.Lsds => system.HasLsd(),
PackType.Sbis => system.HasSbi(),
_ => false,
};
}
#endregion
}
}