diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 18a56a362..e688c3c0f 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -41,7 +41,7 @@ jobs: # !!! This check should be required by GitHub !!! pages-status-check: - if: always() + if: github.event_name == 'pull_request' needs: - markdown-lint - mkdocs-build @@ -58,6 +58,9 @@ jobs: contents: write steps: - uses: actions/checkout@v3 + with: + # the `git-revision-date-localized` plugin needs full history to find page creation date + fetch-depth: 0 - uses: actions/setup-python@v4 with: python-version: 3.x diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 8566f9fe8..febd584e6 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -58,6 +58,8 @@ jobs: - id: create-pull-request uses: peter-evans/create-pull-request@v5 with: + # GitHub won't run workflows off of events from the `github-actions` user + # But also, I want the PR to be created under my name for cosmetic reasons token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} author: ${{ steps.bump-and-commit.outputs.USER_NAME }} <${{ steps.bump-and-commit.outputs.USER_EMAIL }}> branch: ${{ github.actor }}/${{ steps.bump-and-commit.outputs.PACKAGE_VERSION }} diff --git a/docs/output/tokens.md b/docs/output/tokens.md index f0d6b5f1e..ed93d6253 100644 --- a/docs/output/tokens.md +++ b/docs/output/tokens.md @@ -42,6 +42,7 @@ ROMs-Sorted/ When using [DATs](../dats.md), you can make use of console & game information contained in them: - `{datName}` the matching DAT's name, similar to how the `--dir-dat-name` option works +- `{datDescription}` the matching DAT's description, similar to how the `--dir-dat-description` option works - `{datReleaseLanguage}` each of the ROM's language(s) (e.g. `EN`, `ES`, `JA`) - `{datReleaseRegion}` each of the ROM's region(s) (e.g. `USA`, `EUR`, `JPN`, `WORLD`) diff --git a/src/console/logger.ts b/src/console/logger.ts index cf1defd6d..f0cd7dd87 100644 --- a/src/console/logger.ts +++ b/src/console/logger.ts @@ -108,7 +108,8 @@ export default class Logger { const logoSplit = logo.split('\n'); const midLine = Math.min(Math.ceil(logoSplit.length / 2), logoSplit.length - 1); const maxLineLen = logoSplit.reduce((max, line) => Math.max(max, line.length), 0); - logoSplit[midLine - 1] = `${logoSplit[midLine - 1].padEnd(maxLineLen, ' ')} ROM collection manager`; + logoSplit[midLine - 2] = `${logoSplit[midLine - 1].padEnd(maxLineLen, ' ')} ROM collection manager`; + logoSplit[midLine - 1] = `${logoSplit[midLine - 1].padEnd(maxLineLen, ' ')} ${Constants.HOMEPAGE}`; logoSplit[midLine + 1] = `${logoSplit[midLine + 1].padEnd(maxLineLen, ' ')} v${Constants.COMMAND_VERSION}`; this.print(LogLevel.ALWAYS, `${logoSplit.join('\n')}\n\n`); diff --git a/src/modules/argumentsParser.ts b/src/modules/argumentsParser.ts index 3278ce9b6..db07b1e73 100644 --- a/src/modules/argumentsParser.ts +++ b/src/modules/argumentsParser.ts @@ -228,6 +228,12 @@ export default class ArgumentsParser { type: 'boolean', implies: 'dat', }) + .option('dir-dat-description', { + group: groupRomOutput, + description: 'Use the DAT description as the output subdirectory', + type: 'boolean', + implies: 'dat', + }) .option('dir-letter', { group: groupRomOutput, description: 'Append the first letter of the ROM name as an output subdirectory', @@ -526,6 +532,7 @@ Advanced usage: Tokens that are replaced when generating the output (--output) path of a ROM: {datName} The name of the DAT that contains the ROM (e.g. "Nintendo - Game Boy") + {datDescription} The description of the DAT that contains the ROM {datReleaseRegion} The region of the ROM release (e.g. "USA"), each ROM can have multiple {datReleaseLanguage} The language of the ROM release (e.g. "En"), each ROM can have multiple {gameType} The type of the game (e.g. "Retail", "Demo", "Prototype") diff --git a/src/types/logiqx/dat.ts b/src/types/logiqx/dat.ts index 0eeb05cdc..5d290a1b5 100644 --- a/src/types/logiqx/dat.ts +++ b/src/types/logiqx/dat.ts @@ -141,6 +141,10 @@ export default class DAT { .trim(); } + getDescription(): string | undefined { + return this.getHeader().getDescription(); + } + getRomNamesContainDirectories(): boolean { return this.getHeader().getRomNamesContainDirectories() || this.isBiosDat(); diff --git a/src/types/options.ts b/src/types/options.ts index 9fbd26b05..e33fb3ea6 100644 --- a/src/types/options.ts +++ b/src/types/options.ts @@ -42,6 +42,7 @@ export interface OptionsProps { readonly output?: string, readonly dirMirror?: boolean, readonly dirDatName?: boolean, + readonly dirDatDescription?: boolean, readonly dirLetter?: boolean, readonly dirLetterLimit?: number, readonly overwrite?: boolean, @@ -132,6 +133,8 @@ export default class Options implements OptionsProps { readonly dirDatName: boolean; + readonly dirDatDescription: boolean; + readonly dirLetter: boolean; readonly dirLetterLimit: number; @@ -256,6 +259,7 @@ export default class Options implements OptionsProps { this.output = options?.output ?? ''; this.dirMirror = options?.dirMirror ?? false; this.dirDatName = options?.dirDatName ?? false; + this.dirDatDescription = options?.dirDatDescription ?? false; this.dirLetter = options?.dirLetter ?? false; this.dirLetterLimit = options?.dirLetterLimit ?? 0; this.overwrite = options?.overwrite ?? false; @@ -681,6 +685,9 @@ export default class Options implements OptionsProps { if (this.getDirDatName() && dat.getNameShort()) { output = path.join(output, dat.getNameShort()); } + if (this.getDirDatDescription() && dat.getDescription()) { + output = path.join(output, dat.getDescription() as string); + } const dirLetter = this.getDirLetterParsed(romFilenameSanitized, romBasenames); if (dirLetter) { @@ -725,7 +732,15 @@ export default class Options implements OptionsProps { } private static replaceDatTokens(input: string, dat: DAT): string { - return input.replace('{datName}', dat.getName().replace(/[\\/]/g, '_')); + let output = input; + output = output.replace('{datName}', dat.getName().replace(/[\\/]/g, '_')); + + const description = dat.getDescription(); + if (description) { + output = output.replace('{datDescription}', description.replace(/[\\/]/g, '_')); + } + + return output; } private static replaceGameTokens(input: string, game?: Game): string { @@ -852,6 +867,10 @@ export default class Options implements OptionsProps { return this.dirDatName; } + getDirDatDescription(): boolean { + return this.dirDatDescription; + } + getDirLetter(): boolean { return this.dirLetter; } diff --git a/test/modules/argumentsParser.test.ts b/test/modules/argumentsParser.test.ts index cb79a625a..98d4d64c5 100644 --- a/test/modules/argumentsParser.test.ts +++ b/test/modules/argumentsParser.test.ts @@ -248,7 +248,7 @@ describe('options', () => { expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dir-mirror', 'true', '--dir-mirror', 'false']).getDirMirror()).toEqual(false); }); - it('should parse "dir-datname"', () => { + it('should parse "dir-dat-name"', () => { expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dir-dat-name'])).toThrow(/dependent|implication/i); expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '-D']).getDirDatName()).toEqual(true); expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--dir-dat-name']).getDirDatName()).toEqual(true); @@ -259,6 +259,16 @@ describe('options', () => { expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--dir-dat-name', 'true', '--dir-dat-name', 'false']).getDirDatName()).toEqual(false); }); + it('should parse "dir-dat-description"', () => { + expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dir-dat-description'])).toThrow(/dependent|implication/i); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--dir-dat-description']).getDirDatDescription()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--dir-dat-description', 'true']).getDirDatDescription()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--dir-dat-description', 'false']).getDirDatDescription()).toEqual(false); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--dir-dat-description', '--dir-dat-description']).getDirDatDescription()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--dir-dat-description', 'false', '--dir-dat-description', 'true']).getDirDatDescription()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--dir-dat-description', 'true', '--dir-dat-description', 'false']).getDirDatDescription()).toEqual(false); + }); + it('should parse "dir-letter"', () => { expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dir-letter']).getDirLetter()).toEqual(true); expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dir-letter', 'true']).getDirLetter()).toEqual(true); diff --git a/test/types/options.test.ts b/test/types/options.test.ts index 43ea7fe09..bb836cd7c 100644 --- a/test/types/options.test.ts +++ b/test/types/options.test.ts @@ -19,6 +19,7 @@ describe('getOutputDirRoot', () => { ['games/{mister}/', 'games'], ['Roms/{onion}/', 'Roms'], ['{datName}', '.'], + ['{datDescription}', '.'], ])('should find the root dir: %s', (output, expectedPath) => { expect(new Options({ commands: ['copy'], output }).getOutputDirRoot()).toEqual(expectedPath); }); @@ -46,10 +47,11 @@ describe('getOutputFileParsed', () => { describe('token replacement', () => { test.each([ ['foo/{datName}/bar', path.join('foo', 'DAT _ Name', 'bar', 'game.rom')], + ['foo/{datDescription}/bar', path.join('foo', 'DAT _ Description', 'bar', 'game.rom')], ['root/{datReleaseRegion}', path.join('root', 'USA', 'game.rom')], ['root/{datReleaseLanguage}', path.join('root', 'En', 'game.rom')], ])('should replace {dat*}: %s', (output, expectedPath) => { - const dat = new DAT(new Header({ name: 'DAT / Name' }), []); + const dat = new DAT(new Header({ name: 'DAT / Name', description: 'DAT \\ Description' }), []); const release = new Release('Game Name', 'USA', 'En'); expect(new Options({ commands: ['copy'], output }).getOutputFileParsed(dat, dummyInputRomPath, dummyGame, release, 'game.rom')).toEqual(expectedPath); }); @@ -182,12 +184,19 @@ describe('getOutputFileParsed', () => { }); it('should respect "--dir-dat-name"', () => { - const dat = new DAT(new Header({ name: 'system' }), []); + const dat = new DAT(new Header({ name: 'name', description: 'description' }), []); expect(new Options({ commands: ['copy'], output: os.devNull, dirDatName: true }).getOutputFileParsed(dummyDat, dummyInputRomPath, dummyGame, dummyRelease, dummyRomFilename)).toEqual(os.devNull); - expect(new Options({ commands: ['copy'], output: os.devNull, dirDatName: true }).getOutputFileParsed(dat, dummyInputRomPath, dummyGame, dummyRelease, dummyRomFilename)).toEqual(path.join(os.devNull, 'system')); + expect(new Options({ commands: ['copy'], output: os.devNull, dirDatName: true }).getOutputFileParsed(dat, dummyInputRomPath, dummyGame, dummyRelease, dummyRomFilename)).toEqual(path.join(os.devNull, 'name')); expect(new Options({ commands: ['copy'], output: os.devNull, dirDatName: false }).getOutputFileParsed(dat, dummyInputRomPath, dummyGame, dummyRelease, dummyRomFilename)).toEqual(os.devNull); }); + it('should respect "--dir-dat-description"', () => { + const dat = new DAT(new Header({ name: 'name', description: 'description' }), []); + expect(new Options({ commands: ['copy'], output: os.devNull, dirDatDescription: true }).getOutputFileParsed(dummyDat, dummyInputRomPath, dummyGame, dummyRelease, dummyRomFilename)).toEqual(os.devNull); + expect(new Options({ commands: ['copy'], output: os.devNull, dirDatDescription: true }).getOutputFileParsed(dat, dummyInputRomPath, dummyGame, dummyRelease, dummyRomFilename)).toEqual(path.join(os.devNull, 'description')); + expect(new Options({ commands: ['copy'], output: os.devNull, dirDatDescription: false }).getOutputFileParsed(dat, dummyInputRomPath, dummyGame, dummyRelease, dummyRomFilename)).toEqual(os.devNull); + }); + it('should respect "--dir-letter"', () => { expect(new Options({ commands: ['copy'], output: os.devNull, dirLetter: true }).getOutputFileParsed(dummyDat, dummyInputRomPath, dummyGame, dummyRelease, dummyRomFilename)).toEqual(os.devNull); expect(new Options({ commands: ['copy'], output: os.devNull, dirLetter: true }).getOutputFileParsed(dummyDat, dummyInputRomPath, dummyGame, dummyRelease, 'file.rom')).toEqual(path.join(os.devNull, 'F', 'file.rom'));