Merge branch 'master' into quality-changer-plugin

This commit is contained in:
th-ch
2021-10-24 14:03:40 +02:00
committed by GitHub
27 changed files with 503 additions and 221 deletions

View File

@ -30,6 +30,7 @@ const defaultConfig = {
// Disabled plugins // Disabled plugins
shortcuts: { shortcuts: {
enabled: false, enabled: false,
overrideMediaKeys: false,
}, },
downloader: { downloader: {
enabled: false, enabled: false,
@ -59,9 +60,8 @@ const defaultConfig = {
steps: 1, //percentage of volume to change steps: 1, //percentage of volume to change
arrowsShortcut: true, //enable ArrowUp + ArrowDown local shortcuts arrowsShortcut: true, //enable ArrowUp + ArrowDown local shortcuts
globalShortcuts: { globalShortcuts: {
enabled: false, // enable global shortcuts volumeUp: "",
volumeUp: "Shift+PageUp", // Keybind default can be changed volumeDown: ""
volumeDown: "Shift+PageDown"
}, },
savedVolume: undefined //plugin save volume between session here savedVolume: undefined //plugin save volume between session here
}, },

View File

@ -3,11 +3,39 @@ const Store = require("electron-store");
const defaults = require("./defaults"); const defaults = require("./defaults");
const migrations = { 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) => { ">=1.13.0": (store) => {
if (store.get("plugins.discord.listenAlong") === undefined) { if (store.get("plugins.discord.listenAlong") === undefined) {
store.set("plugins.discord.listenAlong", true); 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) => { ">=1.11.0": (store) => {
if (store.get("options.resumeOnStart") === undefined) { if (store.get("options.resumeOnStart") === undefined) {
store.set("options.resumeOnStart", true); store.set("options.resumeOnStart", true);

View File

@ -39,7 +39,9 @@ if (config.get("options.proxy")) {
} }
// Adds debug features like hotkeys for triggering dev tools and reload // 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 // Prevent window being garbage collected
let mainWindow; let mainWindow;
@ -60,7 +62,7 @@ function onClosed() {
function loadPlugins(win) { function loadPlugins(win) {
injectCSS(win.webContents, path.join(__dirname, "youtube-music.css")); injectCSS(win.webContents, path.join(__dirname, "youtube-music.css"));
win.webContents.on("did-finish-load", () => { win.webContents.once("did-finish-load", () => {
if (is.dev()) { if (is.dev()) {
console.log("did finish load"); console.log("did finish load");
win.webContents.openDevTools(); win.webContents.openDevTools();

41
menu.js
View File

@ -7,6 +7,9 @@ const is = require("electron-is");
const { getAllPlugins } = require("./plugins/utils"); const { getAllPlugins } = require("./plugins/utils");
const config = require("./config"); 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 // true only if in-app-menu was loaded on launch
const inAppMenuActive = config.plugins.isEnabled("in-app-menu"); const inAppMenuActive = config.plugins.isEnabled("in-app-menu");
@ -76,6 +79,14 @@ const mainMenuTemplate = (win) => {
config.set("options.resumeOnStart", item.checked); 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() ...(is.windows() || is.linux()
? [ ? [
{ {
@ -149,6 +160,14 @@ const mainMenuTemplate = (win) => {
{ {
label: "Advanced options", label: "Advanced options",
submenu: [ submenu: [
{
label: "Proxy",
type: "checkbox",
checked: !!config.get("options.proxy"),
click: (item) => {
setProxy(item, win);
},
},
{ {
label: "Disable hardware acceleration", label: "Disable hardware acceleration",
type: "checkbox", type: "checkbox",
@ -275,3 +294,25 @@ module.exports.setApplicationMenu = (win) => {
const menu = Menu.buildFromTemplate(menuTemplate); const menu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menu); 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
}
}

View File

@ -1,7 +1,7 @@
{ {
"name": "youtube-music", "name": "youtube-music",
"productName": "YouTube Music", "productName": "YouTube Music",
"version": "1.13.0", "version": "1.14.0",
"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",
@ -52,9 +52,8 @@
"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", "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: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:linux": "yarn run clean && electron-builder --linux -p always -c.snap.publish=github",
"release:mac": "yarn run clean && electron-builder --mac -p always", "release:mac": "yarn run clean && electron-builder --mac -p always",
"release:win": "yarn run clean && electron-builder --win -p always" "release:win": "yarn run clean && electron-builder --win -p always"
@ -67,9 +66,9 @@
"@cliqz/adblocker-electron": "^1.22.6", "@cliqz/adblocker-electron": "^1.22.6",
"@ffmpeg/core": "^0.10.0", "@ffmpeg/core": "^0.10.0",
"@ffmpeg/ffmpeg": "^0.10.0", "@ffmpeg/ffmpeg": "^0.10.0",
"YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.9.0",
"async-mutex": "^0.3.2", "async-mutex": "^0.3.2",
"browser-id3-writer": "^4.4.0", "browser-id3-writer": "^4.4.0",
"custom-electron-prompt": "^1.1.0",
"chokidar": "^3.5.2", "chokidar": "^3.5.2",
"custom-electron-titlebar": "^3.2.7", "custom-electron-titlebar": "^3.2.7",
"discord-rpc": "^3.2.0", "discord-rpc": "^3.2.0",

View File

@ -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;

View File

@ -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");
};

View File

@ -0,0 +1,6 @@
const path = require("path");
const { injectCSS } = require("../utils");
module.exports = win => {
injectCSS(win.webContents, path.join(__dirname, "style.css"));
};

View File

@ -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;
}

View File

@ -145,7 +145,4 @@ module.exports.clear = () => {
}; };
module.exports.connect = connect; module.exports.connect = connect;
module.exports.registerRefresh = (cb) => refreshCallbacks.push(cb); module.exports.registerRefresh = (cb) => refreshCallbacks.push(cb);
/** module.exports.isConnected = () => info.rpc !== null;
* @type {Info}
*/
module.exports.info = Object.defineProperties({}, Object.keys(info).reduce((o, k) => ({ ...o, [k]: { enumerable: true, get: () => info[k] } }), {}));

View File

@ -1,6 +1,6 @@
const { setOptions } = require("../../config/plugins"); const { setOptions } = require("../../config/plugins");
const { edit } = require("../../config"); const { edit } = require("../../config");
const { clear, info, connect, registerRefresh } = require("./back"); const { clear, connect, registerRefresh, isConnected } = require("./back");
let hasRegisterred = false; let hasRegisterred = false;
@ -12,8 +12,8 @@ module.exports = (win, options, refreshMenu) => {
return [ return [
{ {
label: info.rpc !== null ? "Connected" : "Reconnect", label: isConnected() ? "Connected" : "Reconnect",
enabled: info.rpc === null, enabled: !isConnected(),
click: connect, click: connect,
}, },
{ {

View File

@ -1,23 +1,9 @@
const { isEnabled } = require("../../config/plugins");
/* /*
This is used to determine if plugin is actually active This is used to determine if plugin is actually active
(not if its only enabled in options) (not if its only enabled in options)
*/ */
let enabled = false; let enabled = false;
module.exports = (win) => { module.exports = () => enabled = true;
enabled = true;
// youtube-music register some of the target listeners after DOMContentLoaded module.exports.enabled = () => enabled;
// 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;
};

View File

@ -3,27 +3,73 @@ const { ipcRenderer, remote } = require("electron");
const { setOptions } = require("../../config/plugins"); const { setOptions } = require("../../config/plugins");
function $(selector) { return document.querySelector(selector); } function $(selector) { return document.querySelector(selector); }
let api;
module.exports = (options) => { 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); setupPlaybar(options);
setupSliderObserver(options);
setupLocalArrowShortcuts(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",
`<span id="volumeHud" style="${position + mainStyle}"></span>`)
} 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',
`<span id="volumeHud" style="${position + mainStyle}"></span>`)
}
}
let hudFadeTimeout;
function showVolumeHud(volume) {
let volumeHud = $("#volumeHud");
if (!volumeHud) return;
volumeHud.textContent = volume + '%';
volumeHud.style.opacity = 1;
if (hudFadeTimeout) {
clearTimeout(hudFadeTimeout);
} }
firstRun(options); hudFadeTimeout = setTimeout(() => {
volumeHud.style.opacity = 0;
// This way the ipc listener gets cleared either way hudFadeTimeout = null;
ipcRenderer.once("setupVideoPlayerVolumeMousewheel", (_event, toEnable) => { }, 2000);
if (toEnable) }
setupVideoPlayerOnwheel(options);
});
};
/** Add onwheel event to video player */ /** Add onwheel event to video player */
function setupVideoPlayerOnwheel(options) { function setupVideoPlayerOnwheel(options) {
@ -34,35 +80,20 @@ function setupVideoPlayerOnwheel(options) {
}); });
} }
function toPercent(volume) {
return Math.round(Number.parseFloat(volume) * 100);
}
function saveVolume(volume, options) { function saveVolume(volume, options) {
options.savedVolume = volume; options.savedVolume = volume;
setOptions("precise-volume", options); writeOptions(options);
} }
/** Restore saved volume and setup tooltip */ //without this function it would rewrite config 20 time when volume change by 20
function firstRun(options) { let writeTimeout;
const videoStream = $(".video-stream"); function writeOptions(options) {
const slider = $("#volume-slider"); if (writeTimeout) clearTimeout(writeTimeout);
// Those elements load abit after DOMContentLoaded
if (videoStream && slider) { writeTimeout = setTimeout(() => {
// Set saved volume IF it pass checks setOptions("precise-volume", options);
if (options.savedVolume writeTimeout = null;
&& options.savedVolume >= 0 && options.savedVolume <= 100 }, 1500)
&& 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
}
} }
/** Add onwheel event to play bar and also track if play bar is hovered*/ /** 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.addEventListener("mouseleave", () => {
playerbar.classList.remove("on-hover"); 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 */ /** if (toIncrease = false) then volume decrease */
function changeVolume(toIncrease, options) { 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 // Apply volume change if valid
const steps = (options.steps || 1) / 100; const steps = (options.steps || 1);
videoStream.volume = toIncrease ? api.setVolume(toIncrease ?
Math.min(videoStream.volume + steps, 1) : Math.min(api.getVolume() + steps, 100) :
Math.max(videoStream.volume - steps, 0); Math.max(api.getVolume() - steps, 0));
// Save the new volume // Save the new volume
saveVolume(toPercent(videoStream.volume), options); saveVolume(api.getVolume(), options);
// Slider value automatically rounds to multiples of 5
slider.value = options.savedVolume; // change slider position (important)
updateVolumeSlider(options);
// Change tooltips to new value // Change tooltips to new value
setTooltip(options.savedVolume); setTooltip(options.savedVolume);
// Show volume slider on volume change // Show volume slider
showVolumeSlider(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; let volumeHoverTimeoutID;
function showVolumeSlider(slider) { function showVolumeSlider() {
const slider = $("#volume-slider");
// This class display the volume slider if not in minimized mode // This class display the volume slider if not in minimized mode
slider.classList.add("on-hover"); slider.classList.add("on-hover");
// Reset timeout if previous one hasn't completed // Reset timeout if previous one hasn't completed
@ -124,27 +186,6 @@ function showVolumeSlider(slider) {
}, 3000); }, 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) // Set new volume as tooltip for volume slider and icon + expanding slider (appears when window size is small)
const tooltipTargets = [ const tooltipTargets = [
"#volume-slider", "#volume-slider",

View File

@ -1,13 +1,16 @@
const { enabled } = require("./back"); const { enabled } = require("./back");
const { setOptions } = require("../../config/plugins"); const { setOptions } = require("../../config/plugins");
const prompt = require("custom-electron-prompt");
const promptOptions = require("../../providers/prompt-options");
module.exports = (win, options) => [ module.exports = (win, options) => [
{ {
label: "Arrowkeys controls", label: "Local Arrowkeys Controls",
type: "checkbox", type: "checkbox",
checked: !!options.arrowsShortcut, checked: !!options.arrowsShortcut,
click: (item) => { click: item => {
// Dynamically change setting if plugin enabled // Dynamically change setting if plugin is enabled
if (enabled()) { if (enabled()) {
win.webContents.send("setArrowsShortcut", item.checked); win.webContents.send("setArrowsShortcut", item.checked);
} else { // Fallback to usual method if disabled } else { // Fallback to usual method if disabled
@ -15,5 +18,61 @@ module.exports = (win, options) => [
setOptions("precise-volume", 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;
}
}

View File

@ -24,10 +24,10 @@ function overrideAddEventListener() {
module.exports = () => { module.exports = () => {
overrideAddEventListener(); overrideAddEventListener();
// Restore original function after did-finish-load to avoid keeping Element.prototype altered // Restore original function after finished loading to avoid keeping Element.prototype altered
ipcRenderer.once("restoreAddEventListener", () => { // Called from main to make sure page is completly loaded window.addEventListener('load', () => {
Element.prototype.addEventListener = Element.prototype._addEventListener; Element.prototype.addEventListener = Element.prototype._addEventListener;
Element.prototype._addEventListener = undefined; Element.prototype._addEventListener = undefined;
ignored = undefined; ignored = undefined;
}); }, { once: true });
}; };

View File

@ -1,9 +1,11 @@
const { globalShortcut } = require("electron"); const { globalShortcut } = require("electron");
const is = require("electron-is"); const is = require("electron-is");
const electronLocalshortcut = require("electron-localshortcut"); const electronLocalshortcut = require("electron-localshortcut");
const getSongControls = require("../../providers/song-controls"); const getSongControls = require("../../providers/song-controls");
const { setupMPRIS } = require("./mpris"); const { setupMPRIS } = require("./mpris");
const registerCallback = require("../../providers/song-info");
let player;
function _registerGlobalShortcut(webContents, shortcut, action) { function _registerGlobalShortcut(webContents, shortcut, action) {
globalShortcut.register(shortcut, () => { globalShortcut.register(shortcut, () => {
@ -21,47 +23,89 @@ function registerShortcuts(win, options) {
const songControls = getSongControls(win); const songControls = getSongControls(win);
const { playPause, next, previous, search } = songControls; const { playPause, next, previous, search } = songControls;
_registerGlobalShortcut(win.webContents, "MediaPlayPause", playPause); if (options.overrideMediaKeys) {
_registerGlobalShortcut(win.webContents, "MediaNextTrack", next); _registerGlobalShortcut(win.webContents, "MediaPlayPause", playPause);
_registerGlobalShortcut(win.webContents, "MediaPreviousTrack", previous); _registerGlobalShortcut(win.webContents, "MediaNextTrack", next);
_registerGlobalShortcut(win.webContents, "MediaPreviousTrack", previous);
}
_registerLocalShortcut(win, "CommandOrControl+F", search); _registerLocalShortcut(win, "CommandOrControl+F", search);
_registerLocalShortcut(win, "CommandOrControl+L", 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()) { if (is.linux()) {
try { try {
const player = setupMPRIS(); const MPRISPlayer = setupMPRIS();
player.on("raise", () => { MPRISPlayer.on("raise", () => {
win.setSkipTaskbar(false); win.setSkipTaskbar(false);
win.show(); win.show();
}); });
player.on("playpause", playPause); MPRISPlayer.on("play", () => {
player.on("next", next); if (MPRISPlayer.playbackStatus !== 'Playing') {
player.on("previous", previous); 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) { } catch (e) {
console.warn("Error in MPRIS", e); console.warn("Error in MPRIS", e);
} }
} }
const { global, local } = options; const { global, local } = options;
(global || []).forEach(({ shortcut, action }) => { const shortcutOptions = { global, local };
console.debug("Registering global shortcut", shortcut, ":", action);
if (!action || !songControls[action]) {
console.warn("Invalid action", action);
return;
}
_registerGlobalShortcut(win.webContents, shortcut, songControls[action]); for (const optionType in shortcutOptions) {
}); registerAllShortcuts(shortcutOptions[optionType], optionType);
(local || []).forEach(({ shortcut, action }) => { }
console.debug("Registering local shortcut", shortcut, ":", action);
if (!action || !songControls[action]) {
console.warn("Invalid action", action);
return;
}
_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; module.exports = registerShortcuts;

53
plugins/shortcuts/menu.js Normal file
View File

@ -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
}

View File

@ -5,7 +5,6 @@ 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 setupSongInfo = require("./providers/song-info-front");
const plugins = config.plugins.getEnabled(); const plugins = config.plugins.getEnabled();
@ -46,15 +45,15 @@ document.addEventListener("DOMContentLoaded", () => {
// inject song-info provider // inject song-info provider
setupSongInfo(); setupSongInfo();
// inject song-control provider
setupSongControl();
// inject front logger // inject front logger
setupFrontLogger(); setupFrontLogger();
// Add action for reloading // Add action for reloading
global.reload = () => global.reload = () =>
remote.getCurrentWindow().webContents.loadURL(config.get("url")); 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() { function listenForApiLoad() {
@ -77,4 +76,12 @@ function listenForApiLoad() {
function onApiLoaded() { function onApiLoaded() {
document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api })); 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";
}
}
} }

View File

@ -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";
};

View File

@ -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;

View File

@ -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();
}
});
};

View File

@ -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: () => win.webContents.send("playPause"), playPause: () => pressKey(win, "space"),
like: () => pressKey(win, "_"), like: () => pressKey(win, "_"),
dislike: () => pressKey(win, "+"), dislike: () => pressKey(win, "+"),
go10sBack: () => pressKey(win, "h"), go10sBack: () => pressKey(win, "h"),

View File

@ -9,23 +9,11 @@ ipcRenderer.on("update-song-info", async (_, extractedSongInfo) => {
global.songInfo.image = await getImage(global.songInfo.imageSrc); global.songInfo.image = await getImage(global.songInfo.imageSrc);
}); });
const injectListener = () => { module.exports = () => {
const oldXHR = window.XMLHttpRequest; document.addEventListener('apiLoaded', e => {
function newXHR() { document.querySelector('video').addEventListener('loadeddata', () => {
const realXHR = new oldXHR(); const data = e.detail.getPlayerResponse();
realXHR.addEventListener( ipcRenderer.send("song-info-request", JSON.stringify(data));
"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 = injectListener;

View File

@ -2,15 +2,11 @@ const { ipcMain, nativeImage } = require("electron");
const fetch = require("node-fetch"); const fetch = require("node-fetch");
// This selects the progress bar, used for current progress
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
return win.webContents.executeJavaScript( return win.webContents.executeJavaScript(
'document.querySelector("' + progressSelector + '").value' 'document.querySelector("#progress-bar").value'
); );
}; };
@ -32,13 +28,6 @@ const getPausedStatus = async (win) => {
return !title.includes("-"); 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 // Fill songInfo with empty values
/** /**
* @typedef {songInfo} SongInfo * @typedef {songInfo} SongInfo
@ -59,14 +48,13 @@ const songInfo = {
const handleData = async (responseText, win) => { const handleData = async (responseText, win) => {
let data = JSON.parse(responseText); let data = JSON.parse(responseText);
songInfo.title = cleanupName(data?.videoDetails?.title); songInfo.title = cleanupName(data?.videoDetails?.title);
songInfo.artist = songInfo.artist =cleanupName(data?.videoDetails?.author);
(await getArtist(win)) || cleanupName(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;
songInfo.image = await getImage(songInfo.imageSrc); songInfo.image = await getImage(songInfo.imageSrc);
songInfo.uploadDate = data?.microformat?.microformatDataRenderer?.uploadDate; 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)); win.webContents.send("update-song-info", JSON.stringify(songInfo));
}; };
@ -111,23 +99,23 @@ const registerProvider = (win) => {
}; };
const suffixesToRemove = [ const suffixesToRemove = [
" - Topic", " - topic",
"VEVO", "vevo",
" (Performance Video)", " (performance video)",
" (Official Music Video)", " (official music video)",
" (Official Video)", " (official video)",
" (Clip officiel)", " (clip officiel)",
]; ];
function cleanupName(artist) {
if (!artist) { function cleanupName(name) {
return artist; if (!name) return name;
} const lowCaseName = name.toLowerCase();
for (const suffix of suffixesToRemove) { for (const suffix of suffixesToRemove) {
if (artist.endsWith(suffix)) { if (lowCaseName.endsWith(suffix)) {
return artist.slice(0, -suffix.length); return name.slice(0, -suffix.length);
} }
} }
return artist; return name;
} }
module.exports = registerCallback; module.exports = registerCallback;

View File

@ -35,7 +35,7 @@ Install the `youtube-music-bin` package from the AUR. For AUR installation instr
## Available plugins: ## Available plugins:
- **Ad Blocker**: block all ads and tracking out of the box - **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 - **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) - [**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) - **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 - [**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) - **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 - **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 ## Dev

View File

@ -1518,10 +1518,6 @@
dependencies: dependencies:
"@wdio/logger" "6.10.10" "@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: abab@^2.0.3:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
@ -2960,6 +2956,11 @@ cssstyle@^2.2.0:
dependencies: dependencies:
cssom "~0.3.6" 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: custom-electron-titlebar@^3.2.7:
version "3.2.7" version "3.2.7"
resolved "https://registry.yarnpkg.com/custom-electron-titlebar/-/custom-electron-titlebar-3.2.7.tgz#fb249d6180cbda074b1d392bea755fa0743012a8" resolved "https://registry.yarnpkg.com/custom-electron-titlebar/-/custom-electron-titlebar-3.2.7.tgz#fb249d6180cbda074b1d392bea755fa0743012a8"

View File

@ -28,3 +28,9 @@ ytmusic-search-box.ytmusic-nav-bar {
ytmusic-mealbar-promo-renderer { ytmusic-mealbar-promo-renderer {
display: none !important; display: none !important;
} }
/* Disable Image Selection */
img {
-webkit-user-select: none;
user-select: none;
}