mudl/index.js

243 lines
7.3 KiB
JavaScript

#!/usr/bin/env node
/* eslint-disable no-console */
const fs = require('fs').promises;
const os = require('os');
const shitExec = require('child_process').exec;
const Path = require('path');
const readline = require('readline');
async function query(p) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return await new Promise((resolve, reject) => {
rl.question(`${p}: `, (r) => {
resolve(r);
rl.close();
});
});
}
async function yn(q) {
return ['yes', 'y'].includes((await query(q)).toLowerCase());
}
async function exec(...args) {
return new Promise((resolve, reject) => {
shitExec(...args, (error, stdout) => {
if (error) {
reject(error);
} else {
resolve(stdout.trim());
}
});
});
}
const TMP_DIR = `/tmp/mudl_${Math.random().toString(36).substring(7)}`;
async function thing(source, excess) {
if (!source) {
console.log('need a video');
return;
}
const [pAlbum, pArtist, pTitle] = (excess.length >= 1 ? excess.join(' ').trim().match(/^([^/]+)(?:\/([^/]+)(?:\/([^/]+))?)?$/) || [] : []).slice(1).map((item) => {
return item ? item.trim() : item;
});
await fs.mkdir(TMP_DIR);
let files = [];
let inputType = null;
if (['/', '~', '.'].includes(source[0])) {
console.log('got a str8 filename');
inputType = 'file';
files = [{
image: null,
file: source,
album: pAlbum || null,
artist: pArtist || null,
title: pTitle || null,
}];
} else {
console.log('got a link');
inputType = 'url';
console.log(`Temporarily saving to ${TMP_DIR}`);
await exec(`yt-dlp --write-thumbnail -f bestaudio -x -o "${TMP_DIR}/[%(album)s] -- [%(artist)s] -- [%(track_number)s] -- [%(track)s] -- [%(title)s].%(ext)s" ${source}`);
console.log('Done saving');
const filesList = await fs.readdir(TMP_DIR);
files = (await Promise.all(filesList.map(async (rawFilePath) => {
const filePath = Path.parse(rawFilePath);
if (['.png', '.jpg', '.webp'].includes(filePath.ext)) return null;
const rawImagePath = filesList.find((rawP) => {
const p = Path.parse(rawP);
return ['.png', '.jpg', '.webp'].includes(p.ext) && p.name === filePath.name;
});
let album = pAlbum || null;
let artist = pArtist || null;
let title = pTitle || null;
let trackNum = null;
let rawTitle = null;
const fullMatch = rawFilePath.match(/^\[(.*?)\] -- \[(.*?)\] -- \[(.*?)\] -- \[(.*?)\] -- \[(.*?)\]\..*?$/);
if (fullMatch !== null) {
const [yAlbum, yArtist, yTrackNum, yTitle, yRawTitle] = fullMatch.slice(1)
.map(item => (item === 'NA' ? null : item))
.map(item => (item !== null ? item.trim() : null))
.map(item => (item !== null ? item.replace(/(^|\s+)(.)_($|\s+)/g, '$1$2$3') : null));
if (yAlbum && !album) album = yAlbum;
if (yArtist && !artist) artist = yArtist;
if (yTitle && !title) title = yTitle;
if (yTrackNum && !trackNum) trackNum = yTrackNum;
if (yRawTitle && !rawTitle) rawTitle = yRawTitle;
console.log(`Scraped song metadata: ${yAlbum} / ${yArtist} - ${yTitle} # ${yTrackNum}`);
console.log(`Using song metadata: ${album} / ${artist} - ${title} # ${trackNum}`);
} else {
console.log('Unable to scrape any song metadata');
}
if (artist === null && title === null) {
console.log('Don\'t have an artist and title yet, trying to extrapolate from source name');
const rawTitleMatch = rawTitle.match(/^([^-]+)(?:\s*-\s*(.+))?$/);
if (rawTitleMatch[2] !== undefined && rawTitleMatch[2] !== null) {
[sArtist, sTitle] = rawTitleMatch.slice(1).map(item => (item ? item.trim() : item));
const ok = await yn(`Is the following correct: ${sArtist} - ${sTitle}`);
if (ok) {
artist = sArtist;
title = sTitle;
}
} else {
sTitle = rawTitleMatch[1].trim();
const ok = await yn(`Is the title: ${sTitle}`);
if (ok) {
title = sTitle;
}
}
}
if (album === null) album = await query('Album');
if (artist === null) artist = await query('Artist');
if (title === null) title = await query('Title');
if (album === "") album = "Unknown Album";
if (artist === "") album = "Unknown Artist";
if (title === "") throw "Please do not take the piss";
return {
file: Path.join(TMP_DIR, rawFilePath),
image: Path.join(TMP_DIR, rawImagePath),
album,
artist,
title,
};
}))).filter(p => p !== null);
}
for (const {
album, artist, title, trackNum, file, image,
} of files) {
const path = `${os.homedir()}/Music/${album}/${artist} - ${title}.mp3`;
try {
await fs.mkdir(`${os.homedir()}/Music/${album}`, {recursive: true});
} catch (err) {}
let encode = true;
try {
await fs.access(path);
let ok = await yn('Song already exists, replace it?');
if (ok) {
await fs.unlink(path);
} else {
encode = false;
}
} catch (err) {}
if (encode) {
console.log(`Encoding to: ${path}`);
console.log('Checking dimensions of source thumbnail');
let albumArt = image;
const imageSizeOut = await exec(`identify -format '%w %h' "${albumArt}"`);
const imageSize = imageSizeOut.trim().match(/^(\d+) (\d+)$/).slice(1).map(a => parseInt(a));
if (imageSize[0] > (imageSize[1] * 1.2)) {
console.log('Source thumbnail seems like a bad size, trying to scrape for album art elsewhere');
let coverArtPath = `${TMP_DIR}/cover.jpg`;
try {
await exec(`sacad "${artist}" "${album}" 1500 "${coverArtPath}" -d`);
albumArt = coverArtPath;
} catch (err) {
console.log('Error scraping for album art elsewhere, falling back to source thumbnail');
}
} else {
console.log('Source thumbnail seems appropriately sized, using as album art');
}
if (albumArt != null) {
await exec(`ffmpeg -i "${file}" -i "${albumArt}" -map 0:0 -map 1:0 -id3v2_version 3 -metadata:s:v title="Album cover" -metadata:s:v comment="Cover (front)" -q:a 1 "${path}"`);
} else {
await exec(`ffmpeg -i "${file}" -id3v2_version 3 -q:a 1 "${path}"`);
}
}
console.log('Adding id3v2 tags');
const tagArgs = [];
tagArgs.push(['--artist', artist]);
tagArgs.push(['--album', album]);
tagArgs.push(['--song', title]);
if (inputType === 'url') tagArgs.push(['--WXXX', `source:${source.replace(/https:\/\//g, '')}`]);
if (trackNum) tagArgs.push(['--track', trackNum]);
tagArgs.push(['--TXXX', 'mudlver:mudl 1']);
const tagArgsString = tagArgs.map(([tag, val]) => `${tag} "${val}"`).join(' ');
await exec(`id3v2 ${tagArgsString} "${path}"`);
console.log(`Finished: ${album} / ${artist} - ${title}`);
}
console.log('Finished everything, cleaning up...');
await fs.rmdir(TMP_DIR, {recursive: true});
console.log('Done!');
}
(async () => {
try {
await thing(process.argv[2], process.argv.length > 3 ? process.argv.slice(3) : []);
} catch (err) {
console.error(err);
await fs.rmdir(TMP_DIR, {recursive: true});
}
})();
process.on('SIGINT', async () => {
await fs.rmdir(TMP_DIR, {recursive: true});
process.exit();
});