Skip to content

Instantly share code, notes, and snippets.

@nicklasfrahm
Last active June 8, 2019 01:30
Show Gist options
  • Save nicklasfrahm/c6000b1a471ddda02da69ecff1bd6b7f to your computer and use it in GitHub Desktop.
Save nicklasfrahm/c6000b1a471ddda02da69ecff1bd6b7f to your computer and use it in GitHub Desktop.
ID3 music tags
const { promisify } = require('util');
const fs = require('fs');
const path = require('path');
const jimp = require('jimp');
const mm = require('music-metadata');
const Id3Writer = require('browser-id3-writer');
const start = Date.now();
const writeFile = promisify(fs.writeFile);
const readFile = promisify(fs.readFile);
const readdir = promisify(fs.readdir);
const getMp3Files = async dir => {
const allFiles = await readdir(dir);
return allFiles.filter(file => /\.mp3$/.test(file));
};
const resolveFeat = rawArtistsOrTitle => {
const trim = str => str.trim();
const truthy = item => !!item;
const [artistsOrTitle, rawFeatArtists] = rawArtistsOrTitle
.split(/ fe?a?t\. /g)
.map(trim)
.filter(truthy);
const [artistOrTitle, ...otherArtists] = artistsOrTitle
? artistsOrTitle
.split('&')
.map(trim)
.filter(truthy)
: [];
const featArtists = rawFeatArtists
? rawFeatArtists
.split('&')
.map(trim)
.filter(truthy)
: [];
return [artistOrTitle, [...otherArtists, ...featArtists]];
};
const updateId3Tags = async (fileName, fileContent) => {
// Load data into parsers.
const writer = new Id3Writer(fileContent);
const metadata = await mm.parseBuffer(fileContent);
// Update thumbnail to have square shape.
const oldThumbnail = metadata.common.picture[0].data;
const img = await jimp.read(oldThumbnail);
const newThumbnail = await img.cover(512, 512).getBufferAsync(jimp.MIME_JPEG);
writer.setFrame('APIC', {
type: 3,
description: 'Album cover',
data: newThumbnail
});
// Parse song title and
const artists = [];
const [rawArtists, rawTitleWithId] = fileName.split(' - ', 2);
const [mainArtist, featArtists] = resolveFeat(rawArtists);
artists.push(mainArtist, ...featArtists);
const [rawTitleWithRemix] = rawTitleWithId.split('-', 1);
const [rawTitle, rawRemix] = rawTitleWithRemix.split(' (', 2);
const [title, featArtistsFromTitle] = resolveFeat(rawTitle);
artists.push(...featArtistsFromTitle);
// Set song artists.
writer.setFrame('TPE1', artists);
// Set song title.
writer.setFrame('TIT2', rawRemix ? `${title} (${rawRemix.trim()}` : title);
// Set album artist.
writer.setFrame('TPE2', artists[0]);
// Set album title.
writer.setFrame('TALB', rawRemix ? `${title} (${rawRemix.trim()}` : title);
// Add tag to MP3 buffer.
writer.addTag();
return Buffer.from(writer.arrayBuffer);
};
(async () => {
let counter = 0;
const directory = path.resolve(process.argv[2] || '.');
process.stdout.write(`Scanning directory: ${directory}\n`);
const mp3FileNames = await getMp3Files(directory);
const total = mp3FileNames.length;
const digits = total.toString().length;
const promises = mp3FileNames.map(async mp3FileName => {
// Normalize path.
const normalizedPath = path.resolve(
path.normalize(path.join(directory, mp3FileName))
);
// Read MP3 file from disk.
const mp3File = await readFile(normalizedPath);
// Update ID3 tags.
const updatedMp3File = await updateId3Tags(mp3FileName, mp3File);
// Write file to disk.
await writeFile(normalizedPath, updatedMp3File);
// Update progress.
const current = (++counter).toString().padStart(digits, ' ');
process.stdout.write(`\rConverting ID3 tags: ${current}/${total}`);
});
await Promise.all(promises);
if (total) {
process.stdout.write('\n');
}
process.stdout.write(`Processed ${total} in ${Date.now() - start}ms.\n`);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment