using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Threading; using MPF.Utilities; using Newtonsoft.Json.Linq; namespace MPF.Web { // https://stackoverflow.com/questions/1777221/using-cookiecontainer-with-webclient-class public class RedumpWebClient : WebClient { #region Regular Expressions /// /// Regex matching the added field on a disc page /// private readonly Regex addedRegex = new Regex(@"Added(.*?)"); /// /// Regex matching the barcode field on a disc page /// private readonly Regex barcodeRegex = new Regex(@"Barcode(.*?)"); /// /// Regex matching the BCA field on a disc page /// private readonly Regex bcaRegex = new Regex(@"

BCA .*?/>

" + "RowContentsASCII" + "(?.*?)(?.*?)(?.*?)" + "(?.*?)(?.*?)(?.*?)" + "(?.*?)(?.*?)(?.*?)" + "(?.*?)(?.*?)(?.*?)", RegexOptions.Singleline); /// /// Regex matching the category field on a disc page /// private readonly Regex categoryRegex = new Regex(@"Category(.*?)"); /// /// Regex matching the comments field on a disc page /// private readonly Regex commentsRegex = new Regex(@"Comments(.*?)", RegexOptions.Singleline); /// /// Regex matching the contents field on a disc page /// private readonly Regex contentsRegex = new Regex(@"Contents(.*?)", RegexOptions.Singleline); /// /// Regex matching individual disc links on a results page /// private readonly Regex discRegex = new Regex(@""); /// /// Regex matching the disc number or letter field on a disc page /// private readonly Regex discNumberLetterRegex = new Regex(@"\((.*?)\)"); /// /// Regex matching the dumpers on a disc page /// private readonly Regex dumpersRegex = new Regex(@"", RegexOptions.Singleline); /// /// Regex matching the edition field on a disc page /// private readonly Regex editionRegex = new Regex(@"Edition(.*?)"); /// /// Regex matching the error count field on a disc page /// private readonly Regex errorCountRegex = new Regex(@"Errors count(.*?)"); /// /// Regex matching the foreign title field on a disc page /// private readonly Regex foreignTitleRegex = new Regex(@"

(.*?)

"); /// /// Regex matching the "full match" ID list from a WIP disc page /// private readonly Regex fullMatchRegex = new Regex(@"full match ids: (.*?)"); /// /// Regex matching the languages field on a disc page /// private readonly Regex languagesRegex = new Regex(@"\s*"); /// /// Regex matching the last modified field on a disc page /// private readonly Regex lastModifiedRegex = new Regex(@"Last modified(.*?)"); /// /// Regex matching the media field on a disc page /// private readonly Regex mediaRegex = new Regex(@"Media(.*?)"); /// /// Regex matching individual WIP disc links on a results page /// private readonly Regex newDiscRegex = new Regex(@"
"); /// /// Regex matching the "partial match" ID list from a WIP disc page /// private readonly Regex partialMatchRegex = new Regex(@"partial match ids: (.*?)"); /// /// Regex matching the PVD field on a disc page /// private readonly Regex pvdRegex = new Regex(@"

Primary Volume Descriptor (PVD)

" + @"Record / EntryContentsDateTimeGMT" + @"Creation(?.*?)(?.*?)(?.*?)(?.*?)" + @"Modification(?.*?)(?.*?)(?.*?)(?.*?)" + @"Expiration(?.*?)(?.*?)(?.*?)(?.*?)" + @"Effective(?.*?)(?.*?)(?.*?)(?.*?)", RegexOptions.Singleline); /// /// Regex matching the region field on a disc page /// private readonly Regex regionRegex = new Regex(@"Region
"); /// /// Regex matching a double-layer disc ringcode information /// private readonly Regex ringCodeDoubleRegex = new Regex(@"", RegexOptions.Singleline); // Varies based on available fields, like Addtional Mould /// /// Regex matching a single-layer disc ringcode information /// private readonly Regex ringCodeSingleRegex = new Regex(@"", RegexOptions.Singleline); // Varies based on available fields, like Addtional Mould /// /// Regex matching the serial field on a disc page /// private readonly Regex serialRegex = new Regex(@"Serial(.*?)"); /// /// Regex matching the system field on a disc page /// private readonly Regex systemRegex = new Regex(@"System"); /// /// Regex matching the title field on a disc page /// private readonly Regex titleRegex = new Regex(@"

(.*?)

"); /// /// Regex matching the current nonce token for login /// private readonly Regex tokenRegex = new Regex(@""); /// /// Regex matching a single track on a disc page /// private readonly Regex trackRegex = new Regex(@"(?.*?)(?.*?)(?.*?)(?.*?)(?.*?)(?.*?)(?.*?)(?.*?)(?.*?)", RegexOptions.Singleline); /// /// Regex matching the track count on a disc page /// private readonly Regex trackCountRegex = new Regex(@"Number of tracks(.*?)"); /// /// Regex matching the version field on a disc page /// private readonly Regex versionRegex = new Regex(@"Version(.*?)"); /// /// Regex matching the write offset field on a disc page /// private readonly Regex writeOffsetRegex = new Regex(@"Write offset(.*?)"); #endregion #region URLs /// /// Redump disc page URL template /// private const string discPageUrl = @"http://redump.org/disc/{0}/"; /// /// Redump last modified search URL /// private const string lastModifiedUrl = @"http://redump.org/discs/sort/modified/dir/desc?page={0}"; /// /// Redump login page URL /// private const string loginUrl = "http://forum.redump.org/login/"; /// /// Redump CUE pack URL template /// private const string packCuesUrl = @"http://redump.org/cues/{0}/"; /// /// Redump DAT pack URL template /// private const string packDatfileUrl = @"http://redump.org/datfile/{0}/"; /// /// Redump DKEYS pack URL template /// private const string packDkeysUrl = @"http://redump.org/dkeys/{0}/"; /// /// Redump GDI pack URL template /// private const string packGdiUrl = @"http://redump.org/gdi/{0}/"; /// /// Redump KEYS pack URL template /// private const string packKeysUrl = @"http://redump.org/keys/{0}/"; /// /// Redump LSD pack URL template /// private const string packLsdUrl = @"http://redump.org/lsd/{0}/"; /// /// Redump SBI pack URL template /// private const string packSbiUrl = @"http://redump.org/sbi/{0}/"; /// /// Redump quicksearch URL template /// private const string quickSearchUrl = @"http://redump.org/discs/quicksearch/{0}/?page={1}"; /// /// Redump user dumps URL template /// private const string userDumpsUrl = @"http://redump.org/discs/dumper/{0}/?page={1}"; /// /// Redump WIP disc page URL template /// private const string wipDiscPageUrl = @"http://redump.org/newdisc/{0}/"; /// /// Redump WIP dumps queue URL /// private const string wipDumpsUrl = @"http://redump.org/discs-wip/"; #endregion #region URL Extensions private const string changesExt = "changes/"; private const string cueExt = "cue/"; private const string editExt = "edit/"; private const string gdiExt = "gdi/"; private const string keyExt = "key/"; private const string lsdExt = "lsd/"; private const string md5Ext = "md5/"; private const string sbiExt = "sbi/"; private const string sfvExt = "sfv/"; private const string sha1Ext = "sha1/"; #endregion private readonly CookieContainer m_container = new CookieContainer(); /// /// Determines if user is logged into Redump /// public bool LoggedIn { get; set; } = false; /// /// Determines if the user is a staff member /// public bool IsStaff { get; set; } = false; /// /// Get the last downloaded filename, if possible /// /// public string GetLastFilename() { // Try to extract the filename from the Content-Disposition header if (!String.IsNullOrEmpty(this.ResponseHeaders["Content-Disposition"])) return this.ResponseHeaders["Content-Disposition"].Substring(this.ResponseHeaders["Content-Disposition"].IndexOf("filename=") + 9).Replace("\"", ""); return null; } protected override WebRequest GetWebRequest(Uri address) { WebRequest request = base.GetWebRequest(address); if (request is HttpWebRequest webRequest) { webRequest.CookieContainer = m_container; } return request; } #region Features /// /// Login to Redump, if possible /// /// Redump username /// Redump password /// True if the user could be logged in, false otherwise, null on error public bool? Login(string username, string password) { // Credentials verification if (!string.IsNullOrWhiteSpace(username) && !string.IsNullOrWhiteSpace(password)) { Console.WriteLine("Credentials entered, will attempt Redump login..."); } else if (!string.IsNullOrWhiteSpace(username) && string.IsNullOrWhiteSpace(password)) { Console.WriteLine("Only a username was specified, will not attempt Redump login..."); return false; } else if (string.IsNullOrWhiteSpace(username)) { Console.WriteLine("No credentials entered, will not attempt Redump login..."); return false; } try { // Get the current token from the login page var loginPage = DownloadString(loginUrl); string token = this.tokenRegex.Match(loginPage).Groups[1].Value; // Construct the login request Headers[HttpRequestHeader.ContentType] = "application/x-www-form-urlencoded"; Encoding = Encoding.UTF8; var response = UploadString(loginUrl, $"form_sent=1&redirect_url=&csrf_token={token}&req_username={username}&req_password={password}&save_pass=0"); if (response.Contains("Incorrect username and/or password.")) { Console.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 (response.Contains("http://forum.redump.org/forum/9/staff/")) IsStaff = true; return true; } catch (Exception ex) { Console.WriteLine($"An exception occurred while trying to log in: {ex}"); return null; } } /// /// Get the latest version of MPF from GitHub and the release URL /// public (string tag, string url) GetRemoteVersionAndUrl() { Headers["User-Agent"] = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:64.0) Gecko/20100101 Firefox/64.0"; // TODO: Figure out a better way than having this hardcoded... string url = "https://api.github.com/repos/SabreTools/MPF/releases/latest"; string latestReleaseJsonString = DownloadString(url); var latestReleaseJson = JObject.Parse(latestReleaseJsonString); string latestTag = latestReleaseJson["tag_name"].ToString(); string releaseUrl = latestReleaseJson["html_url"].ToString(); return (latestTag, releaseUrl); } /// /// Create a new SubmissionInfo object based on a disc page /// /// Redump disc ID to retrieve /// Filled SubmissionInfo object on success, null on error public SubmissionInfo CreateFromId(int id) { string discData = DownloadSingleSiteID(id); if (string.IsNullOrEmpty(discData)) return null; // Create the new object SubmissionInfo info = new SubmissionInfo(); // Added var match = addedRegex.Match(discData); if (match.Success) { if (DateTime.TryParse(match.Groups[1].Value, out DateTime added)) info.Added = added; else info.Added = null; } // Barcode match = barcodeRegex.Match(discData); if (match.Success) info.CommonDiscInfo.Barcode = WebUtility.HtmlDecode(match.Groups[1].Value); // BCA match = bcaRegex.Match(discData); if (match.Success) { info.Extras.BCA = WebUtility.HtmlDecode(match.Groups[1].Value) .Replace("
", "\n") .Replace("", ""); info.Extras.BCA = Regex.Replace(info.Extras.BCA, @"
", ""); } // Category match = categoryRegex.Match(discData); if (match.Success) info.CommonDiscInfo.Category = Extensions.ToCategory(match.Groups[1].Value); else info.CommonDiscInfo.Category = DiscCategory.Games; // Comments match = commentsRegex.Match(discData); if (match.Success) { info.CommonDiscInfo.Comments = WebUtility.HtmlDecode(match.Groups[1].Value) .Replace("
", "\n") .Replace("ISBN", "[T:ISBN]") + "\n"; } // Contents match = contentsRegex.Match(discData); if (match.Success) { info.CommonDiscInfo.Contents = WebUtility.HtmlDecode(match.Groups[1].Value) .Replace("
", "\n") .Replace("
", ""); info.CommonDiscInfo.Contents = Regex.Replace(info.CommonDiscInfo.Contents, @"
", ""); } // Dumpers var matches = dumpersRegex.Matches(discData); if (matches.Count > 0) { List tempDumpers = new List(); foreach (Match submatch in matches) { tempDumpers.Add(WebUtility.HtmlDecode(submatch.Groups[1].Value)); } info.DumpersAndStatus.Dumpers = tempDumpers.ToArray(); } // Edition match = editionRegex.Match(discData); if (match.Success) info.VersionAndEditions.OtherEditions = WebUtility.HtmlDecode(match.Groups[1].Value); // Error Count match = errorCountRegex.Match(discData); if (match.Success) info.CommonDiscInfo.ErrorsCount = match.Groups[1].Value; // Foreign Title match = foreignTitleRegex.Match(discData); if (match.Success) info.CommonDiscInfo.ForeignTitleNonLatin = WebUtility.HtmlDecode(match.Groups[1].Value); else info.CommonDiscInfo.ForeignTitleNonLatin = null; // Languages matches = languagesRegex.Matches(discData); if (matches.Count > 0) { List tempLanguages = new List(); foreach (Match submatch in matches) { tempLanguages.Add(Extensions.ToLanguage(submatch.Groups[1].Value)); } info.CommonDiscInfo.Languages = tempLanguages.Where(l => l != null).ToArray(); } // Last Modified match = lastModifiedRegex.Match(discData); if (match.Success) { if (DateTime.TryParse(match.Groups[1].Value, out DateTime lastModified)) info.LastModified = lastModified; else info.LastModified = null; } // Media match = mediaRegex.Match(discData); if (match.Success) info.CommonDiscInfo.Media = Converters.ToMediaType(match.Groups[1].Value); // PVD match = pvdRegex.Match(discData); if (match.Success) { info.Extras.PVD = WebUtility.HtmlDecode(match.Groups[1].Value) .Replace("
", "\n") .Replace("
", ""); info.Extras.PVD = Regex.Replace(info.Extras.PVD, @"
", ""); } // Region match = regionRegex.Match(discData); if (match.Success) info.CommonDiscInfo.Region = Extensions.ToRegion(match.Groups[1].Value); // Serial match = serialRegex.Match(discData); if (match.Success) info.CommonDiscInfo.Serial = WebUtility.HtmlDecode(match.Groups[1].Value); // System match = systemRegex.Match(discData); if (match.Success) info.CommonDiscInfo.System = Converters.ToKnownSystem(match.Groups[1].Value); // Title, Disc Number/Letter, Disc Title match = titleRegex.Match(discData); if (match.Success) { string title = WebUtility.HtmlDecode(match.Groups[1].Value); // If we have parenthesis, title is everything before the first one int firstParenLocation = title.IndexOf(" ("); if (firstParenLocation >= 0) { info.CommonDiscInfo.Title = title.Substring(0, firstParenLocation); var subMatches = discNumberLetterRegex.Match(title); for (int i = 1; i < subMatches.Groups.Count; i++) { string subMatch = subMatches.Groups[i].Value; // Disc number or letter if (subMatch.StartsWith("Disc")) info.CommonDiscInfo.DiscNumberLetter = subMatch.Remove(0, "Disc ".Length); // Disc title else info.CommonDiscInfo.DiscTitle = subMatch; } } // Otherwise, leave the title as-is else { info.CommonDiscInfo.Title = title; } } // Tracks matches = trackRegex.Matches(discData); if (matches.Count > 0) { List tempTracks = new List(); foreach (Match submatch in matches) { tempTracks.Add(submatch.Groups[1].Value); } info.TracksAndWriteOffsets.ClrMameProData = string.Join("\n", tempTracks); } // Track Count match = trackCountRegex.Match(discData); if (match.Success) info.TracksAndWriteOffsets.Cuesheet = match.Groups[1].Value; // Version match = versionRegex.Match(discData); if (match.Success) info.VersionAndEditions.Version = WebUtility.HtmlDecode(match.Groups[1].Value); // Write Offset match = writeOffsetRegex.Match(discData); if (match.Success) info.TracksAndWriteOffsets.OtherWriteOffsets = WebUtility.HtmlDecode(match.Groups[1].Value); return info; } /// /// Download the last modified disc pages, until first failure /// /// Output directory to save data to public void DownloadLastModified(string outDir) { // Keep getting last modified pages until there are none left int pageNumber = 1; while (true) { if (!CheckSingleSitePage(string.Format(lastModifiedUrl, pageNumber++), outDir, true)) break; } } /// /// Download the last submitted WIP disc pages /// /// Output directory to save data to public void DownloadLastSubmitted(string outDir) { CheckSingleWIPPage(wipDumpsUrl, outDir, false); } /// /// Download premade packs /// /// Output directory to save data to /// True to use named subfolders to store downloads, false to store directly in the output directory public void DownloadPacks(string outDir, bool useSubfolders) { this.DownloadPacks(packCuesUrl, Extensions.HasCues, "CUEs", outDir, useSubfolders ? "cue" : null); this.DownloadPacks(packDatfileUrl, Extensions.HasDat, "DATs", outDir, useSubfolders ? "dat" : null); this.DownloadPacks(packDkeysUrl, Extensions.HasDkeys, "Decrypted KEYS", outDir, useSubfolders ? "dkey" : null); this.DownloadPacks(packGdiUrl, Extensions.HasGdi, "GDIs", outDir, useSubfolders ? "gdi" : null); this.DownloadPacks(packKeysUrl, Extensions.HasKeys, "KEYS", outDir, useSubfolders ? "keys" : null); this.DownloadPacks(packLsdUrl, Extensions.HasKeys, "LSD", outDir, useSubfolders ? "lsd" : null); this.DownloadPacks(packSbiUrl, Extensions.HasSbi, "SBIs", outDir, useSubfolders ? "sbi" : null); } /// /// Download premade packs for an individual system /// /// RedumpSystem to get all possible packs for /// Output directory to save data to /// True to use named subfolders to store downloads, false to store directly in the output directory public void DownloadPacksForSystem(RedumpSystem system, string outDir, bool useSubfolders) { RedumpSystem?[] systemAsArray = new RedumpSystem?[] { system }; if (Extensions.HasCues.Contains(system)) this.DownloadPacks(packCuesUrl, systemAsArray, "CUEs", outDir, useSubfolders ? "cue" : null); if (Extensions.HasDat.Contains(system)) this.DownloadPacks(packCuesUrl, Extensions.HasDat, "DATs", outDir, useSubfolders ? "dat" : null); if (Extensions.HasDkeys.Contains(system)) this.DownloadPacks(packCuesUrl, Extensions.HasDkeys, "Decrypted KEYS", outDir, useSubfolders ? "dkey" : null); if (Extensions.HasGdi.Contains(system)) this.DownloadPacks(packCuesUrl, Extensions.HasGdi, "GDIs", outDir, useSubfolders ? "gdi" : null); if (Extensions.HasKeys.Contains(system)) this.DownloadPacks(packCuesUrl, Extensions.HasKeys, "KEYS", outDir, useSubfolders ? "keys" : null); if (Extensions.HasLsd.Contains(system)) this.DownloadPacks(packCuesUrl, Extensions.HasKeys, "LSD", outDir, useSubfolders ? "lsd" : null); if (Extensions.HasSbi.Contains(system)) this.DownloadPacks(packCuesUrl, Extensions.HasSbi, "SBIs", outDir, useSubfolders ? "sbi" : null); } /// /// Download the disc pages associated with a given quicksearch query /// /// Query string to attempt to search for public Dictionary DownloadSearchResults(string query) { Dictionary resultPages = new Dictionary(); // 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(); // Keep getting quicksearch pages until there are none left int pageNumber = 1; while (true) { List pageIds = CheckSingleSitePage(string.Format(quickSearchUrl, query, pageNumber++)); foreach (int pageId in pageIds) { resultPages[pageId] = DownloadSingleSiteID(pageId); } if (pageIds.Count <= 1) break; } return resultPages; } /// /// Download the disc pages associated with a given quicksearch query /// /// Query string to attempt to search for /// Output directory to save data to public void DownloadSearchResults(string query, string outDir) { // 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(); // Keep getting quicksearch pages until there are none left int pageNumber = 1; while (true) { if (!CheckSingleSitePage(string.Format(quickSearchUrl, query, pageNumber++), outDir, false)) break; } } /// /// Download the specified range of site disc pages /// /// Output directory to save data to /// Starting ID for the range /// Ending ID for the range (inclusive) public void DownloadSiteRange(string outDir, int minId = 0, int maxId = 0) { if (!LoggedIn) { Console.WriteLine("Site download functionality is only available to Redump members"); return; } for (int id = minId; id <= maxId; id++) { if (DownloadSingleSiteID(id, outDir, true)) Thread.Sleep(5 * 1000); // Intentional sleep here so we don't flood the server } } /// /// Download the disc pages associated with the given user /// /// Username to check discs for /// Output directory to save data to public void DownloadUser(string username, string outDir) { if (!LoggedIn) { Console.WriteLine("User download functionality is only available to Redump members"); return; } // Keep getting user pages until there are none left int pageNumber = 1; while (true) { if (!CheckSingleSitePage(string.Format(userDumpsUrl, username, pageNumber++), outDir, false)) break; } } /// /// Download the specified range of WIP disc pages /// /// RedumpWebClient for all access /// Output directory to save data to /// Starting ID for the range /// Ending ID for the range (inclusive) public void DownloadWIPRange(string outDir, int minId = 0, int maxId = 0) { if (!LoggedIn || !IsStaff) { Console.WriteLine("WIP download functionality is only available to Redump moderators"); return; } for (int id = minId; id <= maxId; id++) { if (DownloadSingleWIPID(id, outDir, true)) Thread.Sleep(5 * 1000); // Intentional sleep here so we don't flood the server } } /// /// Fill out an existing SubmissionInfo object based on a disc page /// /// Existing SubmissionInfo object to fill /// Redump disc ID to retrieve public void FillFromId(SubmissionInfo info, int id) { string discData = DownloadSingleSiteID(id); if (string.IsNullOrEmpty(discData)) return; // Title, Disc Number/Letter, Disc Title var match = titleRegex.Match(discData); if (match.Success) { string title = WebUtility.HtmlDecode(match.Groups[1].Value); // If we have parenthesis, title is everything before the first one int firstParenLocation = title.IndexOf(" ("); if (firstParenLocation >= 0) { info.CommonDiscInfo.Title = title.Substring(0, firstParenLocation); var subMatches = discNumberLetterRegex.Match(title); for (int i = 1; i < subMatches.Groups.Count; i++) { string subMatch = subMatches.Groups[i].Value; // Disc number or letter if (subMatch.StartsWith("Disc")) info.CommonDiscInfo.DiscNumberLetter = subMatch.Remove(0, "Disc ".Length); // Disc title else info.CommonDiscInfo.DiscTitle = subMatch; } } // Otherwise, leave the title as-is else { info.CommonDiscInfo.Title = title; } } // Foreign Title match = foreignTitleRegex.Match(discData); if (match.Success) info.CommonDiscInfo.ForeignTitleNonLatin = WebUtility.HtmlDecode(match.Groups[1].Value); else info.CommonDiscInfo.ForeignTitleNonLatin = null; // Category match = categoryRegex.Match(discData); if (match.Success) info.CommonDiscInfo.Category = Extensions.ToCategory(match.Groups[1].Value); else info.CommonDiscInfo.Category = DiscCategory.Games; // Region match = regionRegex.Match(discData); if (match.Success) info.CommonDiscInfo.Region = Extensions.ToRegion(match.Groups[1].Value); // Languages var matches = languagesRegex.Matches(discData); if (matches.Count > 0) { List tempLanguages = new List(); foreach (Match submatch in matches) tempLanguages.Add(Extensions.ToLanguage(submatch.Groups[1].Value)); info.CommonDiscInfo.Languages = tempLanguages.Where(l => l != null).ToArray(); } // Error count match = errorCountRegex.Match(discData); if (match.Success) { // If the error count is empty, fill from the page if (string.IsNullOrEmpty(info.CommonDiscInfo.ErrorsCount)) info.CommonDiscInfo.ErrorsCount = match.Groups[1].Value; } // Version match = versionRegex.Match(discData); if (match.Success) info.VersionAndEditions.Version = WebUtility.HtmlDecode(match.Groups[1].Value); // Dumpers matches = dumpersRegex.Matches(discData); if (matches.Count > 0) { // Start with any currently listed dumpers List tempDumpers = new List(); if (info.DumpersAndStatus.Dumpers.Length > 0) { foreach (string dumper in info.DumpersAndStatus.Dumpers) tempDumpers.Add(dumper); } foreach (Match submatch in matches) tempDumpers.Add(WebUtility.HtmlDecode(submatch.Groups[1].Value)); info.DumpersAndStatus.Dumpers = tempDumpers.ToArray(); } // Comments match = commentsRegex.Match(discData); if (match.Success) { info.CommonDiscInfo.Comments += (string.IsNullOrEmpty(info.CommonDiscInfo.Comments) ? string.Empty : "\n") + WebUtility.HtmlDecode(match.Groups[1].Value) .Replace("
", "\n") .Replace("ISBN", "[T:ISBN]") + "\n"; } // Contents match = contentsRegex.Match(discData); if (match.Success) { info.CommonDiscInfo.Contents = WebUtility.HtmlDecode(match.Groups[1].Value) .Replace("
", "\n") .Replace("
", ""); info.CommonDiscInfo.Contents = Regex.Replace(info.CommonDiscInfo.Contents, @"
", ""); } // Added match = addedRegex.Match(discData); if (match.Success) { if (DateTime.TryParse(match.Groups[1].Value, out DateTime added)) info.Added = added; else info.Added = null; } // Last Modified match = lastModifiedRegex.Match(discData); if (match.Success) { if (DateTime.TryParse(match.Groups[1].Value, out DateTime lastModified)) info.LastModified = lastModified; else info.LastModified = null; } } /// /// List the disc IDs associated with a given quicksearch query /// /// Query string to attempt to search for /// All disc IDs for the given query, null on error public List ListSearchResults(string query) { List ids = new List(); // 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(); // Keep getting quicksearch pages until there are none left try { int pageNumber = 1; while (true) { List pageIds = CheckSingleSitePage(string.Format(quickSearchUrl, query, pageNumber++)); ids.AddRange(pageIds); if (pageIds.Count <= 1) break; } } catch (Exception ex) { Console.WriteLine($"An exception occurred while trying to log in: {ex}"); return null; } return ids; } /// /// List the disc IDs associated with the given user /// /// Username to check discs for /// All disc IDs for the given user, null on error public List ListUser(string username) { List ids = new List(); if (!LoggedIn) { Console.WriteLine("User download functionality is only available to Redump members"); return ids; } // Keep getting user pages until there are none left try { int pageNumber = 1; while (true) { List pageIds = CheckSingleSitePage(string.Format(userDumpsUrl, username, pageNumber++)); ids.AddRange(pageIds); if (pageIds.Count <= 1) break; } } catch (Exception ex) { Console.WriteLine($"An exception occurred while trying to log in: {ex}"); return null; } return ids; } #endregion #region Single Page Helpers /// /// Process a Redump site page as a list of possible IDs or disc page /// /// Base URL to download using /// List of IDs from the page, empty on error private List CheckSingleSitePage(string url) { List ids = new List(); var dumpsPage = DownloadString(url); // If we have no dumps left if (dumpsPage.Contains("No discs found.")) return ids; // If we have a single disc page already if (dumpsPage.Contains("Download:")) { var value = Regex.Match(dumpsPage, @"/disc/(\d+)/sfv/").Groups[1].Value; if (int.TryParse(value, out int id)) ids.Add(id); return ids; } // Otherwise, traverse each dump on the page var matches = discRegex.Matches(dumpsPage); foreach (Match match in matches) { try { if (int.TryParse(match.Groups[1].Value, out int value)) ids.Add(value); } catch (Exception ex) { Console.WriteLine($"An exception has occurred: {ex}"); continue; } } return ids; } /// /// Process a Redump site page as a list of possible IDs or disc page /// /// Base URL to download using /// Output directory to save data to /// True to return on first error, false otherwise /// True if the page could be downloaded, false otherwise private bool CheckSingleSitePage(string url, string outDir, bool failOnSingle) { var dumpsPage = DownloadString(url); // If we have no dumps left if (dumpsPage.Contains("No discs found.")) return false; // If we have a single disc page already if (dumpsPage.Contains("Download:")) { var value = Regex.Match(dumpsPage, @"/disc/(\d+)/sfv/").Groups[1].Value; if (int.TryParse(value, out int id)) { bool downloaded = DownloadSingleSiteID(id, outDir, false); if (!downloaded && failOnSingle) return false; } return false; } // Otherwise, traverse each dump on the page var matches = discRegex.Matches(dumpsPage); foreach (Match match in matches) { try { if (int.TryParse(match.Groups[1].Value, out int value)) { bool downloaded = DownloadSingleSiteID(value, outDir, false); if (!downloaded && failOnSingle) return false; } } catch (Exception ex) { Console.WriteLine($"An exception has occurred: {ex}"); continue; } } return true; } /// /// Process a Redump WIP page as a list of possible IDs or disc page /// /// RedumpWebClient to access the packs /// List of IDs from the page, empty on error private List CheckSingleWIPPage(string url) { List ids = new List(); var dumpsPage = DownloadString(url); // If we have no dumps left if (dumpsPage.Contains("No discs found.")) return ids; // Otherwise, traverse each dump on the page var matches = newDiscRegex.Matches(dumpsPage); foreach (Match match in matches) { 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 /// /// RedumpWebClient to access the packs /// Output directory to save data to /// True to return on first error, false otherwise /// True if the page could be downloaded, false otherwise private bool CheckSingleWIPPage(string url, string outDir, bool failOnSingle) { var dumpsPage = DownloadString(url); // If we have no dumps left if (dumpsPage.Contains("No discs found.")) return false; // Otherwise, traverse each dump on the page var matches = newDiscRegex.Matches(dumpsPage); foreach (Match match in matches) { try { if (int.TryParse(match.Groups[2].Value, out int value)) { bool downloaded = DownloadSingleWIPID(value, outDir, false); if (!downloaded && failOnSingle) return false; } } catch (Exception ex) { Console.WriteLine($"An exception has occurred: {ex}"); continue; } } return true; } #endregion #region Download Helpers /// /// Download a single pack /// /// Base URL to download using /// System to download packs for /// Byte array containing the downloaded pack, null on error private byte[] DownloadSinglePack(string url, RedumpSystem? system) { try { return DownloadData(string.Format(url, system.ShortName())); } catch (Exception ex) { Console.WriteLine($"An exception has occurred: {ex}"); return null; } } /// /// Download a single pack /// /// Base URL to download using /// System to download packs for /// Output directory to save data to /// Named subfolder for the pack, used optionally private void DownloadSinglePack(string url, RedumpSystem? system, string outDir, string subfolder) { try { string tempfile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString()); DownloadFile(string.Format(url, system.ShortName()), tempfile); MoveOrDelete(tempfile, GetLastFilename(), outDir, subfolder); } catch (Exception ex) { Console.WriteLine($"An exception has occurred: {ex}"); } } /// /// Download an individual site ID data, if possible /// /// Redump disc ID to retrieve /// String containing the page contents if successful, null on error private string DownloadSingleSiteID(int id) { string paddedId = id.ToString().PadLeft(5, '0'); Console.WriteLine($"Processing ID: {paddedId}"); try { string discPage = DownloadString(string.Format(discPageUrl, +id)); if (discPage.Contains($"Disc with ID \"{id}\" doesn't exist")) { Console.WriteLine($"ID {paddedId} could not be found!"); return null; } Console.WriteLine($"ID {paddedId} has been successfully downloaded"); return discPage; } catch (Exception ex) { Console.WriteLine($"An exception has occurred: {ex}"); return null; } } /// /// Download an individual site ID data, if possible /// /// Redump 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 private bool DownloadSingleSiteID(int id, string outDir, bool rename) { string paddedId = id.ToString().PadLeft(5, '0'); string paddedIdDir = Path.Combine(outDir, paddedId); Console.WriteLine($"Processing ID: {paddedId}"); try { string discPage = DownloadString(string.Format(discPageUrl, +id)); if (discPage.Contains($"Disc with ID \"{id}\" doesn't exist")) { try { if (rename) { if (Directory.Exists(paddedIdDir) && rename) Directory.Move(paddedIdDir, paddedIdDir + "-deleted"); else Directory.CreateDirectory(paddedIdDir + "-deleted"); } } catch { } Console.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 (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 = lastModifiedRegex.Match(oldDiscPage); var newResult = 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"); 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"); return false; } } // Create ID subdirectory Directory.CreateDirectory(paddedIdDir); // View Edit History if (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 private string DownloadSingleWIPID(int id) { string paddedId = id.ToString().PadLeft(5, '0'); Console.WriteLine($"Processing ID: {paddedId}"); try { string discPage = DownloadString(string.Format(wipDiscPageUrl, +id)); if (discPage.Contains($"System \"{id}\" doesn't exist")) { Console.WriteLine($"ID {paddedId} could not be found!"); return null; } Console.WriteLine($"ID {paddedId} has been successfully downloaded"); return discPage; } catch (Exception ex) { Console.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 private bool DownloadSingleWIPID(int id, string outDir, bool rename) { string paddedId = id.ToString().PadLeft(5, '0'); string paddedIdDir = Path.Combine(outDir, paddedId); Console.WriteLine($"Processing ID: {paddedId}"); try { string discPage = DownloadString(string.Format(wipDiscPageUrl, +id)); if (discPage.Contains($"System \"{id}\" doesn't exist")) { try { if (rename) { if (Directory.Exists(paddedIdDir) && rename) Directory.Move(paddedIdDir, paddedIdDir + "-deleted"); else Directory.CreateDirectory(paddedIdDir + "-deleted"); } } catch { } Console.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 (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 = fullMatchRegex.Match(oldDiscPage); var newResult = 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"); 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"); return false; } } // Create ID subdirectory Directory.CreateDirectory(paddedIdDir); // HTML using (var discStreamWriter = File.CreateText(Path.Combine(paddedIdDir, "disc.html"))) { discStreamWriter.Write(discPage); } Console.WriteLine($"ID {paddedId} has been successfully downloaded"); return true; } catch (Exception ex) { Console.WriteLine($"An exception has occurred: {ex}"); return false; } } #endregion #region Internal Helpers /// /// Download a set of packs /// /// Base URL to download using /// Systems to download packs for /// Name of the pack that is downloading private Dictionary DownloadPacks(string url, RedumpSystem?[] systems, string title) { var packsDictionary = new Dictionary(); // If we didn't have credentials if (!LoggedIn) systems = systems.Where(s => !Extensions.BannedSystems.Contains(s)).ToArray(); Console.WriteLine($"Downloading {title}"); foreach (var system in systems) { Console.Write($"\r{system.LongName()}{new string(' ', Console.BufferWidth - system.LongName().Length - 1)}"); byte[] pack = DownloadSinglePack(url, system); if (pack != null) packsDictionary.Add(system, pack); } Console.Write($"\rComplete!{new string(' ', Console.BufferWidth - 10)}"); Console.WriteLine(); return packsDictionary; } /// /// Download a set of packs /// /// Base URL to download using /// Systems to download packs for /// Name of the pack that is downloading /// Output directory to save data to /// Named subfolder for the pack, used optionally private void DownloadPacks(string url, RedumpSystem?[] systems, string title, string outDir, string subfolder) { // If we didn't have credentials if (!LoggedIn) systems = systems.Where(s => !Extensions.BannedSystems.Contains(s)).ToArray(); Console.WriteLine($"Downloading {title}"); foreach (var system in systems) { Console.Write($"\r{system.LongName()}{new string(' ', Console.BufferWidth - system.LongName().Length - 1)}"); DownloadSinglePack(url, system, outDir, subfolder); } Console.Write($"\rComplete!{new string(' ', Console.BufferWidth - 10)}"); Console.WriteLine(); } /// /// 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 void MoveOrDelete(string tempfile, string newfile, string outDir, string subfolder) { if (!string.IsNullOrWhiteSpace(newfile)) { if (!string.IsNullOrWhiteSpace(subfolder)) { if (!Directory.Exists(Path.Combine(outDir, subfolder))) Directory.CreateDirectory(Path.Combine(outDir, subfolder)); newfile = Path.Combine(subfolder, newfile); } if (File.Exists(Path.Combine(outDir, newfile))) File.Delete(tempfile); else File.Move(tempfile, Path.Combine(outDir, newfile)); } else File.Delete(tempfile); } #endregion } }