emmercm/igir

View on GitHub
src/modules/reportGenerator.ts

Summary

Maintainability
B
4 hrs
Test Coverage
import path from 'node:path';

import ProgressBar from '../console/progressBar.js';
import FsPoly from '../polyfill/fsPoly.js';
import DATStatus, { GameStatus } from '../types/datStatus.js';
import File from '../types/files/file.js';
import Options from '../types/options.js';
import Module from './module.js';

/**
 * Generate a single report file with information about every DAT processed.
 */
export default class ReportGenerator extends Module {
  private readonly options: Options;

  constructor(options: Options, progressBar: ProgressBar) {
    super(progressBar, ReportGenerator.name);
    this.options = options;
  }

  /**
   * Generate the report.
   */
  async generate(
    scannedRomFiles: File[],
    cleanedOutputFiles: string[],
    datStatuses: DATStatus[],
  ): Promise<void> {
    this.progressBar.logTrace('generating report');

    const reportPath = this.options.getReportOutput();

    const anyGamesFoundAtAll = datStatuses.some((datStatus) =>
      datStatus.anyGamesFound(this.options),
    );
    const matchedFileCsvs = (
      await Promise.all(
        datStatuses
          .filter((datStatus) => datStatus.anyGamesFound(this.options) || !anyGamesFoundAtAll)
          .sort((a, b) => a.getDATName().localeCompare(b.getDATName()))
          .map(async (datsStatus) => datsStatus.toCsv(this.options)),
      )
    )
      .filter((csv) => csv)
      .map((csv, idx) => {
        // Strip the CSV header from everything except the first file
        if (idx === 0) {
          return csv;
        }
        return csv.split('\n').slice(1).join('\n');
      });

    const usedFilePaths = new Set(
      datStatuses
        .flatMap((datStatus) => datStatus.getInputFiles())
        .map((file) => file.getFilePath()),
    );
    const usedHashes = new Set(
      datStatuses.flatMap((datStatus) => datStatus.getInputFiles()).map((file) => file.hashCode()),
    );

    const duplicateFilePaths = scannedRomFiles
      .filter(
        (inputFile) =>
          !usedFilePaths.has(inputFile.getFilePath()) && usedHashes.has(inputFile.hashCode()),
      )
      .map((inputFile) => inputFile.getFilePath())
      .filter((inputFile) => !usedFilePaths.has(inputFile))
      .sort();
    const duplicateCsv = await DATStatus.filesToCsv(duplicateFilePaths, GameStatus.DUPLICATE);

    const unusedFilePaths = scannedRomFiles
      .filter(
        (inputFile) =>
          !usedFilePaths.has(inputFile.getFilePath()) && !usedHashes.has(inputFile.hashCode()),
      )
      .map((inputFile) => inputFile.getFilePath())
      .filter((inputFile) => !usedFilePaths.has(inputFile))
      .sort();
    const unusedCsv = await DATStatus.filesToCsv(unusedFilePaths, GameStatus.UNUSED);

    const cleanedCsv = await DATStatus.filesToCsv(cleanedOutputFiles, GameStatus.DELETED);

    this.progressBar.logInfo(`writing report '${reportPath}'`);
    const reportPathDir = path.dirname(reportPath);
    if (!(await FsPoly.exists(reportPathDir))) {
      await FsPoly.mkdir(reportPathDir, { recursive: true });
    }
    const rows = [...matchedFileCsvs, duplicateCsv, unusedCsv, cleanedCsv].filter((csv) => csv);
    await FsPoly.writeFile(reportPath, rows.join('\n'));
    this.progressBar.logTrace(
      `wrote ${datStatuses.length.toLocaleString()} CSV row${datStatuses.length !== 1 ? 's' : ''}: ${reportPath}`,
    );

    this.progressBar.logTrace('done generating report');
    this.progressBar.done(reportPath);
    this.progressBar.freeze();
  }
}