Fix album art, scrape for album art when appropriate, update tagging, add prompts, change some other useless shit

This commit is contained in:
notgne2 2021-08-14 11:40:33 -07:00
parent 32001cfbcd
commit 7a1c8cf63c
No known key found for this signature in database
GPG Key ID: BB661E172B42A7F8
6 changed files with 187 additions and 35 deletions

57
flake.lock generated Normal file
View File

@ -0,0 +1,57 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1623875721,
"narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "f7e004a55b120c02ecb6219596820fcd32ca8772",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1628958848,
"narHash": "sha256-KhVgsT6Bilea9tCmYJDBikM8wLEFgU2v8891KD84XH0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ee3f6da36808209684df0423f5de653a811104b8",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"npm-buildpackage": {
"locked": {
"lastModified": 1624358188,
"narHash": "sha256-e/2p0CYhfs8GHYjDiJ2sdlne1WYF43MCPfZbRP5VmC4=",
"owner": "serokell",
"repo": "nix-npm-buildpackage",
"rev": "3461855a1dbfe844768d5adac29fe0b542d8b296",
"type": "github"
},
"original": {
"owner": "serokell",
"repo": "nix-npm-buildpackage",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"npm-buildpackage": "npm-buildpackage"
}
}
},
"root": "root",
"version": 7
}

25
flake.nix Normal file
View File

@ -0,0 +1,25 @@
{
description = "Music downloader";
inputs = {
npm-buildpackage.url = "github:serokell/nix-npm-buildpackage";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, npm-buildpackage, flake-utils, ... }: flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs { inherit system; overlays = [ npm-buildpackage.overlay ]; };
in {
packages.mudl = pkgs.buildNpmPackage {
src = ./.;
};
defaultPackage = self.packages.${system}.mudl;
apps.mudl = {
type = "app";
program = "${self.packages.${system}.mudl}/bin/mudl";
};
defaultApp = self.apps.${system}.mudl;
});
}

113
index.js
View File

@ -6,6 +6,25 @@ const fs = require('fs').promises;
const os = require('os'); const os = require('os');
const shitExec = require('child_process').exec; const shitExec = require('child_process').exec;
const Path = require('path'); 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) { async function exec(...args) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -21,13 +40,13 @@ async function exec(...args) {
const TMP_DIR = `/tmp/mudl_${Math.random().toString(36).substring(7)}`; const TMP_DIR = `/tmp/mudl_${Math.random().toString(36).substring(7)}`;
async function thing() { async function thing(source, excess) {
if (!process.argv[2]) { if (!source) {
console.log('need a video'); console.log('need a video');
return; return;
} }
const [pAlbum, pArtist, pTitle] = (process.argv.length >= 3 ? process.argv.slice(3).join(' ').trim().match(/^([^/]+)(?:\/([^/]+)(?:\/([^/]+))?)?$/) || [] : []).slice(1).map((item) => { const [pAlbum, pArtist, pTitle] = (excess.length >= 1 ? excess.join(' ').trim().match(/^([^/]+)(?:\/([^/]+)(?:\/([^/]+))?)?$/) || [] : []).slice(1).map((item) => {
return item ? item.trim() : item; return item ? item.trim() : item;
}); });
@ -36,14 +55,14 @@ async function thing() {
let files = []; let files = [];
let inputType = null; let inputType = null;
if (['/', '~', '.'].includes(process.argv[2][0])) { if (['/', '~', '.'].includes(source[0])) {
console.log('got a str8 filename'); console.log('got a str8 filename');
inputType = 'file'; inputType = 'file';
files = [{ files = [{
image: null, image: null,
file: process.argv[2], file: source,
album: pAlbum || null, album: pAlbum || null,
artist: pArtist || null, artist: pArtist || null,
title: pTitle || null, title: pTitle || null,
@ -53,12 +72,12 @@ async function thing() {
inputType = 'url'; inputType = 'url';
console.log(`Temporarily saving to ${TMP_DIR}`); console.log(`Temporarily saving to ${TMP_DIR}`);
await exec(`youtube-dl --write-thumbnail -f bestaudio -x -o "${TMP_DIR}/[%(album)s] -- [%(artist)s] -- [%(track_number)s] -- [%(track)s] -- [%(title)s].%(ext)s" ${process.argv[2]}`); await exec(`youtube-dl --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'); console.log('Done saving');
const filesList = await fs.readdir(TMP_DIR); const filesList = await fs.readdir(TMP_DIR);
files = filesList.map((rawFilePath) => { files = (await Promise.all(filesList.map(async (rawFilePath) => {
const filePath = Path.parse(rawFilePath); const filePath = Path.parse(rawFilePath);
if (['.png', '.jpg', '.webp'].includes(filePath.ext)) return null; if (['.png', '.jpg', '.webp'].includes(filePath.ext)) return null;
@ -88,21 +107,37 @@ async function thing() {
if (yTitle && !title) title = yTitle; if (yTitle && !title) title = yTitle;
if (yTrackNum && !trackNum) trackNum = yTrackNum; if (yTrackNum && !trackNum) trackNum = yTrackNum;
if (yRawTitle && !rawTitle) rawTitle = yRawTitle; 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) { 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*(.+))?$/); const rawTitleMatch = rawTitle.match(/^([^-]+)(?:\s*-\s*(.+))?$/);
if (rawTitleMatch[2] !== undefined && rawTitleMatch[2] !== null) { if (rawTitleMatch[2] !== undefined && rawTitleMatch[2] !== null) {
[artist, title] = rawTitleMatch.slice(1).map(item => (item ? item.trim() : item)); [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 { } else {
title = rawTitleMatch[1].trim(); sTitle = rawTitleMatch[1].trim();
const ok = await yn(`Is the title: ${sTitle}`);
if (ok) {
title = sTitle;
}
} }
} }
if (album === null) album = 'Unknown'; if (album === null) album = await query('Album');
if (artist === null) artist = 'Unknown'; if (artist === null) artist = await query('Artist');
if (title === null) title = 'Unknown'; if (title === null) title = await query('Title');
return { return {
file: Path.join(TMP_DIR, rawFilePath), file: Path.join(TMP_DIR, rawFilePath),
@ -111,7 +146,7 @@ async function thing() {
artist, artist,
title, title,
}; };
}).filter(p => p !== null); }))).filter(p => p !== null);
} }
for (const { for (const {
@ -122,16 +157,46 @@ async function thing() {
await fs.mkdir(`${os.homedir()}/Music/${album}`, {recursive: true}); await fs.mkdir(`${os.homedir()}/Music/${album}`, {recursive: true});
} catch (err) {} } catch (err) {}
let encode = true;
try { try {
await fs.access(path); await fs.access(path);
console.log("Path already exists..?"); let ok = await yn('Song already exists, replace it?');
} catch (err) { if (ok) {
await fs.unlink(path);
} else {
encode = false;
}
} catch (err) {}
if (encode) {
console.log(`Encoding to: ${path}`); console.log(`Encoding to: ${path}`);
if (image) { console.log('Checking dimensions of source thumbnail');
await exec(`ffmpeg -i "${file}" -i "${image}" -map 0:0 -map 1:0 -id3v2_version 4 -metadata:s:v title="Album cover" -metadata:s:v comment="Cover (front)" "${path}"`);
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 { } else {
await exec(`ffmpeg -i "${file}" -id3v2_version 4 "${path}"`); 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}"`);
} }
} }
@ -139,12 +204,14 @@ async function thing() {
const tagArgs = []; const tagArgs = [];
tagArgs.push(['--artist', artist || 'Unknown']); tagArgs.push(['--artist', artist]);
tagArgs.push(['--album', album || 'Unknown']); tagArgs.push(['--album', album]);
tagArgs.push(['--song', title || 'Unknown']); tagArgs.push(['--song', title]);
if (inputType === 'url') tagArgs.push(['--comment', process.argv[2]]); if (inputType === 'url') tagArgs.push(['--WXXX', `source:${source.replace(/https:\/\//g, '')}`]);
if (trackNum) tagArgs.push(['--track', trackNum]); if (trackNum) tagArgs.push(['--track', trackNum]);
tagArgs.push(['--TXXX', 'mudlver:mudl 1']);
const tagArgsString = tagArgs.map(([tag, val]) => `${tag} "${val}"`).join(' '); const tagArgsString = tagArgs.map(([tag, val]) => `${tag} "${val}"`).join(' ');
await exec(`id3v2 ${tagArgsString} "${path}"`); await exec(`id3v2 ${tagArgsString} "${path}"`);
@ -158,7 +225,7 @@ async function thing() {
(async () => { (async () => {
try { try {
await thing(); await thing(process.argv[2], process.argv.length > 3 ? process.argv.slice(3) : []);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
await fs.rmdir(TMP_DIR, {recursive: true}); await fs.rmdir(TMP_DIR, {recursive: true});

13
package-lock.json generated Normal file
View File

@ -0,0 +1,13 @@
{
"name": "mudl",
"version": "1.0.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"no-op": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/no-op/-/no-op-1.0.3.tgz",
"integrity": "sha1-wb2BMjiQZY/jrvbklcPBA/XuxU0="
}
}
}

10
pnpm-lock.yaml generated
View File

@ -1,10 +0,0 @@
dependencies:
no-op: 1.0.3
lockfileVersion: 5.1
packages:
/no-op/1.0.3:
dev: false
resolution:
integrity: sha1-wb2BMjiQZY/jrvbklcPBA/XuxU0=
specifiers:
no-op: ^1.0.3