Fix album art, scrape for album art when appropriate, update tagging, add prompts, change some other useless shit
This commit is contained in:
parent
32001cfbcd
commit
7a1c8cf63c
57
flake.lock
generated
Normal file
57
flake.lock
generated
Normal 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
25
flake.nix
Normal 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
113
index.js
@ -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
13
package-lock.json
generated
Normal 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
10
pnpm-lock.yaml
generated
@ -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
|
|
Loading…
Reference in New Issue
Block a user