Skip to content

Commit e197087

Browse files
committed
Add downloader (video -> mp3) plugin (in music menu)
1 parent e0f61f1 commit e197087

File tree

9 files changed

+518
-9
lines changed

9 files changed

+518
-9
lines changed

index.js

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ const { isTesting } = require("./utils/testing");
1919
const { setUpTray } = require("./tray");
2020

2121
const app = electron.app;
22+
app.commandLine.appendSwitch(
23+
"js-flags",
24+
// WebAssembly flags
25+
"--experimental-wasm-threads --experimental-wasm-bulk-memory"
26+
);
2227
app.allowRendererProcessReuse = true; // https://github.com/electron/electron/issues/18397
2328

2429
// Adds debug features like hotkeys for triggering dev tools and reload

package.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,18 @@
4747
},
4848
"dependencies": {
4949
"@cliqz/adblocker-electron": "^1.18.3",
50+
"@ffmpeg/core": "^0.8.4",
51+
"@ffmpeg/ffmpeg": "^0.9.5",
5052
"YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.8.0",
53+
"downloads-folder": "^3.0.1",
5154
"electron-debug": "^3.1.0",
5255
"electron-is": "^3.0.0",
5356
"electron-localshortcut": "^3.2.1",
5457
"electron-store": "^6.0.1",
5558
"electron-updater": "^4.3.5",
56-
"node-fetch": "^2.6.1"
59+
"filenamify": "^4.2.0",
60+
"node-fetch": "^2.6.1",
61+
"ytdl-core": "^4.0.3"
5762
},
5863
"devDependencies": {
5964
"electron": "^10.1.3",

plugins/downloader/actions.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const CHANNEL = "downloader";
2+
const ACTIONS = {
3+
ERROR: "error",
4+
};
5+
6+
module.exports = {
7+
CHANNEL: CHANNEL,
8+
ACTIONS: ACTIONS,
9+
};

plugins/downloader/back.js

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const { join } = require("path");
2+
3+
const { dialog } = require("electron");
4+
5+
const { injectCSS, listenAction } = require("../utils");
6+
const { ACTIONS, CHANNEL } = require("./actions.js");
7+
8+
const sendError = (win, err) => {
9+
const dialogOpts = {
10+
type: "info",
11+
buttons: ["OK"],
12+
title: "Error in download!",
13+
message: "Argh! Apologies, download failed…",
14+
detail: err.toString(),
15+
};
16+
dialog.showMessageBox(dialogOpts);
17+
};
18+
19+
function handle(win) {
20+
injectCSS(win.webContents, join(__dirname, "style.css"));
21+
22+
listenAction(CHANNEL, (event, action, error) => {
23+
switch (action) {
24+
case ACTIONS.ERROR:
25+
sendError(win, error);
26+
break;
27+
default:
28+
console.log("Unknown action: " + action);
29+
}
30+
});
31+
}
32+
33+
module.exports = handle;

plugins/downloader/front.js

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const { ElementFromFile, templatePath, triggerAction } = require("../utils");
2+
const { ACTIONS, CHANNEL } = require("./actions.js");
3+
const { downloadVideoToMP3 } = require("./youtube-dl");
4+
5+
let menu = null;
6+
let progress = null;
7+
const downloadButton = ElementFromFile(
8+
templatePath(__dirname, "download.html")
9+
);
10+
11+
const observer = new MutationObserver((mutations, observer) => {
12+
if (!menu) {
13+
menu = document.querySelector("ytmusic-menu-popup-renderer paper-listbox");
14+
}
15+
16+
if (menu && !menu.contains(downloadButton)) {
17+
menu.prepend(downloadButton);
18+
progress = document.querySelector("#ytmcustom-download");
19+
}
20+
});
21+
22+
global.download = () => {
23+
const videoUrl = window.location.href;
24+
25+
downloadVideoToMP3(
26+
videoUrl,
27+
(feedback) => {
28+
if (!progress) {
29+
console.warn("Cannot update progress");
30+
} else {
31+
progress.innerHTML = feedback;
32+
}
33+
},
34+
(error) => {
35+
triggerAction(CHANNEL, ACTIONS.ERROR, error);
36+
},
37+
() => {
38+
if (!progress) {
39+
console.warn("Cannot update progress");
40+
} else {
41+
progress.innerHTML = "Download";
42+
}
43+
}
44+
);
45+
};
46+
47+
function observeMenu() {
48+
observer.observe(document, {
49+
childList: true,
50+
subtree: true,
51+
});
52+
}
53+
54+
module.exports = observeMenu;

plugins/downloader/style.css

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.menu-item {
2+
display: var(--ytmusic-menu-item_-_display);
3+
height: var(--ytmusic-menu-item_-_height);
4+
align-items: var(--ytmusic-menu-item_-_align-items);
5+
padding: var(--ytmusic-menu-item_-_padding);
6+
cursor: pointer;
7+
}
8+
9+
.menu-icon {
10+
flex: var(--ytmusic-menu-item-icon_-_flex);
11+
margin: var(--ytmusic-menu-item-icon_-_margin);
12+
fill: var(--ytmusic-menu-item-icon_-_fill);
13+
}
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<div
2+
class="menu-item ytmusic-menu-popup-renderer"
3+
role="option"
4+
tabindex="-1"
5+
aria-disabled="false"
6+
aria-selected="false"
7+
onclick="download()"
8+
>
9+
<div
10+
class="menu-icon yt-icon-container yt-icon ytmusic-toggle-menu-service-item-renderer"
11+
>
12+
<svg
13+
viewBox="0 0 24 24"
14+
preserveAspectRatio="xMidYMid meet"
15+
focusable="false"
16+
class="style-scope yt-icon"
17+
style="pointer-events: none; display: block; width: 100%; height: 100%;"
18+
>
19+
<g class="style-scope yt-icon">
20+
<path
21+
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"
22+
class="style-scope yt-icon"
23+
/>
24+
<path
25+
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"
26+
class="style-scope yt-icon"
27+
/>
28+
</g>
29+
</svg>
30+
</div>
31+
<div
32+
class="text style-scope ytmusic-toggle-menu-service-item-renderer"
33+
id="ytmcustom-download"
34+
>
35+
Download
36+
</div>
37+
</div>

plugins/downloader/youtube-dl.js

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
const { randomBytes } = require("crypto");
2+
const { writeFileSync } = require("fs");
3+
const { join } = require("path");
4+
5+
const downloadsFolder = require("downloads-folder");
6+
const is = require("electron-is");
7+
const filenamify = require("filenamify");
8+
9+
// Browser version of FFmpeg (in renderer process) instead of loading @ffmpeg/ffmpeg
10+
// because --js-flags cannot be passed in the main process when the app is packaged
11+
// See https://github.com/electron/electron/issues/22705
12+
const FFmpeg = require("@ffmpeg/ffmpeg/dist/ffmpeg.min");
13+
const ytdl = require("ytdl-core");
14+
15+
const { createFFmpeg } = FFmpeg;
16+
const ffmpeg = createFFmpeg({
17+
log: false,
18+
logger: () => {}, // console.log,
19+
progress: () => {}, // console.log,
20+
});
21+
22+
const downloadVideoToMP3 = (videoUrl, sendFeedback, sendError, reinit) => {
23+
sendFeedback("Downloading…");
24+
25+
let videoName = "YouTube Music - Unknown title";
26+
let videoReadableStream;
27+
try {
28+
videoReadableStream = ytdl(videoUrl, {
29+
filter: "audioonly",
30+
quality: "highestaudio",
31+
highWaterMark: 32 * 1024 * 1024, // 32 MB
32+
});
33+
} catch (err) {
34+
sendError(err);
35+
return;
36+
}
37+
38+
const chunks = [];
39+
videoReadableStream
40+
.on("data", (chunk) => {
41+
chunks.push(chunk);
42+
})
43+
.on("progress", (chunkLength, downloaded, total) => {
44+
const progress = Math.floor((downloaded / total) * 100);
45+
sendFeedback("Download: " + progress + "%");
46+
})
47+
.on("info", (info, format) => {
48+
videoName = info.videoDetails.title.replace("|", "").toString("ascii");
49+
if (is.dev()) {
50+
console.log("Downloading video - name:", videoName);
51+
}
52+
})
53+
.on("error", sendError)
54+
.on("end", () => {
55+
const buffer = Buffer.concat(chunks);
56+
toMP3(videoName, buffer, sendFeedback, sendError, reinit);
57+
});
58+
};
59+
60+
const toMP3 = async (videoName, buffer, sendFeedback, sendError, reinit) => {
61+
const safeVideoName = randomBytes(32).toString("hex");
62+
63+
try {
64+
if (!ffmpeg.isLoaded()) {
65+
sendFeedback("Loading…");
66+
await ffmpeg.load();
67+
}
68+
69+
sendFeedback("Preparing file…");
70+
ffmpeg.FS("writeFile", safeVideoName, buffer);
71+
72+
sendFeedback("Converting…");
73+
await ffmpeg.run("-i", safeVideoName, safeVideoName + ".mp3");
74+
75+
const filename = filenamify(videoName + ".mp3", { replacement: "_" });
76+
writeFileSync(
77+
join(downloadsFolder(), filename),
78+
ffmpeg.FS("readFile", safeVideoName + ".mp3")
79+
);
80+
81+
reinit();
82+
} catch (e) {
83+
sendError(e);
84+
}
85+
};
86+
87+
module.exports = {
88+
downloadVideoToMP3,
89+
};

0 commit comments

Comments
 (0)