UNPKG

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
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);