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.

104 lines (103 loc) • 5.02 kB
import path from 'node:path'; import async from 'async'; import { ProgressBarSymbol } from '../console/progressBar.js'; import GameGrouper from '../gameGrouper.js'; import ArrayPoly from '../polyfill/arrayPoly.js'; import FsPoly from '../polyfill/fsPoly.js'; import Module from './module.js'; /** * Create .m3u playlists for multi-disc {@link Game}s. */ export default class PlaylistCreator extends Module { options; constructor(options, progressBar) { super(progressBar, PlaylistCreator.name); this.options = options; } /** * Creates playlists. */ async create(dat, candidates) { if (!this.options.shouldPlaylist()) { return []; } if (candidates.length === 0) { this.progressBar.logTrace(`${dat.getName()}: no candidates to create playlists for`); return []; } this.progressBar.logTrace(`${dat.getName()}: writing playlists`); this.progressBar.setSymbol(ProgressBarSymbol.WRITING); this.progressBar.resetProgress(candidates.length); const writtenPlaylistPaths = []; let remainingCandidates = candidates; // Write playlists for games that have multiple playlist-able files, i.e. from disc merging remainingCandidates = (await async.mapLimit(remainingCandidates, this.options.getWriterThreads(), async (candidate) => { const writtenFile = await this.maybeWritePlaylist(dat, candidate, candidate.getGame().getName()); if (writtenFile === undefined) { // We didn't write a playlist file, keep this candidate for more processing return candidate; } writtenPlaylistPaths.push(writtenFile); })).filter((candidate) => candidate !== undefined); // Write playlists for games that could have been disc merged together but weren't const gameNamesToCandidates = GameGrouper.groupMultiDiscGames(remainingCandidates, (candidate) => candidate.getGame().getName()); remainingCandidates = (await async.mapLimit([...gameNamesToCandidates.entries()], this.options.getWriterThreads(), async ([gameName, candidates]) => { const writtenFile = await this.maybeWritePlaylist(dat, candidates, gameName); if (writtenFile === undefined) { // We didn't write a playlist file, keep this candidate for more processing return candidates; } writtenPlaylistPaths.push(writtenFile); })) .flat() .filter((candidate) => candidate !== undefined); // TODO(cemmer): something with the remaining candidates? this.progressBar.logTrace(`${dat.getName()}: done writing playlists`); return writtenPlaylistPaths; } async maybeWritePlaylist(dat, candidates, playlistBasename) { const playlistFiles = (Array.isArray(candidates) ? candidates : [candidates]) .flatMap((candidate) => candidate.getRomsWithFiles()) .flatMap((romWithFiles) => this.options.shouldWrite() ? romWithFiles.getOutputFile() : romWithFiles.getInputFile()) .filter((outputFile) => this.options .getPlaylistExtensions() .some((ext) => outputFile.getFilePath().toLowerCase().endsWith(ext.toLowerCase()))) .filter(ArrayPoly.filterUniqueMapped((file) => file.getFilePath())); if (playlistFiles.length < 2) { // We shouldn't make a playlist for this game, keep it for more processing return undefined; } this.progressBar.incrementInProgress(); const commonDirectory = PlaylistCreator.getCommonDirectory(playlistFiles); const playlistLines = `${playlistFiles .map((file) => file .getFilePath() .slice(commonDirectory.length) .replace(/^[\\/]/, '') .replaceAll(/[\\/]/g, '/')) .sort() .join('\n')}\n`; if (!(await FsPoly.exists(commonDirectory))) { await FsPoly.mkdir(commonDirectory, { recursive: true }); } const playlistLocation = path.join(commonDirectory, `${playlistBasename}.m3u`); this.progressBar.logInfo(`${dat.getName()}: creating playlist '${playlistLocation}'`); await FsPoly.writeFile(playlistLocation, playlistLines); return playlistLocation; } static getCommonDirectory(files) { const fileDirsSplit = files.map((file) => path.dirname(file.getFilePath()).split(/[\\/]/)); const maxDepth = fileDirsSplit.reduce((max, file) => Math.max(max, file.length), 0); let lastCommonDir = ''; let depth = 0; while (depth <= maxDepth) { const fileSubPaths = fileDirsSplit.map((split) => split.slice(0, depth).join(path.sep)); if (new Set(fileSubPaths).size > 1) { break; } lastCommonDir = fileSubPaths[0]; depth += 1; } return lastCommonDir; } }