Last active
September 22, 2020 19:27
-
-
Save blowery/15fa739be96be0de38407f51c78e11e2 to your computer and use it in GitHub Desktop.
Tesla Media Transformer
This file contains 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
const fs = require("fs"); | |
const path = require("path"); | |
const mkdirp = require("mkdirp"); | |
const mp3tag = require("mp3tag"); | |
const _ = require("lodash"); | |
const TagData = require("mp3tag/tagdata"); | |
// from | |
const FROM_DIR = path.join(__dirname, "Rips"); | |
const TO_DIR = path.join(__dirname, "ForTesla"); | |
console.log(`going from ${FROM_DIR} to ${TO_DIR}`); | |
async function getDirs(path) { | |
let dirs = await fs.promises.readdir(path, { withFileTypes: true }); | |
return dirs.filter((dir) => dir.isDirectory()); | |
} | |
/** @typedef {Object} PathAndName | |
* @property {string} name - The name of the file | |
* @property {string} path - The full path to the file | |
*/ | |
/** @returns {Promise<Array<PathAndName>>} */ | |
async function getMP3s(rootPath) { | |
const files = await fs.promises.readdir(rootPath, { withFileTypes: true }); | |
return files | |
.filter((file) => file.isFile() && file.name.endsWith(".mp3")) | |
.map((file) => ({ | |
name: file.name, | |
path: path.join(rootPath, file.name), | |
})); | |
} | |
async function* tagDataGenerator(files) { | |
for (let file of files) { | |
yield await mp3tag.readHeader(file.path); | |
} | |
} | |
/** | |
* Get a frame buffer as a string | |
* @param {TagData} tag | |
* @param {string} id | |
*/ | |
function getTagString(tag, id) { | |
const buf = tag.getFrameBuffer(id); | |
if (!buf) { | |
return null; | |
} | |
return tag.decoder.decodeString(buf); | |
} | |
async function processAlbum(artist, album) { | |
// make this directory in the target if not there | |
const source = path.join(FROM_DIR, artist, album); | |
const target = path.join(TO_DIR, artist, album); | |
let exists; | |
try { | |
const ok = await fs.promises.access(target); | |
exists = true; | |
} catch (e) { | |
exists = false; | |
} | |
if (!exists) { | |
console.log(`making ${target}`); | |
try { | |
await mkdirp(target); | |
} catch (e) { | |
console.error(e); | |
} | |
} | |
// find files | |
let sourceFiles = await getMP3s(source); | |
console.log(" %d songs", sourceFiles.length); | |
for (let file of sourceFiles) { | |
await fs.promises.copyFile( | |
path.join(file.path), | |
path.join(target, file.name) | |
); | |
//console.log('.'); | |
} | |
// grab the target files | |
let targetFiles = await getMP3s(target); | |
// grab the idv3 tag info for everything | |
/** @type {Array<TagData>} */ | |
let tags = []; | |
for await (const tag of tagDataGenerator(targetFiles)) { | |
tags.push(tag); | |
} | |
// is this is a multidisc set? | |
// if so, we have to add (Disc %d) to the album name on each track to diff | |
const discs = tags.reduce((discs, tag, i) => { | |
let disc = getTagString(tag, "TPOS"); | |
if (!disc) { | |
//console.log(" no TPOS for %s", targetFiles[i].name); | |
return discs; | |
} | |
// this can be either a plain X or X/Y | |
let discNo = Number(disc.split("/")[0]); | |
discs.add(discNo); | |
return discs; | |
}, new Set()); | |
if (discs.size > 1) { | |
console.log(` MultiDisc: (%d)`, discs.size); | |
try { | |
await Promise.all( | |
tags.map((tag) => { | |
const albumTitle = getTagString(tag, "TALB"); | |
const discNo = Number(getTagString(tag, "TPOS").split("/")[0]); | |
const newAlbumTitle = `${albumTitle} (Disc ${discNo})`; | |
tag.setFrameBuffer("TALB", tag.decoder.encodeString(newAlbumTitle)); | |
return tag.save(); | |
}) | |
); | |
} catch (e) { | |
console.error(e); | |
process.exit(1); | |
} | |
} | |
// is this a compilation? | |
// if so, we need to consolidate the track artist down to one value | |
// write the track artist into the title instead, if different from the album artist | |
// use the album artist as the track artist | |
const uniqueArtists = new Set(tags.map((tag) => getTagString(tag, "TPE1"))); | |
const uniqueAlbumArtists = new Set( | |
tags.map((tag) => getTagString(tag, "TPE2")) | |
); | |
uniqueArtists.delete(null); | |
uniqueAlbumArtists.delete(null); | |
const isComp = | |
tags.some((tag) => getTagString(tag, "TCMP") === "1") || | |
uniqueArtists.size > 1; | |
isComp && console.log(` Compilation`); | |
if (isComp) { | |
// gather all of the album artists | |
const artists = Array.from(uniqueAlbumArtists); | |
console.log( | |
" Album Artist(s): %s", | |
artists.length ? artists.join(", ") : "<None Found>" | |
); | |
const albumArtist = | |
artists.length === 0 || artists.length > 1 | |
? "Various Artists" | |
: artists[0]; | |
console.log(' Using "%s"', albumArtist); | |
try { | |
await Promise.all( | |
tags.map((tag) => { | |
const artist = getTagString(tag, "TPE1"); | |
if (artist && artist !== albumArtist) { | |
const trackTitle = getTagString(tag, "TIT2").trim(); | |
const newTrackTitle = `${trackTitle} by ${artist}`; | |
console.log(" title -> ", newTrackTitle); | |
tag.setFrameBuffer("TIT2", tag.decoder.encodeString(newTrackTitle)); | |
} | |
console.log(" artists -> ", albumArtist); | |
tag.setFrameBuffer("TPE1", tag.decoder.encodeString(albumArtist)); | |
tag.setFrameBuffer("TPE2", tag.decoder.encodeString(albumArtist)); | |
return tag.save(); | |
}) | |
); | |
} catch (e) { | |
console.error(e); | |
process.exit(1); | |
} | |
} | |
} | |
(async function () { | |
let artists = await getDirs(FROM_DIR); | |
for (let artist of artists) { | |
console.log(`${artist.name}`); | |
let albums = await getDirs(path.join(FROM_DIR, artist.name)); | |
for (let album of albums) { | |
console.log(` ${album.name}`); | |
await processAlbum(artist.name, album.name); | |
console.log(); | |
} | |
} | |
})(); |
This file contains 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
{ | |
"name": "tesla-transform", | |
"version": "1.0.0", | |
"description": "", | |
"main": "index.js", | |
"scripts": { | |
"test": "echo \"Error: no test specified\" && exit 1" | |
}, | |
"author": "", | |
"license": "ISC", | |
"dependencies": { | |
"lodash": "^4.17.20", | |
"mkdirp": "^1.0.4", | |
"mp3tag": "^1.0.3", | |
"node-dir": "^0.1.17", | |
"prettier": "^2.1.1" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This assumes that it lives in a folder with two subfolders, Rips and TeslaMusic. Run
yarn
first then runnode index.js
to run the transform. It doesn't currently clean out TeslaMusic before transforming, I just runrm -rf TeslaMusic/*
before each run.