Created
April 1, 2023 08:38
-
-
Save 5argon/4394a11a384b0348b528aef6166153e2 to your computer and use it in GitHub Desktop.
Deno script to turn DDR Simfile folder to metadata tagged MP3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 :