243 lines
7.3 KiB
JavaScript
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();
|
|
});
|