Files
youtube-music/plugins/downloader/back-downloader.js
2023-03-04 01:53:19 +02:00

215 lines
6.3 KiB
JavaScript

const { existsSync, mkdirSync, createWriteStream, writeFileSync } = require('fs');
const { ipcMain, app } = require("electron");
const { join } = require("path");
const { Innertube, UniversalCache, Utils } = require('youtubei.js');
const filenamify = require("filenamify");
const ID3Writer = require("browser-id3-writer");
const { fetchFromGenius } = require("../lyrics-genius/back");
const { isEnabled } = require("../../config/plugins");
const { getImage } = require("../../providers/song-info");
const { cropMaxWidth } = require("./utils");
const { sendError } = require("./back");
const { presets } = require('./utils');
/** @type {Innertube} */
let yt;
let options;
module.exports = async (options_) => {
options = options_;
yt = await Innertube.create({ cache: new UniversalCache(false), generate_session_locally: true });
ipcMain.handle("download-song", (_, url) => downloadSong(url));
};
async function downloadSong(url, playlistFolder = undefined, trackId = undefined) {
const metadata = await getMetadata(url);
metadata.trackId = trackId;
const stream = await yt.download(metadata.id, {
type: 'audio', // audio, video or video+audio
quality: 'best', // best, bestefficiency, 144p, 240p, 480p, 720p and so on.
format: 'any' // media container format
});
console.info(`Downloading ${metadata.artist} - ${metadata.title} {${metadata.id}}...`);
const iterableStream = Utils.streamToIterable(stream);
const dir = playlistFolder || options.downloadFolder || app.getPath("downloads");
const name = `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`;
const extension = presets[options.preset]?.extension || 'mp3';
const filename = filenamify(`${name}.${extension}`, {
replacement: "_",
maxLength: 255,
});
const filePath = join(dir, filename);
if (!existsSync(dir)) {
mkdirSync(dir);
}
if (!presets[options.preset]) {
const fileBuffer = await toMP3(iterableStream, metadata);
console.info('writing id3 tags...'); // DELETE
writeFileSync(filePath, await writeID3(fileBuffer, metadata));
console.info('done writing id3 tags!'); // DELETE
} else {
const file = createWriteStream(filePath);
//stream.pipeTo(file);
for await (const chunk of iterableStream) {
file.write(chunk);
}
await ffmpegWriteTags(filePath, metadata, presets[options.preset]?.ffmpegArgs);
}
console.info(`${filePath} - Done!`, '\n');
}
module.exports.downloadSong = downloadSong;
function getIdFromUrl(url) {
const match = url.match(/v=([^&]+)/);
return match ? match[1] : null;
}
async function getMetadata(url) {
const id = getIdFromUrl(url);
const info = await yt.music.getInfo(id);
return {
id: info.basic_info.id,
title: info.basic_info.title,
artist: info.basic_info.author,
album: info.player_overlays?.browser_media_session?.album?.text,
image: info.basic_info.thumbnail[0].url,
};
}
async function writeID3(buffer, metadata) {
try {
const nativeImage = cropMaxWidth(await getImage(metadata.image));
const coverBuffer = nativeImage && !nativeImage.isEmpty() ?
nativeImage.toPNG() : null;
const writer = new ID3Writer(buffer);
// Create the metadata tags
writer
.setFrame("TIT2", metadata.title)
.setFrame("TPE1", [metadata.artist]);
if (metadata.album) {
writer.setFrame("TALB", metadata.album);
}
if (coverBuffer) {
writer.setFrame("APIC", {
type: 3,
data: coverBuffer,
description: "",
});
}
if (isEnabled("lyrics-genius")) {
const lyrics = await fetchFromGenius(metadata);
if (lyrics) {
writer.setFrame("USLT", {
description: '',
lyrics: lyrics,
});
}
}
if (metadata.trackId) {
writer.setFrame("TRCK", metadata.trackId);
}
writer.addTag();
return Buffer.from(writer.arrayBuffer);
} catch (e) {
sendError(e);
}
}
const { randomBytes } = require("crypto");
const Mutex = require("async-mutex").Mutex;
const ffmpeg = require("@ffmpeg/ffmpeg").createFFmpeg({
log: false,
logger: () => { }, // console.log,
progress: () => { }, // console.log,
});
const ffmpegMutex = new Mutex();
async function toMP3(stream, metadata, extension = "mp3") {
const chunks = [];
for await (const chunk of stream) {
chunks.push(chunk);
}
const buffer = Buffer.concat(chunks);
const safeVideoName = randomBytes(32).toString("hex");
const releaseFFmpegMutex = await ffmpegMutex.acquire();
try {
if (!ffmpeg.isLoaded()) {
// sendFeedback("Loading…", 2); // indefinite progress bar after download
await ffmpeg.load();
}
// sendFeedback("Preparing file…");
ffmpeg.FS("writeFile", safeVideoName, buffer);
// sendFeedback("Converting…");
await ffmpeg.run(
"-i",
safeVideoName,
...getFFmpegMetadataArgs(metadata),
safeVideoName + "." + extension
);
// sendFeedback("Saving…");
return ffmpeg.FS("readFile", safeVideoName + "." + extension);
} catch (e) {
sendError(e);
} finally {
releaseFFmpegMutex();
}
}
async function ffmpegWriteTags(filePath, metadata, ffmpegArgs = []) {
const releaseFFmpegMutex = await ffmpegMutex.acquire();
try {
if (!ffmpeg.isLoaded()) {
await ffmpeg.load();
}
await ffmpeg.run(
"-i",
filePath,
...getFFmpegMetadataArgs(metadata),
...ffmpegArgs,
filePath
);
} catch (e) {
sendError(e);
} finally {
releaseFFmpegMutex();
}
}
function getFFmpegMetadataArgs(metadata) {
if (!metadata) {
return;
}
return [
...(metadata.title ? ["-metadata", `title=${metadata.title}`] : []),
...(metadata.artist ? ["-metadata", `artist=${metadata.artist}`] : []),
...(metadata.album ? ["-metadata", `album=${metadata.album}`] : []),
...(metadata.trackId ? ["-metadata", `track=${metadata.trackId}`] : []),
];
};