igir
Version:
🕹 A zero-setup ROM collection manager that sorts, filters, extracts or archives, patches, and reports on collections of any size on any OS.
991 lines (990 loc) • 34.5 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import 'reflect-metadata';
import os from 'node:os';
import path from 'node:path';
import { Expose, instanceToPlain, plainToInstance } from 'class-transformer';
import fg from 'fast-glob';
import micromatch from 'micromatch';
import moment from 'moment';
import { LogLevel } from '../console/logLevel.js';
import Temp from '../globals/temp.js';
import ArrayPoly from '../polyfill/arrayPoly.js';
import FsPoly, { WalkMode } from '../polyfill/fsPoly.js';
import URLPoly from '../polyfill/urlPoly.js';
import Disk from './dats/disk.js';
import IgirException from './exceptions/igirException.js';
import { ChecksumBitmask, } from './files/fileChecksums.js';
export const InputChecksumArchivesMode = {
// Never calculate the checksum of archive files
NEVER: 1,
// Calculate the checksum of archive files if DATs reference archives
AUTO: 2,
// Always calculate the checksum of archive files
ALWAYS: 3,
};
export const InputChecksumArchivesModeInverted = Object.fromEntries(Object.entries(InputChecksumArchivesMode).map(([key, value]) => [value, key]));
export const LinkMode = {
// Create hard links to the original files
HARDLINK: 1,
// Create symbolic links to the original files
SYMLINK: 2,
// Create copy-on-write links to the original files
REFLINK: 3,
};
export const LinkModeInverted = Object.fromEntries(Object.entries(LinkMode).map(([key, value]) => [value, key]));
export const MergeMode = {
// Clones contain all parent ROMs, all games contain BIOS & device ROMs
FULLNONMERGED: 1,
// Clones contain all parent ROMs, BIOS & device ROMsets are separate
NONMERGED: 2,
// Clones exclude all parent ROMs, BIOS & device ROMsets are separate
SPLIT: 3,
// Clones are merged into parent, BIOS & device ROMsets are separate
MERGED: 4,
};
export const MergeModeInverted = Object.fromEntries(Object.entries(MergeMode).map(([key, value]) => [value, key]));
export const GameSubdirMode = {
// Never add the Game name as a subdirectory
NEVER: 1,
// Add the Game name as a subdirectory if it has multiple output files
MULTIPLE: 2,
// Always add the Game name as a subdirectory
ALWAYS: 3,
};
export const GameSubdirModeInverted = Object.fromEntries(Object.entries(GameSubdirMode).map(([key, value]) => [value, key]));
export const FixExtension = {
NEVER: 1,
AUTO: 2,
ALWAYS: 3,
};
export const FixExtensionInverted = Object.fromEntries(Object.entries(FixExtension).map(([key, value]) => [value, key]));
export const MoveDeleteDirs = {
NEVER: 1,
AUTO: 2,
ALWAYS: 3,
};
export const MoveDeleteDirsInverted = Object.fromEntries(Object.entries(MoveDeleteDirs).map(([key, value]) => [value, key]));
export const PreferRevision = {
OLDER: 1,
NEWER: 2,
};
export const PreferRevisionInverted = Object.fromEntries(Object.entries(PreferRevision).map(([key, value]) => [value, key]));
export const ZipFormat = {
TORRENTZIP: 'TORRENTZIP',
RVZSTD: 'RVZSTD',
};
export const ZipFormatInverted = Object.fromEntries(Object.entries(ZipFormat).map(([key, value]) => [value, key]));
/**
* A collection of all options for a single invocation of the application.
*/
export default class Options {
commands;
input;
inputExclude;
inputChecksumQuick;
inputChecksumMin;
inputChecksumMax;
inputChecksumArchives;
dat;
datExclude;
datNameRegex;
datNameRegexExclude;
datDescriptionRegex;
datDescriptionRegexExclude;
datCombine;
datIgnoreParentClone;
patch;
patchExclude;
output;
dirMirror;
dirDatMirror;
dirDatName;
dirDatDescription;
dirLetter;
dirLetterCount;
dirLetterLimit;
dirLetterGroup;
dirGameSubdir;
fixExtension;
overwrite;
overwriteInvalid;
moveDeleteDirs;
cleanExclude;
cleanBackup;
cleanDryRun;
zipFormat;
zipExclude;
zipDatName;
linkMode;
symlinkRelative;
header;
removeHeaders;
trimmedGlob;
trimScanArchives;
mergeRoms;
mergeDiscs;
excludeDisks;
allowExcessSets;
allowIncompleteSets;
filterRegex;
filterRegexExclude;
filterLanguage;
filterRegion;
filterCategoryRegex;
noBios;
onlyBios;
noDevice;
onlyDevice;
noUnlicensed;
onlyUnlicensed;
onlyRetail;
noDebug;
onlyDebug;
noDemo;
onlyDemo;
noBeta;
onlyBeta;
noSample;
onlySample;
noPrototype;
onlyPrototype;
noProgram;
onlyProgram;
noAftermarket;
onlyAftermarket;
noHomebrew;
onlyHomebrew;
noUnverified;
onlyUnverified;
noBad;
onlyBad;
single;
preferGameRegex;
preferRomRegex;
preferVerified;
preferGood;
preferLanguage;
preferRegion;
preferRevision;
preferRetail;
preferParent;
playlistExtensions;
dir2datOutput;
fixdatOutput;
reportOutput;
datThreads;
readerThreads;
writerThreads;
writeRetry;
tempDir;
disableCache;
cachePath;
verbose;
help;
constructor(options) {
this.commands = options?.commands ?? [];
this.input = (options?.input ?? []).map((filePath) => filePath.replaceAll(/[\\/]/g, path.sep));
this.inputExclude = (options?.inputExclude ?? []).map((filePath) => filePath.replaceAll(/[\\/]/g, path.sep));
this.inputChecksumQuick = options?.inputChecksumQuick ?? false;
this.inputChecksumMin = options?.inputChecksumMin;
this.inputChecksumMax = options?.inputChecksumMax;
this.inputChecksumArchives = options?.inputChecksumArchives;
this.dat = (options?.dat ?? []).map((filePath) => filePath.replaceAll(/[\\/]/g, path.sep));
this.datExclude = (options?.datExclude ?? []).map((filePath) => filePath.replaceAll(/[\\/]/g, path.sep));
this.datNameRegex = options?.datNameRegex;
this.datNameRegexExclude = options?.datNameRegexExclude;
this.datDescriptionRegex = options?.datDescriptionRegex;
this.datDescriptionRegexExclude = options?.datDescriptionRegexExclude;
this.datCombine = options?.datCombine ?? false;
this.datIgnoreParentClone = options?.datIgnoreParentClone ?? false;
this.patch = (options?.patch ?? []).map((filePath) => filePath.replaceAll(/[\\/]/g, path.sep));
this.patchExclude = (options?.patchExclude ?? []).map((filePath) => filePath.replaceAll(/[\\/]/g, path.sep));
this.output = options?.output?.replace(/[\\/]/g, path.sep);
this.dirMirror = options?.dirMirror ?? false;
this.dirDatMirror = options?.dirDatMirror ?? false;
this.dirDatName = options?.dirDatName ?? false;
this.dirDatDescription = options?.dirDatDescription ?? false;
this.dirLetter = options?.dirLetter ?? false;
this.dirLetterCount = options?.dirLetterCount ?? 0;
this.dirLetterLimit = options?.dirLetterLimit ?? 0;
this.dirLetterGroup = options?.dirLetterGroup ?? false;
this.dirGameSubdir = options?.dirGameSubdir;
this.fixExtension = options?.fixExtension;
this.overwrite = options?.overwrite ?? false;
this.overwriteInvalid = options?.overwriteInvalid ?? false;
this.moveDeleteDirs = options?.moveDeleteDirs;
this.cleanExclude = (options?.cleanExclude ?? []).map((filePath) => filePath.replaceAll(/[\\/]/g, path.sep));
this.cleanBackup = options?.cleanBackup?.replace(/[\\/]/g, path.sep);
this.cleanDryRun = options?.cleanDryRun ?? false;
this.zipFormat = options?.zipFormat;
this.zipExclude = options?.zipExclude ?? '';
this.zipDatName = options?.zipDatName ?? false;
this.linkMode = options?.linkMode;
this.symlinkRelative = options?.symlinkRelative ?? false;
this.header = options?.header;
this.removeHeaders = options?.removeHeaders;
this.trimmedGlob = options?.trimmedGlob;
this.trimScanArchives = options?.trimScanArchives ?? false;
this.mergeRoms = options?.mergeRoms;
this.mergeDiscs = options?.mergeDiscs ?? false;
this.excludeDisks = options?.excludeDisks ?? false;
this.allowExcessSets = options?.allowExcessSets ?? false;
this.allowIncompleteSets = options?.allowIncompleteSets ?? false;
this.filterRegex = options?.filterRegex;
this.filterRegexExclude = options?.filterRegexExclude;
this.filterLanguage = options?.filterLanguage ?? [];
this.filterRegion = options?.filterRegion ?? [];
this.filterCategoryRegex = options?.filterCategoryRegex;
this.noBios = options?.noBios ?? false;
this.onlyBios = options?.onlyBios ?? false;
this.noDevice = options?.noDevice ?? false;
this.onlyDevice = options?.onlyDevice ?? false;
this.noUnlicensed = options?.noUnlicensed ?? false;
this.onlyUnlicensed = options?.onlyUnlicensed ?? false;
this.onlyRetail = options?.onlyRetail ?? false;
this.noDebug = options?.noDebug ?? false;
this.onlyDebug = options?.onlyDebug ?? false;
this.noDemo = options?.noDemo ?? false;
this.onlyDemo = options?.onlyDemo ?? false;
this.noBeta = options?.noBeta ?? false;
this.onlyBeta = options?.onlyBeta ?? false;
this.noSample = options?.noSample ?? false;
this.onlySample = options?.onlySample ?? false;
this.noPrototype = options?.noPrototype ?? false;
this.onlyPrototype = options?.onlyPrototype ?? false;
this.noProgram = options?.noProgram ?? false;
this.onlyProgram = options?.onlyProgram ?? false;
this.noAftermarket = options?.noAftermarket ?? false;
this.onlyAftermarket = options?.onlyAftermarket ?? false;
this.noHomebrew = options?.noHomebrew ?? false;
this.onlyHomebrew = options?.onlyHomebrew ?? false;
this.noUnverified = options?.noUnverified ?? false;
this.onlyUnverified = options?.onlyUnverified ?? false;
this.noBad = options?.noBad ?? false;
this.onlyBad = options?.onlyBad ?? false;
this.single = options?.single ?? false;
this.preferGameRegex = options?.preferGameRegex;
this.preferRomRegex = options?.preferRomRegex;
this.preferVerified = options?.preferVerified ?? false;
this.preferGood = options?.preferGood ?? false;
this.preferLanguage = options?.preferLanguage ?? [];
this.preferRegion = options?.preferRegion ?? [];
this.preferRevision = options?.preferRevision;
this.preferRetail = options?.preferRetail ?? false;
this.preferParent = options?.preferParent ?? false;
this.playlistExtensions = options?.playlistExtensions ?? [];
this.dir2datOutput = options?.dir2datOutput?.replace(/[\\/]/g, path.sep);
this.fixdatOutput = options?.fixdatOutput?.replace(/[\\/]/g, path.sep);
this.reportOutput = (options?.reportOutput ?? process.cwd()).replaceAll(/[\\/]/g, path.sep);
this.datThreads = Math.max(options?.datThreads ?? 0, 1);
this.readerThreads = Math.max(options?.readerThreads ?? 0, 1);
this.writerThreads = Math.max(options?.writerThreads ?? 0, 1);
this.writeRetry = Math.max(options?.writeRetry ?? 0, 0);
this.tempDir = (options?.tempDir ?? Temp.getTempDir()).replaceAll(/[\\/]/g, path.sep);
this.disableCache = options?.disableCache ?? false;
this.cachePath = options?.cachePath;
this.verbose = options?.verbose ?? 0;
this.help = options?.help ?? false;
}
/**
* Construct a {@link Options} from a generic object, such as one from `yargs`.
*/
static fromObject(obj) {
return plainToInstance(Options, obj, {
enableImplicitConversion: true,
});
}
/**
* Return an object of all options.
*/
toObject() {
return instanceToPlain(this);
}
/**
* Return a JSON representation of all options.
*/
toString() {
return JSON.stringify(this.toObject());
}
// Helpers
static getRegex(pattern) {
if (!pattern?.trim()) {
return undefined;
}
return pattern
.split(/\r?\n/)
.filter((line) => line.length > 0)
.map((line) => {
const flagsMatch = /^\/(.+)\/([a-z]*)$/.exec(line);
if (flagsMatch !== null) {
return new RegExp(flagsMatch[1], flagsMatch[2]);
}
return new RegExp(line);
});
}
// Commands
getCommands() {
return new Set(this.commands.map((c) => c.toLowerCase()));
}
/**
* Was any writing command provided?
*/
shouldWrite() {
return this.writeString() !== undefined;
}
/**
* The writing command that was specified.
*/
writeString() {
return ['copy', 'move', 'link'].find((command) => this.getCommands().has(command));
}
/**
* Was the `copy` command provided?
*/
shouldCopy() {
return this.getCommands().has('copy');
}
/**
* Was the `move` command provided?
*/
shouldMove() {
return this.getCommands().has('move');
}
/**
* Was the `link` command provided?
*/
shouldLink() {
return this.getCommands().has('link');
}
/**
* Was the `extract` command provided?
*/
shouldExtract() {
return this.getCommands().has('extract');
}
/**
* Should a given ROM be extracted?
*/
shouldExtractRom(rom) {
if (rom instanceof Disk) {
return false;
}
return this.shouldExtract();
}
/**
* Was the `zip` command provided?
*/
shouldZip() {
return this.getCommands().has('zip');
}
/**
* Should a given output file path be zipped?
*/
shouldZipRom(rom) {
if (rom instanceof Disk) {
return false;
}
return (this.shouldZip() &&
(!this.getZipExclude() ||
!micromatch.isMatch(rom.getName().replace(/^.[\\/]/, ''), this.getZipExclude())));
}
/**
* Was the 'playlist' command provided?
*/
shouldPlaylist() {
return this.getCommands().has('playlist');
}
/**
* Was the 'dir2dat' command provided?
*/
shouldDir2Dat() {
return this.getCommands().has('dir2dat');
}
/**
* Was the 'fixdat' command provided?
*/
shouldFixdat() {
return this.getCommands().has('fixdat');
}
/**
* Was the `test` command provided?
*/
shouldTest() {
return this.getCommands().has('test');
}
/**
* Was the `clean` command provided?
*/
shouldClean() {
return this.getCommands().has('clean');
}
/**
* Was the `report` command provided?
*/
shouldReport() {
return this.getCommands().has('report');
}
// Options
getInputPaths() {
return this.input;
}
async scanInputFiles(walkCallback) {
return Options.scanPaths(this.input, WalkMode.FILES, walkCallback, this.shouldWrite() || !(this.shouldReport() || this.shouldFixdat()));
}
async scanInputExcludeFiles() {
return Options.scanPaths(this.inputExclude, WalkMode.FILES, undefined, false);
}
/**
* Scan for input files, and input files to exclude, and return the difference.
*/
async scanInputFilesWithoutExclusions(walkCallback) {
const inputFiles = await this.scanInputFiles(walkCallback);
const inputExcludeFiles = new Set(await this.scanInputExcludeFiles());
return inputFiles.filter((inputPath) => !inputExcludeFiles.has(inputPath));
}
/**
* Scan for subdirectories in the input paths.
*/
async scanInputSubdirectories(walkCallback) {
return Options.scanPaths(this.input, WalkMode.DIRECTORIES, walkCallback, false);
}
static async scanPaths(globPatterns, walkMode, walkCallback, requireFiles = true) {
// Limit to scanning one glob pattern at a time to keep memory in check
const uniqueGlobPatterns = globPatterns.reduce(ArrayPoly.reduceUnique(), []);
let globbedPaths = [];
for (const uniqueGlobPattern of uniqueGlobPatterns) {
const paths = await this.globPath(uniqueGlobPattern, walkMode, walkCallback);
// NOTE(cemmer): if `paths` is really large, `globbedPaths.push(...paths)` can hit a stack
// size limit
globbedPaths = [...globbedPaths, ...paths];
}
if (requireFiles && globbedPaths.length === 0) {
throw new IgirException(`no files found in director${globPatterns.length === 1 ? 'y' : 'ies'}: ${globPatterns.map((p) => `'${p}'`).join(', ')}`);
}
// Remove duplicates
return globbedPaths.reduce(ArrayPoly.reduceUnique(), []);
}
static async globPath(inputPath, walkMode, walkCallback) {
// Windows will report that \\.\nul doesn't exist, catch it explicitly
if (inputPath === os.devNull || inputPath.startsWith(os.devNull + path.sep)) {
return [];
}
// Glob the contents of directories
if (await FsPoly.isDirectory(inputPath)) {
return FsPoly.walk(inputPath, walkMode, walkCallback);
}
// If the file exists, don't process it as a glob pattern
if (await FsPoly.exists(inputPath)) {
if (walkCallback !== undefined) {
walkCallback(1);
}
return [inputPath];
}
// fg only uses forward-slash path separators
const inputPathNormalized = inputPath.replaceAll('\\', '/');
// Try to handle globs a little more intelligently (see the JSDoc below)
const inputPathEscaped = await this.sanitizeGlobPattern(inputPathNormalized);
if (!inputPathEscaped) {
// fast-glob will throw with empty-ish inputs
return [];
}
// Otherwise, process it as a glob pattern
const globbedPaths = await fg(inputPathEscaped, {
onlyFiles: walkMode === WalkMode.FILES,
onlyDirectories: walkMode === WalkMode.DIRECTORIES,
});
if (globbedPaths.length === 0) {
if (URLPoly.canParse(inputPath)) {
// Allow URLs, let the scanner modules deal with them
if (walkCallback !== undefined) {
walkCallback(1);
}
return [inputPath];
}
return [];
}
if (walkCallback !== undefined) {
walkCallback(globbedPaths.length);
}
if (path.sep !== '/') {
return globbedPaths.map((globbedPath) => globbedPath.replaceAll(/[\\/]/g, path.sep));
}
return globbedPaths;
}
/**
* Trying to use globs with directory names that resemble glob patterns (e.g. dirs that include
* parentheticals) is problematic. Most of the time globs are at the tail end of the path, so try
* to figure out what leading part of the pattern is just a path, and escape it appropriately,
* and then tack on the glob at the end.
* Example problematic paths:
* ./TOSEC - DAT Pack - Complete (3983) (TOSEC-v2023-07-10)/TOSEC-ISO/Sega*
*/
static async sanitizeGlobPattern(globPattern) {
const pathsSplit = globPattern.split(/[\\/]/);
for (let i = 0; i < pathsSplit.length; i += 1) {
const subPath = pathsSplit.slice(0, i + 1).join('/');
if (subPath !== '' && !(await FsPoly.exists(subPath))) {
const dirname = pathsSplit.slice(0, i).join('/');
if (dirname === '') {
// fg won't let you escape empty strings
return pathsSplit.slice(i).join('/');
}
return `${fg.escapePath(dirname)}/${pathsSplit.slice(i).join('/')}`;
}
}
return globPattern;
}
getInputChecksumQuick() {
return this.inputChecksumQuick;
}
getInputChecksumMin() {
const checksumBitmask = Object.keys(ChecksumBitmask).find((bitmask) => bitmask.toUpperCase() === this.inputChecksumMin?.toUpperCase());
if (!checksumBitmask) {
return undefined;
}
return ChecksumBitmask[checksumBitmask];
}
getInputChecksumMax() {
const checksumBitmask = Object.keys(ChecksumBitmask).find((bitmask) => bitmask.toUpperCase() === this.inputChecksumMax?.toUpperCase());
if (!checksumBitmask) {
return undefined;
}
return ChecksumBitmask[checksumBitmask];
}
getInputChecksumArchives() {
const checksumMode = Object.keys(InputChecksumArchivesMode).find((mode) => mode.toLowerCase() === this.inputChecksumArchives?.toLowerCase());
if (!checksumMode) {
return undefined;
}
return InputChecksumArchivesMode[checksumMode];
}
/**
* Were any DAT paths provided?
*/
usingDats() {
return this.dat.length > 0;
}
getDatPaths() {
return this.dat;
}
async scanDatFiles(walkCallback) {
return Options.scanPaths(this.dat, WalkMode.FILES, walkCallback);
}
async scanDatExcludeFiles() {
return Options.scanPaths(this.datExclude, WalkMode.FILES, undefined, false);
}
/**
* Scan for DAT files, and DAT files to exclude, and return the difference.
*/
async scanDatFilesWithoutExclusions(walkCallback) {
const datFiles = await this.scanDatFiles(walkCallback);
const datExcludeFiles = new Set(await this.scanDatExcludeFiles());
return datFiles.filter((inputPath) => !datExcludeFiles.has(inputPath));
}
getDatNameRegex() {
return Options.getRegex(this.datNameRegex);
}
getDatNameRegexExclude() {
return Options.getRegex(this.datNameRegexExclude);
}
getDatDescriptionRegex() {
return Options.getRegex(this.datDescriptionRegex);
}
getDatDescriptionRegexExclude() {
return Options.getRegex(this.datDescriptionRegexExclude);
}
getDatCombine() {
return this.datCombine;
}
getDatIgnoreParentClone() {
return this.datIgnoreParentClone;
}
getPatchFileCount() {
return this.patch.length;
}
/**
* Scan for patch files, and patch files to exclude, and return the difference.
*/
async scanPatchFilesWithoutExclusions(walkCallback) {
const patchFiles = await this.scanPatchFiles(walkCallback);
const patchExcludeFiles = new Set(await this.scanPatchExcludeFiles());
return patchFiles.filter((patchPath) => !patchExcludeFiles.has(patchPath));
}
async scanPatchFiles(walkCallback) {
return Options.scanPaths(this.patch, WalkMode.FILES, walkCallback);
}
async scanPatchExcludeFiles() {
return Options.scanPaths(this.patchExclude, WalkMode.FILES, undefined, false);
}
getOutput() {
return this.output ?? (this.shouldWrite() ? '' : this.getTempDir());
}
/**
* Get the "root" sub-path of the output dir, the sub-path up until the first replaceable token.
*/
getOutputDirRoot() {
const outputSplit = this.getOutput().split(/[\\/]/);
for (let i = 0; i < outputSplit.length; i += 1) {
if (/\{[a-zA-Z]+\}/.test(outputSplit[i])) {
return outputSplit.slice(0, i).join(path.sep);
}
}
return outputSplit.join(path.sep);
}
getDirMirror() {
return this.dirMirror;
}
getDirDatMirror() {
return this.dirDatMirror;
}
getDirDatName() {
return this.dirDatName;
}
getDirDatDescription() {
return this.dirDatDescription;
}
getDirLetter() {
return this.dirLetter;
}
getDirLetterCount() {
return this.dirLetterCount;
}
getDirLetterLimit() {
return this.dirLetterLimit;
}
getDirLetterGroup() {
return this.dirLetterGroup;
}
getDirGameSubdir() {
const subdirMode = Object.keys(GameSubdirMode).find((mode) => mode.toLowerCase() === this.dirGameSubdir?.toLowerCase());
if (!subdirMode) {
return undefined;
}
return GameSubdirMode[subdirMode];
}
getFixExtension() {
const fixExtensionMode = Object.keys(FixExtension).find((mode) => mode.toLowerCase() === this.fixExtension?.toLowerCase());
if (!fixExtensionMode) {
return undefined;
}
return FixExtension[fixExtensionMode];
}
getOverwrite() {
return this.overwrite;
}
getOverwriteInvalid() {
return this.overwriteInvalid;
}
getMoveDeleteDirs() {
const moveDeleteDirsMode = Object.keys(MoveDeleteDirs).find((mode) => mode.toLowerCase() === this.moveDeleteDirs?.toLowerCase());
if (!moveDeleteDirsMode) {
return undefined;
}
return MoveDeleteDirs[moveDeleteDirsMode];
}
async scanCleanExcludeFiles() {
return Options.scanPaths(this.cleanExclude, WalkMode.FILES, undefined, false);
}
/**
* Scan for output files, and output files to exclude from cleaning, and return the difference.
*/
async scanOutputFilesWithoutCleanExclusions(outputDirs, writtenFiles, walkCallback) {
// Written files that shouldn't be cleaned
const writtenFilesNormalized = new Set(writtenFiles.map((file) => path.normalize(file.getFilePath())));
// Files excluded from cleaning
const cleanExcludedFilesNormalized = new Set((await this.scanCleanExcludeFiles()).map((filePath) => path.normalize(filePath)));
return (await Options.scanPaths(outputDirs, WalkMode.FILES, walkCallback, false))
.filter((filePath) => !writtenFilesNormalized.has(filePath) && !cleanExcludedFilesNormalized.has(filePath))
.sort();
}
getCleanBackup() {
return this.cleanBackup;
}
getCleanDryRun() {
return this.cleanDryRun;
}
getZipFormat() {
const zipFormat = Object.keys(ZipFormat).find((mode) => mode.toLowerCase() === this.zipFormat?.toLowerCase());
if (!zipFormat) {
return undefined;
}
return ZipFormat[zipFormat];
}
getZipExclude() {
return this.zipExclude;
}
getZipDatName() {
return this.zipDatName;
}
getLinkMode() {
const linkMode = Object.keys(LinkMode).find((mode) => mode.toLowerCase() === this.linkMode?.toLowerCase());
if (!linkMode) {
return undefined;
}
return LinkMode[linkMode];
}
getSymlinkRelative() {
return this.symlinkRelative;
}
/**
* Should a file have its contents read to detect any {@link Header}?
*/
shouldReadFileForHeader(filePath) {
return (this.header !== undefined &&
this.header.length > 0 &&
micromatch.isMatch(filePath.replace(/^.[\\/]/, ''), this.header));
}
/**
* Should a file have its contents read to detect any {@link ROMPadding}?
*/
shouldReadFileForTrimming(filePath) {
return (this.trimmedGlob !== undefined &&
this.trimmedGlob.length > 0 &&
micromatch.isMatch(filePath.replace(/^.[\\/]/, ''), this.trimmedGlob));
}
getTrimScanArchives() {
return this.trimScanArchives;
}
/**
* Can the {@link Header} be removed for a {@link extension} during writing?
*/
canRemoveHeader(extension) {
if (this.removeHeaders === undefined) {
// Option wasn't provided, we shouldn't remove headers
return false;
}
if (this.removeHeaders.length === 1 && this.removeHeaders[0] === '') {
// Option was provided without any extensions, we should remove headers from every file
return true;
}
// Option was provided with extensions, we should remove headers on name match
return this.removeHeaders.some((removeHeader) => removeHeader.toLowerCase() === extension.toLowerCase());
}
getMergeRoms() {
const mergeMode = Object.keys(MergeMode).find((mode) => mode.toLowerCase() === this.mergeRoms?.toLowerCase());
if (!mergeMode) {
return undefined;
}
return MergeMode[mergeMode];
}
getMergeDiscs() {
return this.mergeDiscs;
}
getExcludeDisks() {
return this.excludeDisks;
}
getAllowExcessSets() {
return this.allowExcessSets;
}
getAllowIncompleteSets() {
return this.allowIncompleteSets;
}
getFilterRegex() {
return Options.getRegex(this.filterRegex);
}
getFilterRegexExclude() {
return Options.getRegex(this.filterRegexExclude);
}
getFilterLanguage() {
if (this.filterLanguage.length > 0) {
return new Set(Options.filterUniqueUpper(this.filterLanguage));
}
return new Set();
}
getFilterRegion() {
if (this.filterRegion.length > 0) {
return new Set(Options.filterUniqueUpper(this.filterRegion));
}
return new Set();
}
getFilterCategoryRegex() {
return Options.getRegex(this.filterCategoryRegex);
}
getNoBios() {
return this.noBios;
}
getOnlyBios() {
return this.onlyBios;
}
getNoDevice() {
return this.noDevice;
}
getOnlyDevice() {
return this.onlyDevice;
}
getNoUnlicensed() {
return this.noUnlicensed;
}
getOnlyUnlicensed() {
return this.onlyUnlicensed;
}
getOnlyRetail() {
return this.onlyRetail;
}
getNoDebug() {
return this.noDebug;
}
getOnlyDebug() {
return this.onlyDebug;
}
getNoDemo() {
return this.noDemo;
}
getOnlyDemo() {
return this.onlyDemo;
}
getNoBeta() {
return this.noBeta;
}
getOnlyBeta() {
return this.onlyBeta;
}
getNoSample() {
return this.noSample;
}
getOnlySample() {
return this.onlySample;
}
getNoPrototype() {
return this.noPrototype;
}
getOnlyPrototype() {
return this.onlyPrototype;
}
getNoProgram() {
return this.noProgram;
}
getOnlyProgram() {
return this.onlyProgram;
}
getNoAftermarket() {
return this.noAftermarket;
}
getOnlyAftermarket() {
return this.onlyAftermarket;
}
getNoHomebrew() {
return this.noHomebrew;
}
getOnlyHomebrew() {
return this.onlyHomebrew;
}
getNoUnverified() {
return this.noUnverified;
}
getOnlyUnverified() {
return this.onlyUnverified;
}
getNoBad() {
return this.noBad;
}
getOnlyBad() {
return this.onlyBad;
}
getSingle() {
return this.single;
}
getPreferGameRegex() {
return Options.getRegex(this.preferGameRegex);
}
getPreferRomRegex() {
return Options.getRegex(this.preferRomRegex);
}
getPreferVerified() {
return this.preferVerified;
}
getPreferGood() {
return this.preferGood;
}
getPreferLanguages() {
return Options.filterUniqueUpper(this.preferLanguage);
}
getPreferRegions() {
return Options.filterUniqueUpper(this.preferRegion);
}
getPreferRevision() {
const preferRevision = Object.keys(PreferRevision).find((mode) => mode.toLowerCase() === this.preferRevision?.toLowerCase());
if (!preferRevision) {
return undefined;
}
return PreferRevision[preferRevision];
}
getPreferRetail() {
return this.preferRetail;
}
getPreferParent() {
return this.preferParent;
}
getPlaylistExtensions() {
return this.playlistExtensions;
}
getDir2DatOutput() {
return FsPoly.makeLegal(this.dir2datOutput ?? (this.shouldWrite() ? this.getOutputDirRoot() : process.cwd()));
}
getFixdatOutput() {
return FsPoly.makeLegal(this.fixdatOutput ?? (this.shouldWrite() ? this.getOutputDirRoot() : process.cwd()));
}
getReportOutput() {
let { reportOutput } = this;
// Replace date & time tokens
const symbolMatches = reportOutput.match(/%([a-zA-Z])(\1|o)*/g);
if (symbolMatches) {
symbolMatches.reduce(ArrayPoly.reduceUnique(), []).forEach((match) => {
const val = moment().format(match.replace(/^%/, ''));
reportOutput = reportOutput.replace(match, val);
});
}
return FsPoly.makeLegal(reportOutput);
}
getDatThreads() {
return this.datThreads;
}
getReaderThreads() {
return this.readerThreads;
}
getWriterThreads() {
return this.writerThreads;
}
getWriteRetry() {
return this.writeRetry;
}
getTempDir() {
return this.tempDir;
}
getDisableCache() {
return this.disableCache;
}
getCachePath() {
return this.cachePath;
}
getLogLevel() {
if (this.verbose === 1) {
return LogLevel.INFO;
}
if (this.verbose === 2) {
return LogLevel.DEBUG;
}
if (this.verbose >= 3) {
return LogLevel.TRACE;
}
return LogLevel.WARN;
}
getHelp() {
return this.help;
}
static filterUniqueUpper(array) {
return array.map((value) => value.toUpperCase()).reduce(ArrayPoly.reduceUnique(), []);
}
}
__decorate([
Expose({ name: '_' }),
__metadata("design:type", Array)
], Options.prototype, "commands", void 0);