mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-13 11:21:46 +00:00
Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cb6f56feb | |||
| 46285a5ed0 | |||
| 496836b33b | |||
| af127879a5 | |||
| 38ef452801 | |||
| a9a5d99676 | |||
| e5ab50cebd | |||
| 49194f8141 | |||
| 641ae27efd | |||
| 47a5dec465 | |||
| c93eabb400 | |||
| 492a47321d | |||
| c89f6af8c6 | |||
| 9687c6c8e4 | |||
| ef0a89126a | |||
| 8ce71d628d | |||
| ca95d105c8 | |||
| 12568c2b09 | |||
| 82abb4d4d3 | |||
| 3c0a5dbbe5 | |||
| 0b98eef06f | |||
| 18e69c9f2a | |||
| 8f5d06d420 | |||
| 8a299461a0 | |||
| 0c58bec921 | |||
| e0cb132686 | |||
| 2a192f39f9 | |||
| b7ebb7d499 | |||
| fffeac21b7 | |||
| 4387cb485d | |||
| 2a58dc823a | |||
| 8eb38271ff | |||
| 1987ad1d4f | |||
| cc4dae60ef | |||
| 1943116aa1 | |||
| 3485d26b11 | |||
| 4a60aa9f20 | |||
| cda07c9675 | |||
| ca64a77ed0 | |||
| 30e94d1d6f | |||
| b8c6ebfa53 | |||
| b26748ded8 | |||
| f186da0834 | |||
| c52c2d886a | |||
| e5dc1f8a58 | |||
| 6dbf4134de | |||
| e1cc49a74d | |||
| 4489a400b7 | |||
| 28aa1c0b22 | |||
| c540788d20 | |||
| 4ab07dc875 | |||
| 5033de13ef | |||
| 55a8787a16 | |||
| 3515bf364d | |||
| d8f3246e46 | |||
| cd613aaba2 | |||
| c5f84b568b | |||
| 14dc78984f | |||
| fb61dbfa6c | |||
| 33855f17dd | |||
| 8124623142 | |||
| 177ce5721f | |||
| 4fb0b1dd08 | |||
| bbe5a7d50b | |||
| 5bc8e86353 | |||
| 5b00465558 | |||
| 541c7f34b7 | |||
| 0e8e78362b | |||
| cb6a5a478e | |||
| d615030222 | |||
| cb5ef1d6e5 | |||
| 78a7dcb7e8 | |||
| 8cca9f3eeb | |||
| 93d4d3c976 | |||
| 8284b56075 | |||
| b266037bb4 | |||
| cb743de7fd | |||
| 7cf78c6635 | |||
| 7942efa202 | |||
| 792c2931b0 | |||
| a3778af48a | |||
| 163dc7e1d1 | |||
| 0a59122ac2 | |||
| d2a5110f3b | |||
| cf4bbf94e4 | |||
| d7e42471a4 | |||
| d634c41e75 | |||
| 6b88397f82 | |||
| da3c709ff0 | |||
| ccd320d8ff | |||
| 3831e61d10 | |||
| e46e7b74e2 | |||
| b3da77a6bc | |||
| 96a74f8955 | |||
| 3ea17e6f46 | |||
| a8ac2c3af9 | |||
| 2168cbca30 | |||
| cceb45319a | |||
| e985b78241 | |||
| 090ca828c0 | |||
| 3522925dec | |||
| 5faeddb99b | |||
| 1140c3e2e7 | |||
| 3fb08d27c7 | |||
| 250940d083 | |||
| e00be8f010 | |||
| 8b471c0772 | |||
| 88e738c796 | |||
| c76d8c79d8 | |||
| b396431a8b | |||
| d1795a82f7 | |||
| 9b821a0dfe | |||
| bf89842ee8 | |||
| d274e80f75 | |||
| a98b8945fb | |||
| 6b72599f80 | |||
| 4b6fe78a1a | |||
| d000c03fca | |||
| c0a185ba68 | |||
| ef0813e638 | |||
| 25e9f44260 | |||
| 2d6e858e8f | |||
| 53bf7c5068 | |||
| 13fb686188 | |||
| 61c5494588 | |||
| d96fefbc24 | |||
| e18b7c1013 | |||
| f190b51dcc | |||
| ca41c12f7c | |||
| 844edbe2f4 | |||
| 78974c02e5 | |||
| 4508464fd1 | |||
| dd6455a559 |
@ -41,7 +41,6 @@ const defaultConfig = {
|
|||||||
api_root: "http://ws.audioscrobbler.com/2.0/",
|
api_root: "http://ws.audioscrobbler.com/2.0/",
|
||||||
api_key: "04d76faaac8726e60988e14c105d421a", // api key registered by @semvis123
|
api_key: "04d76faaac8726e60988e14c105d421a", // api key registered by @semvis123
|
||||||
secret: "a5d2a36fdf64819290f6982481eaffa2",
|
secret: "a5d2a36fdf64819290f6982481eaffa2",
|
||||||
suffixesToRemove: [' - Topic', 'VEVO'] // removes suffixes of the artist name, for better recognition
|
|
||||||
},
|
},
|
||||||
discord: {
|
discord: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -64,7 +63,19 @@ const defaultConfig = {
|
|||||||
volumeDown: "Shift+PageDown"
|
volumeDown: "Shift+PageDown"
|
||||||
},
|
},
|
||||||
savedVolume: undefined //plugin save volume between session here
|
savedVolume: undefined //plugin save volume between session here
|
||||||
}
|
},
|
||||||
|
sponsorblock: {
|
||||||
|
enabled: false,
|
||||||
|
apiURL: "https://sponsor.ajay.app",
|
||||||
|
categories: [
|
||||||
|
"sponsor",
|
||||||
|
"intro",
|
||||||
|
"outro",
|
||||||
|
"interaction",
|
||||||
|
"selfpromo",
|
||||||
|
"music_offtopic",
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
72
index.js
72
index.js
@ -2,6 +2,7 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const electron = require("electron");
|
const electron = require("electron");
|
||||||
|
const enhanceWebRequest = require("electron-better-web-request").default;
|
||||||
const is = require("electron-is");
|
const is = require("electron-is");
|
||||||
const unhandled = require("electron-unhandled");
|
const unhandled = require("electron-unhandled");
|
||||||
const { autoUpdater } = require("electron-updater");
|
const { autoUpdater } = require("electron-updater");
|
||||||
@ -11,6 +12,7 @@ const { setApplicationMenu } = require("./menu");
|
|||||||
const { fileExists, injectCSS } = require("./plugins/utils");
|
const { fileExists, injectCSS } = require("./plugins/utils");
|
||||||
const { isTesting } = require("./utils/testing");
|
const { isTesting } = require("./utils/testing");
|
||||||
const { setUpTray } = require("./tray");
|
const { setUpTray } = require("./tray");
|
||||||
|
const { setupSongInfo } = require("./providers/song-info");
|
||||||
|
|
||||||
// Catch errors and log them
|
// Catch errors and log them
|
||||||
unhandled({
|
unhandled({
|
||||||
@ -153,10 +155,13 @@ function createMainWindow() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
removeContentSecurityPolicy();
|
||||||
|
|
||||||
return win;
|
return win;
|
||||||
}
|
}
|
||||||
|
|
||||||
app.once("browser-window-created", (event, win) => {
|
app.once("browser-window-created", (event, win) => {
|
||||||
|
setupSongInfo(win);
|
||||||
loadPlugins(win);
|
loadPlugins(win);
|
||||||
|
|
||||||
win.webContents.on("did-fail-load", (
|
win.webContents.on("did-fail-load", (
|
||||||
@ -255,6 +260,35 @@ app.on("ready", () => {
|
|||||||
}, 20000);
|
}, 20000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register appID on windows
|
||||||
|
if (is.windows()) {
|
||||||
|
const appID = "com.github.th-ch.youtube-music";
|
||||||
|
app.setAppUserModelId(appID);
|
||||||
|
const appLocation = process.execPath;
|
||||||
|
const appData = app.getPath("appData");
|
||||||
|
// check shortcut validity if not in dev mode / running portable app
|
||||||
|
if (!is.dev() && !appLocation.startsWith(path.join(appData, "..", "Local", "Temp"))) {
|
||||||
|
const shortcutPath = path.join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "YouTube Music.lnk");
|
||||||
|
try { // check if shortcut is registered and valid
|
||||||
|
const shortcutDetails = electron.shell.readShortcutLink(shortcutPath); // throw error if doesn't exist yet
|
||||||
|
if (shortcutDetails.target !== appLocation || shortcutDetails.appUserModelId !== appID) {
|
||||||
|
throw "needUpdate";
|
||||||
|
}
|
||||||
|
} catch (error) { // if not valid -> Register shortcut
|
||||||
|
electron.shell.writeShortcutLink(
|
||||||
|
shortcutPath,
|
||||||
|
error === "needUpdate" ? "update" : "create",
|
||||||
|
{
|
||||||
|
target: appLocation,
|
||||||
|
cwd: appLocation.slice(0, appLocation.lastIndexOf(path.sep)),
|
||||||
|
description: "YouTube Music Desktop App - including custom plugins",
|
||||||
|
appUserModelId: appID
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mainWindow = createMainWindow();
|
mainWindow = createMainWindow();
|
||||||
setApplicationMenu(mainWindow);
|
setApplicationMenu(mainWindow);
|
||||||
if (config.get("options.restartOnConfigChanges")) {
|
if (config.get("options.restartOnConfigChanges")) {
|
||||||
@ -346,3 +380,41 @@ function showUnresponsiveDialog(win, details) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeContentSecurityPolicy(
|
||||||
|
session = electron.session.defaultSession
|
||||||
|
) {
|
||||||
|
// Allows defining multiple "onHeadersReceived" listeners
|
||||||
|
// by enhancing the session.
|
||||||
|
// Some plugins (e.g. adblocker) also define a "onHeadersReceived" listener
|
||||||
|
enhanceWebRequest(session);
|
||||||
|
|
||||||
|
// Custom listener to tweak the content security policy
|
||||||
|
session.webRequest.onHeadersReceived(function (details, callback) {
|
||||||
|
if (
|
||||||
|
!details.responseHeaders["content-security-policy-report-only"] &&
|
||||||
|
!details.responseHeaders["content-security-policy"]
|
||||||
|
)
|
||||||
|
return callback({ cancel: false });
|
||||||
|
delete details.responseHeaders["content-security-policy-report-only"];
|
||||||
|
delete details.responseHeaders["content-security-policy"];
|
||||||
|
callback({ cancel: false, responseHeaders: details.responseHeaders });
|
||||||
|
});
|
||||||
|
|
||||||
|
// When multiple listeners are defined, apply them all
|
||||||
|
session.webRequest.setResolver("onHeadersReceived", (listeners) => {
|
||||||
|
const response = listeners.reduce(
|
||||||
|
async (accumulator, listener) => {
|
||||||
|
if (accumulator.cancel) {
|
||||||
|
return accumulator;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await listener.apply();
|
||||||
|
return { ...accumulator, ...result };
|
||||||
|
},
|
||||||
|
{ cancel: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -2,6 +2,5 @@ module.exports = {
|
|||||||
globals: {
|
globals: {
|
||||||
__APP__: undefined, // A different app will be launched in each test environment
|
__APP__: undefined, // A different app will be launched in each test environment
|
||||||
},
|
},
|
||||||
testEnvironment: "./tests/environment",
|
|
||||||
testTimeout: 30000, // 30s
|
testTimeout: 30000, // 30s
|
||||||
};
|
};
|
||||||
|
|||||||
445
menu.js
445
menu.js
@ -7,12 +7,13 @@ const is = require("electron-is");
|
|||||||
const { getAllPlugins } = require("./plugins/utils");
|
const { getAllPlugins } = require("./plugins/utils");
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
|
|
||||||
const pluginEnabledMenu = (win, plugin, label = "", hasSubmenu = false) => ({
|
// true only if in-app-menu was loaded on launch
|
||||||
|
const inAppMenuActive = config.plugins.isEnabled("in-app-menu");
|
||||||
|
|
||||||
|
const pluginEnabledMenu = (plugin, label = "", hasSubmenu = false, refreshMenu = undefined) => ({
|
||||||
label: label || plugin,
|
label: label || plugin,
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
checked: config.plugins.isEnabled(plugin),
|
checked: config.plugins.isEnabled(plugin),
|
||||||
//Submenu check used in in-app-menu
|
|
||||||
hasSubmenu: hasSubmenu || undefined,
|
|
||||||
click: (item) => {
|
click: (item) => {
|
||||||
if (item.checked) {
|
if (item.checked) {
|
||||||
config.plugins.enable(plugin);
|
config.plugins.enable(plugin);
|
||||||
@ -20,252 +21,226 @@ const pluginEnabledMenu = (win, plugin, label = "", hasSubmenu = false) => ({
|
|||||||
config.plugins.disable(plugin);
|
config.plugins.disable(plugin);
|
||||||
}
|
}
|
||||||
if (hasSubmenu) {
|
if (hasSubmenu) {
|
||||||
this.setApplicationMenu(win);
|
refreshMenu();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const mainMenuTemplate = (win) => [
|
const mainMenuTemplate = (win) => {
|
||||||
{
|
const refreshMenu = () => {
|
||||||
label: "Plugins",
|
this.setApplicationMenu(win);
|
||||||
submenu: [
|
if (inAppMenuActive) {
|
||||||
...getAllPlugins().map((plugin) => {
|
win.webContents.send("updateMenu", true);
|
||||||
const pluginPath = path.join(__dirname, "plugins", plugin, "menu.js")
|
}
|
||||||
if (existsSync(pluginPath)) {
|
}
|
||||||
if (!config.plugins.isEnabled(plugin)) {
|
return [
|
||||||
return pluginEnabledMenu(win, plugin, "", true);
|
{
|
||||||
|
label: "Plugins",
|
||||||
|
submenu: [
|
||||||
|
...getAllPlugins().map((plugin) => {
|
||||||
|
const pluginPath = path.join(__dirname, "plugins", plugin, "menu.js")
|
||||||
|
if (existsSync(pluginPath)) {
|
||||||
|
if (!config.plugins.isEnabled(plugin)) {
|
||||||
|
return pluginEnabledMenu(plugin, "", true, refreshMenu);
|
||||||
|
}
|
||||||
|
const getPluginMenu = require(pluginPath);
|
||||||
|
return {
|
||||||
|
label: plugin,
|
||||||
|
submenu: [
|
||||||
|
pluginEnabledMenu(plugin, "Enabled", true, refreshMenu),
|
||||||
|
...getPluginMenu(win, config.plugins.getOptions(plugin), refreshMenu),
|
||||||
|
],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const getPluginMenu = require(pluginPath);
|
|
||||||
return {
|
|
||||||
label: plugin,
|
|
||||||
submenu: [
|
|
||||||
pluginEnabledMenu(win, plugin, "Enabled", true),
|
|
||||||
...getPluginMenu(win, config.plugins.getOptions(plugin), () =>
|
|
||||||
module.exports.setApplicationMenu(win)
|
|
||||||
),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return pluginEnabledMenu(win, plugin);
|
return pluginEnabledMenu(plugin);
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Options",
|
label: "Options",
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: "Auto-update",
|
label: "Auto-update",
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
checked: config.get("options.autoUpdates"),
|
checked: config.get("options.autoUpdates"),
|
||||||
click: (item) => {
|
click: (item) => {
|
||||||
config.set("options.autoUpdates", item.checked);
|
config.set("options.autoUpdates", item.checked);
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Resume last song when app starts",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: config.get("options.resumeOnStart"),
|
|
||||||
click: (item) => {
|
|
||||||
config.set("options.resumeOnStart", item.checked);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...(is.windows() || is.linux()
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: "Hide menu",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: config.get("options.hideMenu"),
|
|
||||||
click: (item) => {
|
|
||||||
config.set("options.hideMenu", item.checked);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...(is.windows() || is.macOS()
|
|
||||||
? // Only works on Win/Mac
|
|
||||||
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
|
|
||||||
[
|
|
||||||
{
|
|
||||||
label: "Start at login",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: config.get("options.startAtLogin"),
|
|
||||||
click: (item) => {
|
|
||||||
config.set("options.startAtLogin", item.checked);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
label: "Tray",
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: "Disabled",
|
|
||||||
type: "radio",
|
|
||||||
checked: !config.get("options.tray"),
|
|
||||||
click: () => {
|
|
||||||
config.set("options.tray", false);
|
|
||||||
config.set("options.appVisible", true);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Enabled + app visible",
|
|
||||||
type: "radio",
|
|
||||||
checked:
|
|
||||||
config.get("options.tray") && config.get("options.appVisible"),
|
|
||||||
click: () => {
|
|
||||||
config.set("options.tray", true);
|
|
||||||
config.set("options.appVisible", true);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Enabled + app hidden",
|
|
||||||
type: "radio",
|
|
||||||
checked:
|
|
||||||
config.get("options.tray") && !config.get("options.appVisible"),
|
|
||||||
click: () => {
|
|
||||||
config.set("options.tray", true);
|
|
||||||
config.set("options.appVisible", false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ type: "separator" },
|
|
||||||
{
|
|
||||||
label: "Play/Pause on click",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: config.get("options.trayClickPlayPause"),
|
|
||||||
click: (item) => {
|
|
||||||
config.set("options.trayClickPlayPause", item.checked);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ type: "separator" },
|
|
||||||
{
|
|
||||||
label: "Advanced options",
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: "Disable hardware acceleration",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: config.get("options.disableHardwareAcceleration"),
|
|
||||||
click: (item) => {
|
|
||||||
config.set("options.disableHardwareAcceleration", item.checked);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Restart on config changes",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: config.get("options.restartOnConfigChanges"),
|
|
||||||
click: (item) => {
|
|
||||||
config.set("options.restartOnConfigChanges", item.checked);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Reset App cache when app starts",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: config.get("options.autoResetAppCache"),
|
|
||||||
click: (item) => {
|
|
||||||
config.set("options.autoResetAppCache", item.checked);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ type: "separator" },
|
|
||||||
{
|
|
||||||
label: "Toggle DevTools",
|
|
||||||
// Cannot use "toggleDevTools" role in MacOS
|
|
||||||
click: () => {
|
|
||||||
const { webContents } = win;
|
|
||||||
if (webContents.isDevToolsOpened()) {
|
|
||||||
webContents.closeDevTools();
|
|
||||||
} else {
|
|
||||||
const devToolsOptions = {};
|
|
||||||
webContents.openDevTools(devToolsOptions);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Edit config.json",
|
|
||||||
click: () => {
|
|
||||||
config.edit();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "View",
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: "Reload",
|
|
||||||
click: () => {
|
|
||||||
win.webContents.reload();
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
label: "Resume last song when app starts",
|
||||||
label: "Force Reload",
|
type: "checkbox",
|
||||||
click: () => {
|
checked: config.get("options.resumeOnStart"),
|
||||||
win.webContents.reloadIgnoringCache();
|
click: (item) => {
|
||||||
|
config.set("options.resumeOnStart", item.checked);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
...(is.windows() || is.linux()
|
||||||
{ type: "separator" },
|
? [
|
||||||
{
|
{
|
||||||
label: "Zoom In",
|
label: "Hide menu",
|
||||||
click: () => {
|
type: "checkbox",
|
||||||
win.webContents.setZoomLevel(
|
checked: config.get("options.hideMenu"),
|
||||||
win.webContents.getZoomLevel() + 1
|
click: (item) => {
|
||||||
);
|
config.set("options.hideMenu", item.checked);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(is.windows() || is.macOS()
|
||||||
|
? // Only works on Win/Mac
|
||||||
|
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: "Start at login",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: config.get("options.startAtLogin"),
|
||||||
|
click: (item) => {
|
||||||
|
config.set("options.startAtLogin", item.checked);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
label: "Tray",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Disabled",
|
||||||
|
type: "radio",
|
||||||
|
checked: !config.get("options.tray"),
|
||||||
|
click: () => {
|
||||||
|
config.set("options.tray", false);
|
||||||
|
config.set("options.appVisible", true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Enabled + app visible",
|
||||||
|
type: "radio",
|
||||||
|
checked:
|
||||||
|
config.get("options.tray") && config.get("options.appVisible"),
|
||||||
|
click: () => {
|
||||||
|
config.set("options.tray", true);
|
||||||
|
config.set("options.appVisible", true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Enabled + app hidden",
|
||||||
|
type: "radio",
|
||||||
|
checked:
|
||||||
|
config.get("options.tray") && !config.get("options.appVisible"),
|
||||||
|
click: () => {
|
||||||
|
config.set("options.tray", true);
|
||||||
|
config.set("options.appVisible", false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: "separator" },
|
||||||
|
{
|
||||||
|
label: "Play/Pause on click",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: config.get("options.trayClickPlayPause"),
|
||||||
|
click: (item) => {
|
||||||
|
config.set("options.trayClickPlayPause", item.checked);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
{ type: "separator" },
|
||||||
{
|
{
|
||||||
label: "Zoom Out",
|
label: "Advanced options",
|
||||||
click: () => {
|
submenu: [
|
||||||
win.webContents.setZoomLevel(
|
{
|
||||||
win.webContents.getZoomLevel() - 1
|
label: "Disable hardware acceleration",
|
||||||
);
|
type: "checkbox",
|
||||||
|
checked: config.get("options.disableHardwareAcceleration"),
|
||||||
|
click: (item) => {
|
||||||
|
config.set("options.disableHardwareAcceleration", item.checked);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Restart on config changes",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: config.get("options.restartOnConfigChanges"),
|
||||||
|
click: (item) => {
|
||||||
|
config.set("options.restartOnConfigChanges", item.checked);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reset App cache when app starts",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: config.get("options.autoResetAppCache"),
|
||||||
|
click: (item) => {
|
||||||
|
config.set("options.autoResetAppCache", item.checked);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: "separator" },
|
||||||
|
is.macOS() ?
|
||||||
|
{
|
||||||
|
label: "Toggle DevTools",
|
||||||
|
// Cannot use "toggleDevTools" role in MacOS
|
||||||
|
click: () => {
|
||||||
|
const { webContents } = win;
|
||||||
|
if (webContents.isDevToolsOpened()) {
|
||||||
|
webContents.closeDevTools();
|
||||||
|
} else {
|
||||||
|
const devToolsOptions = {};
|
||||||
|
webContents.openDevTools(devToolsOptions);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} :
|
||||||
|
{ role: "toggleDevTools" },
|
||||||
|
{
|
||||||
|
label: "Edit config.json",
|
||||||
|
click: () => {
|
||||||
|
config.edit();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
{
|
},
|
||||||
label: "Reset Zoom",
|
{
|
||||||
click: () => {
|
label: "View",
|
||||||
win.webContents.setZoomLevel(0);
|
submenu: [
|
||||||
|
{ role: "reload" },
|
||||||
|
{ role: "forceReload" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{ role: "zoomIn" },
|
||||||
|
{ role: "zoomOut" },
|
||||||
|
{ role: "resetZoom" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Navigation",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Go back",
|
||||||
|
click: () => {
|
||||||
|
if (win.webContents.canGoBack()) {
|
||||||
|
win.webContents.goBack();
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
],
|
label: "Go forward",
|
||||||
},
|
click: () => {
|
||||||
{
|
if (win.webContents.canGoForward()) {
|
||||||
label: "Navigation",
|
win.webContents.goForward();
|
||||||
submenu: [
|
}
|
||||||
{
|
},
|
||||||
label: "Go back",
|
|
||||||
click: () => {
|
|
||||||
if (win.webContents.canGoBack()) {
|
|
||||||
win.webContents.goBack();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
label: "Restart App",
|
||||||
label: "Go forward",
|
click: () => {
|
||||||
click: () => {
|
app.relaunch();
|
||||||
if (win.webContents.canGoForward()) {
|
app.quit();
|
||||||
win.webContents.goForward();
|
},
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
{ role: "quit" },
|
||||||
{
|
],
|
||||||
label: "Restart App",
|
},
|
||||||
click: () => {
|
];
|
||||||
app.relaunch();
|
}
|
||||||
app.quit();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Quit App",
|
|
||||||
click: () => {
|
|
||||||
app.quit();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
module.exports.mainMenuTemplate = mainMenuTemplate;
|
module.exports.mainMenuTemplate = mainMenuTemplate;
|
||||||
module.exports.setApplicationMenu = (win) => {
|
module.exports.setApplicationMenu = (win) => {
|
||||||
|
|||||||
33
package.json
33
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "youtube-music",
|
"name": "youtube-music",
|
||||||
"productName": "YouTube Music",
|
"productName": "YouTube Music",
|
||||||
"version": "1.12.0",
|
"version": "1.12.2",
|
||||||
"description": "YouTube Music Desktop App - including custom plugins",
|
"description": "YouTube Music Desktop App - including custom plugins",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "th-ch/youtube-music",
|
"repository": "th-ch/youtube-music",
|
||||||
@ -42,6 +42,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
|
"start:debug": "ELECTRON_ENABLE_LOGGING=1 electron .",
|
||||||
"icon": "rimraf assets/generated && electron-icon-maker --input=assets/youtube-music.png --output=assets/generated",
|
"icon": "rimraf assets/generated && electron-icon-maker --input=assets/youtube-music.png --output=assets/generated",
|
||||||
"generate:package": "node utils/generate-package-json.js",
|
"generate:package": "node utils/generate-package-json.js",
|
||||||
"postinstall": "yarn run icon && yarn run plugins",
|
"postinstall": "yarn run icon && yarn run plugins",
|
||||||
@ -50,6 +51,7 @@
|
|||||||
"build:linux": "yarn run clean && electron-builder --linux",
|
"build:linux": "yarn run clean && electron-builder --linux",
|
||||||
"build:mac": "yarn run clean && electron-builder --mac",
|
"build:mac": "yarn run clean && electron-builder --mac",
|
||||||
"build:win": "yarn run clean && electron-builder --win",
|
"build:win": "yarn run clean && electron-builder --win",
|
||||||
|
"lint": "xo",
|
||||||
"plugins": "yarn run plugin:adblocker && yarn run plugin:autoconfirm",
|
"plugins": "yarn run plugin:adblocker && yarn run plugin:autoconfirm",
|
||||||
"plugin:adblocker": "rimraf plugins/adblocker/ad-blocker-engine.bin && node plugins/adblocker/blocker.js",
|
"plugin:adblocker": "rimraf plugins/adblocker/ad-blocker-engine.bin && node plugins/adblocker/blocker.js",
|
||||||
"plugin:autoconfirm": "yarn run generate:package YoutubeNonStop",
|
"plugin:autoconfirm": "yarn run generate:package YoutubeNonStop",
|
||||||
@ -58,44 +60,47 @@
|
|||||||
"release:win": "yarn run clean && electron-builder --win -p always"
|
"release:win": "yarn run clean && electron-builder --win -p always"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.16.1",
|
"node": ">=12.20",
|
||||||
"npm": "Please use yarn and not npm"
|
"npm": "Please use yarn and not npm"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cliqz/adblocker-electron": "^1.20.4",
|
"@cliqz/adblocker-electron": "^1.22.1",
|
||||||
"@ffmpeg/core": "^0.8.5",
|
"@ffmpeg/core": "^0.10.0",
|
||||||
"@ffmpeg/ffmpeg": "^0.9.7",
|
"@ffmpeg/ffmpeg": "^0.10.0",
|
||||||
"YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.9.0",
|
"YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.9.0",
|
||||||
"async-mutex": "^0.3.1",
|
"async-mutex": "^0.3.1",
|
||||||
"browser-id3-writer": "^4.4.0",
|
"browser-id3-writer": "^4.4.0",
|
||||||
"custom-electron-titlebar": "^3.2.6",
|
"chokidar": "^3.5.1",
|
||||||
|
"custom-electron-titlebar": "^3.2.7",
|
||||||
"discord-rpc": "^3.2.0",
|
"discord-rpc": "^3.2.0",
|
||||||
|
"electron-better-web-request": "^1.0.1",
|
||||||
"electron-debug": "^3.2.0",
|
"electron-debug": "^3.2.0",
|
||||||
"electron-is": "^3.0.0",
|
"electron-is": "^3.0.0",
|
||||||
"electron-localshortcut": "^3.2.1",
|
"electron-localshortcut": "^3.2.1",
|
||||||
"electron-store": "^7.0.3",
|
"electron-store": "^7.0.3",
|
||||||
"electron-unhandled": "^3.0.2",
|
"electron-unhandled": "^3.0.2",
|
||||||
"electron-updater": "^4.3.8",
|
"electron-updater": "^4.3.9",
|
||||||
"filenamify": "^4.2.0",
|
"filenamify": "^4.3.0",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"node-notifier": "^9.0.1",
|
"node-notifier": "^9.0.1",
|
||||||
"open": "^8.0.3",
|
"ytdl-core": "^4.8.3",
|
||||||
"ytdl-core": "^4.5.0",
|
"ytpl": "^2.2.1"
|
||||||
"ytpl": "^2.1.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^11.4.4",
|
"electron": "^12.0.8",
|
||||||
"electron-builder": "^22.10.5",
|
"electron-builder": "^22.10.5",
|
||||||
"electron-devtools-installer": "^3.1.1",
|
"electron-devtools-installer": "^3.1.1",
|
||||||
"electron-icon-maker": "0.0.5",
|
"electron-icon-maker": "0.0.5",
|
||||||
"get-port": "^5.1.1",
|
"get-port": "^5.1.1",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"spectron": "^13.0.0",
|
"spectron": "^14.0.0",
|
||||||
"xo": "^0.38.2"
|
"xo": "^0.40.3"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
"glob-parent": "5.1.2",
|
||||||
|
"minimist": "1.2.5",
|
||||||
"yargs-parser": "18.1.3"
|
"yargs-parser": "18.1.3"
|
||||||
},
|
},
|
||||||
"xo": {
|
"xo": {
|
||||||
|
|||||||
@ -8,7 +8,9 @@ const SOURCES = [
|
|||||||
"https://raw.githubusercontent.com/kbinani/adblock-youtube-ads/master/signed.txt",
|
"https://raw.githubusercontent.com/kbinani/adblock-youtube-ads/master/signed.txt",
|
||||||
// uBlock Origin
|
// uBlock Origin
|
||||||
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt",
|
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt",
|
||||||
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-2020.txt",
|
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-2021.txt",
|
||||||
|
// Fanboy Annoyances
|
||||||
|
"https://secure.fanboy.co.nz/fanboy-annoyance_ubo.txt",
|
||||||
];
|
];
|
||||||
|
|
||||||
const loadAdBlockerEngine = (
|
const loadAdBlockerEngine = (
|
||||||
@ -31,7 +33,17 @@ const loadAdBlockerEngine = (
|
|||||||
...additionalBlockLists,
|
...additionalBlockLists,
|
||||||
];
|
];
|
||||||
|
|
||||||
ElectronBlocker.fromLists(fetch, lists, {}, cachingOptions)
|
ElectronBlocker.fromLists(
|
||||||
|
fetch,
|
||||||
|
lists,
|
||||||
|
{
|
||||||
|
// when generating the engine for caching, do not load network filters
|
||||||
|
// So that enhancing the session works as expected
|
||||||
|
// Allowing to define multiple webRequest listeners
|
||||||
|
loadNetworkFilters: session !== undefined,
|
||||||
|
},
|
||||||
|
cachingOptions
|
||||||
|
)
|
||||||
.then((blocker) => {
|
.then((blocker) => {
|
||||||
if (session) {
|
if (session) {
|
||||||
blocker.enableBlockingInSession(session);
|
blocker.enableBlockingInSession(session);
|
||||||
|
|||||||
@ -1,25 +1,10 @@
|
|||||||
let videoElement = null;
|
const { ontimeupdate } = require("../../providers/video-element");
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations, observer) => {
|
module.exports = () => {
|
||||||
if (!videoElement) {
|
ontimeupdate((videoElement) => {
|
||||||
videoElement = document.querySelector("video");
|
if (videoElement.currentTime === 0 && videoElement.duration !== NaN) {
|
||||||
}
|
// auto-confirm-when-paused plugin can interfere here if not disabled!
|
||||||
|
videoElement.pause();
|
||||||
if (videoElement) {
|
}
|
||||||
videoElement.ontimeupdate = () => {
|
|
||||||
if (videoElement.currentTime === 0 && videoElement.duration !== NaN) {
|
|
||||||
// auto-confirm-when-paused plugin can interfere here if not disabled!
|
|
||||||
videoElement.pause();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function observeVideoElement() {
|
|
||||||
observer.observe(document, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
module.exports = observeVideoElement;
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
const Discord = require("discord-rpc");
|
const Discord = require("discord-rpc");
|
||||||
|
|
||||||
const getSongInfo = require("../../providers/song-info");
|
const registerCallback = require("../../providers/song-info");
|
||||||
|
|
||||||
const rpc = new Discord.Client({
|
const rpc = new Discord.Client({
|
||||||
transport: "ipc",
|
transport: "ipc",
|
||||||
@ -12,8 +12,6 @@ const clientId = "790655993809338398";
|
|||||||
let clearActivity;
|
let clearActivity;
|
||||||
|
|
||||||
module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => {
|
module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => {
|
||||||
const registerCallback = getSongInfo(win);
|
|
||||||
|
|
||||||
// If the page is ready, register the callback
|
// If the page is ready, register the callback
|
||||||
win.once("ready-to-show", () => {
|
win.once("ready-to-show", () => {
|
||||||
rpc.once("ready", () => {
|
rpc.once("ready", () => {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ const CHANNEL = "downloader";
|
|||||||
const ACTIONS = {
|
const ACTIONS = {
|
||||||
ERROR: "error",
|
ERROR: "error",
|
||||||
METADATA: "metadata",
|
METADATA: "metadata",
|
||||||
|
PROGRESS: "progress",
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@ -4,60 +4,73 @@ const { join } = require("path");
|
|||||||
const ID3Writer = require("browser-id3-writer");
|
const ID3Writer = require("browser-id3-writer");
|
||||||
const { dialog, ipcMain } = require("electron");
|
const { dialog, ipcMain } = require("electron");
|
||||||
|
|
||||||
const getSongInfo = require("../../providers/song-info");
|
const registerCallback = require("../../providers/song-info");
|
||||||
const { injectCSS, listenAction } = require("../utils");
|
const { injectCSS, listenAction } = require("../utils");
|
||||||
|
const { cropMaxWidth } = require("./utils");
|
||||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||||
|
const { getImage } = require("../../providers/song-info");
|
||||||
|
|
||||||
const sendError = (win, err) => {
|
const sendError = (win, error) => {
|
||||||
const dialogOpts = {
|
win.setProgressBar(-1); // close progress bar
|
||||||
|
dialog.showMessageBox({
|
||||||
type: "info",
|
type: "info",
|
||||||
buttons: ["OK"],
|
buttons: ["OK"],
|
||||||
title: "Error in download!",
|
title: "Error in download!",
|
||||||
message: "Argh! Apologies, download failed…",
|
message: "Argh! Apologies, download failed…",
|
||||||
detail: err.toString(),
|
detail: error.toString(),
|
||||||
};
|
});
|
||||||
dialog.showMessageBox(dialogOpts);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let metadata = {};
|
let nowPlayingMetadata = {};
|
||||||
|
|
||||||
function handle(win) {
|
function handle(win) {
|
||||||
injectCSS(win.webContents, join(__dirname, "style.css"));
|
injectCSS(win.webContents, join(__dirname, "style.css"));
|
||||||
const registerCallback = getSongInfo(win);
|
|
||||||
registerCallback((info) => {
|
registerCallback((info) => {
|
||||||
metadata = info;
|
nowPlayingMetadata = info;
|
||||||
});
|
});
|
||||||
|
|
||||||
listenAction(CHANNEL, (event, action, error) => {
|
listenAction(CHANNEL, (event, action, arg) => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case ACTIONS.ERROR:
|
case ACTIONS.ERROR: // arg = error
|
||||||
sendError(win, error);
|
sendError(win, arg);
|
||||||
break;
|
break;
|
||||||
case ACTIONS.METADATA:
|
case ACTIONS.METADATA:
|
||||||
event.returnValue = JSON.stringify(metadata);
|
event.returnValue = JSON.stringify(nowPlayingMetadata);
|
||||||
|
break;
|
||||||
|
case ACTIONS.PROGRESS: // arg = progress
|
||||||
|
win.setProgressBar(arg);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log("Unknown action: " + action);
|
console.log("Unknown action: " + action);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on("add-metadata", (event, filePath, songBuffer, currentMetadata) => {
|
ipcMain.on("add-metadata", async (event, filePath, songBuffer, currentMetadata) => {
|
||||||
let fileBuffer = songBuffer;
|
let fileBuffer = songBuffer;
|
||||||
const songMetadata = { ...metadata, ...currentMetadata };
|
const songMetadata = currentMetadata.imageSrcYTPL ? // This means metadata come from ytpl.getInfo();
|
||||||
|
{
|
||||||
|
...currentMetadata,
|
||||||
|
image: cropMaxWidth(await getImage(currentMetadata.imageSrcYTPL))
|
||||||
|
} :
|
||||||
|
{ ...nowPlayingMetadata, ...currentMetadata };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const coverBuffer = songMetadata.image.toPNG();
|
const coverBuffer = songMetadata.image && !songMetadata.image.isEmpty() ?
|
||||||
|
songMetadata.image.toPNG() : null;
|
||||||
|
|
||||||
const writer = new ID3Writer(songBuffer);
|
const writer = new ID3Writer(songBuffer);
|
||||||
|
|
||||||
// Create the metadata tags
|
// Create the metadata tags
|
||||||
writer
|
writer
|
||||||
.setFrame("TIT2", songMetadata.title)
|
.setFrame("TIT2", songMetadata.title)
|
||||||
.setFrame("TPE1", [songMetadata.artist])
|
.setFrame("TPE1", [songMetadata.artist]);
|
||||||
.setFrame("APIC", {
|
if (coverBuffer) {
|
||||||
|
writer.setFrame("APIC", {
|
||||||
type: 3,
|
type: 3,
|
||||||
data: coverBuffer,
|
data: coverBuffer,
|
||||||
description: "",
|
description: ""
|
||||||
});
|
});
|
||||||
|
}
|
||||||
writer.addTag();
|
writer.addTag();
|
||||||
fileBuffer = Buffer.from(writer.arrayBuffer);
|
fileBuffer = Buffer.from(writer.arrayBuffer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -25,6 +25,7 @@ const observer = new MutationObserver((mutations, observer) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const reinit = () => {
|
const reinit = () => {
|
||||||
|
triggerAction(CHANNEL, ACTIONS.PROGRESS, -1); // closes progress bar
|
||||||
if (!progress) {
|
if (!progress) {
|
||||||
console.warn("Cannot update progress");
|
console.warn("Cannot update progress");
|
||||||
} else {
|
} else {
|
||||||
@ -38,22 +39,31 @@ const baseUrl = defaultConfig.url;
|
|||||||
// contextBridge.exposeInMainWorld("downloader", {
|
// contextBridge.exposeInMainWorld("downloader", {
|
||||||
// download: () => {
|
// download: () => {
|
||||||
global.download = () => {
|
global.download = () => {
|
||||||
|
triggerAction(CHANNEL, ACTIONS.PROGRESS, 2); // starts with indefinite progress bar
|
||||||
|
let metadata;
|
||||||
let videoUrl = getSongMenu()
|
let videoUrl = getSongMenu()
|
||||||
.querySelector("ytmusic-menu-navigation-item-renderer")
|
// selector of first button which is always "Start Radio"
|
||||||
.querySelector("#navigation-endpoint")
|
?.querySelector('ytmusic-menu-navigation-item-renderer.iron-selected[tabindex="0"] #navigation-endpoint')
|
||||||
.getAttribute("href");
|
?.getAttribute("href");
|
||||||
videoUrl = !videoUrl
|
if (videoUrl) {
|
||||||
? global.songInfo.url || window.location.href
|
videoUrl = baseUrl + "/" + videoUrl;
|
||||||
: baseUrl + "/" + videoUrl;
|
metadata = null;
|
||||||
|
} else {
|
||||||
|
metadata = global.songInfo;
|
||||||
|
videoUrl = metadata.url || window.location.href;
|
||||||
|
}
|
||||||
|
|
||||||
downloadVideoToMP3(
|
downloadVideoToMP3(
|
||||||
videoUrl,
|
videoUrl,
|
||||||
(feedback) => {
|
(feedback, ratio = undefined) => {
|
||||||
if (!progress) {
|
if (!progress) {
|
||||||
console.warn("Cannot update progress");
|
console.warn("Cannot update progress");
|
||||||
} else {
|
} else {
|
||||||
progress.innerHTML = feedback;
|
progress.innerHTML = feedback;
|
||||||
}
|
}
|
||||||
|
if (ratio) {
|
||||||
|
triggerAction(CHANNEL, ACTIONS.PROGRESS, ratio);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
triggerAction(CHANNEL, ACTIONS.ERROR, error);
|
triggerAction(CHANNEL, ACTIONS.ERROR, error);
|
||||||
@ -61,7 +71,7 @@ global.download = () => {
|
|||||||
},
|
},
|
||||||
reinit,
|
reinit,
|
||||||
pluginOptions,
|
pluginOptions,
|
||||||
global.songInfo
|
metadata
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
// });
|
// });
|
||||||
|
|||||||
@ -2,12 +2,13 @@ const { existsSync, mkdirSync } = require("fs");
|
|||||||
const { join } = require("path");
|
const { join } = require("path");
|
||||||
const { URL } = require("url");
|
const { URL } = require("url");
|
||||||
|
|
||||||
const { dialog, ipcMain } = require("electron");
|
const { dialog } = require("electron");
|
||||||
const is = require("electron-is");
|
const is = require("electron-is");
|
||||||
const ytpl = require("ytpl");
|
const ytpl = require("ytpl");
|
||||||
|
const chokidar = require('chokidar');
|
||||||
|
|
||||||
const { setOptions } = require("../../config/plugins");
|
const { setOptions } = require("../../config/plugins");
|
||||||
const getSongInfo = require("../../providers/song-info");
|
const registerCallback = require("../../providers/song-info");
|
||||||
const { sendError } = require("./back");
|
const { sendError } = require("./back");
|
||||||
const { defaultMenuDownloadLabel, getFolder } = require("./utils");
|
const { defaultMenuDownloadLabel, getFolder } = require("./utils");
|
||||||
|
|
||||||
@ -15,9 +16,8 @@ let downloadLabel = defaultMenuDownloadLabel;
|
|||||||
let metadataURL = undefined;
|
let metadataURL = undefined;
|
||||||
let callbackIsRegistered = false;
|
let callbackIsRegistered = false;
|
||||||
|
|
||||||
module.exports = (win, options, refreshMenu) => {
|
module.exports = (win, options) => {
|
||||||
if (!callbackIsRegistered) {
|
if (!callbackIsRegistered) {
|
||||||
const registerCallback = getSongInfo(win);
|
|
||||||
registerCallback((info) => {
|
registerCallback((info) => {
|
||||||
metadataURL = info.url;
|
metadataURL = info.url;
|
||||||
});
|
});
|
||||||
@ -35,7 +35,16 @@ module.exports = (win, options, refreshMenu) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playlist = await ytpl(playlistID);
|
console.log(`trying to get playlist ID: '${playlistID}'`);
|
||||||
|
let playlist;
|
||||||
|
try {
|
||||||
|
playlist = await ytpl(playlistID, {
|
||||||
|
limit: options.playlistMaxItems || Infinity,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
sendError(win, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const playlistTitle = playlist.title;
|
const playlistTitle = playlist.title;
|
||||||
|
|
||||||
const folder = getFolder(options.downloadFolder);
|
const folder = getFolder(options.downloadFolder);
|
||||||
@ -49,24 +58,40 @@ module.exports = (win, options, refreshMenu) => {
|
|||||||
}
|
}
|
||||||
mkdirSync(playlistFolder, { recursive: true });
|
mkdirSync(playlistFolder, { recursive: true });
|
||||||
|
|
||||||
ipcMain.on("downloader-feedback", (_, feedback) => {
|
dialog.showMessageBox({
|
||||||
downloadLabel = feedback;
|
type: "info",
|
||||||
refreshMenu();
|
buttons: ["OK"],
|
||||||
|
title: "Started Download",
|
||||||
|
message: `Downloading Playlist "${playlistTitle}"`,
|
||||||
|
detail: `(${playlist.items.length} songs)`,
|
||||||
});
|
});
|
||||||
|
|
||||||
downloadLabel = `Downloading "${playlistTitle}"`;
|
|
||||||
refreshMenu();
|
|
||||||
|
|
||||||
if (is.dev()) {
|
if (is.dev()) {
|
||||||
console.log(
|
console.log(
|
||||||
`Downloading playlist "${playlistTitle}" (${playlist.items.length} songs)`
|
`Downloading playlist "${playlistTitle}" (${playlist.items.length} songs)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
playlist.items.slice(0, options.playlistMaxItems).forEach((song) => {
|
const steps = 1 / playlist.items.length;
|
||||||
|
let progress = 0;
|
||||||
|
|
||||||
|
win.setProgressBar(2); // starts with indefinite bar
|
||||||
|
|
||||||
|
let dirWatcher = chokidar.watch(playlistFolder);
|
||||||
|
dirWatcher.on('add', () => {
|
||||||
|
progress += steps;
|
||||||
|
if (progress >= 0.9999) {
|
||||||
|
win.setProgressBar(-1); // close progress bar
|
||||||
|
dirWatcher.close().then(() => dirWatcher = null);
|
||||||
|
} else {
|
||||||
|
win.setProgressBar(progress);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
playlist.items.forEach((song) => {
|
||||||
win.webContents.send(
|
win.webContents.send(
|
||||||
"downloader-download-playlist",
|
"downloader-download-playlist",
|
||||||
song,
|
song.url,
|
||||||
playlistTitle,
|
playlistTitle,
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,3 +3,29 @@ const electron = require("electron");
|
|||||||
module.exports.getFolder = (customFolder) =>
|
module.exports.getFolder = (customFolder) =>
|
||||||
customFolder || (electron.app || electron.remote.app).getPath("downloads");
|
customFolder || (electron.app || electron.remote.app).getPath("downloads");
|
||||||
module.exports.defaultMenuDownloadLabel = "Download playlist";
|
module.exports.defaultMenuDownloadLabel = "Download playlist";
|
||||||
|
|
||||||
|
const orderedQualityList = ["maxresdefault", "hqdefault", "mqdefault", "sdddefault"];
|
||||||
|
module.exports.urlToJPG = (imgUrl, videoId) => {
|
||||||
|
if (!imgUrl || imgUrl.includes(".jpg")) return imgUrl;
|
||||||
|
//it will almost never get further than hqdefault
|
||||||
|
for (const quality of orderedQualityList) {
|
||||||
|
if (imgUrl.includes(quality)) {
|
||||||
|
return `https://img.youtube.com/vi/${videoId}/${quality}.jpg`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `https://img.youtube.com/vi/${videoId}/default.jpg`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.cropMaxWidth = (image) => {
|
||||||
|
const imageSize = image.getSize();
|
||||||
|
// standart youtube artwork width with margins from both sides is 280 + 720 + 280
|
||||||
|
if (imageSize.width === 1280 && imageSize.height === 720) {
|
||||||
|
return image.crop({
|
||||||
|
x: 280,
|
||||||
|
y: 0,
|
||||||
|
width: 720,
|
||||||
|
height: 720
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|||||||
@ -14,7 +14,8 @@ const ytdl = require("ytdl-core");
|
|||||||
|
|
||||||
const { triggerAction, triggerActionSync } = require("../utils");
|
const { triggerAction, triggerActionSync } = require("../utils");
|
||||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||||
const { defaultMenuDownloadLabel, getFolder } = require("./utils");
|
const { getFolder, urlToJPG } = require("./utils");
|
||||||
|
const { cleanupArtistName } = require("../../providers/song-info");
|
||||||
|
|
||||||
const { createFFmpeg } = FFmpeg;
|
const { createFFmpeg } = FFmpeg;
|
||||||
const ffmpeg = createFFmpeg({
|
const ffmpeg = createFFmpeg({
|
||||||
@ -24,7 +25,7 @@ const ffmpeg = createFFmpeg({
|
|||||||
});
|
});
|
||||||
const ffmpegMutex = new Mutex();
|
const ffmpegMutex = new Mutex();
|
||||||
|
|
||||||
const downloadVideoToMP3 = (
|
const downloadVideoToMP3 = async (
|
||||||
videoUrl,
|
videoUrl,
|
||||||
sendFeedback,
|
sendFeedback,
|
||||||
sendError,
|
sendError,
|
||||||
@ -35,6 +36,18 @@ const downloadVideoToMP3 = (
|
|||||||
) => {
|
) => {
|
||||||
sendFeedback("Downloading…");
|
sendFeedback("Downloading…");
|
||||||
|
|
||||||
|
if (metadata === null) {
|
||||||
|
const { videoDetails } = await ytdl.getInfo(videoUrl);
|
||||||
|
const thumbnails = videoDetails?.thumbnails;
|
||||||
|
metadata = {
|
||||||
|
artist: videoDetails?.media?.artist || cleanupArtistName(videoDetails?.author?.name) || "",
|
||||||
|
title: videoDetails?.media?.song || videoDetails?.title || "",
|
||||||
|
imageSrcYTPL: thumbnails ?
|
||||||
|
urlToJPG(thumbnails[thumbnails.length - 1].url, videoDetails?.videoId)
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let videoName = "YouTube Music - Unknown title";
|
let videoName = "YouTube Music - Unknown title";
|
||||||
let videoReadableStream;
|
let videoReadableStream;
|
||||||
try {
|
try {
|
||||||
@ -54,9 +67,10 @@ const downloadVideoToMP3 = (
|
|||||||
.on("data", (chunk) => {
|
.on("data", (chunk) => {
|
||||||
chunks.push(chunk);
|
chunks.push(chunk);
|
||||||
})
|
})
|
||||||
.on("progress", (chunkLength, downloaded, total) => {
|
.on("progress", (_chunkLength, downloaded, total) => {
|
||||||
const progress = Math.floor((downloaded / total) * 100);
|
const ratio = downloaded / total;
|
||||||
sendFeedback("Download: " + progress + "%");
|
const progress = Math.floor(ratio * 100);
|
||||||
|
sendFeedback("Download: " + progress + "%", ratio);
|
||||||
})
|
})
|
||||||
.on("info", (info, format) => {
|
.on("info", (info, format) => {
|
||||||
videoName = info.videoDetails.title.replace("|", "").toString("ascii");
|
videoName = info.videoDetails.title.replace("|", "").toString("ascii");
|
||||||
@ -101,7 +115,7 @@ const toMP3 = async (
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (!ffmpeg.isLoaded()) {
|
if (!ffmpeg.isLoaded()) {
|
||||||
sendFeedback("Loading…");
|
sendFeedback("Loading…", 2); // indefinite progress bar after download
|
||||||
await ffmpeg.load();
|
await ffmpeg.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,6 +149,7 @@ const toMP3 = async (
|
|||||||
ipcRenderer.send("add-metadata", filePath, fileBuffer, {
|
ipcRenderer.send("add-metadata", filePath, fileBuffer, {
|
||||||
artist: metadata.artist,
|
artist: metadata.artist,
|
||||||
title: metadata.title,
|
title: metadata.title,
|
||||||
|
imageSrcYTPL: metadata.imageSrcYTPL
|
||||||
});
|
});
|
||||||
ipcRenderer.once("add-metadata-done", reinit);
|
ipcRenderer.once("add-metadata-done", reinit);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -165,22 +180,16 @@ module.exports = {
|
|||||||
|
|
||||||
ipcRenderer.on(
|
ipcRenderer.on(
|
||||||
"downloader-download-playlist",
|
"downloader-download-playlist",
|
||||||
(_, songMetadata, playlistFolder, options) => {
|
(_, url, playlistFolder, options) => {
|
||||||
const reinit = () =>
|
|
||||||
ipcRenderer.send("downloader-feedback", defaultMenuDownloadLabel);
|
|
||||||
|
|
||||||
downloadVideoToMP3(
|
downloadVideoToMP3(
|
||||||
songMetadata.url,
|
url,
|
||||||
(feedback) => {
|
() => {},
|
||||||
ipcRenderer.send("downloader-feedback", feedback);
|
|
||||||
},
|
|
||||||
(error) => {
|
(error) => {
|
||||||
triggerAction(CHANNEL, ACTIONS.ERROR, error);
|
triggerAction(CHANNEL, ACTIONS.ERROR, error);
|
||||||
reinit();
|
|
||||||
},
|
},
|
||||||
reinit,
|
() => {},
|
||||||
options,
|
options,
|
||||||
songMetadata,
|
null,
|
||||||
playlistFolder
|
playlistFolder
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,83 +1,33 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const { Menu } = require("electron");
|
|
||||||
const electronLocalshortcut = require("electron-localshortcut");
|
const electronLocalshortcut = require("electron-localshortcut");
|
||||||
|
|
||||||
const config = require("../../config");
|
const config = require("../../config");
|
||||||
const { setApplicationMenu } = require("../../menu");
|
|
||||||
const { injectCSS } = require("../utils");
|
const { injectCSS } = require("../utils");
|
||||||
|
|
||||||
//tracks menu visibility
|
//tracks menu visibility
|
||||||
let visible = true;
|
let visible = true;
|
||||||
// win hook for fixing menu
|
|
||||||
let win;
|
|
||||||
|
|
||||||
const originalBuildMenu = Menu.buildFromTemplate;
|
|
||||||
// This function natively gets called on all submenu so no more reason to use recursion
|
|
||||||
Menu.buildFromTemplate = (template) => {
|
|
||||||
// Fix checkboxes and radio buttons
|
|
||||||
updateTemplate(template);
|
|
||||||
|
|
||||||
// return as normal
|
|
||||||
return originalBuildMenu(template);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = (winImport) => {
|
|
||||||
win = winImport;
|
|
||||||
|
|
||||||
|
module.exports = (win) => {
|
||||||
// css for custom scrollbar + disable drag area(was causing bugs)
|
// css for custom scrollbar + disable drag area(was causing bugs)
|
||||||
injectCSS(win.webContents, path.join(__dirname, "style.css"));
|
injectCSS(win.webContents, path.join(__dirname, "style.css"));
|
||||||
|
|
||||||
win.once("ready-to-show", () => {
|
win.once("ready-to-show", () => {
|
||||||
|
|
||||||
setApplicationMenu(win);
|
|
||||||
|
|
||||||
//register keyboard shortcut && hide menu if hideMenu is enabled
|
//register keyboard shortcut && hide menu if hideMenu is enabled
|
||||||
if (config.get("options.hideMenu")) {
|
if (config.get("options.hideMenu")) {
|
||||||
electronLocalshortcut.register(win, "Esc", () => {
|
electronLocalshortcut.register(win, "Esc", () => {
|
||||||
switchMenuVisibility();
|
setMenuVisibility(!visible);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//set menu visibility on load
|
|
||||||
win.webContents.once("did-finish-load", () => {
|
win.webContents.once("did-finish-load", () => {
|
||||||
// fix bug with menu not applying on start when no internet connection available
|
// fix bug with menu not applying on start when no internet connection available
|
||||||
setMenuVisibility(!config.get("options.hideMenu"));
|
setMenuVisibility(!config.get("options.hideMenu"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function setMenuVisibility(value) {
|
||||||
|
visible = value;
|
||||||
|
win.webContents.send("updateMenu", visible);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function switchMenuVisibility() {
|
|
||||||
setMenuVisibility(!visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMenuVisibility(value) {
|
|
||||||
visible = value;
|
|
||||||
win.webContents.send("updateMenu", visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCheckboxesAndRadioButtons(item, isRadio, hasSubmenu) {
|
|
||||||
if (!isRadio) {
|
|
||||||
//fix checkbox
|
|
||||||
item.checked = !item.checked;
|
|
||||||
}
|
|
||||||
//update menu if radio / hasSubmenu
|
|
||||||
if (isRadio || hasSubmenu) {
|
|
||||||
win.webContents.send("updateMenu", true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update checkboxes/radio buttons
|
|
||||||
function updateTemplate(template) {
|
|
||||||
for (let item of template) {
|
|
||||||
// Change onClick of checkbox+radio
|
|
||||||
if ((item.type === "checkbox" || item.type === "radio") && !item.fixed) {
|
|
||||||
const originalOnclick = item.click;
|
|
||||||
item.click = (itemClicked) => {
|
|
||||||
originalOnclick(itemClicked);
|
|
||||||
updateCheckboxesAndRadioButtons(itemClicked, item.type === "radio", item.hasSubmenu);
|
|
||||||
};
|
|
||||||
item.fixed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -10,15 +10,7 @@ module.exports = () => {
|
|||||||
bar.updateTitle(" ");
|
bar.updateTitle(" ");
|
||||||
document.title = "Youtube Music";
|
document.title = "Youtube Music";
|
||||||
|
|
||||||
ipcRenderer.on("updateMenu", function (event, menu) {
|
ipcRenderer.on("updateMenu", function (_event, showMenu) {
|
||||||
if (menu) {
|
bar.updateMenu(showMenu ? remote.Menu.getApplicationMenu() : null);
|
||||||
bar.updateMenu(remote.Menu.getApplicationMenu());
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
bar.updateMenu(null);
|
|
||||||
} catch (e) {
|
|
||||||
//will always throw type error - null isn't menu, but it works
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,11 +4,6 @@
|
|||||||
font-size: 14px !important;
|
font-size: 14px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* allow submenu's to show correctly */
|
|
||||||
.menubar-menu-container {
|
|
||||||
overflow-y: visible !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* fixes scrollbar positioning relative to nav bar */
|
/* fixes scrollbar positioning relative to nav bar */
|
||||||
#nav-bar-background.ytmusic-app-layout {
|
#nav-bar-background.ytmusic-app-layout {
|
||||||
right: 15px !important;
|
right: 15px !important;
|
||||||
|
|||||||
@ -1,25 +1,14 @@
|
|||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const md5 = require('md5');
|
const md5 = require('md5');
|
||||||
const open = require("open");
|
const { shell } = require('electron');
|
||||||
const { setOptions } = require('../../config/plugins');
|
const { setOptions } = require('../../config/plugins');
|
||||||
const getSongInfo = require('../../providers/song-info');
|
const registerCallback = require('../../providers/song-info');
|
||||||
const defaultConfig = require('../../config/defaults');
|
const defaultConfig = require('../../config/defaults');
|
||||||
|
|
||||||
const cleanupArtistName = (config, artist) => {
|
|
||||||
// removes the suffixes of the artist name for more recognition by last.fm
|
|
||||||
const { suffixesToRemove } = config;
|
|
||||||
if (suffixesToRemove === undefined) return artist;
|
|
||||||
|
|
||||||
for (suffix of suffixesToRemove) {
|
|
||||||
artist = artist.replace(suffix, '');
|
|
||||||
}
|
|
||||||
return artist;
|
|
||||||
}
|
|
||||||
|
|
||||||
const createFormData = params => {
|
const createFormData = params => {
|
||||||
// creates the body for in the post request
|
// creates the body for in the post request
|
||||||
const formData = new URLSearchParams();
|
const formData = new URLSearchParams();
|
||||||
for (key in params) {
|
for (const key in params) {
|
||||||
formData.append(key, params[key]);
|
formData.append(key, params[key]);
|
||||||
}
|
}
|
||||||
return formData;
|
return formData;
|
||||||
@ -28,7 +17,7 @@ const createQueryString = (params, api_sig) => {
|
|||||||
// creates a querystring
|
// creates a querystring
|
||||||
const queryData = [];
|
const queryData = [];
|
||||||
params.api_sig = api_sig;
|
params.api_sig = api_sig;
|
||||||
for (key in params) {
|
for (const key in params) {
|
||||||
queryData.push(`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`);
|
queryData.push(`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`);
|
||||||
}
|
}
|
||||||
return '?'+queryData.join('&');
|
return '?'+queryData.join('&');
|
||||||
@ -37,12 +26,12 @@ const createQueryString = (params, api_sig) => {
|
|||||||
const createApiSig = (params, secret) => {
|
const createApiSig = (params, secret) => {
|
||||||
// this function creates the api signature, see: https://www.last.fm/api/authspec
|
// this function creates the api signature, see: https://www.last.fm/api/authspec
|
||||||
const keys = [];
|
const keys = [];
|
||||||
for (key in params) {
|
for (const key in params) {
|
||||||
keys.push(key);
|
keys.push(key);
|
||||||
}
|
}
|
||||||
keys.sort();
|
keys.sort();
|
||||||
let sig = '';
|
let sig = '';
|
||||||
for (key of keys) {
|
for (const key of keys) {
|
||||||
if (String(key) === 'format')
|
if (String(key) === 'format')
|
||||||
continue
|
continue
|
||||||
sig += `${key}${params[key]}`;
|
sig += `${key}${params[key]}`;
|
||||||
@ -69,7 +58,7 @@ const authenticate = async config => {
|
|||||||
// asks the user for authentication
|
// asks the user for authentication
|
||||||
config.token = await createToken(config);
|
config.token = await createToken(config);
|
||||||
setOptions('last-fm', config);
|
setOptions('last-fm', config);
|
||||||
open(`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`);
|
shell.openExternal(`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`);
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,10 +128,8 @@ const setNowPlaying = (songInfo, config) => {
|
|||||||
// this will store the timeout that will trigger addScrobble
|
// this will store the timeout that will trigger addScrobble
|
||||||
let scrobbleTimer = undefined;
|
let scrobbleTimer = undefined;
|
||||||
|
|
||||||
const lastfm = async (win, config) => {
|
const lastfm = async (_win, config) => {
|
||||||
const registerCallback = getSongInfo(win);
|
if (!config.api_root) {
|
||||||
|
|
||||||
if (!config.api_root || !config.suffixesToRemove) {
|
|
||||||
// settings are not present, creating them with the default values
|
// settings are not present, creating them with the default values
|
||||||
config = defaultConfig.plugins['last-fm'];
|
config = defaultConfig.plugins['last-fm'];
|
||||||
config.enabled = true;
|
config.enabled = true;
|
||||||
@ -157,8 +144,6 @@ const lastfm = async (win, config) => {
|
|||||||
registerCallback( songInfo => {
|
registerCallback( songInfo => {
|
||||||
// set remove the old scrobble timer
|
// set remove the old scrobble timer
|
||||||
clearTimeout(scrobbleTimer);
|
clearTimeout(scrobbleTimer);
|
||||||
// make the artist name a bit cleaner
|
|
||||||
songInfo.artist = cleanupArtistName(config, songInfo.artist);
|
|
||||||
if (!songInfo.isPaused) {
|
if (!songInfo.isPaused) {
|
||||||
setNowPlaying(songInfo, config);
|
setNowPlaying(songInfo, config);
|
||||||
// scrobble when the song is half way through, or has passed the 4 minute mark
|
// scrobble when the song is half way through, or has passed the 4 minute mark
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
const { Notification } = require("electron");
|
const { Notification } = require("electron");
|
||||||
const is = require("electron-is");
|
const is = require("electron-is");
|
||||||
const getSongInfo = require("../../providers/song-info");
|
const registerCallback = require("../../providers/song-info");
|
||||||
const { notificationImage } = require("./utils");
|
const { notificationImage } = require("./utils");
|
||||||
|
|
||||||
const { setupInteractive, notifyInteractive } = require("./interactive")
|
const setupInteractive = require("./interactive")
|
||||||
|
|
||||||
const notify = (info, options) => {
|
const notify = (info, options) => {
|
||||||
|
|
||||||
@ -23,38 +23,24 @@ const notify = (info, options) => {
|
|||||||
return currentNotification;
|
return currentNotification;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = (win, options) => {
|
const setup = (options) => {
|
||||||
const isInteractive = is.windows() && options.interactive;
|
|
||||||
//setup interactive notifications for windows
|
|
||||||
if (isInteractive) {
|
|
||||||
setupInteractive(win, options.unpauseNotification);
|
|
||||||
}
|
|
||||||
const registerCallback = getSongInfo(win);
|
|
||||||
let oldNotification;
|
let oldNotification;
|
||||||
let oldURL = "";
|
let currentUrl;
|
||||||
win.once("ready-to-show", () => {
|
|
||||||
// Register the callback for new song information
|
registerCallback(songInfo => {
|
||||||
registerCallback(songInfo => {
|
if (!songInfo.isPaused && (songInfo.url !== currentUrl || options.unpauseNotification)) {
|
||||||
// on pause - reset url? and skip notification
|
// Close the old notification
|
||||||
if (songInfo.isPaused) {
|
oldNotification?.close();
|
||||||
//reset oldURL if unpause notification option is on
|
currentUrl = songInfo.url;
|
||||||
if (options.unpauseNotification) {
|
// This fixes a weird bug that would cause the notification to be updated instead of showing
|
||||||
oldURL = "";
|
setTimeout(() => { oldNotification = notify(songInfo, options) }, 10);
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
// If url isn't the same as last one - send notification
|
|
||||||
if (songInfo.url !== oldURL) {
|
|
||||||
oldURL = songInfo.url;
|
|
||||||
if (isInteractive) {
|
|
||||||
notifyInteractive(songInfo);
|
|
||||||
} else {
|
|
||||||
// Close the old notification
|
|
||||||
oldNotification?.close();
|
|
||||||
// This fixes a weird bug that would cause the notification to be updated instead of showing
|
|
||||||
setTimeout(() => { oldNotification = notify(songInfo, options) }, 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = (win, options) => {
|
||||||
|
// Register the callback for new song information
|
||||||
|
is.windows() && options.interactive ?
|
||||||
|
setupInteractive(win, options.unpauseNotification) :
|
||||||
|
setup(options);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,17 +1,27 @@
|
|||||||
const { notificationImage, icons } = require("./utils");
|
const { notificationImage, icons } = require("./utils");
|
||||||
const getSongControls = require('../../providers/song-controls');
|
const getSongControls = require('../../providers/song-controls');
|
||||||
|
const registerCallback = require("../../providers/song-info");
|
||||||
const notifier = require("node-notifier");
|
const notifier = require("node-notifier");
|
||||||
|
|
||||||
//store song controls reference on launch
|
//store song controls reference on launch
|
||||||
let controls;
|
let controls;
|
||||||
let notificationOnPause;
|
let notificationOnUnpause;
|
||||||
|
|
||||||
//Save controls and onPause option
|
module.exports = (win, unpauseNotification) => {
|
||||||
module.exports.setupInteractive = (win, unpauseNotification) => {
|
//Save controls and onPause option
|
||||||
const { playPause, next, previous } = getSongControls(win);
|
const { playPause, next, previous } = getSongControls(win);
|
||||||
controls = { playPause, next, previous };
|
controls = { playPause, next, previous };
|
||||||
|
notificationOnUnpause = unpauseNotification;
|
||||||
|
|
||||||
notificationOnPause = unpauseNotification;
|
let currentUrl;
|
||||||
|
|
||||||
|
// Register songInfoCallback
|
||||||
|
registerCallback(songInfo => {
|
||||||
|
if (!songInfo.isPaused && (songInfo.url !== currentUrl || notificationOnUnpause)) {
|
||||||
|
currentUrl = songInfo.url;
|
||||||
|
sendToaster(songInfo);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
win.webContents.once("closed", () => {
|
win.webContents.once("closed", () => {
|
||||||
deleteNotification()
|
deleteNotification()
|
||||||
@ -33,7 +43,7 @@ function deleteNotification() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//New notification
|
//New notification
|
||||||
module.exports.notifyInteractive = function sendToaster(songInfo) {
|
function sendToaster(songInfo) {
|
||||||
deleteNotification();
|
deleteNotification();
|
||||||
//download image and get path
|
//download image and get path
|
||||||
let imgSrc = notificationImage(songInfo, true);
|
let imgSrc = notificationImage(songInfo, true);
|
||||||
@ -71,7 +81,7 @@ module.exports.notifyInteractive = function sendToaster(songInfo) {
|
|||||||
// dont delete notification on play/pause
|
// dont delete notification on play/pause
|
||||||
toDelete = undefined;
|
toDelete = undefined;
|
||||||
//manually send notification if not sending automatically
|
//manually send notification if not sending automatically
|
||||||
if (!notificationOnPause) {
|
if (!notificationOnUnpause) {
|
||||||
songInfo.isPaused = false;
|
songInfo.isPaused = false;
|
||||||
sendToaster(songInfo);
|
sendToaster(songInfo);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -194,12 +194,13 @@ function setupLocalArrowShortcuts(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function callback(event) {
|
function callback(event) {
|
||||||
event.preventDefault();
|
|
||||||
switch (event.code) {
|
switch (event.code) {
|
||||||
case "ArrowUp":
|
case "ArrowUp":
|
||||||
|
event.preventDefault();
|
||||||
changeVolume(true, options);
|
changeVolume(true, options);
|
||||||
break;
|
break;
|
||||||
case "ArrowDown":
|
case "ArrowDown":
|
||||||
|
event.preventDefault();
|
||||||
changeVolume(false, options);
|
changeVolume(false, options);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,28 +1,33 @@
|
|||||||
const { ipcRenderer } = require("electron");
|
const { ipcRenderer } = require("electron");
|
||||||
|
const is = require("electron-is");
|
||||||
|
|
||||||
|
let ignored = {
|
||||||
|
id: ["volume-slider", "expand-volume-slider"],
|
||||||
|
types: ["mousewheel", "keydown", "keyup"]
|
||||||
|
};
|
||||||
|
|
||||||
// Override specific listeners of volume-slider by modifying Element.prototype
|
|
||||||
function overrideAddEventListener() {
|
function overrideAddEventListener() {
|
||||||
// Events to ignore
|
|
||||||
const nativeEvents = ["mousewheel", "keydown", "keyup"];
|
|
||||||
// Save native addEventListener
|
// Save native addEventListener
|
||||||
Element.prototype._addEventListener = Element.prototype.addEventListener;
|
Element.prototype._addEventListener = Element.prototype.addEventListener;
|
||||||
// Override addEventListener to Ignore specific events in volume-slider
|
// Override addEventListener to Ignore specific events in volume-slider
|
||||||
Element.prototype.addEventListener = function (type, listener, useCapture = false) {
|
Element.prototype.addEventListener = function (type, listener, useCapture = false) {
|
||||||
if (this.tagName === "TP-YT-PAPER-SLIDER") { // tagName of #volume-slider
|
if (!(
|
||||||
for (const eventType of nativeEvents) {
|
ignored.id.includes(this.id) &&
|
||||||
if (eventType === type) {
|
ignored.types.includes(type)
|
||||||
return;
|
)) {
|
||||||
}
|
this._addEventListener(type, listener, useCapture);
|
||||||
}
|
} else if (is.dev()) {
|
||||||
}//else
|
console.log(`Ignoring event: "${this.id}.${type}()"`);
|
||||||
this._addEventListener(type, listener, useCapture);
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
overrideAddEventListener();
|
overrideAddEventListener();
|
||||||
// Restore original function after did-finish-load to avoid keeping Element.prototype altered
|
// Restore original function after did-finish-load to avoid keeping Element.prototype altered
|
||||||
ipcRenderer.once("restoreAddEventListener", () => { //called from Main to make sure page is completly loaded
|
ipcRenderer.once("restoreAddEventListener", () => { // Called from main to make sure page is completly loaded
|
||||||
Element.prototype.addEventListener = Element.prototype._addEventListener;
|
Element.prototype.addEventListener = Element.prototype._addEventListener;
|
||||||
|
Element.prototype._addEventListener = undefined;
|
||||||
|
ignored = undefined;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
51
plugins/sponsorblock/back.js
Normal file
51
plugins/sponsorblock/back.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
const fetch = require("node-fetch");
|
||||||
|
|
||||||
|
const defaultConfig = require("../../config/defaults");
|
||||||
|
const registerCallback = require("../../providers/song-info");
|
||||||
|
const { sortSegments } = require("./segments");
|
||||||
|
|
||||||
|
let videoID;
|
||||||
|
|
||||||
|
module.exports = (win, options) => {
|
||||||
|
const { apiURL, categories } = {
|
||||||
|
...defaultConfig.plugins.sponsorblock,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
registerCallback(async (info) => {
|
||||||
|
const newURL = info.url || win.webContents.getURL();
|
||||||
|
const newVideoID = new URL(newURL).searchParams.get("v");
|
||||||
|
|
||||||
|
if (videoID !== newVideoID) {
|
||||||
|
videoID = newVideoID;
|
||||||
|
const segments = await fetchSegments(apiURL, categories);
|
||||||
|
win.webContents.send("sponsorblock-skip", segments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchSegments = async (apiURL, categories) => {
|
||||||
|
const sponsorBlockURL = `${apiURL}/api/skipSegments?videoID=${videoID}&categories=${JSON.stringify(
|
||||||
|
categories
|
||||||
|
)}`;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(sponsorBlockURL, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
redirect: "follow",
|
||||||
|
});
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const segments = await resp.json();
|
||||||
|
const sortedSegments = sortSegments(
|
||||||
|
segments.map((submission) => submission.segment)
|
||||||
|
);
|
||||||
|
|
||||||
|
return sortedSegments;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
27
plugins/sponsorblock/front.js
Normal file
27
plugins/sponsorblock/front.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const { ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
const is = require("electron-is");
|
||||||
|
|
||||||
|
const { ontimeupdate } = require("../../providers/video-element");
|
||||||
|
|
||||||
|
let currentSegments = [];
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
ipcRenderer.on("sponsorblock-skip", (_, segments) => {
|
||||||
|
currentSegments = segments;
|
||||||
|
});
|
||||||
|
|
||||||
|
ontimeupdate((videoElement) => {
|
||||||
|
currentSegments.forEach((segment) => {
|
||||||
|
if (
|
||||||
|
videoElement.currentTime >= segment[0] &&
|
||||||
|
videoElement.currentTime <= segment[1]
|
||||||
|
) {
|
||||||
|
videoElement.currentTime = segment[1];
|
||||||
|
if (is.dev()) {
|
||||||
|
console.log("SponsorBlock: skipping segment", segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
29
plugins/sponsorblock/segments.js
Normal file
29
plugins/sponsorblock/segments.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Segments are an array [ [start, end], … ]
|
||||||
|
module.exports.sortSegments = (segments) => {
|
||||||
|
segments.sort((segment1, segment2) =>
|
||||||
|
segment1[0] === segment2[0]
|
||||||
|
? segment1[1] - segment2[1]
|
||||||
|
: segment1[0] - segment2[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
const compiledSegments = [];
|
||||||
|
let currentSegment;
|
||||||
|
|
||||||
|
segments.forEach((segment) => {
|
||||||
|
if (!currentSegment) {
|
||||||
|
currentSegment = segment;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSegment[1] < segment[0]) {
|
||||||
|
compiledSegments.push(currentSegment);
|
||||||
|
currentSegment = segment;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSegment[1] = Math.max(currentSegment[1], segment[1]);
|
||||||
|
});
|
||||||
|
compiledSegments.push(currentSegment);
|
||||||
|
|
||||||
|
return compiledSegments;
|
||||||
|
};
|
||||||
34
plugins/sponsorblock/tests/segments.test.js
Normal file
34
plugins/sponsorblock/tests/segments.test.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
const { sortSegments } = require("../segments");
|
||||||
|
|
||||||
|
test("Segment sorting", () => {
|
||||||
|
expect(
|
||||||
|
sortSegments([
|
||||||
|
[0, 3],
|
||||||
|
[7, 8],
|
||||||
|
[5, 6],
|
||||||
|
])
|
||||||
|
).toEqual([
|
||||||
|
[0, 3],
|
||||||
|
[5, 6],
|
||||||
|
[7, 8],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
sortSegments([
|
||||||
|
[0, 5],
|
||||||
|
[6, 8],
|
||||||
|
[4, 6],
|
||||||
|
])
|
||||||
|
).toEqual([[0, 8]]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
sortSegments([
|
||||||
|
[0, 6],
|
||||||
|
[7, 8],
|
||||||
|
[4, 6],
|
||||||
|
])
|
||||||
|
).toEqual([
|
||||||
|
[0, 6],
|
||||||
|
[7, 8],
|
||||||
|
]);
|
||||||
|
});
|
||||||
@ -1,12 +1,11 @@
|
|||||||
const getSongControls = require('../../providers/song-controls');
|
const getSongControls = require('../../providers/song-controls');
|
||||||
const getSongInfo = require('../../providers/song-info');
|
const registerCallback = require('../../providers/song-info');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
let controls;
|
let controls;
|
||||||
let currentSongInfo;
|
let currentSongInfo;
|
||||||
|
|
||||||
module.exports = win => {
|
module.exports = win => {
|
||||||
const registerCallback = getSongInfo(win);
|
|
||||||
const { playPause, next, previous } = getSongControls(win);
|
const { playPause, next, previous } = getSongControls(win);
|
||||||
controls = { playPause, next, previous };
|
controls = { playPause, next, previous };
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ const {
|
|||||||
TouchBarScrubber,
|
TouchBarScrubber,
|
||||||
} = TouchBar;
|
} = TouchBar;
|
||||||
|
|
||||||
const getSongInfo = require("../../providers/song-info");
|
const registerCallback = require("../../providers/song-info");
|
||||||
const getSongControls = require("../../providers/song-controls");
|
const getSongControls = require("../../providers/song-controls");
|
||||||
|
|
||||||
// Songtitle label
|
// Songtitle label
|
||||||
@ -59,7 +59,6 @@ const touchBar = new TouchBar({
|
|||||||
});
|
});
|
||||||
|
|
||||||
module.exports = (win) => {
|
module.exports = (win) => {
|
||||||
const registerCallback = getSongInfo(win);
|
|
||||||
const { playPause, next, previous, like, dislike } = getSongControls(win);
|
const { playPause, next, previous, like, dislike } = getSongControls(win);
|
||||||
|
|
||||||
// If the page is ready, register the callback
|
// If the page is ready, register the callback
|
||||||
|
|||||||
@ -43,7 +43,7 @@ module.exports.fileExists = (path, callbackIfExists) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
module.exports.injectCSS = (webContents, filepath, cb = undefined) => {
|
module.exports.injectCSS = (webContents, filepath, cb = undefined) => {
|
||||||
webContents.once("did-finish-load", async () => {
|
webContents.on("did-finish-load", async () => {
|
||||||
await webContents.insertCSS(fs.readFileSync(filepath, "utf8"));
|
await webContents.insertCSS(fs.readFileSync(filepath, "utf8"));
|
||||||
if (cb) {
|
if (cb) {
|
||||||
cb();
|
cb();
|
||||||
|
|||||||
@ -5,6 +5,8 @@ const { remote } = require("electron");
|
|||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
const { fileExists } = require("./plugins/utils");
|
const { fileExists } = require("./plugins/utils");
|
||||||
const setupFrontLogger = require("./providers/front-logger");
|
const setupFrontLogger = require("./providers/front-logger");
|
||||||
|
const setupSongControl = require("./providers/song-controls-front");
|
||||||
|
const setupSongInfo = require("./providers/song-info-front");
|
||||||
|
|
||||||
const plugins = config.plugins.getEnabled();
|
const plugins = config.plugins.getEnabled();
|
||||||
|
|
||||||
@ -37,8 +39,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// inject song-info provider
|
// inject song-info provider
|
||||||
const songInfoProviderPath = path.join(__dirname, "providers", "song-info-front.js")
|
setupSongInfo();
|
||||||
fileExists(songInfoProviderPath, require(songInfoProviderPath));
|
|
||||||
|
// inject song-control provider
|
||||||
|
setupSongControl();
|
||||||
|
|
||||||
// inject front logger
|
// inject front logger
|
||||||
setupFrontLogger();
|
setupFrontLogger();
|
||||||
|
|||||||
18
providers/song-controls-front.js
Normal file
18
providers/song-controls-front.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
const { ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
let videoStream = document.querySelector(".video-stream");
|
||||||
|
module.exports = () => {
|
||||||
|
ipcRenderer.on("playPause", () => {
|
||||||
|
if (!videoStream) {
|
||||||
|
videoStream = document.querySelector(".video-stream");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoStream.paused) {
|
||||||
|
videoStream.play();
|
||||||
|
} else {
|
||||||
|
videoStream.yns_pause ?
|
||||||
|
videoStream.yns_pause() :
|
||||||
|
videoStream.pause();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -12,7 +12,7 @@ module.exports = (win) => {
|
|||||||
// Playback
|
// Playback
|
||||||
previous: () => pressKey(win, "k"),
|
previous: () => pressKey(win, "k"),
|
||||||
next: () => pressKey(win, "j"),
|
next: () => pressKey(win, "j"),
|
||||||
playPause: () => pressKey(win, "space"),
|
playPause: () => win.webContents.send("playPause"),
|
||||||
like: () => pressKey(win, "_"),
|
like: () => pressKey(win, "_"),
|
||||||
dislike: () => pressKey(win, "+"),
|
dislike: () => pressKey(win, "+"),
|
||||||
go10sBack: () => pressKey(win, "h"),
|
go10sBack: () => pressKey(win, "h"),
|
||||||
|
|||||||
@ -10,17 +10,16 @@ ipcRenderer.on("update-song-info", async (_, extractedSongInfo) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const injectListener = () => {
|
const injectListener = () => {
|
||||||
var oldXHR = window.XMLHttpRequest;
|
const oldXHR = window.XMLHttpRequest;
|
||||||
function newXHR() {
|
function newXHR() {
|
||||||
var realXHR = new oldXHR();
|
const realXHR = new oldXHR();
|
||||||
realXHR.addEventListener(
|
realXHR.addEventListener(
|
||||||
"readystatechange",
|
"readystatechange",
|
||||||
() => {
|
() => {
|
||||||
if (realXHR.readyState == 4 && realXHR.status == 200) {
|
if (realXHR.readyState === 4 && realXHR.status === 200
|
||||||
if (realXHR.responseURL.includes("/player")) {
|
&& realXHR.responseURL.includes("/player")) {
|
||||||
// if the request contains the song info, send the response to ipcMain
|
// if the request contains the song info, send the response to ipcMain
|
||||||
ipcRenderer.send("song-info-request", realXHR.responseText);
|
ipcRenderer.send("song-info-request", realXHR.responseText);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
false
|
false
|
||||||
|
|||||||
@ -9,18 +9,21 @@ const progressSelector = "#progress-bar";
|
|||||||
// Grab the progress using the selector
|
// Grab the progress using the selector
|
||||||
const getProgress = async (win) => {
|
const getProgress = async (win) => {
|
||||||
// Get current value of the progressbar element
|
// Get current value of the progressbar element
|
||||||
const elapsedSeconds = await win.webContents.executeJavaScript(
|
return win.webContents.executeJavaScript(
|
||||||
'document.querySelector("' + progressSelector + '").value'
|
'document.querySelector("' + progressSelector + '").value'
|
||||||
);
|
);
|
||||||
|
|
||||||
return elapsedSeconds;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Grab the native image using the src
|
// Grab the native image using the src
|
||||||
const getImage = async (src) => {
|
const getImage = async (src) => {
|
||||||
const result = await fetch(src);
|
const result = await fetch(src);
|
||||||
const buffer = await result.buffer();
|
const buffer = await result.buffer();
|
||||||
return nativeImage.createFromBuffer(buffer);
|
const output = nativeImage.createFromBuffer(buffer);
|
||||||
|
if (output.isEmpty() && !src.endsWith(".jpg") && src.includes(".jpg")) { // fix hidden webp files (https://github.com/th-ch/youtube-music/issues/315)
|
||||||
|
return getImage(src.slice(0, src.lastIndexOf(".jpg")+4));
|
||||||
|
} else {
|
||||||
|
return output;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// To find the paused status, we check if the title contains `-`
|
// To find the paused status, we check if the title contains `-`
|
||||||
@ -30,15 +33,10 @@ const getPausedStatus = async (win) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getArtist = async (win) => {
|
const getArtist = async (win) => {
|
||||||
return await win.webContents.executeJavaScript(
|
return win.webContents.executeJavaScript(`
|
||||||
`
|
document.querySelector(".subtitle.ytmusic-player-bar .yt-formatted-string")
|
||||||
var bar = document.getElementsByClassName('subtitle ytmusic-player-bar')[0];
|
?.textContent
|
||||||
var artistName = (bar.getElementsByClassName('yt-formatted-string')[0]) || (bar.getElementsByClassName('byline ytmusic-player-bar')[0]);
|
`);
|
||||||
if (artistName) {
|
|
||||||
artistName.textContent;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill songInfo with empty values
|
// Fill songInfo with empty values
|
||||||
@ -58,7 +56,7 @@ const songInfo = {
|
|||||||
const handleData = async (responseText, win) => {
|
const handleData = async (responseText, win) => {
|
||||||
let data = JSON.parse(responseText);
|
let data = JSON.parse(responseText);
|
||||||
songInfo.title = data?.videoDetails?.title;
|
songInfo.title = data?.videoDetails?.title;
|
||||||
songInfo.artist = await getArtist(win) || data?.videoDetails?.author;
|
songInfo.artist = await getArtist(win) || cleanupArtistName(data?.videoDetails?.author);
|
||||||
songInfo.views = data?.videoDetails?.viewCount;
|
songInfo.views = data?.videoDetails?.viewCount;
|
||||||
songInfo.imageSrc = data?.videoDetails?.thumbnail?.thumbnails?.pop()?.url;
|
songInfo.imageSrc = data?.videoDetails?.thumbnail?.thumbnails?.pop()?.url;
|
||||||
songInfo.songDuration = data?.videoDetails?.lengthSeconds;
|
songInfo.songDuration = data?.videoDetails?.lengthSeconds;
|
||||||
@ -69,15 +67,15 @@ const handleData = async (responseText, win) => {
|
|||||||
win.webContents.send("update-song-info", JSON.stringify(songInfo));
|
win.webContents.send("update-song-info", JSON.stringify(songInfo));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// This variable will be filled with the callbacks once they register
|
||||||
|
const callbacks = [];
|
||||||
|
|
||||||
|
// This function will allow plugins to register callback that will be triggered when data changes
|
||||||
|
const registerCallback = (callback) => {
|
||||||
|
callbacks.push(callback);
|
||||||
|
};
|
||||||
|
|
||||||
const registerProvider = (win) => {
|
const registerProvider = (win) => {
|
||||||
// This variable will be filled with the callbacks once they register
|
|
||||||
const callbacks = [];
|
|
||||||
|
|
||||||
// This function will allow plugins to register callback that will be triggered when data changes
|
|
||||||
const registerCallback = (callback) => {
|
|
||||||
callbacks.push(callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
win.on("page-title-updated", async () => {
|
win.on("page-title-updated", async () => {
|
||||||
// Get and set the new data
|
// Get and set the new data
|
||||||
songInfo.isPaused = await getPausedStatus(win);
|
songInfo.isPaused = await getPausedStatus(win);
|
||||||
@ -98,9 +96,22 @@ const registerProvider = (win) => {
|
|||||||
c(songInfo);
|
c(songInfo);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return registerCallback;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = registerProvider;
|
const suffixesToRemove = [' - Topic', 'VEVO'];
|
||||||
|
function cleanupArtistName(artist) {
|
||||||
|
if (!artist) {
|
||||||
|
return artist;
|
||||||
|
}
|
||||||
|
for (const suffix of suffixesToRemove) {
|
||||||
|
if (artist.endsWith(suffix)) {
|
||||||
|
return artist.slice(0, -suffix.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return artist;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerCallback;
|
||||||
|
module.exports.setupSongInfo = registerProvider;
|
||||||
module.exports.getImage = getImage;
|
module.exports.getImage = getImage;
|
||||||
|
module.exports.cleanupArtistName = cleanupArtistName;
|
||||||
|
|||||||
22
providers/video-element.js
Normal file
22
providers/video-element.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
let videoElement = null;
|
||||||
|
|
||||||
|
module.exports.ontimeupdate = (cb) => {
|
||||||
|
const observer = new MutationObserver((mutations, observer) => {
|
||||||
|
if (!videoElement) {
|
||||||
|
videoElement = document.querySelector("video");
|
||||||
|
if (videoElement) {
|
||||||
|
observer.disconnect();
|
||||||
|
videoElement.ontimeupdate = () => cb(videoElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!videoElement) {
|
||||||
|
observer.observe(document, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
videoElement.ontimeupdate = () => cb(videoElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,3 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment ./tests/environment
|
||||||
|
*/
|
||||||
|
|
||||||
describe("YouTube Music App", () => {
|
describe("YouTube Music App", () => {
|
||||||
const app = global.__APP__;
|
const app = global.__APP__;
|
||||||
|
|
||||||
|
|||||||
7
tray.js
7
tray.js
@ -63,12 +63,7 @@ module.exports.setUpTray = (app, win) => {
|
|||||||
app.quit();
|
app.quit();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{ role: "quit" }
|
||||||
label: "Quit",
|
|
||||||
click: () => {
|
|
||||||
app.quit();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const trayMenu = Menu.buildFromTemplate(template);
|
const trayMenu = Menu.buildFromTemplate(template);
|
||||||
|
|||||||
Reference in New Issue
Block a user