|
#!/usr/bin/env -S deno run -RWE |
|
|
|
/** |
|
* A little utility that renames music files to the format: |
|
* XX. AUTHOR - TITLE.ext |
|
* Made for myself, esp. for Bandcamp albums. |
|
* @author onegen <https://github.com/onegentig> |
|
* @date 2025-04-26 |
|
* @license MIT |
|
*/ |
|
|
|
import { exists } from 'jsr:@std/fs/exists'; |
|
import { basename, extname } from 'jsr:@std/path'; |
|
import { parseFile } from 'npm:music-metadata'; |
|
|
|
/** |
|
* Normalises the name of the file to the format: |
|
* XX. AUTHOR - TITLE.ext |
|
* (retains extension, number is padded to 2 digits). |
|
* @param {string} file Original filename |
|
* @returns {Promise<string>} Normalised filename |
|
*/ |
|
async function normaliseAudioFileName(file: string): Promise<string> { |
|
if (!(await exists(file))) { |
|
console.error(`File ${file} does not exist.`); |
|
return file; |
|
} |
|
|
|
const base = basename(file); |
|
const meta = await parseFile(file); |
|
if (!meta || !meta.common) { |
|
console.error(`Could not parse metadata for ${file}`); |
|
return file; |
|
} |
|
|
|
const num = meta.common?.track?.no |
|
? meta.common.track.no.toString().padStart(2, '0') |
|
: '00'; |
|
const artist = meta.common.artist || '[no artist]'; |
|
const title = meta.common.title || '[no title]'; |
|
const ext = extname(file); |
|
const newFileBase = `${num}. ${artist} - ${title}${ext}`; |
|
const newFile = file.replace(base, newFileBase); |
|
|
|
Deno.rename(file, newFile); |
|
console.info(`Renamed ${file} to ${newFile}`); |
|
return newFile; |
|
} |
|
|
|
/** |
|
* Main function |
|
* @returns {Promise<boolean>} True on success, false on failure. |
|
*/ |
|
async function main() { |
|
if (Deno.args.length < 1) { |
|
console.error( |
|
'Either supply a directory of filename(s) as an argument.' |
|
); |
|
return false; |
|
} |
|
|
|
const files: Array<string> = []; |
|
for (const arg of Deno.args) { |
|
const stat = await Deno.stat(arg); |
|
if (stat.isDirectory) { |
|
for await (const entry of Deno.readDir(arg)) { |
|
if (entry.isFile) { |
|
files.push(`${arg}/${entry.name}`); |
|
} |
|
} |
|
} else if (stat.isFile) { |
|
files.push(arg); |
|
} |
|
} |
|
|
|
if (files.length === 0) { |
|
console.error('No files found.'); |
|
return false; |
|
} |
|
|
|
for (const file of files) |
|
try { |
|
await normaliseAudioFileName(file); |
|
} catch (e) { |
|
console.error(`Error processing ${file}: ${e}`); |
|
} |
|
|
|
console.info('All files processed! Have a nice day!'); |
|
return true; |
|
} |
|
|
|
if (import.meta.main) { |
|
if (await main()) { |
|
Deno.exit(0); |
|
} else { |
|
Deno.exit(1); |
|
} |
|
} |