Skip to content

Commit

Permalink
download using youtubei,js instead of ytdl-core
Browse files Browse the repository at this point in the history
  • Loading branch information
Araxeus committed Mar 3, 2023
1 parent 7bdbab5 commit 8c33989
Show file tree
Hide file tree
Showing 9 changed files with 308 additions and 329 deletions.
5 changes: 0 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,6 @@ if(config.get("options.singleInstanceLock")){
});
}

app.commandLine.appendSwitch(
"js-flags",
// WebAssembly flags
"--experimental-wasm-threads"
);
app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer"); // Required for downloader
app.allowRendererProcessReuse = true; // https://github.com/electron/electron/issues/18397
if (config.get("options.disableHardwareAcceleration")) {
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,18 @@
"electron-store": "^8.1.0",
"electron-unhandled": "^4.0.1",
"electron-updater": "^5.3.0",
"file-type": "^18.2.1",
"filenamify": "^4.3.0",
"howler": "^2.2.3",
"html-to-text": "^9.0.3",
"md5": "^2.3.0",
"mpris-service": "^2.1.2",
"node-fetch": "^2.6.8",
"node-id3": "^0.2.6",
"node-notifier": "^10.0.1",
"simple-youtube-age-restriction-bypass": "https://gitpkg.now.sh/api/pkg.tgz?url=zerodytrash/Simple-YouTube-Age-Restriction-Bypass&commit=v2.5.4",
"vudio": "^2.1.1",
"youtubei.js": "^2.9.0",
"youtubei.js": "^3.1.1",
"ytdl-core": "^4.11.1",
"ytpl": "^2.3.0"
},
Expand Down
193 changes: 193 additions & 0 deletions plugins/downloader/back-downloader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
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 id3 = require('node-id3').Promise;

const { sendError } = require("./back");
const { presets } = require('./utils');

ffmpegWriteTags
/** @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) {
const metadata = await getMetadata(url);

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]) {
await toMP3(iterableStream, filePath, metadata);
console.info('writing id3 tags...'); // DELETE
await writeID3(filePath, metadata).then(() => console.info('done writing id3 tags!')); // DELETE
} else {
const file = createWriteStream(filePath);
//stream.pipeTo(file);
for await (const chunk of iterableStream) {
file.write(chunk);
}
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);
//console.log('got info:' + JSON.stringify(info, null, 2)); // DELETE

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,
};
}

const { getImage } = require("../../providers/song-info");
const { cropMaxWidth } = require("./utils");

async function writeID3(filePath, metadata) {
const tags = {
title: metadata.title,
artist: metadata.artist,
album: metadata.album,
image: {
mime: "image/png",
type: {
id: 3,
name: "front cover"
},
description: "",
imageBuffer: cropMaxWidth(await getImage(metadata.image))?.toPNG(),
}
// TODO: lyrics
};

await id3.write(tags, filePath);
}

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, filePath, 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
);

const fileBuffer = ffmpeg.FS("readFile", safeVideoName + "." + extension);

await writeID3(fileBuffer, metadata);

// sendFeedback("Saving…");

writeFileSync(filePath, fileBuffer);
} 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}`] : []),
];
};
59 changes: 10 additions & 49 deletions plugins/downloader/back.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ const { isEnabled } = require("../../config/plugins");
const { getImage } = require("../../providers/song-info");
const { fetchFromGenius } = require("../lyrics-genius/back");

const sendError = (win, error) => {
let win = {};

const sendError = (error) => {
win.setProgressBar(-1); // close progress bar
dialog.showMessageBox({
type: "info",
Expand All @@ -25,16 +27,21 @@ const sendError = (win, error) => {

let nowPlayingMetadata = {};

function handle(win) {

function handle(win_, options) {
win = win_;
injectCSS(win.webContents, join(__dirname, "style.css"));

require("./back-downloader")(options);

registerCallback((info) => {
nowPlayingMetadata = info;
});

listenAction(CHANNEL, (event, action, arg) => {
switch (action) {
case ACTIONS.ERROR: // arg = error
sendError(win, arg);
sendError(arg);
break;
case ACTIONS.METADATA:
event.returnValue = JSON.stringify(nowPlayingMetadata);
Expand All @@ -46,52 +53,6 @@ function handle(win) {
console.log("Unknown action: " + action);
}
});

ipcMain.on("add-metadata", async (event, filePath, songBuffer, currentMetadata) => {
let fileBuffer = songBuffer;
const songMetadata = currentMetadata.imageSrcYTPL ? // This means metadata come from ytpl.getInfo();
{
...currentMetadata,
image: cropMaxWidth(await getImage(currentMetadata.imageSrcYTPL))
} :
{ ...nowPlayingMetadata, ...currentMetadata };

try {
const coverBuffer = songMetadata.image && !songMetadata.image.isEmpty() ?
songMetadata.image.toPNG() : null;

const writer = new ID3Writer(songBuffer);

// Create the metadata tags
writer
.setFrame("TIT2", songMetadata.title)
.setFrame("TPE1", [songMetadata.artist]);
if (coverBuffer) {
writer.setFrame("APIC", {
type: 3,
data: coverBuffer,
description: ""
});
}
if (isEnabled("lyrics-genius")) {
const lyrics = await fetchFromGenius(songMetadata);
if (lyrics) {
writer.setFrame("USLT", {
description: lyrics,
lyrics: lyrics,
});
}
}
writer.addTag();
fileBuffer = Buffer.from(writer.arrayBuffer);
} catch (error) {
sendError(win, error);
}

writeFileSync(filePath, fileBuffer);
// Notify the youtube-dl file
event.reply("add-metadata-done");
});
}

module.exports = handle;
Expand Down
24 changes: 2 additions & 22 deletions plugins/downloader/front.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const { defaultConfig } = require("../../config");
const { getSongMenu } = require("../../providers/dom-elements");
const { ElementFromFile, templatePath, triggerAction } = require("../utils");
const { ACTIONS, CHANNEL } = require("./actions.js");
const { downloadVideoToMP3 } = require("./youtube-dl");

let menu = null;
let progress = null;
Expand Down Expand Up @@ -61,28 +60,9 @@ global.download = () => {
videoUrl = metadata.url || window.location.href;
}

downloadVideoToMP3(
videoUrl,
(feedback, ratio = undefined) => {
if (!progress) {
console.warn("Cannot update progress");
} else {
progress.innerHTML = feedback;
}
if (ratio) {
triggerAction(CHANNEL, ACTIONS.PROGRESS, ratio);
}
},
(error) => {
triggerAction(CHANNEL, ACTIONS.ERROR, error);
reinit();
},
reinit,
pluginOptions,
metadata
);
ipcRenderer.invoke('download-song', videoUrl).finally(() => triggerAction(CHANNEL, ACTIONS.PROGRESS, -1));
return;
};
// });

function observeMenu(options) {
pluginOptions = { ...pluginOptions, ...options };
Expand Down
Loading

0 comments on commit 8c33989

Please sign in to comment.