'use strict'; (async () => { function die(message, code) { if (message) { console.log(message) } if (code) { process.exit(code); } else { process.exit(0); } } String.prototype.isBase64 = function () { return Buffer.from(this, 'base64').toString('base64').split("=")[0].substring(0, Buffer.from(this, 'base64').toString('base64').split("=")[0].length - 1) === this.substring(0, this.length - 1); } const exec = require('child_process').execFileSync; const fs = require('fs'); const axios = require('axios'); const Genius = require("genius-lyrics"); const g = new Genius.Client(); if (process.argv[2] !== undefined) { global.id = process.argv[2]; if (!id.isBase64() || id.length !== 34) { die("Invalid playlist ID"); } } else { console.log("Please specify playlist ID"); } if (process.argv[3] !== undefined) { global.overrideAlbum = true; global.overrideAlbumName = process.argv[3]; } else { global.overrideAlbum = false; } console.log("Preparing folder structure..."); if (!fs.existsSync("./_youtoo")) fs.mkdirSync("./_youtoo"); if (!fs.existsSync("./_youtoo/Dumps")) fs.mkdirSync("./_youtoo/Dumps"); if (!fs.existsSync("./_youtoo/Result")) fs.mkdirSync("./_youtoo/Result"); if (!fs.existsSync("./_youtoo/Metadata")) fs.mkdirSync("./_youtoo/Metadata"); if (!fs.existsSync("./_youtoo/AlbumArts")) fs.mkdirSync("./_youtoo/AlbumArts"); console.log("Gathering playlist data..."); let playlist = JSON.parse(exec("yt-dlp", [ "--flat-playlist", "--dump-single-json", "https://www.youtube.com/playlist?list=" + id ]).toString()); fs.writeFileSync("./_youtoo/Dumps/PlaylistDataRaw.json", JSON.stringify(playlist, null, 4)); let songs = playlist.entries.map(i => i.id); fs.writeFileSync("./_youtoo/Dumps/PlaylistDataList.json", JSON.stringify(songs, null, 4)); console.log("Found " + songs.length + " songs"); for (let song of songs) { if (!fs.existsSync("./_youtoo/Result/" + song + ".mp3")) { console.log("[" + song + "] Gathering metadata..."); exec("yt-dlp", [ "-o", "./_youtoo/_audio", "--skip-download", "--write-info-json", "https://www.youtube.com/watch?v=" + song ], { stdio: "inherit" }) let metadata = JSON.parse(fs.readFileSync("./_youtoo/_audio.info.json").toString()); fs.writeFileSync("./_youtoo/Dumps/MetadataRaw" + song + ".json", JSON.stringify(metadata, null, 4)); console.log("[" + song + "] Parsing metadata..."); let partists = (metadata.track ?? metadata.alt_title ?? metadata.title).replace(/^([:\"'a-zA-Z0-9 .\-*\_\/\\,&]*)-(.*)/gm, "$1") !== (metadata.track ?? metadata.alt_title ?? metadata.title) ? (metadata.track ?? metadata.alt_title ?? metadata.title).replace(/^([:\"'a-zA-Z0-9 .\-*\_\/\\,&]*)-(.*)/gm, "$1") : ""; let partists2 = (metadata.track ?? metadata.alt_title ?? metadata.title).replace(/(.*)\((feat|ft|with)(\.|:| |)(| |)([:\"'a-zA-Z0-9 .\-*\_\/\\,&]*)\)(.*)/gmi, "$5") !== (metadata.track ?? metadata.alt_title ?? metadata.title) ? (metadata.track ?? metadata.alt_title ?? metadata.title).replace(/(.*)\((feat|ft|with)(\.|:| |)(| |)([:\"'a-zA-Z0-9 .\-*\_\/\\,&]*)\)(.*)/gmi, "$5") : ""; let artistsList = Array.from(new Set([...(metadata.artist ?? metadata.creator ?? metadata.uploader).replace(/(, |,|and |And |&)/gm, "|").split("|").map(i => i.trim()), ...partists.replace(/(, |,|and |And |&)/gm, "|").split("|").map(i => i.trim()), ...partists2.replace(/(, |,|and |And |&)/gm, "|").split("|").map(i => i.trim())])).filter(i => i.trim() !== ""); let songTitle = (metadata.track ?? metadata.alt_title ?? metadata.title).replace(/^([:\"'a-zA-Z0-9 .\-*\_\/\\,&]*)-( |)(.*)(\[(.*)\]|)/gm, "$3").replace(/(.*)\((feat|ft|with)(\.|:| |)(| |)([:\"'a-zA-Z0-9 .\-*\_\/\\,&]*)\)(.*)/gmi, "$1").replace(/(.*)\[(.*)\](.*)/gm, "$1").replace(/(.*)\((from|in|by)(\.|:| |)(| |)([:\"'a-zA-Z0-9 .\-*\_\/\\,&]*)\)(.*)/gmi, "$1").trim(); console.log("[" + song + "] Gathering lyrics..."); let lyricsComplete = false; let lyrics = ""; try { let geniusSearchArtist = artistsList[0]; let geniusSong = (await g.songs.search(geniusSearchArtist + " - " + songTitle))[0]; let geniusTitle = geniusSong.title.replace(/^([:\"'a-zA-Z0-9 .\-*\_\/\\,&]*)-( |)(.*)(\[(.*)\]|)/gm, "$3").replace(/(.*)\((feat|ft|with)(\.|:| |)(| |)([:\"'a-zA-Z0-9 .\-*\_\/\\,&]*)\)(.*)/gmi, "$1").replace(/(.*)\[(.*)\](.*)/gm, "$1").replace(/(.*)\((from|in|by)(\.|:| |)(| |)([:\"'a-zA-Z0-9 .\-*\_\/\\,&]*)\)(.*)/gmi, "$1").trim(); console.log(geniusTitle.toLowerCase() + " <-> " + songTitle.toLowerCase(), geniusTitle.toLowerCase() === songTitle.toLowerCase()); if (geniusTitle.toLowerCase() === songTitle.toLowerCase() && geniusSong.raw.lyrics_state === "complete") { lyricsComplete = true; lyrics = (await geniusSong.lyrics()).trim() } } catch (e) { console.error(e); lyrics = ""; } if (!lyricsComplete) { console.log("[" + song + "] Incomplete/nonexistent lyrics, will try to fetch at every sync."); } let rmeta = { video: metadata.title, title: songTitle, date: metadata.release_year ?? (metadata.upload_date.substring(0, 4) - 1 +1), album: overrideAlbum ? overrideAlbumName : metadata.album ?? ((metadata.track ?? metadata.alt_title ?? metadata.title).replace(/^([:\"'a-zA-Z0-9 .\-*\_\/\\,&]*)-( |)(.*)(\[(.*)\]|)/gm, "$3").replace(/(.*)\((feat|ft|with)(\.|:| |)(| |)([:\"'a-zA-Z0-9 .\-*\_\/\\,&]*)\)(.*)/gmi, "$1").replace(/(.*)\[(.*)\](.*)/gm, "$1").replace(/(.*)\((from|in|by)(\.|:| |)(| |)([:\"'a-zA-Z0-9 .\-*\_\/\\,&]*)\)(.*)/gmi, "$1").trim()), potentialArtists: partists, potentialArtists2: partists2, artists: artistsList, lyrics, complete: lyricsComplete } fs.writeFileSync("./_youtoo/Metadata/" + song + ".json", JSON.stringify(rmeta, null, 4)); console.log("[" + song + "] Downloading audio..."); exec("yt-dlp", [ "-f", "bestaudio[ext=m4a]", "-x", "-o", "./_youtoo/_audio.m4a", "--audio-format", "m4a", "https://www.youtube.com/watch?v=" + song ], { stdio: "inherit" }); console.log("[" + song + "] Downloading thumbnail..."); fs.writeFileSync("./_youtoo/_audio.webp", (await axios.get(metadata.thumbnail, { responseType: "arraybuffer" })).data); console.log("[" + song + "] Processing thumbnail..."); exec("magick", [ "-define", "jpeg:size=1024x1024", "./_youtoo/_audio.webp", "-thumbnail", "512x512^", "-gravity", "center", "-extent", "512x512", "./_youtoo/_audio.jpg" ]); console.log("[" + song + "] Processing audio..."); let additionalArrayItems = [] if (overrideAlbum) { additionalArrayItems = [ "-metadata", "album_artist=" + overrideAlbumName ]; } exec("ffmpeg", [ "-y", "-i", "./_youtoo/_audio.m4a", "-metadata", "artist=" + rmeta.artists.join(", ") + "", "-metadata", "title=" + rmeta.title, ...additionalArrayItems, "-metadata", "album=" + rmeta.album, "-metadata", "publisher=YouTube", "-metadata", "copyright=© " + rmeta.date.toString() + " " + rmeta.artists[0], "-metadata", "date=" + rmeta.date.toString(), "-metadata", "lyrics=" + lyrics, "-metadata", "encoded_by=" + require('./package.json').name + "/" + require('./package.json').version, "./_youtoo/_audio.1.mp3" ], { stdio: "inherit" }); if (!overrideAlbum) { exec("ffmpeg", [ "-y", "-i", "./_youtoo/_audio.1.mp3", "-i", "./_youtoo/_audio.jpg", "-map", "0:0", "-map", "1:0", "-c", "copy", "-id3v2_version", "3", "-metadata:s:v", "title=Album cover", "-metadata:s:v", "comment=Cover (front)", "./_youtoo/_audio.mp3" ], { stdio: "inherit" }); fs.renameSync("./_youtoo/_audio.mp3", "./_youtoo/Result/" + song + ".mp3"); } else { fs.renameSync("./_youtoo/_audio.1.mp3", "./_youtoo/Result/" + song + ".mp3"); } fs.renameSync("./_youtoo/_audio.jpg", "./_youtoo/AlbumArts/" + song + ".jpg"); console.log("[" + song + "] Cleaning up..."); if (fs.existsSync("./_youtoo/_audio.1.mp3")) { fs.rmSync("./_youtoo/_audio.1.mp3"); } if (fs.existsSync("./_youtoo/_audio.jpg")) { fs.rmSync("./_youtoo/_audio.jpg"); } if (fs.existsSync("./_youtoo/_audio.webp")) { fs.rmSync("./_youtoo/_audio.webp"); } if (fs.existsSync("./_youtoo/_audio.m4a")) { fs.rmSync("./_youtoo/_audio.m4a"); } if (fs.existsSync("./_youtoo/_audio.info.json")) { fs.rmSync("./_youtoo/_audio.info.json"); } } else { console.log("[" + song + "] Song already in destination"); } } console.log("Deleting removed songs...") for (let file of fs.readdirSync("./_youtoo/Result")) { let song = file.substring(0, file.length - 4); if (!songs.includes(song)) { console.log("[" + song + "] Deleting..."); fs.unlinkSync("./_youtoo/Result/" + file); if (fs.existsSync("./_youtoo/Metadata/" + song + ".json")) fs.unlinkSync("./_youtoo/Metadata/" + song + ".json"); } } console.log("Cleaning up...") if (fs.existsSync("./_youtoo/Dumps")) { fs.rmSync("./_youtoo/Dumps", { recursive: true }); } console.log("Fetching lyrics for incomplete songs...") for (let file of fs.readdirSync("./_youtoo/Result")) { let song = file.substring(0, file.length - 4); if (fs.existsSync("./_youtoo/Metadata/" + song + ".json")) { console.log("[" + song + "] Fetching metadata..."); let metadata = JSON.parse(fs.readFileSync("./_youtoo/Metadata/" + song + ".json").toString()); if (typeof metadata.complete !== "boolean" || !metadata.complete) { console.log("[" + song + "] Gathering lyrics..."); let lyricsComplete = false; let lyrics = ""; try { let geniusSearchArtist = metadata.artists[0]; let geniusSong = (await g.songs.search(geniusSearchArtist + " - " + metadata.title))[0]; let geniusTitle = geniusSong.title.replace(/^([:\"'a-zA-Z0-9 .\-*\_\/\\,&]*)-( |)(.*)(\[(.*)\]|)/gm, "$3").replace(/(.*)\((feat|ft|with)(\.|:| |)(| |)([:\"'a-zA-Z0-9 .\-*\_\/\\,&]*)\)(.*)/gmi, "$1").replace(/(.*)\[(.*)\](.*)/gm, "$1").replace(/(.*)\((from|in|by)(\.|:| |)(| |)([:\"'a-zA-Z0-9 .\-*\_\/\\,&]*)\)(.*)/gmi, "$1").trim(); console.log(geniusTitle.toLowerCase() + " <-> " + metadata.title.toLowerCase(), geniusTitle.toLowerCase() === metadata.title.toLowerCase()); if (geniusTitle.toLowerCase() === metadata.title.toLowerCase() && geniusSong.raw.lyrics_state === "complete") { lyricsComplete = true; lyrics = (await geniusSong.lyrics()).trim() } } catch (e) { console.error(e); lyrics = ""; } if (!lyricsComplete) { console.log("[" + song + "] Incomplete/nonexistent lyrics, will try to fetch at every sync."); } metadata.complete = lyricsComplete; metadata.lyrics = lyrics; fs.writeFileSync("./_youtoo/Metadata/" + song + ".json", JSON.stringify(metadata, null, 4)); console.log("[" + song + "] Processing audio..."); exec("ffmpeg", [ "-y", "-i", "./_youtoo/Result/" + file, "-c", "copy", "-metadata", "lyrics=" + lyrics, "-metadata", "encoded_by=" + require('./package.json').name + "/" + require('./package.json').version, "./_youtoo/_audio.mp3" ], { stdio: "inherit" }); fs.renameSync("./_youtoo/_audio.mp3", "./_youtoo/Result/" + song + ".mp3"); } } } })()