merge source (#2)

* Added Discord timeout

* Add getOptions in plugin util

* Mutex in ffmpeg conversion (only supports one command at a time)

* Add menu customization in plugin system

* Add ytpl package (playlist info)

* Handle ffmpeg metadata flags when metadata is not present

* Only use artist in file name if present

* Export sendError method

* Handle image not present in metadata util

* Add downloader utils (getFolder and default menu label)

* Pass (optional) existing metadata and subfolder in mp3 converter

* Add listener to download playlist

* Add custom menu in downloader plugin ("download playlist" item)

* nit: fix main CSS style

* Only set the "enable" item in menu if plugin not enabled

* Navigation plugin: inject HTML once CSS is loaded

Co-authored-by: Sem Visscher <semvisscher10@gmail.com>
Co-authored-by: TC <th-ch@users.noreply.github.com>
This commit is contained in:
Araxeus
2021-03-25 01:02:42 +02:00
committed by GitHub
parent 2fb4cbcf02
commit 9a95f435ad
15 changed files with 248 additions and 61 deletions

View File

@ -35,6 +35,10 @@ const defaultConfig = {
ffmpegArgs: [], // e.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s
downloadFolder: undefined, // Custom download folder (absolute path)
},
discord: {
activityTimoutEnabled: true, // if enabled, the discord rich presence gets cleared when music paused after the time specified below
activityTimoutTime: 10 * 60 * 1000 // 10 minutes
},
},
};

View File

@ -24,6 +24,10 @@ function setOptions(plugin, options) {
});
}
function getOptions(plugin) {
return store.get("plugins")[plugin];
}
function enable(plugin) {
setOptions(plugin, { enabled: true });
}
@ -38,4 +42,5 @@ module.exports = {
enable,
disable,
setOptions,
getOptions,
};

48
menu.js
View File

@ -1,26 +1,50 @@
const { existsSync } = require("fs");
const path = require("path");
const { app, Menu } = require("electron");
const is = require("electron-is");
const { getAllPlugins } = require("./plugins/utils");
const config = require("./config");
const pluginEnabledMenu = (plugin, label = "") => ({
label: label || plugin,
type: "checkbox",
checked: config.plugins.isEnabled(plugin),
click: (item) => {
if (item.checked) {
config.plugins.enable(plugin);
} else {
config.plugins.disable(plugin);
}
},
});
const mainMenuTemplate = (win) => [
{
label: "Plugins",
submenu: [
...getAllPlugins().map((plugin) => {
return {
label: plugin,
type: "checkbox",
checked: config.plugins.isEnabled(plugin),
click: (item) => {
if (item.checked) {
config.plugins.enable(plugin);
} else {
config.plugins.disable(plugin);
}
},
};
const pluginPath = path.join(__dirname, "plugins", plugin, "menu.js");
if (!config.plugins.isEnabled(plugin)) {
return pluginEnabledMenu(plugin);
}
if (existsSync(pluginPath)) {
const getPluginMenu = require(pluginPath);
return {
label: plugin,
submenu: [
pluginEnabledMenu(plugin, "Enabled"),
...getPluginMenu(win, config.plugins.getOptions(plugin), () =>
module.exports.setApplicationMenu(win)
),
],
};
}
return pluginEnabledMenu(plugin);
}),
{ type: "separator" },
{

View File

@ -66,6 +66,7 @@
"@ffmpeg/core": "^0.8.5",
"@ffmpeg/ffmpeg": "^0.9.7",
"YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.8.1",
"async-mutex": "^0.3.1",
"browser-id3-writer": "^4.4.0",
"discord-rpc": "^3.2.0",
"downloads-folder": "^3.0.1",
@ -77,7 +78,8 @@
"electron-updater": "^4.3.6",
"filenamify": "^4.2.0",
"node-fetch": "^2.6.1",
"ytdl-core": "^4.4.5"
"ytdl-core": "^4.4.5",
"ytpl": "^2.0.5"
},
"devDependencies": {
"electron": "^11.2.3",

View File

@ -9,7 +9,9 @@ const rpc = new Discord.Client({
// Application ID registered by @semvis123
const clientId = "790655993809338398";
module.exports = (win) => {
let clearActivity;
module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => {
const registerCallback = getSongInfo(win);
// If the page is ready, register the callback
@ -29,7 +31,13 @@ module.exports = (win) => {
// Add an idle icon to show that the song is paused
activityInfo.smallImageKey = "idle";
activityInfo.smallImageText = "idle/paused";
// Set start the timer so the activity gets cleared after a while if enabled
if (activityTimoutEnabled)
clearActivity = setTimeout(()=>rpc.clearActivity(), activityTimoutTime||10,000);
} else {
// stop the clear activity timout
clearTimeout(clearActivity);
// Add the start and end time of the song
const songStartTime = Date.now() - songInfo.elapsedSeconds * 1000;
activityInfo.startTimestamp = songStartTime;

View File

@ -45,19 +45,21 @@ function handle(win) {
let fileBuffer = songBuffer;
try {
const coverBuffer = metadata.image.toPNG();
const writer = new ID3Writer(songBuffer);
if (metadata.image) {
const coverBuffer = metadata.image.toPNG();
// Create the metadata tags
writer
.setFrame("TIT2", metadata.title)
.setFrame("TPE1", [metadata.artist])
.setFrame("APIC", {
type: 3,
data: coverBuffer,
description: "",
});
writer.addTag();
// Create the metadata tags
writer
.setFrame("TIT2", metadata.title)
.setFrame("TPE1", [metadata.artist])
.setFrame("APIC", {
type: 3,
data: coverBuffer,
description: "",
});
writer.addTag();
}
fileBuffer = Buffer.from(writer.arrayBuffer);
} catch (error) {
sendError(win, error);
@ -70,3 +72,4 @@ function handle(win) {
}
module.exports = handle;
module.exports.sendError = sendError;

View File

@ -0,0 +1,63 @@
const { existsSync, mkdirSync } = require("fs");
const { join } = require("path");
const { URL } = require("url");
const { ipcMain } = require("electron");
const is = require("electron-is");
const ytpl = require("ytpl");
const { sendError } = require("./back");
const { defaultMenuDownloadLabel, getFolder } = require("./utils");
let downloadLabel = defaultMenuDownloadLabel;
module.exports = (win, options, refreshMenu) => [
{
label: downloadLabel,
click: async () => {
const currentURL = win.webContents.getURL();
const playlistID = new URL(currentURL).searchParams.get("list");
if (!playlistID) {
sendError(win, new Error("No playlist ID found"));
return;
}
const playlist = await ytpl(playlistID);
const playlistTitle = playlist.title;
const folder = getFolder(options.downloadFolder);
const playlistFolder = join(folder, playlistTitle);
if (existsSync(playlistFolder)) {
sendError(
win,
new Error(`The folder ${playlistFolder} already exists`)
);
return;
}
mkdirSync(playlistFolder, { recursive: true });
ipcMain.on("downloader-feedback", (_, feedback) => {
downloadLabel = feedback;
refreshMenu();
});
downloadLabel = `Downloading "${playlistTitle}"`;
refreshMenu();
if (is.dev()) {
console.log(
`Downloading playlist "${playlistTitle}" (${playlist.items.length} songs)`
);
}
playlist.items.slice(0, options.playlistMaxItems).forEach((song) => {
win.webContents.send(
"downloader-download-playlist",
song,
playlistTitle,
options
);
});
},
},
];

View File

@ -0,0 +1,4 @@
const downloadsFolder = require("downloads-folder");
module.exports.getFolder = (customFolder) => customFolder || downloadsFolder();
module.exports.defaultMenuDownloadLabel = "Download playlist";

View File

@ -1,7 +1,8 @@
const { randomBytes } = require("crypto");
const { writeFileSync } = require("fs");
const { join } = require("path");
const downloadsFolder = require("downloads-folder");
const Mutex = require("async-mutex").Mutex;
const { ipcRenderer } = require("electron");
const is = require("electron-is");
const filenamify = require("filenamify");
@ -12,8 +13,9 @@ const filenamify = require("filenamify");
const FFmpeg = require("@ffmpeg/ffmpeg/dist/ffmpeg.min");
const ytdl = require("ytdl-core");
const { triggerActionSync } = require("../utils");
const { triggerAction, triggerActionSync } = require("../utils");
const { ACTIONS, CHANNEL } = require("./actions.js");
const { defaultMenuDownloadLabel, getFolder } = require("./utils");
const { createFFmpeg } = FFmpeg;
const ffmpeg = createFFmpeg({
@ -21,13 +23,16 @@ const ffmpeg = createFFmpeg({
logger: () => {}, // console.log,
progress: () => {}, // console.log,
});
const ffmpegMutex = new Mutex();
const downloadVideoToMP3 = (
videoUrl,
sendFeedback,
sendError,
reinit,
options
options,
metadata = undefined,
subfolder = ""
) => {
sendFeedback("Downloading…");
@ -66,9 +71,18 @@ const downloadVideoToMP3 = (
}
})
.on("error", sendError)
.on("end", () => {
.on("end", async () => {
const buffer = Buffer.concat(chunks);
toMP3(videoName, buffer, sendFeedback, sendError, reinit, options);
await toMP3(
videoName,
buffer,
sendFeedback,
sendError,
reinit,
options,
metadata,
subfolder
);
});
};
@ -78,10 +92,13 @@ const toMP3 = async (
sendFeedback,
sendError,
reinit,
options
options,
existingMetadata = undefined,
subfolder = ""
) => {
const safeVideoName = randomBytes(32).toString("hex");
const extension = options.extension || "mp3";
const releaseFFmpegMutex = await ffmpegMutex.acquire();
try {
if (!ffmpeg.isLoaded()) {
@ -93,7 +110,7 @@ const toMP3 = async (
ffmpeg.FS("writeFile", safeVideoName, buffer);
sendFeedback("Converting…");
const metadata = getMetadata();
const metadata = existingMetadata || getMetadata();
await ffmpeg.run(
"-i",
safeVideoName,
@ -102,24 +119,31 @@ const toMP3 = async (
safeVideoName + "." + extension
);
const folder = options.downloadFolder || downloadsFolder();
const folder = getFolder(options.downloadFolder);
const name = metadata
? `${metadata.artist} - ${metadata.title}`
? `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`
: videoName;
const filename = filenamify(name + "." + extension, {
replacement: "_",
});
// Add the metadata
sendFeedback("Adding metadata…");
ipcRenderer.send(
"add-metadata",
join(folder, filename),
ffmpeg.FS("readFile", safeVideoName + "." + extension)
);
ipcRenderer.once("add-metadata-done", reinit);
const filePath = join(folder, subfolder, filename);
const fileBuffer = ffmpeg.FS("readFile", safeVideoName + "." + extension);
if (existingMetadata) {
writeFileSync(filePath, fileBuffer);
reinit();
} else {
// Add the metadata
sendFeedback("Adding metadata…");
ipcRenderer.send("add-metadata", filePath, fileBuffer);
ipcRenderer.once("add-metadata-done", reinit);
sendFeedback("Finished converting", metadata);
}
} catch (e) {
sendError(e);
} finally {
releaseFFmpegMutex();
}
};
@ -133,13 +157,34 @@ const getFFmpegMetadataArgs = (metadata) => {
}
return [
"-metadata",
`title=${metadata.title}`,
"-metadata",
`artist=${metadata.artist}`,
...(metadata.title ? ["-metadata", `title=${metadata.title}`] : []),
...(metadata.artist ? ["-metadata", `artist=${metadata.artist}`] : []),
];
};
module.exports = {
downloadVideoToMP3,
};
ipcRenderer.on(
"downloader-download-playlist",
(_, songMetadata, playlistFolder, options) => {
const reinit = () =>
ipcRenderer.send("downloader-feedback", defaultMenuDownloadLabel);
downloadVideoToMP3(
songMetadata.url,
(feedback) => {
ipcRenderer.send("downloader-feedback", feedback);
},
(error) => {
triggerAction(CHANNEL, ACTIONS.ERROR, error);
reinit();
},
reinit,
options,
songMetadata,
playlistFolder
);
}
);

View File

@ -4,7 +4,10 @@ const { injectCSS, listenAction } = require("../utils");
const { ACTIONS, CHANNEL } = require("./actions.js");
function handle(win) {
injectCSS(win.webContents, path.join(__dirname, "style.css"));
injectCSS(win.webContents, path.join(__dirname, "style.css"), () => {
win.webContents.send("navigation-css-ready");
});
listenAction(CHANNEL, (event, action) => {
switch (action) {
case ACTIONS.NEXT:

View File

@ -1,15 +1,19 @@
const { ipcRenderer } = require("electron");
const { ElementFromFile, templatePath } = require("../utils");
function run() {
const forwardButton = ElementFromFile(
templatePath(__dirname, "forward.html")
);
const backButton = ElementFromFile(templatePath(__dirname, "back.html"));
const menu = document.querySelector("ytmusic-pivot-bar-renderer");
ipcRenderer.on("navigation-css-ready", () => {
const forwardButton = ElementFromFile(
templatePath(__dirname, "forward.html")
);
const backButton = ElementFromFile(templatePath(__dirname, "back.html"));
const menu = document.querySelector("ytmusic-pivot-bar-renderer");
if (menu) {
menu.prepend(backButton, forwardButton);
}
if (menu) {
menu.prepend(backButton, forwardButton);
}
});
}
module.exports = run;

View File

@ -20,7 +20,7 @@
preserveAspectRatio="xMidYMid meet"
focusable="false"
class="style-scope iron-icon"
style="pointer-events: none; display: block; width: 100%; height: 100%;"
style="pointer-events: none; display: block; width: 100%; height: 100%"
>
<g class="style-scope iron-icon">
<path

View File

@ -42,9 +42,12 @@ module.exports.fileExists = (path, callbackIfExists) => {
});
};
module.exports.injectCSS = (webContents, filepath) => {
webContents.on("did-finish-load", () => {
webContents.insertCSS(fs.readFileSync(filepath, "utf8"));
module.exports.injectCSS = (webContents, filepath, cb = undefined) => {
webContents.on("did-finish-load", async () => {
await webContents.insertCSS(fs.readFileSync(filepath, "utf8"));
if (cb) {
cb();
}
});
};

View File

@ -1661,6 +1661,13 @@ async-exit-hook@^2.0.1:
resolved "https://registry.yarnpkg.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz#8bd8b024b0ec9b1c01cccb9af9db29bd717dfaf3"
integrity sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==
async-mutex@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.3.1.tgz#7033af665f1c7cebed8b878267a43ba9e77c5f67"
integrity sha512-vRfQwcqBnJTLzVQo72Sf7KIUbcSUP5hNchx6udI1U6LuPQpfePgdjJzlCe76yFZ8pxlLjn9lwcl/Ya0TSOv0Tw==
dependencies:
tslib "^2.1.0"
async@0.9.x:
version "0.9.2"
resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
@ -6147,7 +6154,7 @@ min-indent@^1.0.0:
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
miniget@^4.0.0:
miniget@^4.0.0, miniget@^4.1.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/miniget/-/miniget-4.2.0.tgz#0004e95536b192d95a7d09f4435d67b9285481d0"
integrity sha512-IzTOaNgBw/qEpzkPTE7X2cUVXQfSKbG8w52Emi93zb+Zya2ZFrbmavpixzebuDJD9Ku4ecbaFlC7Y1cEESzQtQ==
@ -8362,6 +8369,11 @@ tslib@^1.8.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
tsutils@^3.17.1:
version "3.20.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.20.0.tgz#ea03ea45462e146b53d70ce0893de453ff24f698"
@ -9065,6 +9077,13 @@ ytdl-core@^4.4.5:
miniget "^4.0.0"
sax "^1.1.3"
ytpl@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/ytpl/-/ytpl-2.0.5.tgz#c56900bccaf96e289304de647bc861121f61223e"
integrity sha512-8hc+f3pijaogj1yoZTCGImMDS4x0ogFPDsx1PefNQ+2EAhJMm1K4brcYT9zpJhPi9SXh+O103pEIHDw3+dAhxA==
dependencies:
miniget "^4.1.0"
zip-stream@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.0.4.tgz#3a8f100b73afaa7d1ae9338d910b321dec77ff3a"

View File

@ -5,7 +5,7 @@
/* Allow window dragging */
ytmusic-nav-bar {
-webkit-user-select: none;
-webkit-app-region : drag;
-webkit-app-region: drag;
}
iron-icon,