", string.Empty);
// 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.IsNullOrWhiteSpace(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 the line doesn't contain this tag, just skip
if (!commentLine.Contains(siteCode.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.CommentsSpecialFields.ContainsKey(siteCode))
info.CommonDiscInfo.CommentsSpecialFields[siteCode] = $"(VERIFY THIS) {commentLine.Replace(siteCode.ShortName(), string.Empty).Trim()}";
// A subset of tags can be multiline
addToLast = IsMultiLine(siteCode);
// 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 != null)
{
if (!string.IsNullOrWhiteSpace(info.CommonDiscInfo.CommentsSpecialFields[lastSiteCode]))
info.CommonDiscInfo.CommentsSpecialFields[lastSiteCode] += "\n";
info.CommonDiscInfo.CommentsSpecialFields[lastSiteCode] += commentLine;
}
else
{
newComments += $"{commentLine}\n";
}
}
}
// Set the new comments field
info.CommonDiscInfo.Comments = newComments;
}
// Contents
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)
.Replace("\r\n", "\n")
.Replace("
\n", "\n")
.Replace("
", string.Empty)
.Replace("
", string.Empty)
.Replace("[+]", string.Empty)
.ReplaceHtmlWithSiteCodes();
oldContents = Regex.Replace(oldContents, @"", string.Empty);
// 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.IsNullOrWhiteSpace(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 the line doesn't contain this tag, just skip
if (!contentLine.Contains(siteCode.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))
info.CommonDiscInfo.ContentsSpecialFields[siteCode] = $"(VERIFY THIS) {contentLine.Replace(siteCode.ShortName(), string.Empty).Trim()}";
// A subset of tags can be multiline
addToLast = IsMultiLine(siteCode);
// 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 != null)
{
if (!string.IsNullOrWhiteSpace(info.CommonDiscInfo.ContentsSpecialFields[lastSiteCode]))
info.CommonDiscInfo.ContentsSpecialFields[lastSiteCode] += "\n";
info.CommonDiscInfo.ContentsSpecialFields[lastSiteCode] += contentLine;
}
else
{
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;
}
}
///
/// Fill in a SubmissionInfo object from Redump, if possible
///
///
Options object representing user-defined options
///
Existing SubmissionInfo object to fill
///
Optional result progress callback
private static void FillFromRedump(Options options, SubmissionInfo info, IProgress
resultProgress = null)
{
// Set the current dumper based on username
info.DumpersAndStatus.Dumpers = new string[] { options.RedumpUsername };
info.MatchedIDs = new List();
using (RedumpWebClient wc = new RedumpWebClient())
{
// Login to Redump
bool? loggedIn = wc.Login(options.RedumpUsername, options.RedumpPassword);
if (loggedIn == null)
{
resultProgress?.Report(Result.Failure("There was an unknown error connecting to Redump"));
return;
}
else if (loggedIn == false)
{
// Don't log the as a failure or error
return;
}
// Loop through all of the hashdata to find matching IDs
bool allFound = true;
resultProgress?.Report(Result.Success("Finding disc matches on Redump..."));
string[] splitData = info.TracksAndWriteOffsets.ClrMameProData.Split('\n');
foreach (string hashData in splitData)
{
allFound &= ValidateSingleTrack(wc, info, hashData, resultProgress);
}
resultProgress?.Report(Result.Success("Match finding complete! " + (info.MatchedIDs.Count > 0
? "Matched IDs: " + string.Join(",", info.MatchedIDs)
: "No matches found")));
// Exit early if one failed or there are no matched IDs
if (!allFound || info.MatchedIDs.Count == 0)
return;
// Find the first matched ID where the track count matches, we can grab a bunch of info from it
int totalMatchedIDsCount = info.MatchedIDs.Count;
for (int i = 0; i < totalMatchedIDsCount; i++)
{
// Skip if the track count doesn't match
if (!ValidateTrackCount(wc, info.MatchedIDs[i], splitData.Length))
continue;
// Fill in the fields from the existing ID
resultProgress?.Report(Result.Success($"Filling fields from existing ID {info.MatchedIDs[i]}..."));
FillFromId(wc, info, info.MatchedIDs[0]);
resultProgress?.Report(Result.Success("Information filling complete!"));
// Set the matched IDs to just the current
info.MatchedIDs = new List { info.MatchedIDs[i] };
break;
}
}
}
///
/// Process a text block and replace with internal identifiers
///
/// Text block to process
/// Processed text block, if possible
private static string ReplaceHtmlWithSiteCodes(this string text)
{
if (string.IsNullOrWhiteSpace(text))
return text;
foreach (SiteCode? siteCode in Enum.GetValues(typeof(SiteCode)))
{
text = text.Replace(siteCode.LongName(), siteCode.ShortName());
}
// For some outdated tags, we need to use alternate names
text = text.Replace("Demos:", ((SiteCode?)SiteCode.PlayableDemos).ShortName());
text = text.Replace("DMI:", ((SiteCode?)SiteCode.DMIHash).ShortName());
text = text.Replace("LucasArts ID:", ((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("SSv2:", ((SiteCode?)SiteCode.SSHash).ShortName());
text = text.Replace("SS version:", ((SiteCode?)SiteCode.SSVersion).ShortName());
text = text.Replace("XeMID:", ((SiteCode?)SiteCode.XeMID).ShortName());
text = text.Replace("XMID:", ((SiteCode?)SiteCode.XMID).ShortName());
return text;
}
///
/// List the disc IDs associated with a given quicksearch query
///
/// RedumpWebClient for making the connection
/// Query string to attempt to search for
/// All disc IDs for the given query, null on error
private static List ListSearchResults(RedumpWebClient wc, 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 = wc.CheckSingleSitePage(string.Format(Constants.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;
}
///
/// Validate a single track against Redump, if possible
///
/// RedumpWebClient for making the connection
/// Existing SubmissionInfo object to fill
/// DAT-formatted hash data to parse out
/// Optional result progress callback
/// True if the track was found, false otherwise
private static bool ValidateSingleTrack(RedumpWebClient wc, SubmissionInfo info, string hashData, IProgress resultProgress = null)
{
// If the line isn't parseable, we can't validate
if (!GetISOHashValues(hashData, out long _, out string _, out string _, out string sha1))
{
resultProgress?.Report(Result.Failure("Line could not be parsed for hash data"));
return false;
}
// Get all matching IDs for the track
List newIds = ListSearchResults(wc, sha1);
// If we got null back, there was an error
if (newIds == null)
{
resultProgress?.Report(Result.Failure("There was an unknown error retrieving information from Redump"));
return false;
}
// If no IDs match any track, then we don't match a disc at all
if (!newIds.Any())
{
info.MatchedIDs = new List();
return false;
}
// If we have multiple tracks, only take IDs that are in common
if (info.MatchedIDs.Any())
info.MatchedIDs = info.MatchedIDs.Intersect(newIds).ToList();
// If we're on the first track, all IDs are added
else
info.MatchedIDs = newIds;
return true;
}
///
/// Validate that the current track count and remote track count match
///
/// RedumpWebClient for making the connection
/// Redump disc ID to retrieve
/// Local count of tracks for the current disc
/// True if the track count matches, false otherwise
private static bool ValidateTrackCount(RedumpWebClient wc, int id, int localCount)
{
// If we can't pull the remote data, we can't match
string discData = wc.DownloadSingleSiteID(id);
if (string.IsNullOrEmpty(discData))
return false;
// Discs with only 1 track don't have a track count listed
var match = Constants.TrackCountRegex.Match(discData);
if (!match.Success && localCount == 1)
return true;
else if (!match.Success)
return false;
// If the count isn't parseable, we're not taking chances
if (!Int32.TryParse(match.Groups[1].Value, out int remoteCount))
return false;
// Finally check to see if the counts match
return localCount == remoteCount;
}
#endregion
#region Helpers
///
/// Format a single site tag to string
///
/// KeyValuePair representing the site tag and value
/// String-formatted tag and value
private static string FormatSiteTag(KeyValuePair kvp)
{
bool isMultiLine = IsMultiLine(kvp.Key);
string line = $"{kvp.Key.ShortName()}{(isMultiLine ? "\n" : " ")}";
// Special case for boolean fields
if (IsBoolean(kvp.Key))
{
if (kvp.Value != true.ToString())
return string.Empty;
return line.Trim();
}
return $"{line}{kvp.Value}{(isMultiLine ? "\n" : string.Empty)}";
}
///
/// Check if a site code is boolean or not
///
/// SiteCode to check
/// True if the code field is a flag with no value, false otherwise
/// TODO: This should move to Extensions at some point
private static bool IsBoolean(SiteCode? siteCode)
{
switch (siteCode)
{
case SiteCode.PostgapType:
case SiteCode.VCD:
return true;
default:
return false;
}
}
///
/// Check if a site code is multi-line or not
///
/// SiteCode to check
/// True if the code field is multiline by default, false otherwise
/// TODO: This should move to Extensions at some point
private static bool IsMultiLine(SiteCode? siteCode)
{
switch (siteCode)
{
case SiteCode.Extras:
case SiteCode.Games:
case SiteCode.GameFootage:
case SiteCode.NetYarozeGames:
case SiteCode.Patches:
case SiteCode.PlayableDemos:
case SiteCode.RollingDemos:
case SiteCode.Savegames:
case SiteCode.TechDemos:
case SiteCode.Videos:
return true;
default:
return false;
}
}
///
/// Order comment code tags according to Redump requirements
///
/// Ordered list of KeyValuePairs representing the tags and values
private static List> OrderCommentTags(Dictionary tags)
{
var sorted = new List>();
// If the input is invalid, just return an empty set
if (tags == null || tags.Count == 0)
return sorted;
// Identifying Info
if (tags.ContainsKey(SiteCode.AlternativeTitle))
sorted.Add(new KeyValuePair(SiteCode.AlternativeTitle, tags[SiteCode.AlternativeTitle]));
if (tags.ContainsKey(SiteCode.AlternativeForeignTitle))
sorted.Add(new KeyValuePair(SiteCode.AlternativeForeignTitle, tags[SiteCode.AlternativeForeignTitle]));
if (tags.ContainsKey(SiteCode.InternalSerialName))
sorted.Add(new KeyValuePair(SiteCode.InternalSerialName, tags[SiteCode.InternalSerialName]));
if (tags.ContainsKey(SiteCode.VolumeLabel))
sorted.Add(new KeyValuePair(SiteCode.VolumeLabel, tags[SiteCode.VolumeLabel]));
if (tags.ContainsKey(SiteCode.XMID))
sorted.Add(new KeyValuePair(SiteCode.XMID, tags[SiteCode.XMID]));
if (tags.ContainsKey(SiteCode.XeMID))
sorted.Add(new KeyValuePair(SiteCode.XeMID, tags[SiteCode.XeMID]));
if (tags.ContainsKey(SiteCode.DMIHash))
sorted.Add(new KeyValuePair(SiteCode.DMIHash, tags[SiteCode.DMIHash]));
if (tags.ContainsKey(SiteCode.PFIHash))
sorted.Add(new KeyValuePair(SiteCode.PFIHash, tags[SiteCode.PFIHash]));
if (tags.ContainsKey(SiteCode.SSHash))
sorted.Add(new KeyValuePair(SiteCode.SSHash, tags[SiteCode.SSHash]));
if (tags.ContainsKey(SiteCode.SSVersion))
sorted.Add(new KeyValuePair(SiteCode.SSVersion, tags[SiteCode.SSVersion]));
if (tags.ContainsKey(SiteCode.BBFCRegistrationNumber))
sorted.Add(new KeyValuePair(SiteCode.BBFCRegistrationNumber, tags[SiteCode.BBFCRegistrationNumber]));
if (tags.ContainsKey(SiteCode.DiscHologramID))
sorted.Add(new KeyValuePair(SiteCode.DiscHologramID, tags[SiteCode.DiscHologramID]));
if (tags.ContainsKey(SiteCode.DNASDiscID))
sorted.Add(new KeyValuePair(SiteCode.DNASDiscID, tags[SiteCode.DNASDiscID]));
if (tags.ContainsKey(SiteCode.ISBN))
sorted.Add(new KeyValuePair(SiteCode.ISBN, tags[SiteCode.ISBN]));
if (tags.ContainsKey(SiteCode.ISSN))
sorted.Add(new KeyValuePair(SiteCode.ISSN, tags[SiteCode.ISSN]));
if (tags.ContainsKey(SiteCode.PPN))
sorted.Add(new KeyValuePair(SiteCode.PPN, tags[SiteCode.PPN]));
if (tags.ContainsKey(SiteCode.VFCCode))
sorted.Add(new KeyValuePair(SiteCode.VFCCode, tags[SiteCode.VFCCode]));
if (tags.ContainsKey(SiteCode.Genre))
sorted.Add(new KeyValuePair(SiteCode.Genre, tags[SiteCode.Genre]));
if (tags.ContainsKey(SiteCode.Series))
sorted.Add(new KeyValuePair(SiteCode.Series, tags[SiteCode.Series]));
if (tags.ContainsKey(SiteCode.PostgapType))
sorted.Add(new KeyValuePair(SiteCode.PostgapType, tags[SiteCode.PostgapType]));
if (tags.ContainsKey(SiteCode.VCD))
sorted.Add(new KeyValuePair(SiteCode.VCD, tags[SiteCode.VCD]));
// Publisher / Company IDs
if (tags.ContainsKey(SiteCode.AcclaimID))
sorted.Add(new KeyValuePair(SiteCode.AcclaimID, tags[SiteCode.AcclaimID]));
if (tags.ContainsKey(SiteCode.ActivisionID))
sorted.Add(new KeyValuePair(SiteCode.ActivisionID, tags[SiteCode.ActivisionID]));
if (tags.ContainsKey(SiteCode.BandaiID))
sorted.Add(new KeyValuePair(SiteCode.BandaiID, tags[SiteCode.BandaiID]));
if (tags.ContainsKey(SiteCode.ElectronicArtsID))
sorted.Add(new KeyValuePair(SiteCode.ElectronicArtsID, tags[SiteCode.ElectronicArtsID]));
if (tags.ContainsKey(SiteCode.FoxInteractiveID))
sorted.Add(new KeyValuePair(SiteCode.FoxInteractiveID, tags[SiteCode.FoxInteractiveID]));
if (tags.ContainsKey(SiteCode.GTInteractiveID))
sorted.Add(new KeyValuePair(SiteCode.GTInteractiveID, tags[SiteCode.GTInteractiveID]));
if (tags.ContainsKey(SiteCode.JASRACID))
sorted.Add(new KeyValuePair(SiteCode.JASRACID, tags[SiteCode.JASRACID]));
if (tags.ContainsKey(SiteCode.KingRecordsID))
sorted.Add(new KeyValuePair(SiteCode.KingRecordsID, tags[SiteCode.KingRecordsID]));
if (tags.ContainsKey(SiteCode.KoeiID))
sorted.Add(new KeyValuePair(SiteCode.KoeiID, tags[SiteCode.KoeiID]));
if (tags.ContainsKey(SiteCode.KonamiID))
sorted.Add(new KeyValuePair(SiteCode.KonamiID, tags[SiteCode.KonamiID]));
if (tags.ContainsKey(SiteCode.LucasArtsID))
sorted.Add(new KeyValuePair(SiteCode.LucasArtsID, tags[SiteCode.LucasArtsID]));
if (tags.ContainsKey(SiteCode.MicrosoftID))
sorted.Add(new KeyValuePair(SiteCode.MicrosoftID, tags[SiteCode.MicrosoftID]));
if (tags.ContainsKey(SiteCode.NaganoID))
sorted.Add(new KeyValuePair(SiteCode.NaganoID, tags[SiteCode.NaganoID]));
if (tags.ContainsKey(SiteCode.NamcoID))
sorted.Add(new KeyValuePair(SiteCode.NamcoID, tags[SiteCode.NamcoID]));
if (tags.ContainsKey(SiteCode.NipponIchiSoftwareID))
sorted.Add(new KeyValuePair(SiteCode.NipponIchiSoftwareID, tags[SiteCode.NipponIchiSoftwareID]));
if (tags.ContainsKey(SiteCode.OriginID))
sorted.Add(new KeyValuePair(SiteCode.OriginID, tags[SiteCode.OriginID]));
if (tags.ContainsKey(SiteCode.PonyCanyonID))
sorted.Add(new KeyValuePair(SiteCode.PonyCanyonID, tags[SiteCode.PonyCanyonID]));
if (tags.ContainsKey(SiteCode.SegaID))
sorted.Add(new KeyValuePair(SiteCode.SegaID, tags[SiteCode.SegaID]));
if (tags.ContainsKey(SiteCode.SelenID))
sorted.Add(new KeyValuePair(SiteCode.SelenID, tags[SiteCode.SelenID]));
if (tags.ContainsKey(SiteCode.SierraID))
sorted.Add(new KeyValuePair(SiteCode.SierraID, tags[SiteCode.SierraID]));
if (tags.ContainsKey(SiteCode.TaitoID))
sorted.Add(new KeyValuePair(SiteCode.TaitoID, tags[SiteCode.TaitoID]));
if (tags.ContainsKey(SiteCode.UbisoftID))
sorted.Add(new KeyValuePair(SiteCode.UbisoftID, tags[SiteCode.UbisoftID]));
if (tags.ContainsKey(SiteCode.ValveID))
sorted.Add(new KeyValuePair(SiteCode.ValveID, tags[SiteCode.ValveID]));
return sorted;
}
///
/// Order content code tags according to Redump requirements
///
/// Ordered list of KeyValuePairs representing the tags and values
private static List> OrderContentTags(Dictionary tags)
{
var sorted = new List>();
// If the input is invalid, just return an empty set
if (tags == null || tags.Count == 0)
return sorted;
// Games
if (tags.ContainsKey(SiteCode.Games))
sorted.Add(new KeyValuePair(SiteCode.Games, tags[SiteCode.Games]));
if (tags.ContainsKey(SiteCode.NetYarozeGames))
sorted.Add(new KeyValuePair(SiteCode.NetYarozeGames, tags[SiteCode.NetYarozeGames]));
// Demos
if (tags.ContainsKey(SiteCode.PlayableDemos))
sorted.Add(new KeyValuePair(SiteCode.PlayableDemos, tags[SiteCode.PlayableDemos]));
if (tags.ContainsKey(SiteCode.RollingDemos))
sorted.Add(new KeyValuePair(SiteCode.RollingDemos, tags[SiteCode.RollingDemos]));
if (tags.ContainsKey(SiteCode.TechDemos))
sorted.Add(new KeyValuePair(SiteCode.TechDemos, tags[SiteCode.TechDemos]));
// Video
if (tags.ContainsKey(SiteCode.GameFootage))
sorted.Add(new KeyValuePair(SiteCode.GameFootage, tags[SiteCode.GameFootage]));
if (tags.ContainsKey(SiteCode.Videos))
sorted.Add(new KeyValuePair(SiteCode.Videos, tags[SiteCode.Videos]));
// Miscellaneous
if (tags.ContainsKey(SiteCode.Patches))
sorted.Add(new KeyValuePair(SiteCode.Patches, tags[SiteCode.Patches]));
if (tags.ContainsKey(SiteCode.Savegames))
sorted.Add(new KeyValuePair(SiteCode.Savegames, tags[SiteCode.Savegames]));
if (tags.ContainsKey(SiteCode.Extras))
sorted.Add(new KeyValuePair(SiteCode.Extras, tags[SiteCode.Extras]));
return sorted;
}
#endregion
}
}