Files
youtube-music/index.js
Araxeus c3d90d8b27 update Electron
+ update electron-unhandled
2022-04-07 22:09:54 +03:00

480 lines
14 KiB
JavaScript

"use strict";
const path = require("path");
const electron = require("electron");
const enhanceWebRequest = require("electron-better-web-request").default;
const is = require("electron-is");
const unhandled = require("electron-unhandled");
const { autoUpdater } = require("electron-updater");
const config = require("./config");
const { setApplicationMenu } = require("./menu");
const { fileExists, injectCSS } = require("./plugins/utils");
const { isTesting } = require("./utils/testing");
const { setUpTray } = require("./tray");
const { setupSongInfo } = require("./providers/song-info");
const { setupAppControls, restart } = require("./providers/app-controls");
// Catch errors and log them
unhandled({
logger: console.error,
showDialog: false,
});
// Disable Node options if the env var is set
process.env.NODE_OPTIONS = "";
const app = electron.app;
// Prevent window being garbage collected
let mainWindow;
autoUpdater.autoDownload = false;
if(config.get("options.singleInstanceLock")){
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) app.quit();
app.on('second-instance', () => {
if (!mainWindow) return;
if (mainWindow.isMinimized()) mainWindow.restore();
if (!mainWindow.isVisible()) mainWindow.show();
mainWindow.focus();
});
}
app.commandLine.appendSwitch(
"js-flags",
// WebAssembly flags
"--experimental-wasm-threads"
);
app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer"); // Required for downloader
app.allowRendererProcessReuse = true; // https://github.com/electron/electron/issues/18397
if (config.get("options.disableHardwareAcceleration")) {
if (is.dev()) {
console.log("Disabling hardware acceleration");
}
app.disableHardwareAcceleration();
}
if (is.linux() && config.plugins.isEnabled("shortcuts")) {
//stops chromium from launching it's own mpris service
app.commandLine.appendSwitch('disable-features', 'MediaSessionService');
}
if (config.get("options.proxy")) {
app.commandLine.appendSwitch("proxy-server", config.get("options.proxy"));
}
// Adds debug features like hotkeys for triggering dev tools and reload
require("electron-debug")({
showDevTools: false //disable automatic devTools on new window
});
let icon = "assets/youtube-music.png";
if (process.platform == "win32") {
icon = "assets/generated/icon.ico";
} else if (process.platform == "darwin") {
icon = "assets/generated/icon.icns";
}
function onClosed() {
// Dereference the window
// For multiple windows store them in an array
mainWindow = null;
}
function loadPlugins(win) {
injectCSS(win.webContents, path.join(__dirname, "youtube-music.css"));
win.webContents.once("did-finish-load", () => {
if (is.dev()) {
console.log("did finish load");
win.webContents.openDevTools();
}
});
config.plugins.getEnabled().forEach(([plugin, options]) => {
console.log("Loaded plugin - " + plugin);
const pluginPath = path.join(__dirname, "plugins", plugin, "back.js");
fileExists(pluginPath, () => {
const handle = require(pluginPath);
handle(win, options);
});
});
}
function createMainWindow() {
const windowSize = config.get("window-size");
const windowMaximized = config.get("window-maximized");
const windowPosition = config.get("window-position");
const useInlineMenu = config.plugins.isEnabled("in-app-menu");
const win = new electron.BrowserWindow({
icon: icon,
width: windowSize.width,
height: windowSize.height,
backgroundColor: "#000",
show: false,
webPreferences: {
// TODO: re-enable contextIsolation once it can work with ffmepg.wasm
// Possible bundling? https://github.com/ffmpegwasm/ffmpeg.wasm/issues/126
contextIsolation: false,
preload: path.join(__dirname, "preload.js"),
nodeIntegrationInSubFrames: true,
affinity: "main-window", // main window, and addition windows should work in one process
...(isTesting()
? {
// Only necessary when testing with Spectron
contextIsolation: false,
nodeIntegration: true,
}
: undefined),
},
frame: !is.macOS() && !useInlineMenu,
titleBarStyle: useInlineMenu
? "hidden"
: is.macOS()
? "hiddenInset"
: "default",
autoHideMenuBar: config.get("options.hideMenu"),
});
if (windowPosition) {
const { x, y } = windowPosition;
const winSize = win.getSize();
const displaySize =
electron.screen.getDisplayNearestPoint(windowPosition).bounds;
if (
x + winSize[0] < displaySize.x - 8 ||
x - winSize[0] > displaySize.x + displaySize.width ||
y < displaySize.y - 8 ||
y > displaySize.y + displaySize.height
) {
//Window is offscreen
if (is.dev()) {
console.log(
`Window tried to render offscreen, windowSize=${winSize}, displaySize=${displaySize}, position=${windowPosition}`
);
}
} else {
win.setPosition(x, y);
}
}
if (windowMaximized) {
win.maximize();
}
const urlToLoad = config.get("options.resumeOnStart")
? config.get("url")
: config.defaultConfig.url;
win.webContents.loadURL(urlToLoad);
win.on("closed", onClosed);
win.on("move", () => {
if (win.isMaximized()) return;
let position = win.getPosition();
lateSave("window-position", { x: position[0], y: position[1] });
});
let winWasMaximized;
win.on("resize", () => {
const windowSize = win.getSize();
const isMaximized = win.isMaximized();
if (winWasMaximized !== isMaximized) {
winWasMaximized = isMaximized;
config.set("window-maximized", isMaximized);
}
if (!isMaximized) {
lateSave("window-size", {
width: windowSize[0],
height: windowSize[1],
});
}
});
let savedTimeouts = {};
function lateSave(key, value) {
if (savedTimeouts[key]) clearTimeout(savedTimeouts[key]);
savedTimeouts[key] = setTimeout(() => {
config.set(key, value);
savedTimeouts[key] = undefined;
}, 1000)
}
win.webContents.on("render-process-gone", (event, webContents, details) => {
showUnresponsiveDialog(win, details);
});
win.once("ready-to-show", () => {
if (config.get("options.appVisible")) {
win.show();
}
});
removeContentSecurityPolicy();
return win;
}
app.once("browser-window-created", (event, win) => {
if (config.get("options.overrideUserAgent")) {
// User agents are from https://developers.whatismybrowser.com/useragents/explore/
const originalUserAgent = win.webContents.userAgent;
const userAgents = {
mac: "Mozilla/5.0 (Macintosh; Intel Mac OS X 12.1; rv:95.0) Gecko/20100101 Firefox/95.0",
windows: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0",
linux: "Mozilla/5.0 (Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0",
}
const updatedUserAgent =
is.macOS() ? userAgents.mac :
is.windows() ? userAgents.windows :
userAgents.linux;
win.webContents.userAgent = updatedUserAgent;
app.userAgentFallback = updatedUserAgent;
win.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => {
// this will only happen if login failed, and "retry" was pressed
if (win.webContents.getURL().startsWith("https://accounts.google.com") && details.url.startsWith("https://accounts.google.com")) {
details.requestHeaders["User-Agent"] = originalUserAgent;
}
cb({ requestHeaders: details.requestHeaders });
});
}
setupSongInfo(win);
loadPlugins(win);
setupAppControls();
win.webContents.on("did-fail-load", (
_event,
errorCode,
errorDescription,
validatedURL,
isMainFrame,
frameProcessId,
frameRoutingId,
) => {
const log = JSON.stringify({
error: "did-fail-load",
errorCode,
errorDescription,
validatedURL,
isMainFrame,
frameProcessId,
frameRoutingId,
}, null, "\t");
if (is.dev()) {
console.log(log);
}
if( !(config.plugins.isEnabled("in-app-menu") && errorCode === -3)) { // -3 is a false positive with in-app-menu
win.webContents.send("log", log);
win.webContents.loadFile(path.join(__dirname, "error.html"));
}
});
win.webContents.on("will-prevent-unload", (event) => {
event.preventDefault();
});
win.webContents.on(
"new-window",
(e, url, frameName, disposition, options) => {
// hook on new opened window
// at now new window in mainWindow renderer process.
// Also, this will automatically get an option `nodeIntegration=false`(not override to true, like in iframe's) - like in regular browsers
options.webPreferences.affinity = "main-window";
}
);
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
// Unregister all shortcuts.
electron.globalShortcut.unregisterAll();
});
app.on("activate", () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
mainWindow = createMainWindow();
} else if (!mainWindow.isVisible()) {
mainWindow.show();
}
});
app.on("ready", () => {
if (config.get("options.autoResetAppCache")) {
// Clear cache after 20s
const clearCacheTimeout = setTimeout(() => {
if (is.dev()) {
console.log("Clearing app cache.");
}
electron.session.defaultSession.clearCache();
clearTimeout(clearCacheTimeout);
}, 20000);
}
// Register appID on windows
if (is.windows()) {
const appID = "com.github.th-ch.youtube-music";
app.setAppUserModelId(appID);
const appLocation = process.execPath;
const appData = app.getPath("appData");
// check shortcut validity if not in dev mode / running portable app
if (!is.dev() && !appLocation.startsWith(path.join(appData, "..", "Local", "Temp"))) {
const shortcutPath = path.join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "YouTube Music.lnk");
try { // check if shortcut is registered and valid
const shortcutDetails = electron.shell.readShortcutLink(shortcutPath); // throw error if doesn't exist yet
if (shortcutDetails.target !== appLocation || shortcutDetails.appUserModelId !== appID) {
throw "needUpdate";
}
} catch (error) { // if not valid -> Register shortcut
electron.shell.writeShortcutLink(
shortcutPath,
error === "needUpdate" ? "update" : "create",
{
target: appLocation,
cwd: appLocation.slice(0, appLocation.lastIndexOf(path.sep)),
description: "YouTube Music Desktop App - including custom plugins",
appUserModelId: appID
}
);
}
}
}
mainWindow = createMainWindow();
setApplicationMenu(mainWindow);
setUpTray(app, mainWindow);
// Autostart at login
app.setLoginItemSettings({
openAtLogin: config.get("options.startAtLogin"),
});
if (!is.dev() && config.get("options.autoUpdates")) {
const updateTimeout = setTimeout(() => {
autoUpdater.checkForUpdatesAndNotify();
clearTimeout(updateTimeout);
}, 2000);
autoUpdater.on("update-available", () => {
const downloadLink =
"https://github.com/th-ch/youtube-music/releases/latest";
const dialogOpts = {
type: "info",
buttons: ["OK", "Download", "Disable updates"],
title: "Application Update",
message: "A new version is available",
detail: `A new version is available and can be downloaded at ${downloadLink}`,
};
electron.dialog.showMessageBox(dialogOpts).then((dialogOutput) => {
switch (dialogOutput.response) {
// Download
case 1:
electron.shell.openExternal(downloadLink);
break;
// Disable updates
case 2:
config.set("options.autoUpdates", false);
break;
default:
break;
}
});
});
}
if (config.get("options.hideMenu") && !config.get("options.hideMenuWarned")) {
electron.dialog.showMessageBox(mainWindow, {
type: 'info', title: 'Hide Menu Enabled',
message: "Menu is hidden, use 'Alt' to show it (or 'Escape' if using in-app-menu)"
});
config.set("options.hideMenuWarned", true);
}
// Optimized for Mac OS X
if (is.macOS() && !config.get("options.appVisible")) {
app.dock.hide();
}
let forceQuit = false;
app.on("before-quit", () => {
forceQuit = true;
});
if (is.macOS() || config.get("options.tray")) {
mainWindow.on("close", (event) => {
// Hide the window instead of quitting (quit is available in tray options)
if (!forceQuit) {
event.preventDefault();
mainWindow.hide();
}
});
}
});
function showUnresponsiveDialog(win, details) {
if (!!details) {
console.log("Unresponsive Error!\n"+JSON.stringify(details, null, "\t"))
}
electron.dialog.showMessageBox(win, {
type: "error",
title: "Window Unresponsive",
message: "The Application is Unresponsive",
details: "We are sorry for the inconvenience! please choose what to do:",
buttons: ["Wait", "Relaunch", "Quit"],
cancelId: 0
}).then( result => {
switch (result.response) {
case 1: restart(); break;
case 2: app.quit(); break;
}
});
}
function removeContentSecurityPolicy(
session = electron.session.defaultSession
) {
// Allows defining multiple "onHeadersReceived" listeners
// by enhancing the session.
// Some plugins (e.g. adblocker) also define a "onHeadersReceived" listener
enhanceWebRequest(session);
// Custom listener to tweak the content security policy
session.webRequest.onHeadersReceived(function (details, callback) {
if (
!details.responseHeaders["content-security-policy-report-only"] &&
!details.responseHeaders["content-security-policy"]
)
return callback({ cancel: false });
delete details.responseHeaders["content-security-policy-report-only"];
delete details.responseHeaders["content-security-policy"];
callback({ cancel: false, responseHeaders: details.responseHeaders });
});
// When multiple listeners are defined, apply them all
session.webRequest.setResolver("onHeadersReceived", (listeners) => {
const response = listeners.reduce(
async (accumulator, listener) => {
if (accumulator.cancel) {
return accumulator;
}
const result = await listener.apply();
return { ...accumulator, ...result };
},
{ cancel: false }
);
return response;
});
}