emmercm/igir

View on GitHub
src/modules/fixdatCreator.ts

Summary

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

import moment from 'moment';

import ProgressBar, { ProgressBarSymbol } from '../console/progressBar.js';
import Package from '../globals/package.js';
import fsPoly from '../polyfill/fsPoly.js';
import DAT from '../types/dats/dat.js';
import Header from '../types/dats/logiqx/header.js';
import LogiqxDAT from '../types/dats/logiqx/logiqxDat.js';
import Parent from '../types/dats/parent.js';
import Options from '../types/options.js';
import OutputFactory from '../types/outputFactory.js';
import ReleaseCandidate from '../types/releaseCandidate.js';
import Module from './module.js';

/**
 * Create a "fixdat" that contains every {@link Game} that has at least one {@link ROM} that wasn't
 * found, and therefore the {@link Game} was not written to the output.
 */
export default class FixdatCreator extends Module {
  private readonly options: Options;

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

  /**
   * Create & write a fixdat.
   */
  async create(
    originalDat: DAT,
    parentsToCandidates: Map<Parent, ReleaseCandidate[]>,
  ): Promise<string | undefined> {
    if (!this.options.shouldFixdat()) {
      return undefined;
    }

    this.progressBar.logTrace(`${originalDat.getNameShort()}: generating a fixdat`);
    this.progressBar.setSymbol(ProgressBarSymbol.WRITING);
    this.progressBar.reset(1);

    // Create an easily searchable index of every ROM that has a ReleaseCandidate
    const writtenRomHashCodes = new Set(
      [...parentsToCandidates.values()]
        .flat()
        .flatMap((releaseCandidate) => releaseCandidate.getRomsWithFiles())
        .map((romWithFiles) => romWithFiles.getRom())
        .map((rom) => rom.hashCode()),
    );
    // Find all the games that have at least one missing ROM
    const gamesWithMissingRoms = originalDat
      .getGames()
      .filter((game) => !game.getRoms().every((rom) => writtenRomHashCodes.has(rom.hashCode())));
    if (gamesWithMissingRoms.length === 0) {
      this.progressBar.logDebug(
        `${originalDat.getNameShort()}: not creating a fixdat, all games were found`,
      );
      return undefined;
    }

    let fixdatDir = process.cwd();
    if (this.options.shouldWrite()) {
      try {
        fixdatDir = this.getDatOutputDirRoot(originalDat);
      } catch (error) {
        this.progressBar.logWarn(
          `${originalDat.getNameShort()}: failed to get output directory: ${error}`,
        );
      }
    }
    if (!(await fsPoly.exists(fixdatDir))) {
      await fsPoly.mkdir(fixdatDir, { recursive: true });
    }

    // Construct a new DAT header
    const date = moment().format('YYYYMMDD-HHmmss');
    const header = new Header({
      name: `${originalDat.getHeader().getName()} fixdat`.trim(),
      description: `${originalDat.getHeader().getDescription()} fixdat`.trim(),
      version: date,
      date,
      url: Package.HOMEPAGE,
      comment: [
        `fixdat generated by ${Package.NAME} v${Package.VERSION}`,
        `Original DAT: ${originalDat.toString()}`,
        `Input paths: ${this.options
          .getInputPaths()
          .map((val) => `'${val}'`)
          .join(', ')}`,
        `Output path: ${fixdatDir}`,
      ].join('\n'),
    });

    // Construct a new DAT and write it to the output dir
    const fixdat = new LogiqxDAT(header, gamesWithMissingRoms);
    const fixdatContents = fixdat.toXmlDat();
    const fixdatPath = path.join(fixdatDir, fixdat.getFilename());
    this.progressBar.logInfo(`${originalDat.getNameShort()}: writing fixdat to '${fixdatPath}'`);
    await fsPoly.writeFile(fixdatPath, fixdatContents);

    this.progressBar.logTrace(`${originalDat.getNameShort()}: done generating a fixdat`);
    return fixdatPath;
  }

  private getDatOutputDirRoot(dat: DAT): string {
    return OutputFactory.getDir(
      new Options({
        ...this.options,
        // Chop off any path slugs that contain replaceable tokens, such that OutputFactory won't
        // throw an exception
        output: this.options.getOutputDirRoot(),
      }),
      dat,
    );
  }
}