Skip to content

Commit

Permalink
Add downloader (video -> mp3) plugin (in music menu)
Browse files Browse the repository at this point in the history
  • Loading branch information
th-ch committed Nov 21, 2020
1 parent e0f61f1 commit e197087
Show file tree
Hide file tree
Showing 9 changed files with 518 additions and 9 deletions.
5 changes: 5 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ const { isTesting } = require("./utils/testing");
const { setUpTray } = require("./tray");

const app = electron.app;
app.commandLine.appendSwitch(
"js-flags",
// WebAssembly flags
"--experimental-wasm-threads --experimental-wasm-bulk-memory"
);
app.allowRendererProcessReuse = true; // https://github.com/electron/electron/issues/18397

// Adds debug features like hotkeys for triggering dev tools and reload
Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,18 @@
},
"dependencies": {
"@cliqz/adblocker-electron": "^1.18.3",
"@ffmpeg/core": "^0.8.4",
"@ffmpeg/ffmpeg": "^0.9.5",
"YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.8.0",
"downloads-folder": "^3.0.1",
"electron-debug": "^3.1.0",
"electron-is": "^3.0.0",
"electron-localshortcut": "^3.2.1",
"electron-store": "^6.0.1",
"electron-updater": "^4.3.5",
"node-fetch": "^2.6.1"
"filenamify": "^4.2.0",
"node-fetch": "^2.6.1",
"ytdl-core": "^4.0.3"
},
"devDependencies": {
"electron": "^10.1.3",
Expand Down
9 changes: 9 additions & 0 deletions plugins/downloader/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const CHANNEL = "downloader";
const ACTIONS = {
ERROR: "error",
};

module.exports = {
CHANNEL: CHANNEL,
ACTIONS: ACTIONS,
};
33 changes: 33 additions & 0 deletions plugins/downloader/back.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const { join } = require("path");

const { dialog } = require("electron");

const { injectCSS, listenAction } = require("../utils");
const { ACTIONS, CHANNEL } = require("./actions.js");

const sendError = (win, err) => {
const dialogOpts = {
type: "info",
buttons: ["OK"],
title: "Error in download!",
message: "Argh! Apologies, download failed…",
detail: err.toString(),
};
dialog.showMessageBox(dialogOpts);
};

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

listenAction(CHANNEL, (event, action, error) => {
switch (action) {
case ACTIONS.ERROR:
sendError(win, error);
break;
default:
console.log("Unknown action: " + action);
}
});
}

module.exports = handle;
54 changes: 54 additions & 0 deletions plugins/downloader/front.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const { ElementFromFile, templatePath, triggerAction } = require("../utils");
const { ACTIONS, CHANNEL } = require("./actions.js");
const { downloadVideoToMP3 } = require("./youtube-dl");

let menu = null;
let progress = null;
const downloadButton = ElementFromFile(
templatePath(__dirname, "download.html")
);

const observer = new MutationObserver((mutations, observer) => {
if (!menu) {
menu = document.querySelector("ytmusic-menu-popup-renderer paper-listbox");
}

if (menu && !menu.contains(downloadButton)) {
menu.prepend(downloadButton);
progress = document.querySelector("#ytmcustom-download");
}
});

global.download = () => {
const videoUrl = window.location.href;

downloadVideoToMP3(
videoUrl,
(feedback) => {
if (!progress) {
console.warn("Cannot update progress");
} else {
progress.innerHTML = feedback;
}
},
(error) => {
triggerAction(CHANNEL, ACTIONS.ERROR, error);
},
() => {
if (!progress) {
console.warn("Cannot update progress");
} else {
progress.innerHTML = "Download";
}
}
);
};

function observeMenu() {
observer.observe(document, {
childList: true,
subtree: true,
});
}

module.exports = observeMenu;
13 changes: 13 additions & 0 deletions plugins/downloader/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.menu-item {
display: var(--ytmusic-menu-item_-_display);
height: var(--ytmusic-menu-item_-_height);
align-items: var(--ytmusic-menu-item_-_align-items);
padding: var(--ytmusic-menu-item_-_padding);
cursor: pointer;
}

.menu-icon {
flex: var(--ytmusic-menu-item-icon_-_flex);
margin: var(--ytmusic-menu-item-icon_-_margin);
fill: var(--ytmusic-menu-item-icon_-_fill);
}
37 changes: 37 additions & 0 deletions plugins/downloader/templates/download.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<div
class="menu-item ytmusic-menu-popup-renderer"
role="option"
tabindex="-1"
aria-disabled="false"
aria-selected="false"
onclick="download()"
>
<div
class="menu-icon yt-icon-container yt-icon ytmusic-toggle-menu-service-item-renderer"
>
<svg
viewBox="0 0 24 24"
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope yt-icon"
style="pointer-events: none; display: block; width: 100%; height: 100%;"
>
<g class="style-scope yt-icon">
<path
d="M25.462,19.105v6.848H4.515v-6.848H0.489v8.861c0,1.111,0.9,2.012,2.016,2.012h24.967c1.115,0,2.016-0.9,2.016-2.012v-8.861H25.462z"
class="style-scope yt-icon"
/>
<path
d="M14.62,18.426l-5.764-6.965c0,0-0.877-0.828,0.074-0.828s3.248,0,3.248,0s0-0.557,0-1.416c0-2.449,0-6.906,0-8.723c0,0-0.129-0.494,0.615-0.494c0.75,0,4.035,0,4.572,0c0.536,0,0.524,0.416,0.524,0.416c0,1.762,0,6.373,0,8.742c0,0.768,0,1.266,0,1.266s1.842,0,2.998,0c1.154,0,0.285,0.867,0.285,0.867s-4.904,6.51-5.588,7.193C15.092,18.979,14.62,18.426,14.62,18.426z"
class="style-scope yt-icon"
/>
</g>
</svg>
</div>
<div
class="text style-scope ytmusic-toggle-menu-service-item-renderer"
id="ytmcustom-download"
>
Download
</div>
</div>
89 changes: 89 additions & 0 deletions plugins/downloader/youtube-dl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
const { randomBytes } = require("crypto");
const { writeFileSync } = require("fs");
const { join } = require("path");

const downloadsFolder = require("downloads-folder");
const is = require("electron-is");
const filenamify = require("filenamify");

// Browser version of FFmpeg (in renderer process) instead of loading @ffmpeg/ffmpeg
// because --js-flags cannot be passed in the main process when the app is packaged
// See https://github.com/electron/electron/issues/22705
const FFmpeg = require("@ffmpeg/ffmpeg/dist/ffmpeg.min");
const ytdl = require("ytdl-core");

const { createFFmpeg } = FFmpeg;
const ffmpeg = createFFmpeg({
log: false,
logger: () => {}, // console.log,
progress: () => {}, // console.log,
});

const downloadVideoToMP3 = (videoUrl, sendFeedback, sendError, reinit) => {
sendFeedback("Downloading…");

let videoName = "YouTube Music - Unknown title";
let videoReadableStream;
try {
videoReadableStream = ytdl(videoUrl, {
filter: "audioonly",
quality: "highestaudio",
highWaterMark: 32 * 1024 * 1024, // 32 MB
});
} catch (err) {
sendError(err);
return;
}

const chunks = [];
videoReadableStream
.on("data", (chunk) => {
chunks.push(chunk);
})
.on("progress", (chunkLength, downloaded, total) => {
const progress = Math.floor((downloaded / total) * 100);
sendFeedback("Download: " + progress + "%");
})
.on("info", (info, format) => {
videoName = info.videoDetails.title.replace("|", "").toString("ascii");
if (is.dev()) {
console.log("Downloading video - name:", videoName);
}
})
.on("error", sendError)
.on("end", () => {
const buffer = Buffer.concat(chunks);
toMP3(videoName, buffer, sendFeedback, sendError, reinit);
});
};

const toMP3 = async (videoName, buffer, sendFeedback, sendError, reinit) => {
const safeVideoName = randomBytes(32).toString("hex");

try {
if (!ffmpeg.isLoaded()) {
sendFeedback("Loading…");
await ffmpeg.load();
}

sendFeedback("Preparing file…");
ffmpeg.FS("writeFile", safeVideoName, buffer);

sendFeedback("Converting…");
await ffmpeg.run("-i", safeVideoName, safeVideoName + ".mp3");

const filename = filenamify(videoName + ".mp3", { replacement: "_" });
writeFileSync(
join(downloadsFolder(), filename),
ffmpeg.FS("readFile", safeVideoName + ".mp3")
);

reinit();
} catch (e) {
sendError(e);
}
};

module.exports = {
downloadVideoToMP3,
};
Loading

0 comments on commit e197087

Please sign in to comment.