diff --git a/config/defaults.js b/config/defaults.js
index 552a4995..5bb74e09 100644
--- a/config/defaults.js
+++ b/config/defaults.js
@@ -30,6 +30,7 @@ const defaultConfig = {
// Disabled plugins
shortcuts: {
enabled: false,
+ overrideMediaKeys: false,
},
downloader: {
enabled: false,
@@ -59,9 +60,8 @@ const defaultConfig = {
steps: 1, //percentage of volume to change
arrowsShortcut: true, //enable ArrowUp + ArrowDown local shortcuts
globalShortcuts: {
- enabled: false, // enable global shortcuts
- volumeUp: "Shift+PageUp", // Keybind default can be changed
- volumeDown: "Shift+PageDown"
+ volumeUp: "",
+ volumeDown: ""
},
savedVolume: undefined //plugin save volume between session here
},
diff --git a/config/store.js b/config/store.js
index f66c1f9a..d6451cee 100644
--- a/config/store.js
+++ b/config/store.js
@@ -8,6 +8,27 @@ const migrations = {
store.set("plugins.discord.listenAlong", true);
}
},
+ ">=1.12.0": (store) => {
+ const options = store.get("plugins.shortcuts")
+ let updated = false;
+ for (const optionType of ["global", "local"]) {
+ if (Array.isArray(options[optionType])) {
+ const updatedOptions = {};
+ for (const optionObject of options[optionType]) {
+ if (optionObject.action && optionObject.shortcut) {
+ updatedOptions[optionObject.action] = optionObject.shortcut;
+ }
+ }
+
+ options[optionType] = updatedOptions;
+ updated = true;
+ }
+ }
+
+ if (updated) {
+ store.set("plugins.shortcuts", options);
+ }
+ },
">=1.11.0": (store) => {
if (store.get("options.resumeOnStart") === undefined) {
store.set("options.resumeOnStart", true);
diff --git a/index.js b/index.js
index c54d2555..90096ecf 100644
--- a/index.js
+++ b/index.js
@@ -39,7 +39,9 @@ if (config.get("options.proxy")) {
}
// Adds debug features like hotkeys for triggering dev tools and reload
-require("electron-debug")();
+require("electron-debug")({
+ showDevTools: false //disable automatic devTools on new window
+});
// Prevent window being garbage collected
let mainWindow;
@@ -60,7 +62,7 @@ function onClosed() {
function loadPlugins(win) {
injectCSS(win.webContents, path.join(__dirname, "youtube-music.css"));
- win.webContents.on("did-finish-load", () => {
+ win.webContents.once("did-finish-load", () => {
if (is.dev()) {
console.log("did finish load");
win.webContents.openDevTools();
diff --git a/menu.js b/menu.js
index 72b2cc66..afea532d 100644
--- a/menu.js
+++ b/menu.js
@@ -7,6 +7,9 @@ const is = require("electron-is");
const { getAllPlugins } = require("./plugins/utils");
const config = require("./config");
+const prompt = require("custom-electron-prompt");
+const promptOptions = require("./providers/prompt-options");
+
// true only if in-app-menu was loaded on launch
const inAppMenuActive = config.plugins.isEnabled("in-app-menu");
@@ -76,6 +79,14 @@ const mainMenuTemplate = (win) => {
config.set("options.resumeOnStart", item.checked);
},
},
+ {
+ label: "Remove upgrade button",
+ type: "checkbox",
+ checked: config.get("options.removeUpgradeButton"),
+ click: (item) => {
+ config.set("options.removeUpgradeButton", item.checked);
+ },
+ },
...(is.windows() || is.linux()
? [
{
@@ -149,6 +160,14 @@ const mainMenuTemplate = (win) => {
{
label: "Advanced options",
submenu: [
+ {
+ label: "Proxy",
+ type: "checkbox",
+ checked: !!config.get("options.proxy"),
+ click: (item) => {
+ setProxy(item, win);
+ },
+ },
{
label: "Disable hardware acceleration",
type: "checkbox",
@@ -275,3 +294,25 @@ module.exports.setApplicationMenu = (win) => {
const menu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menu);
};
+
+async function setProxy(item, win) {
+ const output = await prompt({
+ title: 'Set Proxy',
+ label: 'Enter Proxy Address: (leave empty to disable)',
+ value: config.get("options.proxy"),
+ type: 'input',
+ inputAttrs: {
+ type: 'url',
+ placeholder: "Example: 'socks5://127.0.0.1:9999"
+ },
+ width: 450,
+ ...promptOptions()
+ }, win);
+
+ if (typeof output === "string") {
+ config.set("options.proxy", output);
+ item.checked = output !== "";
+ } else { //user pressed cancel
+ item.checked = !item.checked; //reset checkbox
+ }
+}
diff --git a/package.json b/package.json
index 71063a70..3c2af2eb 100644
--- a/package.json
+++ b/package.json
@@ -52,9 +52,8 @@
"build:mac": "yarn run clean && electron-builder --mac",
"build:win": "yarn run clean && electron-builder --win",
"lint": "xo",
- "plugins": "yarn run plugin:adblocker && yarn run plugin:autoconfirm",
+ "plugins": "yarn run plugin:adblocker",
"plugin:adblocker": "rimraf plugins/adblocker/ad-blocker-engine.bin && node plugins/adblocker/blocker.js",
- "plugin:autoconfirm": "yarn run generate:package YoutubeNonStop",
"release:linux": "yarn run clean && electron-builder --linux -p always -c.snap.publish=github",
"release:mac": "yarn run clean && electron-builder --mac -p always",
"release:win": "yarn run clean && electron-builder --win -p always"
@@ -67,9 +66,9 @@
"@cliqz/adblocker-electron": "^1.22.6",
"@ffmpeg/core": "^0.10.0",
"@ffmpeg/ffmpeg": "^0.10.0",
- "YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.9.0",
"async-mutex": "^0.3.2",
"browser-id3-writer": "^4.4.0",
+ "custom-electron-prompt": "^1.1.0",
"chokidar": "^3.5.2",
"custom-electron-titlebar": "^3.2.7",
"discord-rpc": "^3.2.0",
diff --git a/plugins/audio-compressor/front.js b/plugins/audio-compressor/front.js
new file mode 100644
index 00000000..4f41a4d7
--- /dev/null
+++ b/plugins/audio-compressor/front.js
@@ -0,0 +1,25 @@
+const applyCompressor = () => {
+ const videoElement = document.querySelector("video");
+
+ // If video element is not loaded yet try again
+ if(videoElement === null) {
+ setTimeout(applyCompressor, 500);
+ return;
+ }
+
+ const audioContext = new AudioContext();
+
+ let compressor = audioContext.createDynamicsCompressor();
+ compressor.threshold.value = -50;
+ compressor.ratio.value = 12;
+ compressor.knee.value = 40;
+ compressor.attack.value = 0;
+ compressor.release.value = 0.25;
+
+ const source = audioContext.createMediaElementSource(videoElement);
+
+ source.connect(compressor);
+ compressor.connect(audioContext.destination);
+};
+
+module.exports = applyCompressor;
\ No newline at end of file
diff --git a/plugins/auto-confirm-when-paused/front.js b/plugins/auto-confirm-when-paused/front.js
deleted file mode 100644
index 0ead302e..00000000
--- a/plugins/auto-confirm-when-paused/front.js
+++ /dev/null
@@ -1,12 +0,0 @@
-// Define global chrome object to be compliant with the extension code
-global.chrome = {
- runtime: {
- getManifest: () => ({
- version: 1
- })
- }
-};
-
-module.exports = () => {
- require("YoutubeNonStop/autoconfirm.js");
-};
diff --git a/plugins/discord/back.js b/plugins/discord/back.js
index 3c3ab5fd..9dc6f9fe 100644
--- a/plugins/discord/back.js
+++ b/plugins/discord/back.js
@@ -145,7 +145,4 @@ module.exports.clear = () => {
};
module.exports.connect = connect;
module.exports.registerRefresh = (cb) => refreshCallbacks.push(cb);
-/**
- * @type {Info}
- */
-module.exports.info = Object.defineProperties({}, Object.keys(info).reduce((o, k) => ({ ...o, [k]: { enumerable: true, get: () => info[k] } }), {}));
+module.exports.isConnected = () => info.rpc !== null;
diff --git a/plugins/discord/menu.js b/plugins/discord/menu.js
index 9750dabe..49a087d6 100644
--- a/plugins/discord/menu.js
+++ b/plugins/discord/menu.js
@@ -1,6 +1,6 @@
const { setOptions } = require("../../config/plugins");
const { edit } = require("../../config");
-const { clear, info, connect, registerRefresh } = require("./back");
+const { clear, connect, registerRefresh, isConnected } = require("./back");
let hasRegisterred = false;
@@ -12,8 +12,8 @@ module.exports = (win, options, refreshMenu) => {
return [
{
- label: info.rpc !== null ? "Connected" : "Reconnect",
- enabled: info.rpc === null,
+ label: isConnected() ? "Connected" : "Reconnect",
+ enabled: !isConnected(),
click: connect,
},
{
diff --git a/plugins/precise-volume/back.js b/plugins/precise-volume/back.js
index 93ebea9f..f891ce32 100644
--- a/plugins/precise-volume/back.js
+++ b/plugins/precise-volume/back.js
@@ -1,23 +1,9 @@
-const { isEnabled } = require("../../config/plugins");
-
/*
This is used to determine if plugin is actually active
(not if its only enabled in options)
*/
let enabled = false;
-module.exports = (win) => {
- enabled = true;
+module.exports = () => enabled = true;
- // youtube-music register some of the target listeners after DOMContentLoaded
- // did-finish-load is called after all elements finished loading, including said listeners
- // Thats the reason the timing is controlled from main
- win.webContents.once("did-finish-load", () => {
- win.webContents.send("restoreAddEventListener");
- win.webContents.send("setupVideoPlayerVolumeMousewheel", !isEnabled("hide-video-player"));
- });
-};
-
-module.exports.enabled = () => {
- return enabled;
-};
+module.exports.enabled = () => enabled;
diff --git a/plugins/precise-volume/front.js b/plugins/precise-volume/front.js
index 7195b5bf..84241fb8 100644
--- a/plugins/precise-volume/front.js
+++ b/plugins/precise-volume/front.js
@@ -3,27 +3,73 @@ const { ipcRenderer, remote } = require("electron");
const { setOptions } = require("../../config/plugins");
function $(selector) { return document.querySelector(selector); }
+let api;
module.exports = (options) => {
+ document.addEventListener('apiLoaded', e => {
+ api = e.detail;
+ firstRun(options);
+ })
+};
+
+/** Restore saved volume and setup tooltip */
+function firstRun(options) {
+ if (typeof options.savedVolume === "number") {
+ // Set saved volume as tooltip
+ setTooltip(options.savedVolume);
+
+ if (api.getVolume() !== options.savedVolume) {
+ api.setVolume(options.savedVolume);
+ }
+ }
setupPlaybar(options);
- setupSliderObserver(options);
-
setupLocalArrowShortcuts(options);
- if (options.globalShortcuts?.enabled) {
- setupGlobalShortcuts(options);
+ setupGlobalShortcuts(options);
+
+ const noVid = $("#main-panel")?.computedStyleMap().get("display").value === "none";
+ injectVolumeHud(noVid);
+ if (!noVid) {
+ setupVideoPlayerOnwheel(options);
+ }
+}
+
+function injectVolumeHud(noVid) {
+ if (noVid) {
+ const position = "top: 18px; right: 60px; z-index: 999; position: absolute;";
+ const mainStyle = "font-size: xx-large; padding: 10px; transition: opacity 1s";
+
+ $(".center-content.ytmusic-nav-bar").insertAdjacentHTML("beforeend",
+ ``)
+ } else {
+ const position = `top: 10px; left: 10px; z-index: 999; position: absolute;`;
+ const mainStyle = "font-size: xxx-large; padding: 10px; transition: opacity 0.6s; webkit-text-stroke: 1px black; font-weight: 600;";
+
+ $("#song-video").insertAdjacentHTML('afterend',
+ ``)
+ }
+}
+
+let hudFadeTimeout;
+
+function showVolumeHud(volume) {
+ let volumeHud = $("#volumeHud");
+ if (!volumeHud) return;
+
+ volumeHud.textContent = volume + '%';
+ volumeHud.style.opacity = 1;
+
+ if (hudFadeTimeout) {
+ clearTimeout(hudFadeTimeout);
}
- firstRun(options);
-
- // This way the ipc listener gets cleared either way
- ipcRenderer.once("setupVideoPlayerVolumeMousewheel", (_event, toEnable) => {
- if (toEnable)
- setupVideoPlayerOnwheel(options);
- });
-};
+ hudFadeTimeout = setTimeout(() => {
+ volumeHud.style.opacity = 0;
+ hudFadeTimeout = null;
+ }, 2000);
+}
/** Add onwheel event to video player */
function setupVideoPlayerOnwheel(options) {
@@ -34,35 +80,20 @@ function setupVideoPlayerOnwheel(options) {
});
}
-function toPercent(volume) {
- return Math.round(Number.parseFloat(volume) * 100);
-}
-
function saveVolume(volume, options) {
options.savedVolume = volume;
- setOptions("precise-volume", options);
+ writeOptions(options);
}
-/** Restore saved volume and setup tooltip */
-function firstRun(options) {
- const videoStream = $(".video-stream");
- const slider = $("#volume-slider");
- // Those elements load abit after DOMContentLoaded
- if (videoStream && slider) {
- // Set saved volume IF it pass checks
- if (options.savedVolume
- && options.savedVolume >= 0 && options.savedVolume <= 100
- && Math.abs(slider.value - options.savedVolume) < 5
- // If plugin was disabled and volume changed then diff>4
- ) {
- videoStream.volume = options.savedVolume / 100;
- slider.value = options.savedVolume;
- }
- // Set current volume as tooltip
- setTooltip(toPercent(videoStream.volume));
- } else {
- setTimeout(firstRun, 500, options); // Try again in 500 milliseconds
- }
+//without this function it would rewrite config 20 time when volume change by 20
+let writeTimeout;
+function writeOptions(options) {
+ if (writeTimeout) clearTimeout(writeTimeout);
+
+ writeTimeout = setTimeout(() => {
+ setOptions("precise-volume", options);
+ writeTimeout = null;
+ }, 1500)
}
/** Add onwheel event to play bar and also track if play bar is hovered*/
@@ -83,32 +114,63 @@ function setupPlaybar(options) {
playerbar.addEventListener("mouseleave", () => {
playerbar.classList.remove("on-hover");
});
+
+ setupSliderObserver(options);
+}
+
+/** Save volume + Update the volume tooltip when volume-slider is manually changed */
+function setupSliderObserver(options) {
+ const sliderObserver = new MutationObserver(mutations => {
+ for (const mutation of mutations) {
+ // This checks that volume-slider was manually set
+ if (mutation.oldValue !== mutation.target.value &&
+ (typeof options.savedVolume !== "number" || Math.abs(options.savedVolume - mutation.target.value) > 4)) {
+ // Diff>4 means it was manually set
+ setTooltip(mutation.target.value);
+ saveVolume(mutation.target.value, options);
+ }
+ }
+ });
+
+ // Observing only changes in 'value' of volume-slider
+ sliderObserver.observe($("#volume-slider"), {
+ attributeFilter: ["value"],
+ attributeOldValue: true
+ });
}
/** if (toIncrease = false) then volume decrease */
function changeVolume(toIncrease, options) {
- // Need to change both the actual volume and the slider
- const videoStream = $(".video-stream");
- const slider = $("#volume-slider");
// Apply volume change if valid
- const steps = (options.steps || 1) / 100;
- videoStream.volume = toIncrease ?
- Math.min(videoStream.volume + steps, 1) :
- Math.max(videoStream.volume - steps, 0);
+ const steps = (options.steps || 1);
+ api.setVolume(toIncrease ?
+ Math.min(api.getVolume() + steps, 100) :
+ Math.max(api.getVolume() - steps, 0));
// Save the new volume
- saveVolume(toPercent(videoStream.volume), options);
- // Slider value automatically rounds to multiples of 5
- slider.value = options.savedVolume;
+ saveVolume(api.getVolume(), options);
+
+ // change slider position (important)
+ updateVolumeSlider(options);
+
// Change tooltips to new value
setTooltip(options.savedVolume);
- // Show volume slider on volume change
- showVolumeSlider(slider);
+ // Show volume slider
+ showVolumeSlider();
+ // Show volume HUD
+ showVolumeHud(options.savedVolume);
+}
+
+function updateVolumeSlider(options) {
+ // Slider value automatically rounds to multiples of 5
+ $("#volume-slider").value = options.savedVolume > 0 && options.savedVolume < 5 ?
+ 5 : options.savedVolume;
}
let volumeHoverTimeoutID;
-function showVolumeSlider(slider) {
+function showVolumeSlider() {
+ const slider = $("#volume-slider");
// This class display the volume slider if not in minimized mode
slider.classList.add("on-hover");
// Reset timeout if previous one hasn't completed
@@ -124,27 +186,6 @@ function showVolumeSlider(slider) {
}, 3000);
}
-/** Save volume + Update the volume tooltip when volume-slider is manually changed */
-function setupSliderObserver(options) {
- const sliderObserver = new MutationObserver(mutations => {
- for (const mutation of mutations) {
- // This checks that volume-slider was manually set
- if (mutation.oldValue !== mutation.target.value &&
- (!options.savedVolume || Math.abs(options.savedVolume - mutation.target.value) > 4)) {
- // Diff>4 means it was manually set
- setTooltip(mutation.target.value);
- saveVolume(mutation.target.value, options);
- }
- }
- });
-
- // Observing only changes in 'value' of volume-slider
- sliderObserver.observe($("#volume-slider"), {
- attributeFilter: ["value"],
- attributeOldValue: true
- });
-}
-
// Set new volume as tooltip for volume slider and icon + expanding slider (appears when window size is small)
const tooltipTargets = [
"#volume-slider",
diff --git a/plugins/precise-volume/menu.js b/plugins/precise-volume/menu.js
index d4ed37a7..06ee84b9 100644
--- a/plugins/precise-volume/menu.js
+++ b/plugins/precise-volume/menu.js
@@ -1,13 +1,16 @@
const { enabled } = require("./back");
const { setOptions } = require("../../config/plugins");
+const prompt = require("custom-electron-prompt");
+const promptOptions = require("../../providers/prompt-options");
+
module.exports = (win, options) => [
{
- label: "Arrowkeys controls",
+ label: "Local Arrowkeys Controls",
type: "checkbox",
checked: !!options.arrowsShortcut,
- click: (item) => {
- // Dynamically change setting if plugin enabled
+ click: item => {
+ // Dynamically change setting if plugin is enabled
if (enabled()) {
win.webContents.send("setArrowsShortcut", item.checked);
} else { // Fallback to usual method if disabled
@@ -15,5 +18,61 @@ module.exports = (win, options) => [
setOptions("precise-volume", options);
}
}
+ },
+ {
+ label: "Global Hotkeys",
+ type: "checkbox",
+ checked: !!options.globalShortcuts.volumeUp || !!options.globalShortcuts.volumeDown,
+ click: item => promptGlobalShortcuts(win, options, item)
+ },
+ {
+ label: "Set Custom Volume Steps",
+ click: () => promptVolumeSteps(win, options)
}
];
+
+// Helper function for globalShortcuts prompt
+const kb = (label_, value_, default_) => { return { value: value_, label: label_, default: default_ || undefined }; };
+
+async function promptVolumeSteps(win, options) {
+ const output = await prompt({
+ title: "Volume Steps",
+ label: "Choose Volume Increase/Decrease Steps",
+ value: options.steps || 1,
+ type: "counter",
+ counterOptions: { minimum: 0, maximum: 100, multiFire: true },
+ width: 380,
+ ...promptOptions()
+ }, win)
+
+ if (output || output === 0) { // 0 is somewhat valid
+ options.steps = output;
+ setOptions("precise-volume", options);
+ }
+}
+
+async function promptGlobalShortcuts(win, options, item) {
+ const output = await prompt({
+ title: "Global Volume Keybinds",
+ label: "Choose Global Volume Keybinds:",
+ type: "keybind",
+ keybindOptions: [
+ kb("Increase Volume", "volumeUp", options.globalShortcuts?.volumeUp),
+ kb("Decrease Volume", "volumeDown", options.globalShortcuts?.volumeDown)
+ ],
+ ...promptOptions()
+ }, win)
+
+ if (output) {
+ for (const { value, accelerator } of output) {
+ options.globalShortcuts[value] = accelerator;
+ }
+
+ setOptions("precise-volume", options);
+
+ item.checked = !!options.globalShortcuts.volumeUp || !!options.globalShortcuts.volumeDown;
+ } else {
+ // Reset checkbox if prompt was canceled
+ item.checked = !item.checked;
+ }
+}
diff --git a/plugins/precise-volume/preload.js b/plugins/precise-volume/preload.js
index edc5f20c..6a0fd482 100644
--- a/plugins/precise-volume/preload.js
+++ b/plugins/precise-volume/preload.js
@@ -24,10 +24,10 @@ function overrideAddEventListener() {
module.exports = () => {
overrideAddEventListener();
- // 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
+ // Restore original function after finished loading to avoid keeping Element.prototype altered
+ window.addEventListener('load', () => {
Element.prototype.addEventListener = Element.prototype._addEventListener;
Element.prototype._addEventListener = undefined;
ignored = undefined;
- });
+ }, { once: true });
};
diff --git a/plugins/shortcuts/back.js b/plugins/shortcuts/back.js
index ba12ba42..c32d0f8d 100644
--- a/plugins/shortcuts/back.js
+++ b/plugins/shortcuts/back.js
@@ -1,9 +1,11 @@
const { globalShortcut } = require("electron");
const is = require("electron-is");
const electronLocalshortcut = require("electron-localshortcut");
-
const getSongControls = require("../../providers/song-controls");
const { setupMPRIS } = require("./mpris");
+const registerCallback = require("../../providers/song-info");
+
+let player;
function _registerGlobalShortcut(webContents, shortcut, action) {
globalShortcut.register(shortcut, () => {
@@ -21,47 +23,89 @@ function registerShortcuts(win, options) {
const songControls = getSongControls(win);
const { playPause, next, previous, search } = songControls;
- _registerGlobalShortcut(win.webContents, "MediaPlayPause", playPause);
- _registerGlobalShortcut(win.webContents, "MediaNextTrack", next);
- _registerGlobalShortcut(win.webContents, "MediaPreviousTrack", previous);
+ if (options.overrideMediaKeys) {
+ _registerGlobalShortcut(win.webContents, "MediaPlayPause", playPause);
+ _registerGlobalShortcut(win.webContents, "MediaNextTrack", next);
+ _registerGlobalShortcut(win.webContents, "MediaPreviousTrack", previous);
+ }
+
_registerLocalShortcut(win, "CommandOrControl+F", search);
_registerLocalShortcut(win, "CommandOrControl+L", search);
+ registerCallback(songInfo => {
+ if (player) {
+ player.metadata = {
+ 'mpris:length': songInfo.songDuration * 60 * 1000 * 1000, // In microseconds
+ 'mpris:artUrl': songInfo.imageSrc,
+ 'xesam:title': songInfo.title,
+ 'xesam:artist': songInfo.artist
+ };
+ if (!songInfo.isPaused) {
+ player.playbackStatus = "Playing"
+ }
+ }
+ }
+ )
if (is.linux()) {
try {
- const player = setupMPRIS();
+ const MPRISPlayer = setupMPRIS();
- player.on("raise", () => {
+ MPRISPlayer.on("raise", () => {
win.setSkipTaskbar(false);
win.show();
});
- player.on("playpause", playPause);
- player.on("next", next);
- player.on("previous", previous);
+ MPRISPlayer.on("play", () => {
+ if (MPRISPlayer.playbackStatus !== 'Playing') {
+ MPRISPlayer.playbackStatus = 'Playing';
+ playPause()
+ }
+ });
+ MPRISPlayer.on("pause", () => {
+ if (MPRISPlayer.playbackStatus !== 'Paused') {
+ MPRISPlayer.playbackStatus = 'Paused';
+ playPause()
+ }
+ });
+ MPRISPlayer.on("next", () => {
+ next()
+ });
+ MPRISPlayer.on("previous", () => {
+ previous()
+ });
+
+ player = MPRISPlayer
+
} catch (e) {
console.warn("Error in MPRIS", e);
}
}
const { global, local } = options;
- (global || []).forEach(({ shortcut, action }) => {
- console.debug("Registering global shortcut", shortcut, ":", action);
- if (!action || !songControls[action]) {
- console.warn("Invalid action", action);
- return;
- }
+ const shortcutOptions = { global, local };
- _registerGlobalShortcut(win.webContents, shortcut, songControls[action]);
- });
- (local || []).forEach(({ shortcut, action }) => {
- console.debug("Registering local shortcut", shortcut, ":", action);
- if (!action || !songControls[action]) {
- console.warn("Invalid action", action);
- return;
- }
+ for (const optionType in shortcutOptions) {
+ registerAllShortcuts(shortcutOptions[optionType], optionType);
+ }
- _registerLocalShortcut(win, shortcut, songControls[action]);
- });
+ function registerAllShortcuts(container, type) {
+ for (const action in container) {
+ if (!container[action]) {
+ continue; // Action accelerator is empty
+ }
+
+ console.debug(`Registering ${type} shortcut`, container[action], ":", action);
+ if (!songControls[action]) {
+ console.warn("Invalid action", action);
+ continue;
+ }
+
+ if (type === "global") {
+ _registerGlobalShortcut(win.webContents, container[action], songControls[action]);
+ } else { // type === "local"
+ _registerLocalShortcut(win, local[action], songControls[action]);
+ }
+ }
+ }
}
module.exports = registerShortcuts;
diff --git a/plugins/shortcuts/menu.js b/plugins/shortcuts/menu.js
new file mode 100644
index 00000000..20f21233
--- /dev/null
+++ b/plugins/shortcuts/menu.js
@@ -0,0 +1,53 @@
+const { setOptions } = require("../../config/plugins");
+const prompt = require("custom-electron-prompt");
+const promptOptions = require("../../providers/prompt-options");
+
+module.exports = (win, options) => [
+ {
+ label: "Set Global Song Controls",
+ click: () => promptKeybind(options, win)
+ },
+ {
+ label: "Override MediaKeys",
+ type: "checkbox",
+ checked: options.overrideMediaKeys,
+ click: item => setOption(options, "overrideMediaKeys", item.checked)
+ }
+];
+
+function setOption(options, key = null, newValue = null) {
+ if (key && newValue !== null) {
+ options[key] = newValue;
+ }
+
+ setOptions("shortcuts", options);
+}
+
+// Helper function for keybind prompt
+const kb = (label_, value_, default_) => { return { value: value_, label: label_, default: default_ }; };
+
+async function promptKeybind(options, win) {
+ const output = await prompt({
+ title: "Global Keybinds",
+ label: "Choose Global Keybinds for Songs Control:",
+ type: "keybind",
+ keybindOptions: [ // If default=undefined then no default is used
+ kb("Previous", "previous", options.global?.previous),
+ kb("Play / Pause", "playPause", options.global?.playPause),
+ kb("Next", "next", options.global?.next)
+ ],
+ height: 270,
+ ...promptOptions()
+ }, win);
+
+ if (output) {
+ if (!options.global) {
+ options.global = {};
+ }
+ for (const { value, accelerator } of output) {
+ options.global[value] = accelerator;
+ }
+ setOption(options);
+ }
+ // else -> pressed cancel
+}
diff --git a/preload.js b/preload.js
index 5a09c845..d100e954 100644
--- a/preload.js
+++ b/preload.js
@@ -5,11 +5,12 @@ const { remote } = require("electron");
const config = require("./config");
const { fileExists } = require("./plugins/utils");
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();
+let api;
+
plugins.forEach(([plugin, options]) => {
const preloadPath = path.join(__dirname, "plugins", plugin, "preload.js");
fileExists(preloadPath, () => {
@@ -38,16 +39,46 @@ document.addEventListener("DOMContentLoaded", () => {
});
});
+ // wait for complete load of youtube api
+ listenForApiLoad();
+
// inject song-info provider
setupSongInfo();
- // inject song-control provider
- setupSongControl();
-
// inject front logger
setupFrontLogger();
// Add action for reloading
global.reload = () =>
remote.getCurrentWindow().webContents.loadURL(config.get("url"));
+
+ // Blocks the "Are You Still There?" popup by setting the last active time to Date.now every 15min
+ setInterval(() => window._lact = Date.now(), 900000);
});
+
+function listenForApiLoad() {
+ api = document.querySelector('#movie_player');
+ if (api) {
+ onApiLoaded();
+ return;
+ }
+
+ const observer = new MutationObserver(() => {
+ api = document.querySelector('#movie_player');
+ if (api) {
+ observer.disconnect();
+ onApiLoaded();
+ }
+ })
+
+ observer.observe(document.documentElement, { childList: true, subtree: true });
+}
+
+function onApiLoaded() {
+ document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api }));
+
+ // Remove upgrade button
+ if (config.get("options.removeUpgradeButton")) {
+ document.querySelector('ytmusic-pivot-bar-item-renderer[tab-id="SPunlimited"]')?.style.display = "none";
+ }
+}
diff --git a/providers/prompt-custom-titlebar.js b/providers/prompt-custom-titlebar.js
new file mode 100644
index 00000000..affa9206
--- /dev/null
+++ b/providers/prompt-custom-titlebar.js
@@ -0,0 +1,14 @@
+const customTitlebar = require("custom-electron-titlebar");
+
+module.exports = () => {
+ new customTitlebar.Titlebar({
+ backgroundColor: customTitlebar.Color.fromHex("#050505"),
+ minimizable: false,
+ maximizable: false,
+ menu: null
+ });
+ const mainStyle = document.querySelector("#container").style;
+ mainStyle.width = "100%";
+ mainStyle.position = "fixed";
+ mainStyle.border = "unset";
+};
diff --git a/providers/prompt-options.js b/providers/prompt-options.js
new file mode 100644
index 00000000..16f02a99
--- /dev/null
+++ b/providers/prompt-options.js
@@ -0,0 +1,18 @@
+const path = require("path");
+const is = require("electron-is");
+
+const iconPath = path.join(__dirname, "..", "assets", "youtube-music-tray.png");
+const customTitlebarPath = path.join(__dirname, "prompt-custom-titlebar.js");
+
+const promptOptions = is.macOS() ? {
+ customStylesheet: "dark",
+ icon: iconPath
+} : {
+ customStylesheet: "dark",
+ // The following are used for custom titlebar
+ frame: false,
+ customScript: customTitlebarPath,
+ enableRemoteModule: true
+};
+
+module.exports = () => promptOptions;
diff --git a/providers/song-controls-front.js b/providers/song-controls-front.js
deleted file mode 100644
index 1860e761..00000000
--- a/providers/song-controls-front.js
+++ /dev/null
@@ -1,18 +0,0 @@
-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();
- }
- });
-};
diff --git a/providers/song-controls.js b/providers/song-controls.js
index 5d3ad953..7f43df11 100644
--- a/providers/song-controls.js
+++ b/providers/song-controls.js
@@ -12,7 +12,7 @@ module.exports = (win) => {
// Playback
previous: () => pressKey(win, "k"),
next: () => pressKey(win, "j"),
- playPause: () => win.webContents.send("playPause"),
+ playPause: () => pressKey(win, "space"),
like: () => pressKey(win, "_"),
dislike: () => pressKey(win, "+"),
go10sBack: () => pressKey(win, "h"),
diff --git a/readme.md b/readme.md
index 4a704638..e6e57401 100644
--- a/readme.md
+++ b/readme.md
@@ -35,7 +35,6 @@ Install the `youtube-music-bin` package from the AUR. For AUR installation instr
## Available plugins:
- **Ad Blocker**: block all ads and tracking out of the box
-- **Auto confirm when paused**: when the ["Continue Watching?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png) modal appears, automatically click "Yes"
- **Blur navigation bar**: makes navigation bar transparent and blurry
- **Disable autoplay**: makes every song start in "paused" mode
- [**Discord**](https://discord.com/): show your friends what you listen to with [Rich Presence](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png)
@@ -52,6 +51,7 @@ Install the `youtube-music-bin` package from the AUR. For AUR installation instr
- [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock): skips non-music parts
- **Taskbar media control**: control app from your [Windows taskbar](https://user-images.githubusercontent.com/78568641/111916130-24a35e80-8a82-11eb-80c8-5021c1aa27f4.png)
- **Touchbar**: custom TouchBar layout for macOS
+- **Auto confirm when paused** (Always Enabled): disable the ["Continue Watching?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png) popup that pause music after a certain time
## Dev
diff --git a/yarn.lock b/yarn.lock
index 74e407dc..8230315b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1518,10 +1518,6 @@
dependencies:
"@wdio/logger" "6.10.10"
-"YoutubeNonStop@git://github.com/lawfx/YoutubeNonStop.git#v0.9.0":
- version "0.0.0"
- resolved "git://github.com/lawfx/YoutubeNonStop.git#7b6b97b31bb3fd2078179660db0fd3fcc7062259"
-
abab@^2.0.3:
version "2.0.5"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
@@ -2960,6 +2956,11 @@ cssstyle@^2.2.0:
dependencies:
cssom "~0.3.6"
+custom-electron-prompt@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/custom-electron-prompt/-/custom-electron-prompt-1.1.0.tgz#611b790047c91f6b532c7861355a0e1f9a81aef2"
+ integrity sha512-YZYmwZnMOdoWROUlJ+rEMHYsp4XJNNqLj6sZnx5aKBJ8cprEjKP4L5wfo6U+yyX/L9fxVOtvYD0Mp8ki5I9Kow==
+
custom-electron-titlebar@^3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/custom-electron-titlebar/-/custom-electron-titlebar-3.2.7.tgz#fb249d6180cbda074b1d392bea755fa0743012a8"
diff --git a/youtube-music.css b/youtube-music.css
index fe5eb326..67a74dc0 100644
--- a/youtube-music.css
+++ b/youtube-music.css
@@ -28,3 +28,9 @@ ytmusic-search-box.ytmusic-nav-bar {
ytmusic-mealbar-promo-renderer {
display: none !important;
}
+
+/* Disable Image Selection */
+img {
+ -webkit-user-select: none;
+ user-select: none;
+}