126 lines
3.7 KiB
TypeScript
126 lines
3.7 KiB
TypeScript
import { stat, readFile, readdir, copyFile } from "node:fs/promises";
|
|
import { fileURLToPath } from "node:url";
|
|
import { join, extname, dirname, resolve } from "node:path";
|
|
import { createHash } from "node:crypto";
|
|
import { parseDwi } from "./parseDwi";
|
|
import { parseSm } from "./parseSm";
|
|
|
|
const FILENAME = fileURLToPath(import.meta.url);
|
|
const REPO_ROOT = dirname(dirname(dirname(FILENAME)));
|
|
|
|
type RawSimfile = Omit<Simfile, "mix" | "title"> & {
|
|
title: string;
|
|
titletranslit: string | null;
|
|
banner: string | null;
|
|
bannerHash: string | null;
|
|
bannerFilename: string | null;
|
|
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);
|
|
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;
|
|
} 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 };
|