romtool
Version:
Collection of tools for managing your PSX rom collection
324 lines (277 loc) • 9.33 kB
JavaScript
const Promise = require("bluebird");
const c = require("chalk");
const { prompt } = require("inquirer");
const { resolve, basename, join, extname } = require("path");
const { rename, outputFile, remove } = require("fs-extra");
const ora = require("ora");
const { regex: REGEX_TRACKS } = require("../find/findMultiTrack");
const { REGEX_DISC } = require("../find/findMultiDiscs");
const {
getCLIName,
logFactory,
escapeRegExp,
findDirectoriesMatching,
clock,
flatten
} = require("../../utils/misc");
const { findSourceFiles } = require("../../utils/cues");
const { readDirForFiles } = require("../../utils/filesystem");
const ORIGINAL_FOLDER = `${getCLIName()}-original`;
const REGEX_MOVED_FOLDER = new RegExp(escapeRegExp(ORIGINAL_FOLDER), "i");
let log;
let spinner;
let isVerbose;
function spinnerMessage(
text,
startMsg = "please wait...",
symbol = c`{cyan ℹ}`
) {
spinner.stopAndPersist({ text: c`${text}`, symbol });
spinner.start(c`${startMsg}`);
}
const REGEX_DISC_CUE = new RegExp(`${REGEX_DISC.source}.*\\.cue`);
async function handler({ path, exclude, confirm, verbose, revert }) {
isVerbose = verbose;
log = logFactory(verbose);
log.info(c`{blue playlist} generator`);
const startingPath = resolve(path);
const searchRegex = revert ? /\.m3u|\.CD[0-9]/i : REGEX_DISC_CUE;
const userExcludeReg = exclude
? new RegExp(escapeRegExp(exclude), "i")
: undefined;
let excludeReg = revert
? REGEX_TRACKS
: new RegExp(`${REGEX_TRACKS.source}|\\.m3u`);
if (userExcludeReg) {
excludeReg = new RegExp(`${excludeReg.source}|${userExcludeReg.source}`);
}
log.info(c`searching -> {magenta ${startingPath}}`);
revert && log.info(c`{cyan revert} mode is enabled...`);
log.verbose(
c`using the search pattern -> {green ${searchRegex}}, excluding -> {cyan ${excludeReg ||
"none"}}`
);
spinner = ora("please wait...").start();
let stats = {};
const matched = await findDirectoriesMatching({
regex: searchRegex,
spinner,
depthLimit: 1,
exclude: excludeReg,
path: startingPath,
onStats: results => (stats = results)
});
const validFolders = revert ? matched : await filterValidFolders(matched);
spinnerMessage(
c`Found {green ${validFolders.length}} / {cyan ${
stats.dirs
}} matching folders`
);
if (!validFolders.length) {
spinner.fail("No matching folders we're found");
return process.exit(1);
}
let foldersToUse = validFolders;
if (confirm) {
if (revert) {
spinner.succeed(c`choose which {magenta folders} to revert`);
} else {
spinner.succeed(
c`choose which {magenta folders} to generate {blue playlists} for`
);
}
const result = await prompt({
type: "checkbox",
name: "selected",
message: "choose",
default: [],
choices: validFolders.map(x => ({
name: basename(x),
checked: true
}))
});
foldersToUse = foldersToUse.filter(x =>
result.selected.includes(basename(x))
);
if (foldersToUse.length === 0) {
spinner.fail("No folders were selected... exiting");
return process.exit(0);
}
}
if (revert) {
const results = await Promise.map(foldersToUse, async folder => {
const files = flatten(
await findSourceFiles({
spinner,
folders: folder,
searchReg: /\.(CD[0-9]|m3u)/i,
excludeReg: REGEX_MOVED_FOLDER
}).map(item => item.files)
);
spinnerMessage(c`reverting {blue ${basename(folder)}}`);
isVerbose &&
spinnerMessage(
c`{magenta ${folder}} => \n\t{cyan ${files.join("\n\t")}}`
);
try {
const m3uFile = files.filter(file => extname(file) === ".m3u")[0];
if (m3uFile) {
isVerbose && spinnerMessage(c`{red deleting} -> {cyan ${m3uFile}}`);
await remove(m3uFile);
}
} catch (error) {
spinnerMessage(c`{red failed to delete m3u}\n${error}`);
return false;
}
const cdFiles = files.filter(file => /\.CD[0-9]/i.test(extname(file)));
try {
await Promise.all(
cdFiles.map(file => {
const newFilename = file.replace(/\.CD[0-9]/i, ".cue");
isVerbose &&
spinnerMessage(
c`{yellow renaming} -> {cyan ${basename(
file
)}} -> {magenta ${basename(newFilename)}}`
);
return rename(file, newFilename);
})
);
} catch (error) {
spinnerMessage(
c`{red failed to rename CDx files to CUE files}\n${error}`
);
return false;
}
return true;
});
const successes = results.filter(result => result);
if (successes.length === 0) {
spinner.fail(c`unable to revert {red any} folders...`);
return process.exit(1);
} else if (successes.length < results.length) {
spinner.warn(c`only {yellow some} files were successfully reverted...`);
} else if (successes.length === results.length) {
spinner.succeed(c`all files {green reverted} successfully!`);
}
return process.exit(0);
}
const folderMap = await findSourceFiles({
spinner,
folders: foldersToUse,
searchReg: REGEX_DISC,
excludeReg: REGEX_MOVED_FOLDER
});
const results = await createPlaylist(folderMap);
// TODO - implement & remove
const success = results.filter(x => x.success);
spinner.succeed(
c`{magenta created} playlists for {green ${success.length}} / {blue ${
results.length
}} games!`
);
return true;
}
async function filterValidFolders(folders) {
const excluded = new Set();
const validFolders = await Promise.filter(folders, async folder => {
const files = await readDirForFiles(folder);
isVerbose &&
spinnerMessage(
c`{magenta ${folder}} => \n\t{cyan ${files.join("\n\t")}}`
);
const discCues = files.filter(file => REGEX_DISC_CUE.test(file));
const discBins = discCues.filter(cue =>
files.find(file => file === cue.replace(".cue", ".bin"))
);
isVerbose &&
spinnerMessage(
`found cues -> ${discCues.length}, found bins -> ${discBins.length}`
);
if (discCues.length && discCues.length === discBins.length) {
return true;
}
if (discCues.length === 0 || discBins.length === 0) {
isVerbose && spinnerMessage(`no cues or bins were found in ${folder}`);
}
if (discCues.length !== discBins.length) {
isVerbose &&
spinnerMessage(
c`{yellow ${folder}} has {cyan ${
discCues.length
}} cues but only {magenta ${discBins.length} bins}`
);
}
const hasTracks = files.some(x => REGEX_TRACKS.test(x));
if (hasTracks) {
excluded.add(folder);
}
return false;
});
if (excluded.size) {
spinnerMessage(
c`the following {magenta ${excluded.size}} folders were {yellow excluded}`
);
excluded.forEach(x => spinnerMessage(c` -> {cyan ${basename(x)}}`));
spinnerMessage(
c`run {blue '${getCLIName()} merge -h'} for more {magenta info} on how to merge them`
);
}
return validFolders;
}
// folderMap => [{folder: "some/folder", files: [1, 3.cue, 3.bin]}]
async function createPlaylist(folderMap) {
const timer = clock();
const results = await Promise.map(folderMap, async ({ folder, files }) => {
const name = basename(folder);
const cues = files.filter(file => /\.cue/i.test(file));
spinnerMessage(
c`starting {green ${name}} with -> {cyan ${cues.length}} discs`
);
isVerbose && spinnerMessage(c`{magenta path} -> ${folder}`);
isVerbose && spinnerMessage(c`\n\t {cyan -> ${cues.join("\n\t-> ")}}`);
// rename .cue to CDX
const discReg = /\(Disc ([0-9])\)/i;
const promises = cues
.map(cue => {
const result = cue.replace(".cue", `.CD${cue.match(discReg)[1]}`);
isVerbose &&
spinnerMessage(
c`{green ${basename(cue)}} -> {cyan ${basename(result)}}`
);
spinner.text = `-> ${result}`;
return { old: cue, result };
})
.map(async ({ old, result }) => {
try {
await rename(old, result);
isVerbose &&
spinnerMessage(c`{magenta renamed} -> ${basename(result)}`);
return result;
} catch (err) {
spinnerMessage(
c`{red failed} to generate playlist for {red ${basename(old)}}`
);
spinnerMessage(c`{red ${err}}`);
}
});
// Create the m3u file
const filename = join(folder, `${basename(folder)}.m3u`);
try {
const newNames = await Promise.all(promises);
const content = newNames.map(x => basename(x)).join("\n");
spinnerMessage(c`creating {cyan ${filename}}`);
isVerbose && spinnerMessage(c`contents ->\n\t{blue ${content}}`);
await outputFile(filename, content);
return { filename, success: true };
} catch (error) {
spinnerMessage(c`{red failed} to save playlist{red \n${error}}`);
return { filename, success: false };
}
});
spinnerMessage(
c`took {cyan ${clock(timer) / 1000}s} to generate the {magenta playlists}`
);
return results;
}
module.exports = handler;