diff --git a/README.MD b/README.MD
index 115ee792..041bce85 100644
--- a/README.MD
+++ b/README.MD
@@ -85,6 +85,7 @@ Below are a list of the included namespaces with links to their README files:
- [SabreTools.Metadata.DatFiles](https://github.com/SabreTools/SabreTools.Serialization/tree/main/SabreTools.Metadata.DatFiles)
- [SabreTools.Metadata.DatItems](https://github.com/SabreTools/SabreTools.Serialization/tree/main/SabreTools.Metadata.Datitems)
- [SabreTools.Metadata.Filter](https://github.com/SabreTools/SabreTools.Serialization/tree/main/SabreTools.Metadata.Filter)
+- [SabreTools.Metadata.Reports](https://github.com/SabreTools/SabreTools.Serialization/tree/main/SabreTools.Metadata.Reports)
- [SabreTools.ObjectIdentifier](https://github.com/SabreTools/SabreTools.Serialization/tree/main/SabreTools.ObjectIdentifier)
- [SabreTools.Serialization.CrossModel](https://github.com/SabreTools/SabreTools.Serialization/tree/main/SabreTools.Serialization.CrossModel)
- [SabreTools.Serialization.Readers](https://github.com/SabreTools/SabreTools.Serialization/tree/main/SabreTools.Serialization.Readers)
diff --git a/SabreTools.Metadata.Reports/BaseReport.cs b/SabreTools.Metadata.Reports/BaseReport.cs
new file mode 100644
index 00000000..e361abc3
--- /dev/null
+++ b/SabreTools.Metadata.Reports/BaseReport.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using SabreTools.Logging;
+using SabreTools.Metadata.DatFiles;
+
+#pragma warning disable IDE0290 // Use primary constructor
+namespace SabreTools.Metadata.Reports
+{
+ ///
+ /// Base class for a report output format
+ ///
+ public abstract class BaseReport
+ {
+ #region Logging
+
+ ///
+ /// Logging object
+ ///
+ protected readonly Logger _logger = new();
+
+ #endregion
+
+ ///
+ /// Set of DatStatistics objects to use for formatting
+ ///
+ protected List _statistics;
+
+ ///
+ /// Create a new report from the filename
+ ///
+ /// List of statistics objects to set
+ public BaseReport(List statsList)
+ {
+ _statistics = statsList;
+ }
+
+ ///
+ /// Create and open an output file for writing direct from a set of statistics
+ ///
+ /// Name of the file to write to
+ /// True if baddumps should be included in output, false otherwise
+ /// True if nodumps should be included in output, false otherwise
+ /// True if the error that is thrown should be thrown back to the caller, false otherwise
+ /// True if the report was written correctly, false otherwise
+ public bool WriteToFile(string? outfile, bool baddumpCol, bool nodumpCol, bool throwOnError = false)
+ {
+ InternalStopwatch watch = new($"Writing statistics to '{outfile}");
+
+ try
+ {
+ // Try to create the output file
+ FileStream stream = File.Create(outfile ?? string.Empty);
+ if (stream is null)
+ {
+ _logger.Warning($"File '{outfile}' could not be created for writing! Please check to see if the file is writable");
+ return false;
+ }
+
+ // Write to the stream
+ bool result = WriteToStream(stream, baddumpCol, nodumpCol, throwOnError);
+
+ // Dispose of the stream
+ stream.Dispose();
+ }
+ catch (Exception ex) when (!throwOnError)
+ {
+ _logger.Error(ex);
+ return false;
+ }
+ finally
+ {
+ watch.Stop();
+ }
+
+ return true;
+ }
+
+ ///
+ /// Write a set of statistics to an input stream
+ ///
+ /// Stream to write to
+ /// True if baddumps should be included in output, false otherwise
+ /// True if nodumps should be included in output, false otherwise
+ /// True if the error that is thrown should be thrown back to the caller, false otherwise
+ /// True if the report was written correctly, false otherwise
+ public abstract bool WriteToStream(Stream stream, bool baddumpCol, bool nodumpCol, bool throwOnError = false);
+ }
+}
diff --git a/SabreTools.Metadata.Reports/Enums.cs b/SabreTools.Metadata.Reports/Enums.cs
new file mode 100644
index 00000000..2ffdae60
--- /dev/null
+++ b/SabreTools.Metadata.Reports/Enums.cs
@@ -0,0 +1,38 @@
+namespace SabreTools.Metadata.Reports
+{
+ ///
+ /// Determine which format to output Stats to
+ ///
+ public enum StatReportFormat
+ {
+ ///
+ /// Only output to the console
+ ///
+ None,
+
+ ///
+ /// Console-formatted
+ ///
+ Textfile,
+
+ ///
+ /// ClrMamePro HTML
+ ///
+ HTML,
+
+ ///
+ /// Comma-Separated Values (Standardized)
+ ///
+ CSV,
+
+ ///
+ /// Semicolon-Separated Values (Standardized)
+ ///
+ SSV,
+
+ ///
+ /// Tab-Separated Values (Standardized)
+ ///
+ TSV,
+ }
+}
diff --git a/SabreTools.Metadata.Reports/Formats/CommaSeparatedValue.cs b/SabreTools.Metadata.Reports/Formats/CommaSeparatedValue.cs
new file mode 100644
index 00000000..d8aa8db2
--- /dev/null
+++ b/SabreTools.Metadata.Reports/Formats/CommaSeparatedValue.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using SabreTools.Metadata.DatFiles;
+
+namespace SabreTools.Metadata.Reports.Formats
+{
+ ///
+ /// Represents a comma-separated value file
+ ///
+ public sealed class CommaSeparatedValue : SeparatedValue
+ {
+ ///
+ /// Create a new report from the filename
+ ///
+ /// List of statistics objects to set
+ public CommaSeparatedValue(List statsList) : base(statsList)
+ {
+ _delim = ',';
+ }
+ }
+}
diff --git a/SabreTools.Metadata.Reports/Formats/Html.cs b/SabreTools.Metadata.Reports/Formats/Html.cs
new file mode 100644
index 00000000..6a4fa834
--- /dev/null
+++ b/SabreTools.Metadata.Reports/Formats/Html.cs
@@ -0,0 +1,329 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+#if NET40_OR_GREATER || NETCOREAPP || NETSTANDARD
+using System.Net;
+#endif
+using System.Text;
+using System.Xml;
+using SabreTools.Hashing;
+using SabreTools.Metadata.DatFiles;
+using SabreTools.Text.Extensions;
+using ItemStatus = SabreTools.Data.Models.Metadata.ItemStatus;
+using ItemType = SabreTools.Data.Models.Metadata.ItemType;
+
+#pragma warning disable IDE0290 // Use primary constructor
+namespace SabreTools.Metadata.Reports.Formats
+{
+ ///
+ /// HTML report format
+ ///
+ /// TODO: Make output standard width, without making the entire thing a table
+ public class Html : BaseReport
+ {
+ ///
+ /// Create a new report from the filename
+ ///
+ /// List of statistics objects to set
+ public Html(List statsList)
+ : base(statsList)
+ {
+ }
+
+ ///
+ public override bool WriteToStream(Stream stream, bool baddumpCol, bool nodumpCol, bool throwOnError = false)
+ {
+ try
+ {
+ XmlTextWriter xtw = new(stream, Encoding.UTF8)
+ {
+ Formatting = Formatting.Indented,
+ IndentChar = '\t',
+ Indentation = 1
+ };
+
+ // Write out the header
+ WriteHeader(xtw, baddumpCol, nodumpCol);
+
+ // Now process each of the statistics
+ for (int i = 0; i < _statistics.Count; i++)
+ {
+ // Get the current statistic
+ DatStatistics stat = _statistics[i];
+
+ // If we have a directory statistic
+ if (stat.IsDirectory)
+ {
+ WriteMidSeparator(xtw, baddumpCol, nodumpCol);
+ WriteIndividual(xtw, stat, baddumpCol, nodumpCol);
+
+ // If we have anything but the last value, write the separator
+ if (i < _statistics.Count - 1)
+ {
+ WriteFooterSeparator(xtw, baddumpCol, nodumpCol);
+ WriteMidHeader(xtw, baddumpCol, nodumpCol);
+ }
+ }
+
+ // If we have a normal statistic
+ else
+ {
+ WriteIndividual(xtw, stat, baddumpCol, nodumpCol);
+ }
+ }
+
+ WriteFooter(xtw);
+#if NET452_OR_GREATER
+ xtw.Dispose();
+#endif
+ }
+ catch (Exception ex) when (!throwOnError)
+ {
+ _logger.Error(ex);
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Write out the header to the stream, if any exists
+ ///
+ /// XmlTextWriter to write to
+ /// True if baddumps should be included in output, false otherwise
+ /// True if nodumps should be included in output, false otherwise
+ private static void WriteHeader(XmlTextWriter xtw, bool baddumpCol, bool nodumpCol)
+ {
+ xtw.WriteDocType("html", null, null, null);
+ xtw.WriteStartElement("html");
+
+ xtw.WriteStartElement("header");
+ xtw.WriteElementString("title", "DAT Statistics Report");
+ xtw.WriteElementString("style", @"
+body {
+ background-color: lightgray;
+}
+.dir {
+ color: #0088FF;
+}");
+ xtw.WriteEndElement(); // header
+
+ xtw.WriteStartElement("body");
+
+ xtw.WriteElementString("h2", $"DAT Statistics Report ({DateTime.Now:d})");
+
+ xtw.WriteStartElement("table");
+ xtw.WriteAttributeString("border", "1");
+ xtw.WriteAttributeString("cellpadding", "5");
+ xtw.WriteAttributeString("cellspacing", "0");
+ xtw.Flush();
+
+ // Now write the mid header for those who need it
+ WriteMidHeader(xtw, baddumpCol, nodumpCol);
+ }
+
+ ///
+ /// Write out the mid-header to the stream, if any exists
+ ///
+ /// XmlTextWriter to write to
+ /// True if baddumps should be included in output, false otherwise
+ /// True if nodumps should be included in output, false otherwise
+ private static void WriteMidHeader(XmlTextWriter xtw, bool baddumpCol, bool nodumpCol)
+ {
+ xtw.WriteStartElement("tr");
+ xtw.WriteAttributeString("bgcolor", "gray");
+
+ xtw.WriteElementString("th", "File Name");
+
+ xtw.WriteStartElement("th");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString("Total Size");
+ xtw.WriteEndElement(); // th
+
+ xtw.WriteStartElement("th");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString("Games");
+ xtw.WriteEndElement(); // th
+
+ xtw.WriteStartElement("th");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString("Roms");
+ xtw.WriteEndElement(); // th
+
+ xtw.WriteStartElement("th");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString("Disks");
+ xtw.WriteEndElement(); // th
+
+ xtw.WriteStartElement("th");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString("# with CRC");
+ xtw.WriteEndElement(); // th
+
+ xtw.WriteStartElement("th");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString("# with MD5");
+ xtw.WriteEndElement(); // th
+
+ xtw.WriteStartElement("th");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString("# with SHA-1");
+ xtw.WriteEndElement(); // th
+
+ xtw.WriteStartElement("th");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString("# with SHA-256");
+ xtw.WriteEndElement(); // th
+
+ if (baddumpCol)
+ {
+ xtw.WriteStartElement("th");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString("Baddumps");
+ xtw.WriteEndElement(); // th
+ }
+
+ if (nodumpCol)
+ {
+ xtw.WriteStartElement("th");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString("Nodumps");
+ xtw.WriteEndElement(); // th
+ }
+
+ xtw.WriteEndElement(); // tr
+ xtw.Flush();
+ }
+
+ ///
+ /// Write a single set of statistics
+ ///
+ /// XmlTextWriter to write to
+ /// DatStatistics object to write out
+ /// True if baddumps should be included in output, false ot
+ /// herwise
+ /// True if nodumps should be included in output, false otherwise
+ private static void WriteIndividual(XmlTextWriter xtw, DatStatistics stat, bool baddumpCol, bool nodumpCol)
+ {
+ bool isDirectory = stat.DisplayName!.StartsWith("DIR: ");
+
+ xtw.WriteStartElement("tr");
+ if (isDirectory)
+ xtw.WriteAttributeString("class", "dir");
+
+#if NET20 || NET35
+ xtw.WriteElementString("td", isDirectory ? stat.DisplayName.Remove(0, 5) : stat.DisplayName);
+#else
+ xtw.WriteElementString("td", isDirectory ? WebUtility.HtmlEncode(stat.DisplayName.Remove(0, 5)) : WebUtility.HtmlEncode(stat.DisplayName));
+#endif
+
+ xtw.WriteStartElement("td");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString(NumberHelper.GetBytesReadable(stat.TotalSize));
+ xtw.WriteEndElement(); // td
+
+ xtw.WriteStartElement("td");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString(stat.MachineCount.ToString());
+ xtw.WriteEndElement(); // td
+
+ xtw.WriteStartElement("td");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString(stat.GetItemCount(ItemType.Rom).ToString());
+ xtw.WriteEndElement(); // td
+
+ xtw.WriteStartElement("td");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString(stat.GetItemCount(ItemType.Disk).ToString());
+ xtw.WriteEndElement(); // td
+
+ xtw.WriteStartElement("td");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString(stat.GetHashCount(HashType.CRC32).ToString());
+ xtw.WriteEndElement(); // td
+
+ xtw.WriteStartElement("td");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString(stat.GetHashCount(HashType.MD5).ToString());
+ xtw.WriteEndElement(); // td
+
+ xtw.WriteStartElement("td");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString(stat.GetHashCount(HashType.SHA1).ToString());
+ xtw.WriteEndElement(); // td
+
+ xtw.WriteStartElement("td");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString(stat.GetHashCount(HashType.SHA256).ToString());
+ xtw.WriteEndElement(); // td
+
+ if (baddumpCol)
+ {
+ xtw.WriteStartElement("td");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString(stat.GetStatusCount(ItemStatus.BadDump).ToString());
+ xtw.WriteEndElement(); // td
+ }
+
+ if (nodumpCol)
+ {
+ xtw.WriteStartElement("td");
+ xtw.WriteAttributeString("align", "right");
+ xtw.WriteString(stat.GetStatusCount(ItemStatus.Nodump).ToString());
+ xtw.WriteEndElement(); // td
+ }
+
+ xtw.WriteEndElement(); // tr
+ xtw.Flush();
+ }
+
+ ///
+ /// Write out the separator to the stream, if any exists
+ ///
+ /// XmlTextWriter to write to
+ /// True if baddumps should be included in output, false otherwise
+ /// True if nodumps should be included in output, false otherwise
+ private static void WriteMidSeparator(XmlTextWriter xtw, bool baddumpCol, bool nodumpCol)
+ {
+ xtw.WriteStartElement("tr");
+
+ xtw.WriteStartElement("td");
+ xtw.WriteAttributeString("colspan", baddumpCol && nodumpCol ? "12" : (baddumpCol ^ nodumpCol ? "11" : "10"));
+ xtw.WriteEndElement(); // td
+
+ xtw.WriteEndElement(); // tr
+ xtw.Flush();
+ }
+
+ ///
+ /// Write out the footer-separator to the stream, if any exists
+ ///
+ /// XmlTextWriter to write to
+ /// True if baddumps should be included in output, false otherwise
+ /// True if nodumps should be included in output, false otherwise
+ private static void WriteFooterSeparator(XmlTextWriter xtw, bool baddumpCol, bool nodumpCol)
+ {
+ xtw.WriteStartElement("tr");
+ xtw.WriteAttributeString("border", "0");
+
+ xtw.WriteStartElement("td");
+ xtw.WriteAttributeString("colspan", baddumpCol && nodumpCol ? "12" : (baddumpCol ^ nodumpCol ? "11" : "10"));
+ xtw.WriteEndElement(); // td
+
+ xtw.WriteEndElement(); // tr
+ xtw.Flush();
+ }
+
+ ///
+ /// Write out the footer to the stream, if any exists
+ ///
+ /// XmlTextWriter to write to
+ private static void WriteFooter(XmlTextWriter xtw)
+ {
+ xtw.WriteEndElement(); // table
+ xtw.WriteEndElement(); // body
+ xtw.WriteEndElement(); // html
+ xtw.Flush();
+ }
+ }
+}
diff --git a/SabreTools.Metadata.Reports/Formats/SemicolonSeparatedValue.cs b/SabreTools.Metadata.Reports/Formats/SemicolonSeparatedValue.cs
new file mode 100644
index 00000000..870858c5
--- /dev/null
+++ b/SabreTools.Metadata.Reports/Formats/SemicolonSeparatedValue.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using SabreTools.Metadata.DatFiles;
+
+namespace SabreTools.Metadata.Reports.Formats
+{
+ ///
+ /// Represents a semicolon-separated value file
+ ///
+ public sealed class SemicolonSeparatedValue : SeparatedValue
+ {
+ ///
+ /// Create a new report from the filename
+ ///
+ /// List of statistics objects to set
+ public SemicolonSeparatedValue(List statsList) : base(statsList)
+ {
+ _delim = ';';
+ }
+ }
+}
diff --git a/SabreTools.Metadata.Reports/Formats/SeparatedValue.cs b/SabreTools.Metadata.Reports/Formats/SeparatedValue.cs
new file mode 100644
index 00000000..f0fff99f
--- /dev/null
+++ b/SabreTools.Metadata.Reports/Formats/SeparatedValue.cs
@@ -0,0 +1,148 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using SabreTools.Hashing;
+using SabreTools.Metadata.DatFiles;
+using SabreTools.Text.SeparatedValue;
+using ItemStatus = SabreTools.Data.Models.Metadata.ItemStatus;
+using ItemType = SabreTools.Data.Models.Metadata.ItemType;
+
+#pragma warning disable IDE0290 // Use primary constructor
+namespace SabreTools.Metadata.Reports.Formats
+{
+ ///
+ /// Separated-Value report format
+ ///
+ public abstract class SeparatedValue : BaseReport
+ {
+ ///
+ /// Represents the delimiter between fields
+ ///
+ protected char _delim;
+
+ ///
+ /// Create a new report from the filename
+ ///
+ /// List of statistics objects to set
+ public SeparatedValue(List statsList)
+ : base(statsList)
+ {
+ }
+
+ ///
+ public override bool WriteToStream(Stream stream, bool baddumpCol, bool nodumpCol, bool throwOnError = false)
+ {
+ try
+ {
+ Writer svw = new(stream, Encoding.UTF8)
+ {
+ Separator = _delim,
+ Quotes = true,
+ };
+
+ // Write out the header
+ WriteHeader(svw, baddumpCol, nodumpCol);
+
+ // Now process each of the statistics
+ for (int i = 0; i < _statistics.Count; i++)
+ {
+ // Get the current statistic
+ DatStatistics stat = _statistics[i];
+
+ // If we have a directory statistic
+ if (stat.IsDirectory)
+ {
+ WriteIndividual(svw, stat, baddumpCol, nodumpCol);
+
+ // If we have anything but the last value, write the separator
+ if (i < _statistics.Count - 1)
+ WriteFooterSeparator(svw);
+ }
+
+ // If we have a normal statistic
+ else
+ {
+ WriteIndividual(svw, stat, baddumpCol, nodumpCol);
+ }
+ }
+
+ svw.Dispose();
+ }
+ catch (Exception ex) when (!throwOnError)
+ {
+ _logger.Error(ex);
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Write out the header to the stream, if any exists
+ ///
+ /// Writer to write to
+ /// True if baddumps should be included in output, false otherwise
+ /// True if nodumps should be included in output, false otherwise
+ private static void WriteHeader(Writer svw, bool baddumpCol, bool nodumpCol)
+ {
+ string[] headers =
+ [
+ "File Name",
+ "Total Size",
+ "Games",
+ "Roms",
+ "Disks",
+ "# with CRC",
+ "# with MD5",
+ "# with SHA-1",
+ "# with SHA-256",
+ "# with SHA-384",
+ "# with SHA-512",
+ baddumpCol ? "BadDumps" : string.Empty,
+ nodumpCol ? "Nodumps" : string.Empty,
+ ];
+ svw.WriteHeader(headers);
+ svw.Flush();
+ }
+
+ ///
+ /// Write a single set of statistics
+ ///
+ /// Writer to write to
+ /// DatStatistics object to write out
+ /// True if baddumps should be included in output, false otherwise
+ /// True if nodumps should be included in output, false otherwise
+ private static void WriteIndividual(Writer svw, DatStatistics stat, bool baddumpCol, bool nodumpCol)
+ {
+ string[] values =
+ [
+ stat.DisplayName!,
+ stat.TotalSize.ToString(),
+ stat.MachineCount.ToString(),
+ stat.GetItemCount(ItemType.Rom).ToString(),
+ stat.GetItemCount(ItemType.Disk).ToString(),
+ stat.GetHashCount(HashType.CRC32).ToString(),
+ stat.GetHashCount(HashType.MD5).ToString(),
+ stat.GetHashCount(HashType.SHA1).ToString(),
+ stat.GetHashCount(HashType.SHA256).ToString(),
+ stat.GetHashCount(HashType.SHA384).ToString(),
+ stat.GetHashCount(HashType.SHA512).ToString(),
+ baddumpCol ? stat.GetStatusCount(ItemStatus.BadDump).ToString() : string.Empty,
+ nodumpCol ? stat.GetStatusCount(ItemStatus.Nodump).ToString() : string.Empty,
+ ];
+ svw.WriteValues(values);
+ svw.Flush();
+ }
+
+ ///
+ /// Write out the footer-separator to the stream, if any exists
+ ///
+ /// Writer to write to
+ private static void WriteFooterSeparator(Writer svw)
+ {
+ svw.WriteString("\n");
+ svw.Flush();
+ }
+ }
+}
diff --git a/SabreTools.Metadata.Reports/Formats/TabSeparatedValue.cs b/SabreTools.Metadata.Reports/Formats/TabSeparatedValue.cs
new file mode 100644
index 00000000..23537777
--- /dev/null
+++ b/SabreTools.Metadata.Reports/Formats/TabSeparatedValue.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using SabreTools.Metadata.DatFiles;
+
+namespace SabreTools.Metadata.Reports.Formats
+{
+ ///
+ /// Represents a tab-separated value file
+ ///
+ public sealed class TabSeparatedValue : SeparatedValue
+ {
+ ///
+ /// Create a new report from the filename
+ ///
+ /// List of statistics objects to set
+ public TabSeparatedValue(List statsList) : base(statsList)
+ {
+ _delim = '\t';
+ }
+ }
+}
diff --git a/SabreTools.Metadata.Reports/Formats/Textfile.cs b/SabreTools.Metadata.Reports/Formats/Textfile.cs
new file mode 100644
index 00000000..44b1fd77
--- /dev/null
+++ b/SabreTools.Metadata.Reports/Formats/Textfile.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using SabreTools.Hashing;
+using SabreTools.Metadata.DatFiles;
+using SabreTools.Text.Extensions;
+using ItemStatus = SabreTools.Data.Models.Metadata.ItemStatus;
+using ItemType = SabreTools.Data.Models.Metadata.ItemType;
+
+#pragma warning disable IDE0290 // Use primary constructor
+namespace SabreTools.Metadata.Reports.Formats
+{
+ ///
+ /// Textfile report format
+ ///
+ public class Textfile : BaseReport
+ {
+ ///
+ /// Create a new report from the filename
+ ///
+ /// List of statistics objects to set
+ public Textfile(List statsList)
+ : base(statsList)
+ {
+ }
+
+ ///
+ public override bool WriteToStream(Stream stream, bool baddumpCol, bool nodumpCol, bool throwOnError = false)
+ {
+ try
+ {
+ StreamWriter sw = new(stream);
+
+ // Now process each of the statistics
+ for (int i = 0; i < _statistics.Count; i++)
+ {
+ // Get the current statistic
+ DatStatistics stat = _statistics[i];
+
+ // If we have a directory statistic
+ if (stat.IsDirectory)
+ {
+ WriteIndividual(sw, stat, baddumpCol, nodumpCol);
+
+ // If we have anything but the last value, write the separator
+ if (i < _statistics.Count - 1)
+ WriteFooterSeparator(sw);
+ }
+
+ // If we have a normal statistic
+ else
+ {
+ WriteIndividual(sw, stat, baddumpCol, nodumpCol);
+ }
+ }
+
+ sw.Dispose();
+ }
+ catch (Exception ex) when (!throwOnError)
+ {
+ _logger.Error(ex);
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Write a single set of statistics
+ ///
+ /// StreamWriter to write to
+ /// DatStatistics object to write out
+ /// True if baddumps should be included in output, false otherwise
+ /// True if nodumps should be included in output, false otherwise
+ private static void WriteIndividual(StreamWriter sw, DatStatistics stat, bool baddumpCol, bool nodumpCol)
+ {
+ var line = new StringBuilder();
+
+ line.AppendLine($"'{stat.DisplayName}':");
+ line.AppendLine($"--------------------------------------------------");
+ line.AppendLine($" Uncompressed size: {NumberHelper.GetBytesReadable(stat!.TotalSize)}");
+ line.AppendLine($" Games found: {stat.MachineCount}");
+ line.AppendLine($" Roms found: {stat.GetItemCount(ItemType.Rom)}");
+ line.AppendLine($" Disks found: {stat.GetItemCount(ItemType.Disk)}");
+ line.AppendLine($" Roms with CRC-32: {stat.GetHashCount(HashType.CRC32)}");
+ line.AppendLine($" Roms with MD5: {stat.GetHashCount(HashType.MD5)}");
+ line.AppendLine($" Roms with SHA-1: {stat.GetHashCount(HashType.SHA1)}");
+ line.AppendLine($" Roms with SHA-256: {stat.GetHashCount(HashType.SHA256)}");
+ line.AppendLine($" Roms with SHA-384: {stat.GetHashCount(HashType.SHA384)}");
+ line.AppendLine($" Roms with SHA-512: {stat.GetHashCount(HashType.SHA512)}");
+
+ if (baddumpCol)
+ line.AppendLine($" Roms with BadDump status: {stat.GetStatusCount(ItemStatus.BadDump)}");
+
+ if (nodumpCol)
+ line.AppendLine($" Roms with Nodump status: {stat.GetStatusCount(ItemStatus.Nodump)}");
+
+ // For spacing between DATs
+ line.AppendLine();
+ line.AppendLine();
+
+ sw.Write(line.ToString());
+ sw.Flush();
+ }
+
+ ///
+ /// Write out the footer-separator to the stream, if any exists
+ ///
+ /// StreamWriter to write to
+ private static void WriteFooterSeparator(StreamWriter sw)
+ {
+ sw.Write("\n");
+ sw.Flush();
+ }
+ }
+}
diff --git a/SabreTools.Metadata.Reports/README.MD b/SabreTools.Metadata.Reports/README.MD
new file mode 100644
index 00000000..111302e6
--- /dev/null
+++ b/SabreTools.Metadata.Reports/README.MD
@@ -0,0 +1,3 @@
+# SabreTools.Metadata.Reports
+
+This library contains report generating functionality used by metadata format processing.
diff --git a/SabreTools.Metadata.Reports/SabreTools.Metadata.Reports.csproj b/SabreTools.Metadata.Reports/SabreTools.Metadata.Reports.csproj
new file mode 100644
index 00000000..a80bbd43
--- /dev/null
+++ b/SabreTools.Metadata.Reports/SabreTools.Metadata.Reports.csproj
@@ -0,0 +1,44 @@
+
+
+
+
+ net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0;netstandard2.0;netstandard2.1
+ true
+ false
+ false
+ true
+ latest
+ NU1902;NU1903
+ enable
+ true
+ snupkg
+ true
+ 2.3.0
+
+
+ Matt Nadareski
+ Report functionality for metadata file processing
+ Copyright (c) Matt Nadareski 2016-2026
+ https://github.com/SabreTools/
+ README.md
+ https://github.com/SabreTools/SabreTools.Serialization
+ git
+ metadata dat datfile report
+ MIT
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SabreTools.Serialization.sln b/SabreTools.Serialization.sln
index 5c811755..044ddb2d 100644
--- a/SabreTools.Serialization.sln
+++ b/SabreTools.Serialization.sln
@@ -47,6 +47,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Metadata.DatFile
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Metadata.DatFiles.Test", "SabreTools.Metadata.DatFiles.Test\SabreTools.Metadata.DatFiles.Test.csproj", "{EFF85ED6-27D6-4076-A935-9792D2975078}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Metadata.Reports", "SabreTools.Metadata.Reports\SabreTools.Metadata.Reports.csproj", "{6CDE2071-784B-4072-9EAE-33F0AE00566C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -321,6 +323,18 @@ Global
{EFF85ED6-27D6-4076-A935-9792D2975078}.Release|x64.Build.0 = Release|Any CPU
{EFF85ED6-27D6-4076-A935-9792D2975078}.Release|x86.ActiveCfg = Release|Any CPU
{EFF85ED6-27D6-4076-A935-9792D2975078}.Release|x86.Build.0 = Release|Any CPU
+ {6CDE2071-784B-4072-9EAE-33F0AE00566C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6CDE2071-784B-4072-9EAE-33F0AE00566C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6CDE2071-784B-4072-9EAE-33F0AE00566C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6CDE2071-784B-4072-9EAE-33F0AE00566C}.Debug|x64.Build.0 = Debug|Any CPU
+ {6CDE2071-784B-4072-9EAE-33F0AE00566C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6CDE2071-784B-4072-9EAE-33F0AE00566C}.Debug|x86.Build.0 = Debug|Any CPU
+ {6CDE2071-784B-4072-9EAE-33F0AE00566C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6CDE2071-784B-4072-9EAE-33F0AE00566C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6CDE2071-784B-4072-9EAE-33F0AE00566C}.Release|x64.ActiveCfg = Release|Any CPU
+ {6CDE2071-784B-4072-9EAE-33F0AE00566C}.Release|x64.Build.0 = Release|Any CPU
+ {6CDE2071-784B-4072-9EAE-33F0AE00566C}.Release|x86.ActiveCfg = Release|Any CPU
+ {6CDE2071-784B-4072-9EAE-33F0AE00566C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/SabreTools.Serialization/SabreTools.Serialization.csproj b/SabreTools.Serialization/SabreTools.Serialization.csproj
index 0e55eabf..4db86006 100644
--- a/SabreTools.Serialization/SabreTools.Serialization.csproj
+++ b/SabreTools.Serialization/SabreTools.Serialization.csproj
@@ -46,6 +46,8 @@
+
+
diff --git a/publish-nix.sh b/publish-nix.sh
index 43ef7555..a53062e7 100755
--- a/publish-nix.sh
+++ b/publish-nix.sh
@@ -84,6 +84,7 @@ if [ $NO_BUILD = false ]; then
dotnet pack SabreTools.Metadata.DatFiles/SabreTools.Metadata.DatFiles.csproj --output $BUILD_FOLDER
dotnet pack SabreTools.Metadata.DatItems/SabreTools.Metadata.DatItems.csproj --output $BUILD_FOLDER
dotnet pack SabreTools.Metadata.Filter/SabreTools.Metadata.Filter.csproj --output $BUILD_FOLDER
+ dotnet pack SabreTools.Metadata.Reports/SabreTools.Metadata.Reports.csproj --output $BUILD_FOLDER
dotnet pack SabreTools.ObjectIdentifier/SabreTools.ObjectIdentifier.csproj --output $BUILD_FOLDER
dotnet pack SabreTools.Serialization.CrossModel/SabreTools.Serialization.CrossModel.csproj --output $BUILD_FOLDER
dotnet pack SabreTools.Serialization.Readers/SabreTools.Serialization.Readers.csproj --output $BUILD_FOLDER
diff --git a/publish-win.ps1 b/publish-win.ps1
index e3ef7611..2a869e1a 100644
--- a/publish-win.ps1
+++ b/publish-win.ps1
@@ -75,6 +75,7 @@ if (!$NO_BUILD.IsPresent) {
dotnet pack SabreTools.Metadata.DatFiles\SabreTools.Metadata.DatFiles.csproj --output $BUILD_FOLDER
dotnet pack SabreTools.Metadata.DatItems\SabreTools.Metadata.DatItems.csproj --output $BUILD_FOLDER
dotnet pack SabreTools.Metadata.Filter\SabreTools.Metadata.Filter.csproj --output $BUILD_FOLDER
+ dotnet pack SabreTools.Metadata.Reports\SabreTools.Metadata.Reports.csproj --output $BUILD_FOLDER
dotnet pack SabreTools.ObjectIdentifier\SabreTools.ObjectIdentifier.csproj --output $BUILD_FOLDER
dotnet pack SabreTools.Serialization.CrossModel\SabreTools.Serialization.CrossModel.csproj --output $BUILD_FOLDER
dotnet pack SabreTools.Serialization.Readers\SabreTools.Serialization.Readers.csproj --output $BUILD_FOLDER