DDRCompanion/lib/stepcharts/parseSimfile.ts

127 lines
3.7 KiB
TypeScript
Raw Permalink Normal View History

2024-05-15 17:57:16 +00:00
import { stat, readFile, readdir, copyFile } from "node:fs/promises";
2024-05-16 01:04:15 +00:00
import { fileURLToPath } from "node:url";
import { join, extname, dirname, resolve } from "node:path";
import { createHash } from "node:crypto";
2024-05-15 17:57:16 +00:00
import { parseDwi } from "./parseDwi";
import { parseSm } from "./parseSm";
2024-05-16 01:04:15 +00:00
const FILENAME = fileURLToPath(import.meta.url);
const REPO_ROOT = dirname(dirname(dirname(FILENAME)));
2024-05-15 17:57:16 +00:00
type RawSimfile = Omit<Simfile, "mix" | "title"> & {
title: string;
titletranslit: string | null;
banner: string | null;
2024-05-16 01:04:15 +00:00
bannerHash: string | null;
bannerFilename: string | null;
2024-05-15 17:57:16 +00:00
displayBpm: string | undefined;
};
type Parser = (simfileSource: string, titleDir: string) => Promise<RawSimfile>;
const parsers: Record<string, Parser> = {
".sm": parseSm,
".dwi": parseDwi,
};
async function getSongFile(songDir: string): Promise<string> {
const files = await readdir(songDir);
// TODO: support more than .sm
const extensions = Object.keys(parsers);
const songFile = files.find((f) => extensions.some((ext) => f.endsWith(ext)));
if (!songFile) {
throw new Error(`No song file found in ${songDir}`);
}
return songFile;
}
function toSafeName(name: string): string {
let name2 = name;
name2 = name2.replace(".png", "");
name2 = name2.replace(/\s/g, "-").replace(/[^\w]/g, "_");
return `${name}.png`;
}
function getBpms(sm: RawSimfile): number[] {
const chart = Object.values(sm.charts)[0];
return chart.bpm.map((b) => b.bpm);
}
async function parseSimfile(
rootDir: string,
mixDir: string,
titleDir: string,
): Promise<Omit<Simfile, "mix">> {
const stepchartSongDirPath = join(rootDir, mixDir, titleDir);
const songFile = await getSongFile(stepchartSongDirPath);
const stepchartPath = join(stepchartSongDirPath, songFile);
const extension = extname(stepchartPath);
const parser = parsers[extension];
if (!parser) {
throw new Error(`No parser registered for extension: ${extension}`);
}
const fileContents = await readFile(stepchartPath);
const rawStepchart = await parser(
fileContents.toString(),
stepchartSongDirPath,
);
const bannerPath = join(stepchartSongDirPath, rawStepchart.banner);
try {
const bannerData = await readFile(bannerPath);
2024-05-16 01:04:15 +00:00
const hash = createHash("sha256").update(bannerData).digest("hex");
const ext = extname(rawStepchart.banner);
const filename = `${hash}${ext}`;
const outPath = join(REPO_ROOT, "public", "bannerImages", filename);
copyFile(bannerPath, outPath);
rawStepchart.bannerHash = hash;
rawStepchart.bannerFilename = filename;
2024-05-15 17:57:16 +00:00
} catch (e) {}
// if (bannerMeta !== undefined) {
// const publicName = toSafeName(`${mixDir}-${rawStepchart.banner}`);
// const srcPath = resolve(stepchartSongDirPath, rawStepchart.banner);
// const destPath = resolve("public/bannerImages", publicName);
// await copyFile(srcPath, destPath);
// await copyFile(
// join(stepchartSongDirPath, rawStepchart.banner),
// join("components/bannerImages", publicName),
// );
// rawStepchart.banner = publicName;
// } else {
// rawStepchart.banner = null;
// }
const bpms = getBpms(rawStepchart);
const minBpm = Math.round(Math.min(...bpms));
const maxBpm = Math.round(Math.max(...bpms));
const displayBpm =
minBpm === maxBpm ? minBpm.toString() : `${minBpm}-${maxBpm}`;
return {
...rawStepchart,
title: {
titleName: rawStepchart.title,
translitTitleName: rawStepchart.titletranslit ?? null,
titleDir,
banner: rawStepchart.banner,
},
minBpm,
maxBpm,
displayBpm,
stopCount: Object.values(rawStepchart.charts)[0].stops.length,
};
}
export { parseSimfile };
export type { RawSimfile };