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..3653de1b 100644
--- a/config/store.js
+++ b/config/store.js
@@ -3,11 +3,39 @@ const Store = require("electron-store");
const defaults = require("./defaults");
const migrations = {
+ ">=1.14.0": (store) => {
+ if (
+ typeof store.get("plugins.precise-volume.globalShortcuts") !== "object"
+ ) {
+ store.set("plugins.precise-volume.globalShortcuts", {});
+ }
+ },
">=1.13.0": (store) => {
if (store.get("plugins.discord.listenAlong") === undefined) {
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..aa4e39e5 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "youtube-music",
"productName": "YouTube Music",
- "version": "1.13.0",
+ "version": "1.14.0",
"description": "YouTube Music Desktop App - including custom plugins",
"license": "MIT",
"repository": "th-ch/youtube-music",
@@ -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/blur-nav-bar/back.js b/plugins/blur-nav-bar/back.js
new file mode 100644
index 00000000..1c95289f
--- /dev/null
+++ b/plugins/blur-nav-bar/back.js
@@ -0,0 +1,6 @@
+const path = require("path");
+const { injectCSS } = require("../utils");
+
+module.exports = win => {
+ injectCSS(win.webContents, path.join(__dirname, "style.css"));
+};
diff --git a/plugins/blur-nav-bar/style.css b/plugins/blur-nav-bar/style.css
new file mode 100644
index 00000000..e64b7526
--- /dev/null
+++ b/plugins/blur-nav-bar/style.css
@@ -0,0 +1,8 @@
+#nav-bar-background {
+ background: rgba(0, 0, 0, 0.3) !important;
+ backdrop-filter: blur(18px) !important;
+}
+
+#nav-bar-divider {
+ display: none !important;
+}
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 67ae8f7c..6c7c1b16 100644
--- a/preload.js
+++ b/preload.js
@@ -5,7 +5,6 @@ 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();
@@ -46,15 +45,15 @@ document.addEventListener("DOMContentLoaded", () => {
// 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() {
@@ -77,4 +76,12 @@ function listenForApiLoad() {
function onApiLoaded() {
document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api }));
+
+ // Remove upgrade button
+ if (config.get("options.removeUpgradeButton")) {
+ const upgradeButtton = document.querySelector('ytmusic-pivot-bar-item-renderer[tab-id="SPunlimited"]')
+ if (upgradeButtton) {
+ upgradeButtton.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/providers/song-info-front.js b/providers/song-info-front.js
index fccc54a8..80c2e990 100644
--- a/providers/song-info-front.js
+++ b/providers/song-info-front.js
@@ -9,23 +9,11 @@ ipcRenderer.on("update-song-info", async (_, extractedSongInfo) => {
global.songInfo.image = await getImage(global.songInfo.imageSrc);
});
-const injectListener = () => {
- const oldXHR = window.XMLHttpRequest;
- function newXHR() {
- const realXHR = new oldXHR();
- realXHR.addEventListener(
- "readystatechange",
- () => {
- if (realXHR.readyState === 4 && realXHR.status === 200
- && realXHR.responseURL.includes("/player")) {
- // if the request contains the song info, send the response to ipcMain
- ipcRenderer.send("song-info-request", realXHR.responseText);
- }
- },
- false
- );
- return realXHR;
- }
- window.XMLHttpRequest = newXHR;
+module.exports = () => {
+ document.addEventListener('apiLoaded', e => {
+ document.querySelector('video').addEventListener('loadeddata', () => {
+ const data = e.detail.getPlayerResponse();
+ ipcRenderer.send("song-info-request", JSON.stringify(data));
+ });
+ })
};
-module.exports = injectListener;
diff --git a/providers/song-info.js b/providers/song-info.js
index 6e5a119c..3f4ad435 100644
--- a/providers/song-info.js
+++ b/providers/song-info.js
@@ -2,15 +2,11 @@ const { ipcMain, nativeImage } = require("electron");
const fetch = require("node-fetch");
-// This selects the progress bar, used for current progress
-const progressSelector = "#progress-bar";
-
-
// Grab the progress using the selector
const getProgress = async (win) => {
// Get current value of the progressbar element
return win.webContents.executeJavaScript(
- 'document.querySelector("' + progressSelector + '").value'
+ 'document.querySelector("#progress-bar").value'
);
};
@@ -32,13 +28,6 @@ const getPausedStatus = async (win) => {
return !title.includes("-");
};
-const getArtist = async (win) => {
- return win.webContents.executeJavaScript(`
- document.querySelector(".subtitle.ytmusic-player-bar .yt-formatted-string")
- ?.textContent
- `);
-}
-
// Fill songInfo with empty values
/**
* @typedef {songInfo} SongInfo
@@ -59,14 +48,13 @@ const songInfo = {
const handleData = async (responseText, win) => {
let data = JSON.parse(responseText);
songInfo.title = cleanupName(data?.videoDetails?.title);
- songInfo.artist =
- (await getArtist(win)) || cleanupName(data?.videoDetails?.author);
+ songInfo.artist =cleanupName(data?.videoDetails?.author);
songInfo.views = data?.videoDetails?.viewCount;
songInfo.imageSrc = data?.videoDetails?.thumbnail?.thumbnails?.pop()?.url;
songInfo.songDuration = data?.videoDetails?.lengthSeconds;
songInfo.image = await getImage(songInfo.imageSrc);
songInfo.uploadDate = data?.microformat?.microformatDataRenderer?.uploadDate;
- songInfo.url = data?.microformat?.microformatDataRenderer?.urlCanonical;
+ songInfo.url = data?.microformat?.microformatDataRenderer?.urlCanonical?.split("&")[0];
win.webContents.send("update-song-info", JSON.stringify(songInfo));
};
@@ -111,23 +99,23 @@ const registerProvider = (win) => {
};
const suffixesToRemove = [
- " - Topic",
- "VEVO",
- " (Performance Video)",
- " (Official Music Video)",
- " (Official Video)",
- " (Clip officiel)",
+ " - topic",
+ "vevo",
+ " (performance video)",
+ " (official music video)",
+ " (official video)",
+ " (clip officiel)",
];
-function cleanupName(artist) {
- if (!artist) {
- return artist;
- }
+
+function cleanupName(name) {
+ if (!name) return name;
+ const lowCaseName = name.toLowerCase();
for (const suffix of suffixesToRemove) {
- if (artist.endsWith(suffix)) {
- return artist.slice(0, -suffix.length);
+ if (lowCaseName.endsWith(suffix)) {
+ return name.slice(0, -suffix.length);
}
}
- return artist;
+ return name;
}
module.exports = registerCallback;
diff --git a/readme.md b/readme.md
index c47170c1..e6e57401 100644
--- a/readme.md
+++ b/readme.md
@@ -35,7 +35,7 @@ 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)
- **Downloader**: downloads MP3 [directly from the interface](https://user-images.githubusercontent.com/61631665/129977677-83a7d067-c192-45e1-98ae-b5a4927393be.png) [(youtube-dl)](https://github.com/ytdl-org/youtube-dl)
@@ -51,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;
+}