Skip to content

Instantly share code, notes, and snippets.

@5argon
Created April 1, 2023 08:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 5argon/4394a11a384b0348b528aef6166153e2 to your computer and use it in GitHub Desktop.
Save 5argon/4394a11a384b0348b528aef6166153e2 to your computer and use it in GitHub Desktop.
Deno script to turn DDR Simfile folder to metadata tagged MP3
import {
join,
dirname,
resolve,
basename,
} from "https://deno.land/std/path/mod.ts";
import { ensureDir } from "https://deno.land/std/fs/mod.ts";
interface Notes {
mode: string;
difficulty: string;
level: number;
}
function processNotes(input: string): Notes[] {
const notesArr: Notes[] = [];
const lines = input.split("\n").map((line) => line.trim());
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith("#NOTES:")) {
const modeLine = lines[i + 1];
const difficultyLine = lines[i + 3];
const levelLine = lines[i + 4];
const mode = modeLine.split(":")[0].trim();
const difficulty = difficultyLine.split(":")[0].trim();
const level = parseInt(levelLine.split(":")[0].trim());
notesArr.push({ mode, difficulty, level });
}
}
return notesArr;
}
interface SongInfo {
title: string;
artist: string;
music: string;
banner: string;
notes: Notes[];
}
function extractSongInfo(fileContent: string): SongInfo {
const info: SongInfo = {
title: "",
artist: "",
music: "",
banner: "",
notes: processNotes(fileContent),
};
const lines = fileContent.split("\n").map((line) => line.trim());
for (const line of lines) {
if (line.startsWith("#TITLE:")) {
info.title = line.slice(7, -1);
} else if (line.startsWith("#ARTIST:")) {
info.artist = line.slice(8, -1);
} else if (line.startsWith("#MUSIC:")) {
info.music = line.slice(7, -1);
} else if (line.startsWith("#BANNER:")) {
info.banner = line.slice(8, -1);
}
}
return info;
}
async function findJacketFromBanner(
dirPath: string,
fileName: string
): Promise<string | undefined> {
const ext = fileName.slice(fileName.lastIndexOf(".")); // get file extension
const newFileName = `${fileName.slice(
0,
fileName.lastIndexOf(".")
)}-jacket${ext}`; // add "-jacket" to file name
const jacketPath = join(dirPath, newFileName); // construct path to "-jacket" image
try {
await Deno.readFile(jacketPath); // try to read "-jacket" image
return basename(jacketPath);
} catch (err) {
if (err instanceof Deno.errors.NotFound) {
return undefined; // return undefined if "-jacket" image is not found
}
throw err; // re-throw other errors
}
}
async function processAll(
directory: string,
outputDirectory: string
): Promise<void> {
for (const entry of Deno.readDirSync(directory)) {
const filePath = join(directory, entry.name);
if (entry.isDirectory) {
await processAll(filePath, outputDirectory);
} else if (entry.isFile && filePath.endsWith(".sm")) {
const fileDir = dirname(filePath);
const ddrMix = basename(resolve(fileDir, ".."));
const fileContents = await Deno.readTextFile(filePath);
const songInfo = extractSongInfo(fileContents);
const difficultyComment = songInfo.notes
.map((x) => `${x.mode}/${x.level}/${x.difficulty}`)
.join(",");
const jacket = await findJacketFromBanner(fileDir, songInfo.banner);
const albumArt = jacket ? jacket : songInfo.banner;
const destination = join(outputDirectory, ddrMix);
await ensureDir(destination);
const command: string[] = [
"ffmpeg",
"-y",
"-i",
join(fileDir, songInfo.music),
"-i",
join(fileDir, albumArt),
"-map",
"0",
"-metadata",
`title=${songInfo.title}`,
"-metadata",
`artist=${songInfo.artist}`,
"-metadata",
`album=${ddrMix}`,
"-metadata",
`comment=${difficultyComment}`,
"-map",
"1",
"-metadata:s:v",
`title="Album Cover"`,
"-metadata:s:v",
`comment="Cover (front)"`,
`${join(destination, songInfo.title)}.mp3`,
];
const process = Deno.run({ cmd: command });
await process.status();
}
}
}
if (Deno.args.length !== 2) {
console.log(
"Usage: deno run --allow-read --allow-write simfile-to-mp3.ts <input directory> <output directory>"
);
Deno.exit(1);
}
const input = Deno.args[0];
const output = Deno.args[1];
await processAll(input, output);
@5argon
Copy link
Author

5argon commented Apr 1, 2023

ffmpeg required since Deno will run it. Input args are input and output folder.

Input folder : Simfile folder contains mix folder, then song folder, then .sm + ogg + ... inside.

Output folder: Any.

MP3 generated tagged with :

  • Title : From .sm file
  • Artist : From .sm file
  • File name : Same as Title
  • Album cover : Use "-jacket" of the .sm if exist, otherwise use BANNER from .sm file.
  • Comment : Contains difficulty levels dump from .sm file

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment