Reorganize quarantine zone

This commit is contained in:
Matt Nadareski
2026-07-05 00:48:11 -04:00
parent fad4d07f9d
commit 04c0740306
26 changed files with 526 additions and 394 deletions

View File

@@ -2,9 +2,9 @@ using System;
using SabreTools.RedumpLib.Data;
using SabreTools.RedumpLib.RedumpOrg;
using Xunit;
using DiscSubpath = SabreTools.RedumpLib.RedumpOrg.DiscSubpath;
using PackType = SabreTools.RedumpLib.RedumpOrg.PackType;
using PhysicalSystem = SabreTools.RedumpLib.RedumpOrg.PhysicalSystem;
using DiscSubpath = SabreTools.RedumpLib.RedumpOrg.Data.DiscSubpath;
using PackType = SabreTools.RedumpLib.RedumpOrg.Data.PackType;
using PhysicalSystem = SabreTools.RedumpLib.RedumpOrg.Data.PhysicalSystem;
namespace SabreTools.RedumpLib.Test.RedumpOrg
{

View File

@@ -10,7 +10,14 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using SabreTools.RedumpLib.Data;
using SabreTools.RedumpLib.RedumpOrg.Data;
using SabreTools.RedumpLib.Web;
using Constants = SabreTools.RedumpLib.RedumpOrg.Data.Constants;
using DiscSubpath = SabreTools.RedumpLib.RedumpOrg.Data.DiscSubpath;
using Language = SabreTools.RedumpLib.RedumpOrg.Data.Language;
using PackType = SabreTools.RedumpLib.RedumpOrg.Data.PackType;
using PhysicalSystem = SabreTools.RedumpLib.RedumpOrg.Data.PhysicalSystem;
using Region = SabreTools.RedumpLib.RedumpOrg.Data.Region;
namespace SabreTools.RedumpLib.RedumpOrg
{

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SabreTools.RedumpLib.RedumpOrg.Data;
namespace SabreTools.RedumpLib.RedumpOrg.Converters
{

View File

@@ -1,6 +1,7 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SabreTools.RedumpLib.RedumpOrg.Data;
namespace SabreTools.RedumpLib.RedumpOrg.Converters
{

View File

@@ -1,6 +1,7 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SabreTools.RedumpLib.RedumpOrg.Data;
namespace SabreTools.RedumpLib.RedumpOrg.Converters
{

View File

@@ -1,7 +1,6 @@
using System.Text.RegularExpressions;
using SabreTools.RedumpLib.Data;
namespace SabreTools.RedumpLib.RedumpOrg
namespace SabreTools.RedumpLib.RedumpOrg.Data
{
public static class Constants
{

View File

@@ -3,7 +3,7 @@ using SabreTools.RedumpLib.RedumpOrg.Attributes;
using HumanReadableAttribute = SabreTools.RedumpLib.Attributes.HumanReadableAttribute;
using LanguageCodeAttribute = SabreTools.RedumpLib.Attributes.LanguageCodeAttribute;
namespace SabreTools.RedumpLib.RedumpOrg
namespace SabreTools.RedumpLib.RedumpOrg.Data
{
/// <summary>
/// List of all disc subpaths

View File

@@ -1,10 +1,11 @@
using System;
using System.Collections.Generic;
using SabreTools.RedumpLib.Attributes;
using SabreTools.RedumpLib.Data;
using MediaType = SabreTools.RedumpLib.Data.MediaType;
using SystemAttribute = SabreTools.RedumpLib.RedumpOrg.Attributes.SystemAttribute;
using SystemCategory = SabreTools.RedumpLib.Data.SystemCategory;
namespace SabreTools.RedumpLib.RedumpOrg
namespace SabreTools.RedumpLib.RedumpOrg.Data
{
/// <summary>
/// Information pertaining to Redump systems
@@ -42,7 +43,7 @@ namespace SabreTools.RedumpLib.RedumpOrg
case MediaType.BD100:
case MediaType.BD128:
// Extract the size from the hashes
long size = Data.Extensions.ExtractSizeFromHashData(info.TracksAndWriteOffsets.ClrMameProData);
long size = RedumpLib.Data.Extensions.ExtractSizeFromHashData(info.TracksAndWriteOffsets.ClrMameProData);
if (info.SizeAndChecksums.Layerbreak3 != default)
info.CommonDiscInfo.Media = MediaType.BD128;

View File

@@ -2,9 +2,11 @@ using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using SabreTools.RedumpLib.Converters;
using SabreTools.RedumpLib.Data;
using DiscCategory = SabreTools.RedumpLib.Data.DiscCategory;
using MediaType = SabreTools.RedumpLib.Data.MediaType;
using SiteCode = SabreTools.RedumpLib.Data.SiteCode;
namespace SabreTools.RedumpLib.RedumpOrg.Sections
namespace SabreTools.RedumpLib.RedumpOrg.Data.Sections
{
/// <summary>
/// Common disc info section of New Disc Form

View File

@@ -4,7 +4,7 @@ using Newtonsoft.Json;
using SabreTools.RedumpLib.Converters;
using SabreTools.RedumpLib.Data;
namespace SabreTools.RedumpLib.RedumpOrg.Sections
namespace SabreTools.RedumpLib.RedumpOrg.Data.Sections
{
/// <summary>
/// Copy protection section of New Disc form

View File

@@ -2,7 +2,7 @@ using System;
using Newtonsoft.Json;
using SabreTools.RedumpLib.Data;
namespace SabreTools.RedumpLib.RedumpOrg.Sections
namespace SabreTools.RedumpLib.RedumpOrg.Data.Sections
{
/// <summary>
/// Dumpers and status section of New Disc form (Moderator only)

View File

@@ -1,7 +1,7 @@
using System;
using Newtonsoft.Json;
namespace SabreTools.RedumpLib.RedumpOrg.Sections
namespace SabreTools.RedumpLib.RedumpOrg.Data.Sections
{
/// <summary>
/// Dumping info section for moderation

View File

@@ -3,7 +3,7 @@ using Newtonsoft.Json;
using SabreTools.RedumpLib.Converters;
using SabreTools.RedumpLib.Data;
namespace SabreTools.RedumpLib.RedumpOrg.Sections
namespace SabreTools.RedumpLib.RedumpOrg.Data.Sections
{
/// <summary>
/// EDC section of New Disc form (PSX only)

View File

@@ -1,7 +1,7 @@
using System;
using Newtonsoft.Json;
namespace SabreTools.RedumpLib.RedumpOrg.Sections
namespace SabreTools.RedumpLib.RedumpOrg.Data.Sections
{
/// <summary>
/// Extras section of New Disc form

View File

@@ -1,7 +1,7 @@
using System;
using Newtonsoft.Json;
namespace SabreTools.RedumpLib.RedumpOrg.Sections
namespace SabreTools.RedumpLib.RedumpOrg.Data.Sections
{
/// <summary>
/// Parent/Clone relationship section of New Disc form

View File

@@ -1,7 +1,7 @@
using System;
using Newtonsoft.Json;
namespace SabreTools.RedumpLib.RedumpOrg.Sections
namespace SabreTools.RedumpLib.RedumpOrg.Data.Sections
{
/// <summary>
/// Size &amp; checksums section of New Disc form (DVD/BD/UMD-based)

View File

@@ -1,7 +1,7 @@
using System;
using Newtonsoft.Json;
namespace SabreTools.RedumpLib.RedumpOrg.Sections
namespace SabreTools.RedumpLib.RedumpOrg.Data.Sections
{
/// <summary>
/// Tracks and write offsets section of New Disc form (CD/GD-based)

View File

@@ -1,7 +1,7 @@
using System;
using Newtonsoft.Json;
namespace SabreTools.RedumpLib.RedumpOrg.Sections
namespace SabreTools.RedumpLib.RedumpOrg.Data.Sections
{
/// <summary>
/// Version and editions section of New Disc form

View File

@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using SabreTools.RedumpLib.RedumpOrg.Sections;
using SabreTools.RedumpLib.RedumpOrg.Data.Sections;
namespace SabreTools.RedumpLib.RedumpOrg
namespace SabreTools.RedumpLib.RedumpOrg.Data
{
/// <summary>
/// redump.org submission page information

View File

@@ -3,6 +3,10 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using SabreTools.RedumpLib.Data;
using SabreTools.RedumpLib.Web;
using DiscSubpath = SabreTools.RedumpLib.RedumpOrg.Data.DiscSubpath;
using Language = SabreTools.RedumpLib.RedumpOrg.Data.Language;
using PhysicalSystem = SabreTools.RedumpLib.RedumpOrg.Data.PhysicalSystem;
using Region = SabreTools.RedumpLib.RedumpOrg.Data.Region;
namespace SabreTools.RedumpLib.RedumpOrg
{

View File

@@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using SabreTools.RedumpLib.Data;
using PackType = SabreTools.RedumpLib.RedumpOrg.Data.PackType;
using PhysicalSystem = SabreTools.RedumpLib.RedumpOrg.Data.PhysicalSystem;
namespace SabreTools.RedumpLib.RedumpOrg
{

View File

@@ -0,0 +1,471 @@
using System;
using System.Collections.Generic;
#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
using System.Net;
#endif
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using SabreTools.RedumpLib.Data;
using Constants = SabreTools.RedumpLib.RedumpOrg.Data.Constants;
using Language = SabreTools.RedumpLib.RedumpOrg.Data.Language;
using SubmissionInfo = SabreTools.RedumpLib.RedumpOrg.Data.SubmissionInfo;
namespace SabreTools.RedumpLib.RedumpOrg.Tools
{
public static class Builder
{
#region Creation
/// <summary>
/// Fill out an existing SubmissionInfo object based on a disc page
/// </summary>
/// <param name="client">RedumpClient for making the connection</param>
/// <param name="info">Existing SubmissionInfo object to fill</param>
/// <param name="id">Redump disc ID to retrieve</param>
/// <param name="includeAllData">True to include all pullable information, false to do bare minimum</param>
public static async Task<bool> FillFromId(Client client, SubmissionInfo info, int id, bool includeAllData)
{
var discData = await client.DownloadSingleDiscPage(id);
if (string.IsNullOrEmpty(discData))
return false;
// Title, Disc Number/Letter, Disc Title
var match = Constants.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(" (") ?? -1;
if (title is not null && firstParenLocation >= 0)
{
#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER
info.CommonDiscInfo.Title = title[..firstParenLocation];
#else
info.CommonDiscInfo.Title = title.Substring(0, firstParenLocation);
#endif
var submatches = Constants.DiscNumberLetterRegex.Matches(title);
foreach (Match? submatch in submatches)
{
if (submatch is null)
continue;
var submatchValue = submatch.Groups[1].Value;
// Disc number or letter
if (submatchValue.StartsWith("Disc"))
#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER
info.CommonDiscInfo.DiscNumberLetter = submatchValue["Disc ".Length..];
#else
info.CommonDiscInfo.DiscNumberLetter = submatchValue.Remove(0, "Disc ".Length);
#endif
// Issue number
else if (ulong.TryParse(submatchValue, out _))
info.CommonDiscInfo.Title += $" ({submatchValue})";
// Disc title
else
info.CommonDiscInfo.DiscTitle = submatchValue;
}
}
// Otherwise, leave the title as-is
else
{
info.CommonDiscInfo.Title = title;
}
}
// Foreign Title
match = Constants.ForeignTitleRegex.Match(discData);
if (match.Success)
info.CommonDiscInfo.ForeignTitleNonLatin = WebUtility.HtmlDecode(match.Groups[1].Value);
// Category
match = Constants.CategoryRegex.Match(discData);
if (match.Success)
info.CommonDiscInfo.Category = match.Groups[1].Value.ToDiscCategory();
else
info.CommonDiscInfo.Category = DiscCategory.Games;
// Region
if (info.CommonDiscInfo.Region is null)
{
match = Constants.RegionRegex.Match(discData);
if (match.Success)
info.CommonDiscInfo.Region = Data.Extensions.ToRegion(match.Groups[1].Value);
}
// Languages
var matches = Constants.LanguagesRegex.Matches(discData);
if (matches.Count > 0)
{
var tempLanguages = new List<Language?>();
foreach (Match? submatch in matches)
{
if (submatch is null)
continue;
var language = Data.Extensions.ToLanguage(submatch.Groups[1].Value);
if (language is not null)
tempLanguages.Add(language);
}
info.CommonDiscInfo.Languages = [.. tempLanguages];
}
// Serial
if (includeAllData)
{
// TODO: Re-enable if there's a way of verifying against a disc
//match = Constants.SerialRegex.Match(discData);
//if (match.Success)
// info.CommonDiscInfo.Serial = $"(VERIFY THIS) {WebUtility.HtmlDecode(match.Groups[1].Value)}";
}
// Error count
if (string.IsNullOrEmpty(info.CommonDiscInfo.ErrorsCount))
{
match = Constants.ErrorCountRegex.Match(discData);
if (match.Success)
info.CommonDiscInfo.ErrorsCount = match.Groups[1].Value;
}
// Version
if (info.VersionAndEditions.Version is null)
{
match = Constants.VersionRegex.Match(discData);
if (match.Success)
info.VersionAndEditions.Version = $"(VERIFY THIS) {WebUtility.HtmlDecode(match.Groups[1].Value)}";
}
// Dumpers
matches = Constants.DumpersRegex.Matches(discData);
if (matches.Count > 0)
{
// Start with any currently listed dumpers
var tempDumpers = new List<string>();
if (info.DumpersAndStatus.Dumpers is not null && info.DumpersAndStatus.Dumpers.Length > 0)
{
foreach (string dumper in info.DumpersAndStatus.Dumpers)
tempDumpers.Add(dumper);
}
foreach (Match? submatch in matches)
{
if (submatch is null)
continue;
string? dumper = WebUtility.HtmlDecode(submatch.Groups[1].Value);
if (dumper is not null)
tempDumpers.Add(dumper);
}
info.DumpersAndStatus.Dumpers = [.. tempDumpers];
}
// PS3 DiscKey
if (string.IsNullOrEmpty(info.Extras.DiscKey))
{
// Validate key is not NULL
match = Constants.PS3DiscKey.Match(discData);
if (match.Success && match.Groups[1].Value != "<span class=\"null\">NULL</span>")
info.Extras.DiscKey = match.Groups[1].Value;
}
// TODO: Unify handling of fields that can include site codes (Comments/Contents)
// Comments
if (includeAllData)
{
match = Constants.CommentsRegex.Match(discData);
if (match.Success)
{
// Process the old comments block
string oldComments = info.CommonDiscInfo.Comments
+ (string.IsNullOrEmpty(info.CommonDiscInfo.Comments) ? string.Empty : "\n")
+ (WebUtility.HtmlDecode(match.Groups[1].Value) ?? string.Empty)
.Replace("\r\n", "\n")
.Replace("<br />\n", "\n")
.Replace("<br />", string.Empty)
.Replace("</div>", string.Empty)
.Replace("[+]", string.Empty)
.ReplaceHtmlWithSiteCodes();
oldComments = Regex.Replace(oldComments, @"<div .*?>", string.Empty, RegexOptions.Compiled);
// Create state variables
bool addToLast = false;
SiteCode? lastSiteCode = null;
string newComments = string.Empty;
// Process the comments block line-by-line
string[] commentsSeparated = oldComments.Split('\n');
for (int i = 0; i < commentsSeparated.Length; i++)
{
string commentLine = commentsSeparated[i].Trim();
// If we have an empty line, we want to treat this as intentional
if (string.IsNullOrEmpty(commentLine))
{
addToLast = false;
lastSiteCode = null;
newComments += $"{commentLine}\n";
continue;
}
// Otherwise, we need to find what tag is in use
bool foundTag = false;
foreach (SiteCode? siteCode in Enum.GetValues(typeof(SiteCode)))
{
// If we have a null site code, just skip
if (siteCode is null)
continue;
// If the line doesn't contain this tag, just skip
var shortName = siteCode.ShortName();
if (shortName is null || !commentLine.Contains(shortName))
continue;
// Mark as having found a tag
foundTag = true;
// Cache the current site code
lastSiteCode = siteCode;
// A subset of tags can be multiline
addToLast = siteCode.IsMultiLine();
// Skip certain site codes because of data issues
if (ShouldSkipSiteCode(siteCode))
continue;
// If we don't already have this site code, add it to the dictionary
if (!info.CommonDiscInfo.CommentsSpecialFields.ContainsKey(siteCode.Value))
info.CommonDiscInfo.CommentsSpecialFields[siteCode.Value] = $"(VERIFY THIS) {commentLine.Replace(shortName, string.Empty).Trim()}";
// Otherwise, append the value to the existing key
else
info.CommonDiscInfo.CommentsSpecialFields[siteCode.Value] += $", {commentLine.Replace(shortName, string.Empty).Trim()}";
break;
}
// If we didn't find a known tag, just add the line, just in case
if (!foundTag)
{
if (addToLast && lastSiteCode is not null && !ShouldSkipSiteCode(lastSiteCode))
{
if (!string.IsNullOrEmpty(info.CommonDiscInfo.CommentsSpecialFields![lastSiteCode.Value]))
info.CommonDiscInfo.CommentsSpecialFields[lastSiteCode.Value] += "\n";
info.CommonDiscInfo.CommentsSpecialFields[lastSiteCode.Value] += commentLine;
}
else if (!addToLast || lastSiteCode is null)
{
newComments += $"{commentLine}\n";
}
}
}
// Set the new comments field
info.CommonDiscInfo.Comments = newComments;
}
}
// Contents
if (includeAllData)
{
match = Constants.ContentsRegex.Match(discData);
if (match.Success)
{
// Process the old contents block
string oldContents = info.CommonDiscInfo.Contents
+ (string.IsNullOrEmpty(info.CommonDiscInfo.Contents) ? string.Empty : "\n")
+ (WebUtility.HtmlDecode(match.Groups[1].Value) ?? string.Empty)
.Replace("\r\n", "\n")
.Replace("<br />\n", "\n")
.Replace("<br />", string.Empty)
.Replace("</div>", string.Empty)
.Replace("[+]", string.Empty)
.ReplaceHtmlWithSiteCodes();
oldContents = Regex.Replace(oldContents, @"<div .*?>", string.Empty, RegexOptions.Compiled);
// Create state variables
bool addToLast = false;
SiteCode? lastSiteCode = null;
string newContents = string.Empty;
// Process the contents block line-by-line
string[] contentsSeparated = oldContents.Split('\n');
for (int i = 0; i < contentsSeparated.Length; i++)
{
string contentLine = contentsSeparated[i].Trim();
// If we have an empty line, we want to treat this as intentional
if (string.IsNullOrEmpty(contentLine))
{
addToLast = false;
lastSiteCode = null;
newContents += $"{contentLine}\n";
continue;
}
// Otherwise, we need to find what tag is in use
bool foundTag = false;
foreach (SiteCode? siteCode in Enum.GetValues(typeof(SiteCode)))
{
// If we have a null site code, just skip
if (siteCode is null)
continue;
// If the line doesn't contain this tag, just skip
var shortName = siteCode.ShortName();
if (shortName is null || !contentLine.Contains(shortName))
continue;
// Cache the current site code
lastSiteCode = siteCode;
// If we don't already have this site code, add it to the dictionary
if (!info.CommonDiscInfo.ContentsSpecialFields.ContainsKey(siteCode.Value))
info.CommonDiscInfo.ContentsSpecialFields[siteCode.Value] = $"(VERIFY THIS) {contentLine.Replace(shortName, string.Empty).Trim()}";
// A subset of tags can be multiline
addToLast = siteCode.IsMultiLine();
// Mark as having found a tag
foundTag = true;
break;
}
// If we didn't find a known tag, just add the line, just in case
if (!foundTag)
{
if (addToLast && lastSiteCode is not null && !ShouldSkipSiteCode(lastSiteCode))
{
if (!string.IsNullOrEmpty(info.CommonDiscInfo.ContentsSpecialFields![lastSiteCode.Value]))
info.CommonDiscInfo.ContentsSpecialFields[lastSiteCode.Value] += "\n";
info.CommonDiscInfo.ContentsSpecialFields[lastSiteCode.Value] += contentLine;
}
else if (!addToLast || lastSiteCode is null)
{
newContents += $"{contentLine}\n";
}
}
}
// Set the new contents field
info.CommonDiscInfo.Contents = newContents;
}
}
// Added
match = Constants.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 = Constants.LastModifiedRegex.Match(discData);
if (match.Success)
{
if (DateTime.TryParse(match.Groups[1].Value, out DateTime lastModified))
info.LastModified = lastModified;
else
info.LastModified = null;
}
return true;
}
/// <summary>
/// Determine if a site code should be skipped on pulling
/// </summary>
private static bool ShouldSkipSiteCode(SiteCode? siteCode)
{
#pragma warning disable IDE0072
return siteCode switch
{
// Multiple
SiteCode.HighSierraVolumeDescriptor
or SiteCode.InternalSerialName
or SiteCode.Multisession
or SiteCode.VolumeLabel => true,
// Audio CD
SiteCode.RingNonZeroDataStart
or SiteCode.RingPerfectAudioOffset
or SiteCode.UniversalHash => true,
// Microsoft Xbox and Xbox 360
SiteCode.DMIHash
or SiteCode.PFIHash
or SiteCode.SSHash
or SiteCode.SSVersion
or SiteCode.XMID
or SiteCode.XeMID => true,
// Microsoft Xbox One and Series X/S
SiteCode.Filename => true,
SiteCode.TitleID => true,
// Nintendo Gamecube
SiteCode.InternalName => true,
// Protection
SiteCode.Protection => true,
_ => false,
};
#pragma warning restore IDE0072
}
#endregion
#region Helpers
/// <summary>
/// Process a text block and replace with internal identifiers
/// </summary>
/// <param name="text">Text block to process</param>
/// <returns>Processed text block, if possible</returns>
internal static string ReplaceHtmlWithSiteCodes(this string text)
{
// Empty strings are ignored
if (text.Length == 0)
return text;
foreach (SiteCode? siteCode in Enum.GetValues(typeof(SiteCode)))
{
var longname = siteCode.LongName();
if (!string.IsNullOrEmpty(longname))
text = text.Replace(longname, siteCode.ShortName());
}
// For some outdated tags, we need to use alternate names
text = text.Replace("<b>Demos</b>:", ((SiteCode?)SiteCode.PlayableDemos).ShortName());
text = text.Replace("<b>Disney ID</b>:", ((SiteCode?)SiteCode.DisneyInteractiveID).ShortName());
text = text.Replace("DMI:", ((SiteCode?)SiteCode.DMIHash).ShortName());
text = text.Replace("<b>LucasArts ID</b>:", ((SiteCode?)SiteCode.LucasArtsID).ShortName());
text = text.Replace("PFI:", ((SiteCode?)SiteCode.PFIHash).ShortName());
text = text.Replace("SS:", ((SiteCode?)SiteCode.SSHash).ShortName());
text = text.Replace("SSv1:", ((SiteCode?)SiteCode.SSHash).ShortName());
text = text.Replace("<b>SSv1</b>:", ((SiteCode?)SiteCode.SSHash).ShortName());
text = text.Replace("SSv2:", ((SiteCode?)SiteCode.SSHash).ShortName());
text = text.Replace("<b>SSv2</b>:", ((SiteCode?)SiteCode.SSHash).ShortName());
text = text.Replace("SS version:", ((SiteCode?)SiteCode.SSVersion).ShortName());
text = text.Replace("Universal Hash (SHA-1):", ((SiteCode?)SiteCode.UniversalHash).ShortName());
text = text.Replace("XeMID:", ((SiteCode?)SiteCode.XeMID).ShortName());
text = text.Replace("XMID:", ((SiteCode?)SiteCode.XMID).ShortName());
return text;
}
#endregion
}
}

View File

@@ -3,9 +3,12 @@ using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using SabreTools.RedumpLib.Data;
using SabreTools.RedumpLib.RedumpOrg.Sections;
using SabreTools.RedumpLib.RedumpOrg.Data;
using SabreTools.RedumpLib.RedumpOrg.Data.Sections;
using PhysicalSystem = SabreTools.RedumpLib.RedumpOrg.Data.PhysicalSystem;
using SubmissionInfo = SabreTools.RedumpLib.RedumpOrg.Data.SubmissionInfo;
namespace SabreTools.RedumpLib.RedumpOrg
namespace SabreTools.RedumpLib.RedumpOrg.Tools
{
public static class Formatter
{
@@ -273,7 +276,7 @@ namespace SabreTools.RedumpLib.RedumpOrg
List<int>? partiallyMatchedIDs)
{
// Extract the size from the hashes
long size = Data.Extensions.ExtractSizeFromHashData(tawo?.ClrMameProData);
long size = RedumpLib.Data.Extensions.ExtractSizeFromHashData(tawo?.ClrMameProData);
output.AppendLine("Common Disc Info:");

View File

@@ -1,8 +1,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using SabreTools.RedumpLib.Data;
using Constants = SabreTools.RedumpLib.RedumpOrg.Data.Constants;
using SubmissionInfo = SabreTools.RedumpLib.RedumpOrg.Data.SubmissionInfo;
namespace SabreTools.RedumpLib.RedumpOrg
namespace SabreTools.RedumpLib.RedumpOrg.Tools
{
public static class Validator
{

View File

@@ -1,6 +1,12 @@
using System;
using System.Text;
using SabreTools.RedumpLib.Data;
using SabreTools.RedumpLib.RedumpOrg.Data;
using DiscSubpath = SabreTools.RedumpLib.RedumpOrg.Data.DiscSubpath;
using Language = SabreTools.RedumpLib.RedumpOrg.Data.Language;
using PackType = SabreTools.RedumpLib.RedumpOrg.Data.PackType;
using PhysicalSystem = SabreTools.RedumpLib.RedumpOrg.Data.PhysicalSystem;
using Region = SabreTools.RedumpLib.RedumpOrg.Data.Region;
// TODO: Errors should validate number or number range (# or #-#)

View File

@@ -397,373 +397,6 @@ namespace SabreTools.RedumpLib.Tools
return true;
}
/// <summary>
/// Fill out an existing SubmissionInfo object based on a disc page
/// </summary>
/// <param name="client">RedumpClient for making the connection</param>
/// <param name="info">Existing SubmissionInfo object to fill</param>
/// <param name="id">Redump disc ID to retrieve</param>
/// <param name="includeAllData">True to include all pullable information, false to do bare minimum</param>
public static async Task<bool> FillFromId(RedumpOrg.Client client, RedumpOrg.SubmissionInfo info, int id, bool includeAllData)
{
var discData = await client.DownloadSingleDiscPage(id);
if (string.IsNullOrEmpty(discData))
return false;
// Title, Disc Number/Letter, Disc Title
var match = RedumpOrg.Constants.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(" (") ?? -1;
if (title is not null && firstParenLocation >= 0)
{
#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER
info.CommonDiscInfo.Title = title[..firstParenLocation];
#else
info.CommonDiscInfo.Title = title.Substring(0, firstParenLocation);
#endif
var submatches = RedumpOrg.Constants.DiscNumberLetterRegex.Matches(title);
foreach (Match? submatch in submatches)
{
if (submatch is null)
continue;
var submatchValue = submatch.Groups[1].Value;
// Disc number or letter
if (submatchValue.StartsWith("Disc"))
#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER
info.CommonDiscInfo.DiscNumberLetter = submatchValue["Disc ".Length..];
#else
info.CommonDiscInfo.DiscNumberLetter = submatchValue.Remove(0, "Disc ".Length);
#endif
// Issue number
else if (ulong.TryParse(submatchValue, out _))
info.CommonDiscInfo.Title += $" ({submatchValue})";
// Disc title
else
info.CommonDiscInfo.DiscTitle = submatchValue;
}
}
// Otherwise, leave the title as-is
else
{
info.CommonDiscInfo.Title = title;
}
}
// Foreign Title
match = RedumpOrg.Constants.ForeignTitleRegex.Match(discData);
if (match.Success)
info.CommonDiscInfo.ForeignTitleNonLatin = WebUtility.HtmlDecode(match.Groups[1].Value);
// Category
match = RedumpOrg.Constants.CategoryRegex.Match(discData);
if (match.Success)
info.CommonDiscInfo.Category = match.Groups[1].Value.ToDiscCategory();
else
info.CommonDiscInfo.Category = DiscCategory.Games;
// Region
if (info.CommonDiscInfo.Region is null)
{
match = RedumpOrg.Constants.RegionRegex.Match(discData);
if (match.Success)
info.CommonDiscInfo.Region = RedumpOrg.Extensions.ToRegion(match.Groups[1].Value);
}
// Languages
var matches = RedumpOrg.Constants.LanguagesRegex.Matches(discData);
if (matches.Count > 0)
{
var tempLanguages = new List<RedumpOrg.Language?>();
foreach (Match? submatch in matches)
{
if (submatch is null)
continue;
var language = RedumpOrg.Extensions.ToLanguage(submatch.Groups[1].Value);
if (language is not null)
tempLanguages.Add(language);
}
info.CommonDiscInfo.Languages = [.. tempLanguages];
}
// Serial
if (includeAllData)
{
// TODO: Re-enable if there's a way of verifying against a disc
//match = Constants.SerialRegex.Match(discData);
//if (match.Success)
// info.CommonDiscInfo.Serial = $"(VERIFY THIS) {WebUtility.HtmlDecode(match.Groups[1].Value)}";
}
// Error count
if (string.IsNullOrEmpty(info.CommonDiscInfo.ErrorsCount))
{
match = RedumpOrg.Constants.ErrorCountRegex.Match(discData);
if (match.Success)
info.CommonDiscInfo.ErrorsCount = match.Groups[1].Value;
}
// Version
if (info.VersionAndEditions.Version is null)
{
match = RedumpOrg.Constants.VersionRegex.Match(discData);
if (match.Success)
info.VersionAndEditions.Version = $"(VERIFY THIS) {WebUtility.HtmlDecode(match.Groups[1].Value)}";
}
// Dumpers
matches = RedumpOrg.Constants.DumpersRegex.Matches(discData);
if (matches.Count > 0)
{
// Start with any currently listed dumpers
var tempDumpers = new List<string>();
if (info.DumpersAndStatus.Dumpers is not null && info.DumpersAndStatus.Dumpers.Length > 0)
{
foreach (string dumper in info.DumpersAndStatus.Dumpers)
tempDumpers.Add(dumper);
}
foreach (Match? submatch in matches)
{
if (submatch is null)
continue;
string? dumper = WebUtility.HtmlDecode(submatch.Groups[1].Value);
if (dumper is not null)
tempDumpers.Add(dumper);
}
info.DumpersAndStatus.Dumpers = [.. tempDumpers];
}
// PS3 DiscKey
if (string.IsNullOrEmpty(info.Extras.DiscKey))
{
// Validate key is not NULL
match = RedumpOrg.Constants.PS3DiscKey.Match(discData);
if (match.Success && match.Groups[1].Value != "<span class=\"null\">NULL</span>")
info.Extras.DiscKey = match.Groups[1].Value;
}
// TODO: Unify handling of fields that can include site codes (Comments/Contents)
// Comments
if (includeAllData)
{
match = RedumpOrg.Constants.CommentsRegex.Match(discData);
if (match.Success)
{
// Process the old comments block
string oldComments = info.CommonDiscInfo.Comments
+ (string.IsNullOrEmpty(info.CommonDiscInfo.Comments) ? string.Empty : "\n")
+ (WebUtility.HtmlDecode(match.Groups[1].Value) ?? string.Empty)
.Replace("\r\n", "\n")
.Replace("<br />\n", "\n")
.Replace("<br />", string.Empty)
.Replace("</div>", string.Empty)
.Replace("[+]", string.Empty)
.ReplaceHtmlWithSiteCodes();
oldComments = Regex.Replace(oldComments, @"<div .*?>", string.Empty, RegexOptions.Compiled);
// Create state variables
bool addToLast = false;
SiteCode? lastSiteCode = null;
string newComments = string.Empty;
// Process the comments block line-by-line
string[] commentsSeparated = oldComments.Split('\n');
for (int i = 0; i < commentsSeparated.Length; i++)
{
string commentLine = commentsSeparated[i].Trim();
// If we have an empty line, we want to treat this as intentional
if (string.IsNullOrEmpty(commentLine))
{
addToLast = false;
lastSiteCode = null;
newComments += $"{commentLine}\n";
continue;
}
// Otherwise, we need to find what tag is in use
bool foundTag = false;
foreach (SiteCode? siteCode in Enum.GetValues(typeof(SiteCode)))
{
// If we have a null site code, just skip
if (siteCode is null)
continue;
// If the line doesn't contain this tag, just skip
var shortName = siteCode.ShortName();
if (shortName is null || !commentLine.Contains(shortName))
continue;
// Mark as having found a tag
foundTag = true;
// Cache the current site code
lastSiteCode = siteCode;
// A subset of tags can be multiline
addToLast = siteCode.IsMultiLine();
// Skip certain site codes because of data issues
if (ShouldSkipSiteCode(siteCode))
continue;
// If we don't already have this site code, add it to the dictionary
if (!info.CommonDiscInfo.CommentsSpecialFields.ContainsKey(siteCode.Value))
info.CommonDiscInfo.CommentsSpecialFields[siteCode.Value] = $"(VERIFY THIS) {commentLine.Replace(shortName, string.Empty).Trim()}";
// Otherwise, append the value to the existing key
else
info.CommonDiscInfo.CommentsSpecialFields[siteCode.Value] += $", {commentLine.Replace(shortName, string.Empty).Trim()}";
break;
}
// If we didn't find a known tag, just add the line, just in case
if (!foundTag)
{
if (addToLast && lastSiteCode is not null && !ShouldSkipSiteCode(lastSiteCode))
{
if (!string.IsNullOrEmpty(info.CommonDiscInfo.CommentsSpecialFields![lastSiteCode.Value]))
info.CommonDiscInfo.CommentsSpecialFields[lastSiteCode.Value] += "\n";
info.CommonDiscInfo.CommentsSpecialFields[lastSiteCode.Value] += commentLine;
}
else if (!addToLast || lastSiteCode is null)
{
newComments += $"{commentLine}\n";
}
}
}
// Set the new comments field
info.CommonDiscInfo.Comments = newComments;
}
}
// Contents
if (includeAllData)
{
match = RedumpOrg.Constants.ContentsRegex.Match(discData);
if (match.Success)
{
// Process the old contents block
string oldContents = info.CommonDiscInfo.Contents
+ (string.IsNullOrEmpty(info.CommonDiscInfo.Contents) ? string.Empty : "\n")
+ (WebUtility.HtmlDecode(match.Groups[1].Value) ?? string.Empty)
.Replace("\r\n", "\n")
.Replace("<br />\n", "\n")
.Replace("<br />", string.Empty)
.Replace("</div>", string.Empty)
.Replace("[+]", string.Empty)
.ReplaceHtmlWithSiteCodes();
oldContents = Regex.Replace(oldContents, @"<div .*?>", string.Empty, RegexOptions.Compiled);
// Create state variables
bool addToLast = false;
SiteCode? lastSiteCode = null;
string newContents = string.Empty;
// Process the contents block line-by-line
string[] contentsSeparated = oldContents.Split('\n');
for (int i = 0; i < contentsSeparated.Length; i++)
{
string contentLine = contentsSeparated[i].Trim();
// If we have an empty line, we want to treat this as intentional
if (string.IsNullOrEmpty(contentLine))
{
addToLast = false;
lastSiteCode = null;
newContents += $"{contentLine}\n";
continue;
}
// Otherwise, we need to find what tag is in use
bool foundTag = false;
foreach (SiteCode? siteCode in Enum.GetValues(typeof(SiteCode)))
{
// If we have a null site code, just skip
if (siteCode is null)
continue;
// If the line doesn't contain this tag, just skip
var shortName = siteCode.ShortName();
if (shortName is null || !contentLine.Contains(shortName))
continue;
// Cache the current site code
lastSiteCode = siteCode;
// If we don't already have this site code, add it to the dictionary
if (!info.CommonDiscInfo.ContentsSpecialFields.ContainsKey(siteCode.Value))
info.CommonDiscInfo.ContentsSpecialFields[siteCode.Value] = $"(VERIFY THIS) {contentLine.Replace(shortName, string.Empty).Trim()}";
// A subset of tags can be multiline
addToLast = siteCode.IsMultiLine();
// Mark as having found a tag
foundTag = true;
break;
}
// If we didn't find a known tag, just add the line, just in case
if (!foundTag)
{
if (addToLast && lastSiteCode is not null && !ShouldSkipSiteCode(lastSiteCode))
{
if (!string.IsNullOrEmpty(info.CommonDiscInfo.ContentsSpecialFields![lastSiteCode.Value]))
info.CommonDiscInfo.ContentsSpecialFields[lastSiteCode.Value] += "\n";
info.CommonDiscInfo.ContentsSpecialFields[lastSiteCode.Value] += contentLine;
}
else if (!addToLast || lastSiteCode is null)
{
newContents += $"{contentLine}\n";
}
}
}
// Set the new contents field
info.CommonDiscInfo.Contents = newContents;
}
}
// Added
match = RedumpOrg.Constants.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 = RedumpOrg.Constants.LastModifiedRegex.Match(discData);
if (match.Success)
{
if (DateTime.TryParse(match.Groups[1].Value, out DateTime lastModified))
info.LastModified = lastModified;
else
info.LastModified = null;
}
return true;
}
/// <summary>
/// Inject information from a seed SubmissionInfo into the existing one
/// </summary>