using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using BurnOutSharp; using MPF.Core.Data; using MPF.Core.Utilities; using MPF.Modules; using Newtonsoft.Json; using RedumpLib.Data; using RedumpLib.Web; namespace MPF.Library { public static class InfoTool { #region Information Extraction /// /// Extract all of the possible information from a given input combination /// /// Output folder to write to /// Output filename to use as the base path /// Drive object representing the current drive /// Currently selected system /// Currently selected media type /// Options object representing user-defined options /// Parameters object representing what to send to the internal program /// Optional result progress callback /// Optional protection progress callback /// SubmissionInfo populated based on outputs, null on error public static async Task ExtractOutputInformation( string outputDirectory, string outputFilename, Drive drive, RedumpSystem? system, MediaType? mediaType, Options options, BaseParameters parameters, IProgress resultProgress = null, IProgress protectionProgress = null) { // Ensure the current disc combination should exist if (!system.MediaTypes().Contains(mediaType)) return null; // Sanitize the output filename to strip off any potential extension outputFilename = Path.GetFileNameWithoutExtension(outputFilename); // Check that all of the relevant files are there (bool foundFiles, List missingFiles) = FoundAllFiles(outputDirectory, outputFilename, parameters, false); if (!foundFiles) { resultProgress.Report(Result.Failure($"There were files missing from the output:\n{string.Join("\n", missingFiles)}")); return null; } // Create the SubmissionInfo object with all user-inputted values by default string combinedBase = Path.Combine(outputDirectory, outputFilename); SubmissionInfo info = new SubmissionInfo() { CommonDiscInfo = new CommonDiscInfoSection() { System = system, Media = mediaType.ToDiscType(), Title = (options.AddPlaceholders ? Template.RequiredValue : string.Empty), ForeignTitleNonLatin = (options.AddPlaceholders ? Template.OptionalValue : string.Empty), DiscNumberLetter = (options.AddPlaceholders ? Template.OptionalValue : string.Empty), DiscTitle = (options.AddPlaceholders ? Template.OptionalValue : string.Empty), Category = null, Region = null, Languages = null, Serial = (options.AddPlaceholders ? Template.RequiredIfExistsValue : string.Empty), Barcode = (options.AddPlaceholders ? Template.OptionalValue : string.Empty), Contents = (options.AddPlaceholders ? Template.OptionalValue : string.Empty), Comments = string.Empty, }, VersionAndEditions = new VersionAndEditionsSection() { Version = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""), OtherEditions = (options.AddPlaceholders ? "Original (VERIFY THIS)" : ""), }, TracksAndWriteOffsets = new TracksAndWriteOffsetsSection(), }; // Get specific tool output handling parameters.GenerateSubmissionInfo(info, combinedBase, drive, options.IncludeArtifacts); // Get a list of matching IDs for each line in the DAT if (!string.IsNullOrEmpty(info.TracksAndWriteOffsets.ClrMameProData) && options.HasRedumpLogin) FillFromRedump(options, info, resultProgress); // If we have both ClrMamePro and Size and Checksums data, remove the ClrMamePro if (!string.IsNullOrWhiteSpace(info.SizeAndChecksums.CRC32)) info.TracksAndWriteOffsets.ClrMameProData = null; // Add the volume label to comments, if possible if (!string.IsNullOrWhiteSpace(drive?.VolumeLabel) && !info.CommonDiscInfo.Comments.Contains(Constants.VolumeLabelCommentField)) info.CommonDiscInfo.Comments += $"{Constants.VolumeLabelCommentField} {drive.VolumeLabel}\n"; // Extract info based generically on MediaType switch (mediaType) { case MediaType.CDROM: case MediaType.GDROM: // TODO: Verify GD-ROM outputs this info.CommonDiscInfo.Layer0MasteringRing = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0MasteringSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0ToolstampMasteringCode = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0MouldSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer1MouldSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0AdditionalMould = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); break; case MediaType.DVD: case MediaType.HDDVD: case MediaType.BluRay: // If we have a single-layer disc if (info.SizeAndChecksums.Layerbreak == default) { info.CommonDiscInfo.Layer0MasteringRing = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0MasteringSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0ToolstampMasteringCode = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0MouldSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer1MouldSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0AdditionalMould = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); } // If we have a dual-layer disc else { info.CommonDiscInfo.Layer0MasteringRing = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0MasteringSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0ToolstampMasteringCode = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0MouldSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0AdditionalMould = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer1MasteringRing = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer1MasteringSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer1ToolstampMasteringCode = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer1MouldSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); } break; case MediaType.NintendoGameCubeGameDisc: info.CommonDiscInfo.Layer0MasteringRing = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0MasteringSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0ToolstampMasteringCode = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0MouldSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer1MouldSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0AdditionalMould = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.Extras.BCA = info.Extras.BCA ?? (options.AddPlaceholders ? Template.RequiredValue : ""); break; case MediaType.NintendoWiiOpticalDisc: // If we have a single-layer disc if (info.SizeAndChecksums.Layerbreak == default) { info.CommonDiscInfo.Layer0MasteringRing = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0MasteringSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0ToolstampMasteringCode = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0MouldSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer1MouldSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0AdditionalMould = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); } // If we have a dual-layer disc else { info.CommonDiscInfo.Layer0MasteringRing = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0MasteringSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0ToolstampMasteringCode = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0MouldSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0AdditionalMould = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer1MasteringRing = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer1MasteringSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer1ToolstampMasteringCode = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer1MouldSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); } info.Extras.DiscKey = (options.AddPlaceholders ? Template.RequiredValue : ""); info.Extras.BCA = info.Extras.BCA ?? (options.AddPlaceholders ? Template.RequiredValue : ""); break; case MediaType.UMD: // If we have a single-layer disc if (info.SizeAndChecksums.Layerbreak == default) { info.CommonDiscInfo.Layer0MasteringRing = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0MasteringSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0ToolstampMasteringCode = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0MouldSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); } // If we have a dual-layer disc else { info.CommonDiscInfo.Layer0MasteringRing = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0MasteringSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0ToolstampMasteringCode = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer0MouldSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer1MasteringRing = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer1MasteringSID = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); info.CommonDiscInfo.Layer1ToolstampMasteringCode = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); } info.SizeAndChecksums.CRC32 = info.SizeAndChecksums.CRC32 ?? (options.AddPlaceholders ? Template.RequiredValue + " [Not automatically generated for UMD]" : ""); info.SizeAndChecksums.MD5 = info.SizeAndChecksums.MD5 ?? (options.AddPlaceholders ? Template.RequiredValue + " [Not automatically generated for UMD]" : ""); info.SizeAndChecksums.SHA1 = info.SizeAndChecksums.SHA1 ?? (options.AddPlaceholders ? Template.RequiredValue + " [Not automatically generated for UMD]" : ""); info.TracksAndWriteOffsets.ClrMameProData = null; break; } // Extract info based specifically on RedumpSystem switch (system) { case RedumpSystem.AcornArchimedes: info.CommonDiscInfo.Region = info.CommonDiscInfo.Region ?? Region.UK; break; case RedumpSystem.AppleMacintosh: case RedumpSystem.EnhancedCD: case RedumpSystem.IBMPCcompatible: case RedumpSystem.PalmOS: case RedumpSystem.PocketPC: case RedumpSystem.RainbowDisc: if (string.IsNullOrWhiteSpace(info.CommonDiscInfo.Comments)) info.CommonDiscInfo.Comments += $"{Constants.ISBNCommentField} {(options.AddPlaceholders ? Template.OptionalValue : "")}"; resultProgress?.Report(Result.Success("Running copy protection scan... this might take a while!")); info.CopyProtection.Protection = await GetCopyProtection(drive, options, protectionProgress); resultProgress?.Report(Result.Success("Copy protection scan complete!")); break; case RedumpSystem.AudioCD: case RedumpSystem.DVDAudio: case RedumpSystem.SuperAudioCD: info.CommonDiscInfo.Category = info.CommonDiscInfo.Category ?? DiscCategory.Audio; break; case RedumpSystem.BandaiPlaydiaQuickInteractiveSystem: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); info.CommonDiscInfo.Region = info.CommonDiscInfo.Region ?? Region.Japan; break; case RedumpSystem.BDVideo: info.CommonDiscInfo.Category = info.CommonDiscInfo.Category ?? DiscCategory.BonusDiscs; info.CopyProtection.Protection = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); break; case RedumpSystem.CommodoreAmigaCD: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); break; case RedumpSystem.CommodoreAmigaCD32: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); info.CommonDiscInfo.Region = info.CommonDiscInfo.Region ?? Region.Europe; break; case RedumpSystem.CommodoreAmigaCDTV: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); info.CommonDiscInfo.Region = info.CommonDiscInfo.Region ?? Region.Europe; break; case RedumpSystem.DVDVideo: info.CommonDiscInfo.Category = info.CommonDiscInfo.Category ?? DiscCategory.BonusDiscs; break; case RedumpSystem.FujitsuFMTownsseries: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); info.CommonDiscInfo.Region = info.CommonDiscInfo.Region ?? Region.Japan; break; case RedumpSystem.FujitsuFMTownsMarty: info.CommonDiscInfo.Region = info.CommonDiscInfo.Region ?? Region.Japan; break; case RedumpSystem.IncredibleTechnologiesEagle: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); break; case RedumpSystem.KonamieAmusement: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); break; case RedumpSystem.KonamiFireBeat: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); break; case RedumpSystem.KonamiSystemGV: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); break; case RedumpSystem.KonamiSystem573: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); break; case RedumpSystem.KonamiTwinkle: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); break; case RedumpSystem.MattelHyperScan: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); break; case RedumpSystem.NamcoSegaNintendoTriforce: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); break; case RedumpSystem.NavisoftNaviken21: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); info.CommonDiscInfo.Region = info.CommonDiscInfo.Region ?? Region.Japan; break; case RedumpSystem.NECPC88series: info.CommonDiscInfo.Region = info.CommonDiscInfo.Region ?? Region.Japan; break; case RedumpSystem.NECPC98series: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); info.CommonDiscInfo.Region = info.CommonDiscInfo.Region ?? Region.Japan; break; case RedumpSystem.NECPCFXPCFXGA: info.CommonDiscInfo.Region = info.CommonDiscInfo.Region ?? Region.Japan; break; case RedumpSystem.SegaChihiro: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); break; case RedumpSystem.SegaDreamcast: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); break; case RedumpSystem.SegaNaomi: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); break; case RedumpSystem.SegaNaomi2: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); break; case RedumpSystem.SegaTitanVideo: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); break; case RedumpSystem.SharpX68000: info.CommonDiscInfo.Region = info.CommonDiscInfo.Region ?? Region.Japan; break; case RedumpSystem.SNKNeoGeoCD: info.CommonDiscInfo.EXEDateBuildDate = (options.AddPlaceholders ? Template.RequiredValue : ""); break; case RedumpSystem.SonyPlayStation: if (drive != null) { resultProgress?.Report(Result.Success("Checking for anti-modchip strings... this might take a while!")); info.CopyProtection.AntiModchip = await GetAntiModchipDetected(drive) ? YesNo.Yes : YesNo.No; resultProgress?.Report(Result.Success("Anti-modchip string scan complete!")); } // Special case for DIC only if (parameters.InternalProgram == InternalProgram.DiscImageCreator) { resultProgress?.Report(Result.Success("Checking for LibCrypt status... this might take a while!")); GetLibCryptDetected(info, combinedBase); resultProgress?.Report(Result.Success("LibCrypt status checking complete!")); } break; case RedumpSystem.SonyPlayStation2: info.CommonDiscInfo.LanguageSelection = new LanguageSelection?[] { LanguageSelection.BiosSettings, LanguageSelection.LanguageSelector, LanguageSelection.OptionsMenu }; break; case RedumpSystem.SonyPlayStation3: info.Extras.DiscKey = (options.AddPlaceholders ? Template.RequiredValue : ""); info.Extras.DiscID = (options.AddPlaceholders ? Template.RequiredValue : ""); break; case RedumpSystem.TomyKissSite: info.CommonDiscInfo.Region = info.CommonDiscInfo.Region ?? Region.Japan; break; case RedumpSystem.ZAPiTGamesGameWaveFamilyEntertainmentSystem: info.CopyProtection.Protection = (options.AddPlaceholders ? Template.RequiredIfExistsValue : ""); break; } // Set the category if it's not overriden info.CommonDiscInfo.Category = info.CommonDiscInfo.Category ?? DiscCategory.Games; // Comments is one of the few fields with odd handling if (string.IsNullOrEmpty(info.CommonDiscInfo.Comments)) info.CommonDiscInfo.Comments = (options.AddPlaceholders ? Template.OptionalValue : ""); return info; } /// /// Ensures that all required output files have been created /// /// Output folder to write to /// Output filename to use as the base path /// Parameters object representing what to send to the internal program /// True if this is a check done before a dump, false if done after /// Tuple of true if all required files exist, false otherwise and a list representing missing files public static (bool, List) FoundAllFiles(string outputDirectory, string outputFilename, BaseParameters parameters, bool preCheck) { // First, sanitized the output filename to strip off any potential extension outputFilename = Path.GetFileNameWithoutExtension(outputFilename); // Then get the base path for all checking string basePath = Path.Combine(outputDirectory, outputFilename); // Finally, let the parameters say if all files exist return parameters.CheckAllOutputFilesExist(basePath, preCheck); } /// /// Get the existence of an anti-modchip string from a PlayStation disc, if possible /// /// Drive object representing the current drive /// Anti-modchip existence if possible, false on error private static async Task GetAntiModchipDetected(Drive drive) => await Protection.GetPlayStationAntiModchipDetected($"{drive.Letter}:\\"); /// /// Get the current copy protection scheme, if possible /// /// Drive object representing the current drive /// Options object that determines what to scan /// Optional progress callback /// Copy protection scheme if possible, null on error private static async Task GetCopyProtection(Drive drive, Options options, IProgress progress = null) { if (options.ScanForProtection && drive != null) { (bool success, string output) = await Protection.RunProtectionScanOnPath($"{drive.Letter}:\\", options, progress); if (success) return output; else return "An error occurred while scanning!"; } return "(CHECK WITH PROTECTIONID)"; } /// /// Get the full lines from the input file, if possible /// /// file location /// True if should read as binary, false otherwise (default) /// Full text of the file, null on error private static string GetFullFile(string filename, bool binary = false) { // If the file doesn't exist, we can't get info from it if (!File.Exists(filename)) return null; // If we're reading as binary if (binary) { byte[] bytes = File.ReadAllBytes(filename); return BitConverter.ToString(bytes).Replace("-", string.Empty); } return string.Join("\n", File.ReadAllLines(filename)); } /// /// Get the split values for ISO-based media /// /// String representing the combined hash data /// True if extraction was successful, false otherwise private static bool GetISOHashValues(string hashData, out long size, out string crc32, out string md5, out string sha1) { size = -1; crc32 = null; md5 = null; sha1 = null; if (string.IsNullOrWhiteSpace(hashData)) return false; Regex hashreg = new Regex(@" /// Get if LibCrypt data is detected in the subchannel file, if possible /// /// Base submission info to fill in specifics for /// Base filename and path to use for checking /// Status of the LibCrypt data, if possible private static void GetLibCryptDetected(SubmissionInfo info, string basePath) { bool? psLibCryptStatus = Protection.GetLibCryptDetected(basePath + ".sub"); if (psLibCryptStatus == true) { // Guard against false positives if (File.Exists(basePath + "_subIntention.txt")) { string libCryptData = GetFullFile(basePath + "_subIntention.txt") ?? ""; if (string.IsNullOrEmpty(libCryptData)) { info.CopyProtection.LibCrypt = YesNo.No; } else { info.CopyProtection.LibCrypt = YesNo.Yes; info.CopyProtection.LibCryptData = libCryptData; } } else { info.CopyProtection.LibCrypt = YesNo.No; } } else if (psLibCryptStatus == false) { info.CopyProtection.LibCrypt = YesNo.No; } else { info.CopyProtection.LibCrypt = YesNo.NULL; info.CopyProtection.LibCryptData = "LibCrypt could not be detected because subchannel file is missing"; } } #endregion #region Information Output /// /// Compress log files to save space /// /// Output folder to write to /// Output filename to use as the base path /// Parameters object to use to derive log file paths /// True if the process succeeded, false otherwise public static bool CompressLogFiles(string outputDirectory, string outputFilename, BaseParameters parameters) { // Prepare the necessary paths outputFilename = Path.GetFileNameWithoutExtension(outputFilename); string combinedBase = Path.Combine(outputDirectory, outputFilename); string archiveName = combinedBase + "_logs.zip"; // Get the list of log files from the parameters object var files = parameters.GetLogFilePaths(combinedBase); if (!files.Any()) return true; // Add the log files to the archive and delete the uncompressed file after ZipArchive zf = null; try { zf = ZipFile.Open(archiveName, ZipArchiveMode.Create); foreach (string file in files) { string entryName = file.Substring(outputDirectory.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); zf.CreateEntryFromFile(file, entryName); try { File.Delete(file); } catch { } } return true; } catch { return false; } finally { zf?.Dispose(); } } /// /// Format the output data in a human readable way, separating each printed line into a new item in the list /// /// Information object that should contain normalized values /// List of strings representing each line of an output file, null on error public static List FormatOutputData(SubmissionInfo info) { // Check to see if the inputs are valid if (info == null) return null; try { // Sony-printed discs have layers in the opposite order var system = info.CommonDiscInfo.System; bool reverseOrder = system.HasReversedRingcodes(); // Common Disc Info section List output = new List { "Common Disc Info:" }; AddIfExists(output, Template.TitleField, info.CommonDiscInfo.Title, 1); AddIfExists(output, Template.ForeignTitleField, info.CommonDiscInfo.ForeignTitleNonLatin, 1); AddIfExists(output, Template.DiscNumberField, info.CommonDiscInfo.DiscNumberLetter, 1); AddIfExists(output, Template.DiscTitleField, info.CommonDiscInfo.DiscTitle, 1); AddIfExists(output, Template.SystemField, info.CommonDiscInfo.System.LongName(), 1); AddIfExists(output, Template.MediaTypeField, GetFixedMediaType( info.CommonDiscInfo.Media.ToMediaType(), info.SizeAndChecksums.Size, info.SizeAndChecksums.Layerbreak, info.SizeAndChecksums.Layerbreak2, info.SizeAndChecksums.Layerbreak3), 1); AddIfExists(output, Template.CategoryField, info.CommonDiscInfo.Category.LongName(), 1); AddIfExists(output, Template.MatchingIDsField, info.MatchedIDs, 1); AddIfExists(output, Template.RegionField, info.CommonDiscInfo.Region.LongName() ?? "SPACE! (CHANGE THIS)", 1); AddIfExists(output, Template.LanguagesField, (info.CommonDiscInfo.Languages ?? new Language?[] { null }).Select(l => l.LongName() ?? "Klingon (CHANGE THIS)").ToArray(), 1); AddIfExists(output, Template.PlaystationLanguageSelectionViaField, (info.CommonDiscInfo.LanguageSelection ?? new LanguageSelection?[] { }).Select(l => l.LongName()).ToArray(), 1); AddIfExists(output, Template.DiscSerialField, info.CommonDiscInfo.Serial, 1); // All ringcode information goes in an indented area output.Add(""); output.Add("\tRingcode Information:"); // If we have a triple-layer disc if (info.SizeAndChecksums.Layerbreak3 != default) { AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.MasteringRingField, info.CommonDiscInfo.Layer0MasteringRing, 2); AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.MasteringSIDField, info.CommonDiscInfo.Layer0MasteringSID, 2); AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.ToolstampField, info.CommonDiscInfo.Layer0ToolstampMasteringCode, 2); AddIfExists(output, "Data Side " + Template.MouldSIDField, info.CommonDiscInfo.Layer0MouldSID, 2); AddIfExists(output, "Data Side " + Template.AdditionalMouldField, info.CommonDiscInfo.Layer0AdditionalMould, 2); AddIfExists(output, "Layer 1 " + Template.MasteringRingField, info.CommonDiscInfo.Layer1MasteringRing, 2); AddIfExists(output, "Layer 1 " + Template.MasteringSIDField, info.CommonDiscInfo.Layer1MasteringSID, 2); AddIfExists(output, "Layer 1 " + Template.ToolstampField, info.CommonDiscInfo.Layer1ToolstampMasteringCode, 2); AddIfExists(output, "Label Side " + Template.MouldSIDField, info.CommonDiscInfo.Layer1MouldSID, 2); AddIfExists(output, "Label Side " + Template.AdditionalMouldField, info.CommonDiscInfo.Layer1AdditionalMould, 2); AddIfExists(output, "Layer 2 " + Template.MasteringRingField, info.CommonDiscInfo.Layer2MasteringRing, 2); AddIfExists(output, "Layer 2 " + Template.MasteringSIDField, info.CommonDiscInfo.Layer2MasteringSID, 2); AddIfExists(output, "Layer 2 " + Template.ToolstampField, info.CommonDiscInfo.Layer2ToolstampMasteringCode, 2); AddIfExists(output, (reverseOrder ? "Layer 3 (Inner) " : "Layer 3 (Outer) ") + Template.MasteringRingField, info.CommonDiscInfo.Layer3MasteringRing, 2); AddIfExists(output, (reverseOrder ? "Layer 3 (Inner) " : "Layer 3 (Outer) ") + Template.MasteringSIDField, info.CommonDiscInfo.Layer3MasteringSID, 2); AddIfExists(output, (reverseOrder ? "Layer 3 (Inner) " : "Layer 3 (Outer) ") + Template.ToolstampField, info.CommonDiscInfo.Layer3ToolstampMasteringCode, 2); } // If we have a triple-layer disc else if (info.SizeAndChecksums.Layerbreak2 != default) { AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.MasteringRingField, info.CommonDiscInfo.Layer0MasteringRing, 2); AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.MasteringSIDField, info.CommonDiscInfo.Layer0MasteringSID, 2); AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.ToolstampField, info.CommonDiscInfo.Layer0ToolstampMasteringCode, 2); AddIfExists(output, "Data Side " + Template.MouldSIDField, info.CommonDiscInfo.Layer0MouldSID, 2); AddIfExists(output, "Data Side " + Template.AdditionalMouldField, info.CommonDiscInfo.Layer0AdditionalMould, 2); AddIfExists(output, "Layer 1 " + Template.MasteringRingField, info.CommonDiscInfo.Layer1MasteringRing, 2); AddIfExists(output, "Layer 1 " + Template.MasteringSIDField, info.CommonDiscInfo.Layer1MasteringSID, 2); AddIfExists(output, "Layer 1 " + Template.ToolstampField, info.CommonDiscInfo.Layer1ToolstampMasteringCode, 2); AddIfExists(output, "Label Side " + Template.MouldSIDField, info.CommonDiscInfo.Layer1MouldSID, 2); AddIfExists(output, "Label Side " + Template.AdditionalMouldField, info.CommonDiscInfo.Layer1AdditionalMould, 2); AddIfExists(output, (reverseOrder ? "Layer 2 (Inner) " : "Layer 2 (Outer) ") + Template.MasteringRingField, info.CommonDiscInfo.Layer2MasteringRing, 2); AddIfExists(output, (reverseOrder ? "Layer 2 (Inner) " : "Layer 2 (Outer) ") + Template.MasteringSIDField, info.CommonDiscInfo.Layer2MasteringSID, 2); AddIfExists(output, (reverseOrder ? "Layer 2 (Inner) " : "Layer 2 (Outer) ") + Template.ToolstampField, info.CommonDiscInfo.Layer2ToolstampMasteringCode, 2); } // If we have a dual-layer disc else if (info.SizeAndChecksums.Layerbreak != default) { AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.MasteringRingField, info.CommonDiscInfo.Layer0MasteringRing, 2); AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.MasteringSIDField, info.CommonDiscInfo.Layer0MasteringSID, 2); AddIfExists(output, (reverseOrder ? "Layer 0 (Outer) " : "Layer 0 (Inner) ") + Template.ToolstampField, info.CommonDiscInfo.Layer0ToolstampMasteringCode, 2); AddIfExists(output, "Data Side " + Template.MouldSIDField, info.CommonDiscInfo.Layer0MouldSID, 2); AddIfExists(output, "Data Side " + Template.AdditionalMouldField, info.CommonDiscInfo.Layer0AdditionalMould, 2); AddIfExists(output, (reverseOrder ? "Layer 1 (Inner) " : "Layer 1 (Outer) ") + Template.MasteringRingField, info.CommonDiscInfo.Layer1MasteringRing, 2); AddIfExists(output, (reverseOrder ? "Layer 1 (Inner) " : "Layer 1 (Outer) ") + Template.MasteringSIDField, info.CommonDiscInfo.Layer1MasteringSID, 2); AddIfExists(output, (reverseOrder ? "Layer 1 (Inner) " : "Layer 1 (Outer) ") + Template.ToolstampField, info.CommonDiscInfo.Layer1ToolstampMasteringCode, 2); AddIfExists(output, "Label Side " + Template.MouldSIDField, info.CommonDiscInfo.Layer1MouldSID, 2); AddIfExists(output, "Label Side " + Template.AdditionalMouldField, info.CommonDiscInfo.Layer1AdditionalMould, 2); } // If we have a single-layer disc else { AddIfExists(output, "Data Side " + Template.MasteringRingField, info.CommonDiscInfo.Layer0MasteringRing, 2); AddIfExists(output, "Data Side " + Template.MasteringSIDField, info.CommonDiscInfo.Layer0MasteringSID, 2); AddIfExists(output, "Data Side " + Template.ToolstampField, info.CommonDiscInfo.Layer0ToolstampMasteringCode, 2); AddIfExists(output, "Data Side " + Template.MouldSIDField, info.CommonDiscInfo.Layer0MouldSID, 2); AddIfExists(output, "Data Side " + Template.AdditionalMouldField, info.CommonDiscInfo.Layer0AdditionalMould, 2); AddIfExists(output, "Label Side " + Template.MasteringRingField, info.CommonDiscInfo.Layer1MasteringRing, 2); AddIfExists(output, "Label Side " + Template.MasteringSIDField, info.CommonDiscInfo.Layer1MasteringSID, 2); AddIfExists(output, "Label Side " + Template.ToolstampField, info.CommonDiscInfo.Layer1ToolstampMasteringCode, 2); AddIfExists(output, "Label Side " + Template.MouldSIDField, info.CommonDiscInfo.Layer1MouldSID, 2); AddIfExists(output, "Label Side " + Template.AdditionalMouldField, info.CommonDiscInfo.Layer1AdditionalMould, 2); } AddIfExists(output, Template.BarcodeField, info.CommonDiscInfo.Barcode, 1); AddIfExists(output, Template.EXEDateBuildDate, info.CommonDiscInfo.EXEDateBuildDate, 1); AddIfExists(output, Template.ErrorCountField, info.CommonDiscInfo.ErrorsCount, 1); AddIfExists(output, Template.CommentsField, info.CommonDiscInfo.Comments.Trim(), 1); AddIfExists(output, Template.ContentsField, info.CommonDiscInfo.Contents.Trim(), 1); // Version and Editions section output.Add(""); output.Add("Version and Editions:"); AddIfExists(output, Template.VersionField, info.VersionAndEditions.Version, 1); AddIfExists(output, Template.EditionField, info.VersionAndEditions.OtherEditions, 1); // EDC section if (info.CommonDiscInfo.System == RedumpSystem.SonyPlayStation) { output.Add(""); output.Add("EDC:"); AddIfExists(output, Template.PlayStationEDCField, info.EDC.EDC.LongName(), 1); } // Parent/Clone Relationship section // output.Add(""); output.Add("Parent/Clone Relationship:"); // AddIfExists(output, Template.ParentIDField, info.ParentID); // AddIfExists(output, Template.RegionalParentField, info.RegionalParent.ToString()); // Extras section if (info.Extras.PVD != null || info.Extras.PIC != null || info.Extras.BCA != null) { output.Add(""); output.Add("Extras:"); AddIfExists(output, Template.PVDField, info.Extras.PVD?.Trim(), 1); AddIfExists(output, Template.PlayStation3WiiDiscKeyField, info.Extras.DiscKey, 1); AddIfExists(output, Template.PlayStation3DiscIDField, info.Extras.DiscID, 1); AddIfExists(output, Template.PICField, info.Extras.PIC, 1); AddIfExists(output, Template.HeaderField, info.Extras.Header, 1); AddIfExists(output, Template.GameCubeWiiBCAField, info.Extras.BCA, 1); AddIfExists(output, Template.XBOXSSRanges, info.Extras.SecuritySectorRanges, 1); } // Copy Protection section if (info.CopyProtection.Protection != null || info.CopyProtection.AntiModchip != YesNo.NULL || info.CopyProtection.LibCrypt != YesNo.NULL) { output.Add(""); output.Add("Copy Protection:"); if (info.CommonDiscInfo.System == RedumpSystem.SonyPlayStation) { AddIfExists(output, Template.PlayStationAntiModchipField, info.CopyProtection.AntiModchip.LongName(), 1); AddIfExists(output, Template.PlayStationLibCryptField, info.CopyProtection.LibCrypt.LongName(), 1); AddIfExists(output, Template.SubIntentionField, info.CopyProtection.LibCryptData, 1); } AddIfExists(output, Template.CopyProtectionField, info.CopyProtection.Protection, 1); AddIfExists(output, Template.SubIntentionField, info.CopyProtection.SecuROMData, 1); } // Dumpers and Status section // output.Add(""); output.Add("Dumpers and Status"); // AddIfExists(output, Template.StatusField, info.Status.Name()); // AddIfExists(output, Template.OtherDumpersField, info.OtherDumpers); // Tracks and Write Offsets section if (!string.IsNullOrWhiteSpace(info.TracksAndWriteOffsets.ClrMameProData)) { output.Add(""); output.Add("Tracks and Write Offsets:"); AddIfExists(output, Template.DATField, info.TracksAndWriteOffsets.ClrMameProData + "\n", 1); AddIfExists(output, Template.CuesheetField, info.TracksAndWriteOffsets.Cuesheet, 1); AddIfExists(output, Template.WriteOffsetField, info.TracksAndWriteOffsets.OtherWriteOffsets, 1); } // Size & Checksum section else { output.Add(""); output.Add("Size & Checksum:"); AddIfExists(output, Template.LayerbreakField, (info.SizeAndChecksums.Layerbreak == default ? null : info.SizeAndChecksums.Layerbreak.ToString()), 1); AddIfExists(output, Template.SizeField, info.SizeAndChecksums.Size.ToString(), 1); AddIfExists(output, Template.CRC32Field, info.SizeAndChecksums.CRC32, 1); AddIfExists(output, Template.MD5Field, info.SizeAndChecksums.MD5, 1); AddIfExists(output, Template.SHA1Field, info.SizeAndChecksums.SHA1, 1); } // Make sure there aren't any instances of two blank lines in a row string last = null; for (int i = 0; i < output.Count;) { if (output[i] == last && string.IsNullOrWhiteSpace(last)) { output.RemoveAt(i); } else { last = output[i]; i++; } } return output; } catch { // We don't care what the error is return null; } } /// /// Get the adjusted name of the media based on layers, if applicable /// /// MediaType to get the proper name for /// Size of the current media /// First layerbreak value, as applicable /// Second layerbreak value, as applicable /// Third ayerbreak value, as applicable /// String representation of the media, including layer specification public static string GetFixedMediaType(MediaType? mediaType, long size, long layerbreak, long layerbreak2, long layerbreak3) { switch (mediaType) { case MediaType.DVD: if (layerbreak != default) return $"{mediaType.LongName()}-9"; else return $"{mediaType.LongName()}-5"; case MediaType.BluRay: if (layerbreak3 != default) return $"{mediaType.LongName()}-128"; else if (layerbreak2 != default) return $"{mediaType.LongName()}-100"; else if (layerbreak != default && size > 53_687_063_712) return $"{mediaType.LongName()}-66"; else if (layerbreak != default) return $"{mediaType.LongName()}-50"; else if (size > 26_843_531_856) return $"{mediaType.LongName()}-33"; else return $"{mediaType.LongName()}-25"; case MediaType.UMD: if (layerbreak != default) return $"{mediaType.LongName()}-DL"; else return $"{mediaType.LongName()}-SL"; default: return mediaType.LongName(); } } /// /// Write the data to the output folder /// /// Output folder to write to /// Preformatted list of lines to write out to the file /// True on success, false on error public static bool WriteOutputData(string outputDirectory, List lines) { // Check to see if the inputs are valid if (lines == null) return false; // Now write out to a generic file try { using (StreamWriter sw = new StreamWriter(File.Open(Path.Combine(outputDirectory, "!submissionInfo.txt"), FileMode.Create, FileAccess.Write))) { foreach (string line in lines) sw.WriteLine(line); } } catch (Exception ex) { // We don't care what the error is right now return false; } return true; } /// /// Write the data to the output folder /// /// Output folder to write to /// SubmissionInfo object representing the JSON to write out to the file /// True on success, false on error public static bool WriteOutputData(string outputDirectory, SubmissionInfo info) { // Check to see if the input is valid if (info == null) return false; // Now write out to the JSON try { using (var fs = File.Create(Path.Combine(outputDirectory, "!submissionInfo.json.gz"))) using (var gs = new GZipStream(fs, CompressionMode.Compress)) { string json = JsonConvert.SerializeObject(info, Formatting.Indented); byte[] jsonBytes = Encoding.UTF8.GetBytes(json); gs.Write(jsonBytes, 0, jsonBytes.Length); } } catch (Exception ex) { // We don't care what the error is right now return false; } return true; } /// /// Add the properly formatted key and value, if possible /// /// Output list /// Name of the output key to write /// Name of the output value to write /// Number of tabs to indent the line private static void AddIfExists(List output, string key, string value, int indent) { // If there's no valid value to write if (value == null) return; string prefix = ""; for (int i = 0; i < indent; i++) prefix += "\t"; // If the value contains a newline value = value.Replace("\r\n", "\n"); if (value.Contains("\n")) { output.Add(prefix + key + ":"); output.Add(""); string[] values = value.Split('\n'); foreach (string val in values) output.Add(val); output.Add(""); } // For all regular values else { output.Add(prefix + key + ": " + value); } } /// /// Add the properly formatted key and value, if possible /// /// Output list /// Name of the output key to write /// Name of the output value to write /// Number of tabs to indent the line private static void AddIfExists(List output, string key, string[] value, int indent) { // If there's no valid value to write if (value == null || value.Length == 0) return; AddIfExists(output, key, string.Join(", ", value), indent); } /// /// Add the properly formatted key and value, if possible /// /// Output list /// Name of the output key to write /// Name of the output value to write /// Number of tabs to indent the line private static void AddIfExists(List output, string key, List value, int indent) { // If there's no valid value to write if (value == null || value.Count() == 0) return; AddIfExists(output, key, string.Join(", ", value.Select(o => o.ToString())), indent); } #endregion #region Normalization /// /// Normalize a split set of paths /// /// Directory name to normalize /// Filename to normalize public static (string, string) NormalizeOutputPaths(string directory, string filename) { try { // Cache if we had a directory separator or not bool endedWithDirectorySeparator = directory.EndsWith(Path.DirectorySeparatorChar.ToString()) || directory.EndsWith(Path.AltDirectorySeparatorChar.ToString()); // Combine the path to make things separate easier string combinedPath = Path.Combine(directory, filename); // If we have have a blank path, just return if (string.IsNullOrWhiteSpace(combinedPath)) return (directory, filename); // Now get the normalized paths directory = Path.GetDirectoryName(combinedPath); filename = Path.GetFileName(combinedPath); // Take care of extra path characters directory = new StringBuilder(directory) .Replace(':', '_', 0, directory.LastIndexOf(':') == -1 ? 0 : directory.LastIndexOf(':')).ToString(); // Sanitize everything else foreach (char c in Path.GetInvalidPathChars()) directory = directory.Replace(c, '_'); foreach (char c in Path.GetInvalidFileNameChars()) filename = filename.Replace(c, '_'); // If we had a directory separator at the end before, add it again if (endedWithDirectorySeparator) directory += Path.DirectorySeparatorChar; // If we have a root directory, sanitize if (Directory.Exists(directory)) { var possibleRootDir = new DirectoryInfo(directory); if (possibleRootDir.Parent == null) directory = directory.Replace($"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}", $"{Path.DirectorySeparatorChar}"); } } catch { } return (directory, filename); } #endregion #region Web Calls /// /// Fill out an existing SubmissionInfo object based on a disc page /// /// RedumpWebClient for making the connection /// Existing SubmissionInfo object to fill /// Redump disc ID to retrieve private static void FillFromId(RedumpWebClient wc, SubmissionInfo info, int id) { string discData = wc.DownloadSingleSiteID(id); if (string.IsNullOrEmpty(discData)) return; // 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(" ("); if (firstParenLocation >= 0) { info.CommonDiscInfo.Title = title.Substring(0, firstParenLocation); var subMatches = Constants.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 = Constants.ForeignTitleRegex.Match(discData); if (match.Success) info.CommonDiscInfo.ForeignTitleNonLatin = WebUtility.HtmlDecode(match.Groups[1].Value); else info.CommonDiscInfo.ForeignTitleNonLatin = null; // Category match = Constants.CategoryRegex.Match(discData); if (match.Success) info.CommonDiscInfo.Category = Extensions.ToDiscCategory(match.Groups[1].Value); else info.CommonDiscInfo.Category = DiscCategory.Games; // Region match = Constants.RegionRegex.Match(discData); if (match.Success) info.CommonDiscInfo.Region = Extensions.ToRegion(match.Groups[1].Value); // Languages var matches = Constants.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(); } // Serial // TODO: Re-enable if there's a way of verifying against a disc //match = Constants.SerialRegex.Match(discData); //if (match.Success) // info.CommonDiscInfo.Serial = WebUtility.HtmlDecode(match.Groups[1].Value); // Error count match = Constants.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 = Constants.VersionRegex.Match(discData); if (match.Success) info.VersionAndEditions.Version = WebUtility.HtmlDecode(match.Groups[1].Value); // Dumpers matches = Constants.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 // TODO: Re-enable when there's a better way of parsing comments //match = Constants.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") // .ReplaceCommentFieldsWithTags() + "\n"; //} // Contents match = Constants.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 = 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 resultProgress?.Report(Result.Success("Finding disc matches on Redump...")); string[] splitData = info.TracksAndWriteOffsets.ClrMameProData.Split('\n'); foreach (string hashData in splitData) { 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 there are no matched IDs if (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 comment block and replace with internal identifiers /// /// Comments block to process /// Processed comments block, if possible private static string ReplaceCommentFieldsWithTags(this string comments) { if (string.IsNullOrWhiteSpace(comments)) return comments; return comments .Replace(Constants.AcclaimIDCommentString, Constants.AcclaimIDCommentField) .Replace(Constants.ActivisionIDCommentString, Constants.ActivisionIDCommentField) .Replace(Constants.AlternativeTitleCommentString, Constants.AlternativeTitleCommentField) .Replace(Constants.AlternativeForeignTitleCommentString, Constants.AlternativeForeignTitleCommentField) .Replace(Constants.BandaiIDCommentString, Constants.BandaiIDCommentField) .Replace(Constants.BBFCRegistrationNumberCommentString, Constants.BBFCRegistrationNumberCommentField) .Replace(Constants.DNASDiscIDCommentString, Constants.DNASDiscIDCommentField) .Replace(Constants.ElectronicArtsIDCommentString, Constants.ElectronicArtsIDCommentField) .Replace(Constants.ExtrasCommentString, Constants.ExtrasCommentField) .Replace(Constants.FoxInteractiveIDCommentString, Constants.FoxInteractiveIDCommentField) .Replace(Constants.GameFootageCommentString, Constants.GameFootageCommentField) .Replace(Constants.GenreCommentString, Constants.GenreCommentField) .Replace(Constants.GTInteractiveIDCommentString, Constants.GTInteractiveIDCommentField) .Replace(Constants.InternalSerialNameCommentString, Constants.InternalSerialNameCommentField) .Replace(Constants.ISBNCommentString, Constants.ISBNCommentField) .Replace(Constants.ISSNCommentString, Constants.ISSNCommentField) .Replace(Constants.JASRACIDCommentString, Constants.JASRACIDCommentField) .Replace(Constants.KingRecordsIDCommentString, Constants.KingRecordsIDCommentField) .Replace(Constants.KoeiIDCommentString, Constants.KoeiIDCommentField) .Replace(Constants.KonamiIDCommentString, Constants.KonamiIDCommentField) .Replace(Constants.LucasArtsIDCommentString, Constants.LucasArtsIDCommentField) .Replace(Constants.NaganoIDCommentString, Constants.NaganoIDCommentField) .Replace(Constants.NamcoIDCommentString, Constants.NamcoIDCommentField) .Replace(Constants.NetYarozeGamesCommentSring, Constants.NetYarozeGamesCommentField) .Replace(Constants.NipponIchiSoftwareIDCommentString, Constants.NipponIchiSoftwareIDCommentField) .Replace(Constants.OriginIDCommentString, Constants.OriginIDCommentField) .Replace(Constants.PatchesCommentString, Constants.PatchesCommentField) .Replace(Constants.PlayableDemosCommentString, Constants.PlayableDemosCommentField) .Replace(Constants.PonyCanyonIDCommentString, Constants.PonyCanyonIDCommentField) .Replace(Constants.PostgapTypeCommentString, Constants.PostgapTypeCommentField) .Replace(Constants.PPNCommentString, Constants.PPNCommentField) .Replace(Constants.RollingDemosCommentString, Constants.RollingDemosCommentField) .Replace(Constants.SavegamesCommentString, Constants.SavegamesCommentField) .Replace(Constants.SegaIDCommentString, Constants.SegaIDCommentField) .Replace(Constants.SelenIDCommentString, Constants.SelenIDCommentField) .Replace(Constants.SeriesCommentString, Constants.SeriesCommentField) .Replace(Constants.TaitoIDCommentString, Constants.TaitoIDCommentField) .Replace(Constants.TechDemosCommentString, Constants.TechDemosCommentField) .Replace(Constants.UbisoftIDCommentString, Constants.UbisoftIDCommentField) .Replace(Constants.ValveIDCommentString, Constants.ValveIDCommentField) .Replace(Constants.VGCCodeCommentString, Constants.VFCCodeCommentField) .Replace(Constants.VideosCommentString, Constants.VideosCommentField) .Replace(Constants.VolumeLabelCommentString, Constants.VolumeLabelCommentField) .Replace(Constants.VCDCommentString, Constants.VCDCommentField); } /// /// 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 private static void 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)) return; // 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; } // If no IDs match any track, then we don't match a disc at all if (!newIds.Any()) { info.MatchedIDs = new List(); return; } // 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; } /// /// 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 } }