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;
|
||||
});
|
||||
}
|
115
index.js
115
index.js
@ -6,6 +6,25 @@ 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) => {
|
||||
@ -21,13 +40,13 @@ async function exec(...args) {
|
||||
|
||||
const TMP_DIR = `/tmp/mudl_${Math.random().toString(36).substring(7)}`;
|
||||
|
||||
async function thing() {
|
||||
if (!process.argv[2]) {
|
||||
async function thing(source, excess) {
|
||||
if (!source) {
|
||||
console.log('need a video');
|
||||
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;
|
||||
});
|
||||
|
||||
@ -36,14 +55,14 @@ async function thing() {
|
||||
let files = [];
|
||||
let inputType = null;
|
||||
|
||||
if (['/', '~', '.'].includes(process.argv[2][0])) {
|
||||
if (['/', '~', '.'].includes(source[0])) {
|
||||
console.log('got a str8 filename');
|
||||
|
||||
inputType = 'file';
|
||||
|
||||
files = [{
|
||||
image: null,
|
||||
file: process.argv[2],
|
||||
file: source,
|
||||
album: pAlbum || null,
|
||||
artist: pArtist || null,
|
||||
title: pTitle || null,
|
||||
@ -53,12 +72,12 @@ async function thing() {
|
||||
inputType = 'url';
|
||||
|
||||
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');
|
||||
|
||||
const filesList = await fs.readdir(TMP_DIR);
|
||||
|
||||
files = filesList.map((rawFilePath) => {
|
||||
files = (await Promise.all(filesList.map(async (rawFilePath) => {
|
||||
const filePath = Path.parse(rawFilePath);
|
||||
|
||||
if (['.png', '.jpg', '.webp'].includes(filePath.ext)) return null;
|
||||
@ -88,21 +107,37 @@ async function thing() {
|
||||
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) {
|
||||
[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 {
|
||||
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 (artist === null) artist = 'Unknown';
|
||||
if (title === null) title = 'Unknown';
|
||||
if (album === null) album = await query('Album');
|
||||
if (artist === null) artist = await query('Artist');
|
||||
if (title === null) title = await query('Title');
|
||||
|
||||
return {
|
||||
file: Path.join(TMP_DIR, rawFilePath),
|
||||
@ -111,7 +146,7 @@ async function thing() {
|
||||
artist,
|
||||
title,
|
||||
};
|
||||
}).filter(p => p !== null);
|
||||
}))).filter(p => p !== null);
|
||||
}
|
||||
|
||||
for (const {
|
||||
@ -122,16 +157,46 @@ async function thing() {
|
||||
await fs.mkdir(`${os.homedir()}/Music/${album}`, {recursive: true});
|
||||
} catch (err) {}
|
||||
|
||||
let encode = true;
|
||||
|
||||
try {
|
||||
await fs.access(path);
|
||||
console.log("Path already exists..?");
|
||||
} catch (err) {
|
||||
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}`);
|
||||
|
||||
if (image) {
|
||||
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}"`);
|
||||
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 {
|
||||
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,16 +204,18 @@ async function thing() {
|
||||
|
||||
const tagArgs = [];
|
||||
|
||||
tagArgs.push(['--artist', artist || 'Unknown']);
|
||||
tagArgs.push(['--album', album || 'Unknown']);
|
||||
tagArgs.push(['--song', title || 'Unknown']);
|
||||
if (inputType === 'url') tagArgs.push(['--comment', process.argv[2]]);
|
||||
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: ${album} / ${artist} - ${title}`);
|
||||
}
|
||||
|
||||
console.log('Finished everything, cleaning up...');
|
||||
@ -158,7 +225,7 @@ async function thing() {
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await thing();
|
||||
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});
|
||||
|
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="
|
||||
}
|
||||
}
|
||||
}
|
@ -11,4 +11,4 @@
|
||||
"dependencies": {
|
||||
"no-op": "^1.0.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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