From 7a1c8cf63c1aeb6463734d780149abba95ef55fe Mon Sep 17 00:00:00 2001 From: notgne2 Date: Sat, 14 Aug 2021 11:40:33 -0700 Subject: [PATCH] Fix album art, scrape for album art when appropriate, update tagging, add prompts, change some other useless shit --- flake.lock | 57 +++++++++++++++++++++++ flake.nix | 25 ++++++++++ index.js | 115 ++++++++++++++++++++++++++++++++++++---------- package-lock.json | 13 ++++++ package.json | 2 +- pnpm-lock.yaml | 10 ---- 6 files changed, 187 insertions(+), 35 deletions(-) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 package-lock.json delete mode 100644 pnpm-lock.yaml diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..cba2e43 --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..9bd758b --- /dev/null +++ b/flake.nix @@ -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; + }); +} diff --git a/index.js b/index.js index f25b552..05aac95 100644 --- a/index.js +++ b/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}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d502b06 --- /dev/null +++ b/package-lock.json @@ -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=" + } + } +} diff --git a/package.json b/package.json index 9778468..cad53f1 100644 --- a/package.json +++ b/package.json @@ -11,4 +11,4 @@ "dependencies": { "no-op": "^1.0.3" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index a79052e..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -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