diff --git a/config/defaults.js b/config/defaults.js new file mode 100644 index 0000000000..ad4e5772c5 --- /dev/null +++ b/config/defaults.js @@ -0,0 +1,37 @@ +const defaultConfig = { + "window-size": { + width: 1100, + height: 550, + }, + url: "https://music.youtube.com", + options: { + tray: false, + appVisible: true, + autoUpdates: true, + hideMenu: false, + startAtLogin: false, + disableHardwareAcceleration: false, + }, + plugins: { + // Enabled plugins + navigation: { + enabled: true, + }, + shortcuts: { + enabled: true, + }, + adblocker: { + enabled: true, + cache: true, + additionalBlockLists: [], // Additional list of filters, e.g "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt" + }, + // Disabled plugins + downloader: { + enabled: false, + ffmpegArgs: [], // e.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s + downloadFolder: undefined, // Custom download folder (absolute path) + }, + }, +}; + +module.exports = defaultConfig; diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000000..31f787e8a1 --- /dev/null +++ b/config/index.js @@ -0,0 +1,18 @@ +const plugins = require("./plugins"); +const store = require("./store"); + +const set = (key, value) => { + store.set(key, value); +}; + +const get = (key) => { + return store.get(key); +}; + +module.exports = { + get, + set, + edit: () => store.openInEditor(), + watch: (cb) => store.onDidAnyChange(cb), + plugins, +}; diff --git a/config/plugins.js b/config/plugins.js new file mode 100644 index 0000000000..03962c3d07 --- /dev/null +++ b/config/plugins.js @@ -0,0 +1,41 @@ +const store = require("./store"); + +function getEnabled() { + const plugins = store.get("plugins"); + const enabledPlugins = Object.entries(plugins).filter(([plugin, options]) => + isEnabled(plugin) + ); + return enabledPlugins; +} + +function isEnabled(plugin) { + const pluginConfig = store.get("plugins")[plugin]; + return pluginConfig !== undefined && pluginConfig.enabled; +} + +function setOptions(plugin, options) { + const plugins = store.get("plugins"); + store.set("plugins", { + ...plugins, + [plugin]: { + ...plugins[plugin], + ...options, + }, + }); +} + +function enable(plugin) { + setOptions(plugin, { enabled: true }); +} + +function disable(plugin) { + setOptions(plugin, { enabled: false }); +} + +module.exports = { + isEnabled, + getEnabled, + enable, + disable, + setOptions, +}; diff --git a/config/store.js b/config/store.js new file mode 100644 index 0000000000..85c39be7f1 --- /dev/null +++ b/config/store.js @@ -0,0 +1,40 @@ +const Store = require("electron-store"); + +const defaults = require("./defaults"); + +const migrations = { + ">=1.7.0": (store) => { + const enabledPlugins = store.get("plugins"); + if (!Array.isArray(enabledPlugins)) { + console.warn("Plugins are not in array format, cannot migrate"); + return; + } + + // Include custom options + const plugins = { + adblocker: { + enabled: true, + cache: true, + additionalBlockLists: [], + }, + downloader: { + enabled: false, + ffmpegArgs: [], // e.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s + downloadFolder: undefined, // Custom download folder (absolute path) + }, + }; + enabledPlugins.forEach((enabledPlugin) => { + plugins[enabledPlugin] = { + ...plugins[enabledPlugin], + enabled: true, + }; + }); + store.set("plugins", plugins); + }, +}; + +module.exports = new Store({ + defaults, + clearInvalidConfig: false, + migrations, +}); diff --git a/index.js b/index.js index de4218d37b..fd35b2e03f 100644 --- a/index.js +++ b/index.js @@ -5,18 +5,8 @@ const electron = require("electron"); const is = require("electron-is"); const { autoUpdater } = require("electron-updater"); +const config = require("./config"); const { setApplicationMenu } = require("./menu"); -const { - autoUpdate, - disableHardwareAcceleration, - getEnabledPlugins, - hideMenu, - isAppVisible, - isTrayEnabled, - setOptions, - store, - startAtLogin, -} = require("./store"); const { fileExists, injectCSS } = require("./plugins/utils"); const { isTesting } = require("./utils/testing"); const { setUpTray } = require("./tray"); @@ -28,7 +18,7 @@ app.commandLine.appendSwitch( "--experimental-wasm-threads --experimental-wasm-bulk-memory" ); app.allowRendererProcessReuse = true; // https://github.com/electron/electron/issues/18397 -if (disableHardwareAcceleration()) { +if (config.get("options.disableHardwareAcceleration")) { if (is.dev()) { console.log("Disabling hardware acceleration"); } @@ -64,19 +54,19 @@ function loadPlugins(win) { } }); - getEnabledPlugins().forEach((plugin) => { + 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); + handle(win, options); }); }); } function createMainWindow() { - const windowSize = store.get("window-size"); - const windowMaximized = store.get("window-maximized"); + const windowSize = config.get("window-size"); + const windowMaximized = config.get("window-maximized"); const win = new electron.BrowserWindow({ icon: icon, @@ -94,31 +84,34 @@ function createMainWindow() { }, frame: !is.macOS(), titleBarStyle: is.macOS() ? "hiddenInset" : "default", - autoHideMenuBar: hideMenu(), + autoHideMenuBar: config.get("options.hideMenu"), }); if (windowMaximized) { win.maximize(); } - win.webContents.loadURL(store.get("url")); + win.webContents.loadURL(config.get("url")); win.on("closed", onClosed); win.on("move", () => { let position = win.getPosition(); - store.set("window-position", { x: position[0], y: position[1] }); + config.set("window-position", { x: position[0], y: position[1] }); }); win.on("resize", () => { const windowSize = win.getSize(); - store.set("window-maximized", win.isMaximized()); + config.set("window-maximized", win.isMaximized()); if (!win.isMaximized()) { - store.set("window-size", { width: windowSize[0], height: windowSize[1] }); + config.set("window-size", { + width: windowSize[0], + height: windowSize[1], + }); } }); win.once("ready-to-show", () => { - if (isAppVisible()) { + if (config.get("options.appVisible")) { win.show(); } }); @@ -143,7 +136,7 @@ app.on("browser-window-created", (event, win) => { win.webContents.on("did-navigate-in-page", () => { const url = win.webContents.getURL(); if (url.startsWith("https://music.youtube.com")) { - store.set("url", url); + config.set("url", url); } }); @@ -196,14 +189,17 @@ app.on("activate", () => { app.on("ready", () => { mainWindow = createMainWindow(); setApplicationMenu(mainWindow); + config.watch(() => { + setApplicationMenu(mainWindow); + }); setUpTray(app, mainWindow); // Autostart at login app.setLoginItemSettings({ - openAtLogin: startAtLogin(), + openAtLogin: config.get("options.startAtLogin"), }); - if (!is.dev() && autoUpdate()) { + if (!is.dev() && config.get("options.autoUpdates")) { autoUpdater.checkForUpdatesAndNotify(); autoUpdater.on("update-available", () => { const downloadLink = @@ -223,7 +219,7 @@ app.on("ready", () => { break; // Disable updates case 2: - setOptions({ autoUpdates: false }); + config.set("options.autoUpdates", false); break; default: break; @@ -234,7 +230,7 @@ app.on("ready", () => { // Optimized for Mac OS X if (is.macOS()) { - if (!isAppVisible()) { + if (!config.get("options.appVisible")) { app.dock.hide(); } } @@ -244,7 +240,7 @@ app.on("ready", () => { forceQuit = true; }); - if (is.macOS() || isTrayEnabled()) { + 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) { diff --git a/menu.js b/menu.js index 0932238c4e..9107854f93 100644 --- a/menu.js +++ b/menu.js @@ -2,36 +2,34 @@ const { app, Menu } = require("electron"); const is = require("electron-is"); const { getAllPlugins } = require("./plugins/utils"); -const { - isPluginEnabled, - enablePlugin, - disablePlugin, - autoUpdate, - hideMenu, - isAppVisible, - isTrayEnabled, - setOptions, - startAtLogin, - disableHardwareAcceleration, -} = require("./store"); +const config = require("./config"); const mainMenuTemplate = (win) => [ { label: "Plugins", - submenu: getAllPlugins().map((plugin) => { - return { - label: plugin, - type: "checkbox", - checked: isPluginEnabled(plugin), - click: (item) => { - if (item.checked) { - enablePlugin(plugin); - } else { - disablePlugin(plugin); - } + submenu: [ + ...getAllPlugins().map((plugin) => { + return { + label: plugin, + type: "checkbox", + checked: config.plugins.isEnabled(plugin), + click: (item) => { + if (item.checked) { + config.plugins.enable(plugin); + } else { + config.plugins.disable(plugin); + } + }, + }; + }), + { type: "separator" }, + { + label: "Advanced options", + click: () => { + config.edit(); }, - }; - }), + }, + ], }, { label: "Options", @@ -39,17 +37,17 @@ const mainMenuTemplate = (win) => [ { label: "Auto-update", type: "checkbox", - checked: autoUpdate(), + checked: config.get("options.autoUpdates"), click: (item) => { - setOptions({ autoUpdates: item.checked }); + config.set("options.autoUpdates", item.checked); }, }, { label: "Disable hardware acceleration", type: "checkbox", - checked: disableHardwareAcceleration(), + checked: config.get("options.disableHardwareAcceleration"), click: (item) => { - setOptions({ disableHardwareAcceleration: item.checked }); + config.set("options.disableHardwareAcceleration", item.checked); }, }, ...(is.windows() || is.linux() @@ -57,9 +55,9 @@ const mainMenuTemplate = (win) => [ { label: "Hide menu", type: "checkbox", - checked: hideMenu(), + checked: config.get("options.hideMenu"), click: (item) => { - setOptions({ hideMenu: item.checked }); + config.set("options.hideMenu", item.checked); }, }, ] @@ -71,9 +69,9 @@ const mainMenuTemplate = (win) => [ { label: "Start at login", type: "checkbox", - checked: startAtLogin(), + checked: config.get("options.startAtLogin"), click: (item) => { - setOptions({ startAtLogin: item.checked }); + config.set("options.startAtLogin", item.checked); }, }, ] @@ -84,23 +82,35 @@ const mainMenuTemplate = (win) => [ { label: "Disabled", type: "radio", - checked: !isTrayEnabled(), - click: () => setOptions({ tray: false, appVisible: true }), + checked: !config.get("options.tray"), + click: () => { + config.set("options.tray", false); + config.set("options.appVisible", true); + }, }, { label: "Enabled + app visible", type: "radio", - checked: isTrayEnabled() && isAppVisible(), - click: () => setOptions({ tray: true, appVisible: true }), + checked: + config.get("options.tray") && config.get("options.appVisible"), + click: () => { + config.set("options.tray", true); + config.set("options.appVisible", true); + }, }, { label: "Enabled + app hidden", type: "radio", - checked: isTrayEnabled() && !isAppVisible(), - click: () => setOptions({ tray: true, appVisible: false }), + checked: + config.get("options.tray") && !config.get("options.appVisible"), + click: () => { + config.set("options.tray", true); + config.set("options.appVisible", false); + }, }, ], }, + { type: "separator" }, { label: "Toggle DevTools", // Cannot use "toggleDevTools" role in MacOS @@ -114,6 +124,12 @@ const mainMenuTemplate = (win) => [ } }, }, + { + label: "Advanced options", + click: () => { + config.edit(); + }, + }, ], }, ]; diff --git a/package.json b/package.json index a1e92f35b2..243d864ad5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "youtube-music", "productName": "YouTube Music", - "version": "1.6.5", + "version": "1.7.0", "description": "YouTube Music Desktop App - including custom plugins", "license": "MIT", "repository": "th-ch/youtube-music", diff --git a/plugins/adblocker/back.js b/plugins/adblocker/back.js index 25ba5da9f6..96d9036196 100644 --- a/plugins/adblocker/back.js +++ b/plugins/adblocker/back.js @@ -1,2 +1,7 @@ const { loadAdBlockerEngine } = require("./blocker"); -module.exports = (win) => loadAdBlockerEngine(win.webContents.session); +module.exports = (win, options) => + loadAdBlockerEngine( + win.webContents.session, + options.cache, + options.additionalBlockLists + ); diff --git a/plugins/adblocker/blocker.js b/plugins/adblocker/blocker.js index e17abbb93c..7a236c95ad 100644 --- a/plugins/adblocker/blocker.js +++ b/plugins/adblocker/blocker.js @@ -1,4 +1,4 @@ -const { promises } = require("fs"); // used for caching +const { existsSync, promises, unlinkSync } = require("fs"); // used for caching const path = require("path"); const { ElectronBlocker } = require("@cliqz/adblocker-electron"); @@ -8,16 +8,28 @@ const SOURCES = [ "https://raw.githubusercontent.com/kbinani/adblock-youtube-ads/master/signed.txt", ]; -const loadAdBlockerEngine = (session = undefined) => +const loadAdBlockerEngine = ( + session = undefined, + cache = true, + additionalBlockLists = [] +) => { + const adBlockerCache = path.resolve(__dirname, "ad-blocker-engine.bin"); + if (!cache && existsSync(adBlockerCache)) { + unlinkSync(adBlockerCache); + } + const cachingOptions = cache + ? { + path: adBlockerCache, + read: promises.readFile, + write: promises.writeFile, + } + : undefined; + ElectronBlocker.fromLists( fetch, - SOURCES, + [...SOURCES, ...additionalBlockLists], {}, - { - path: path.resolve(__dirname, "ad-blocker-engine.bin"), - read: promises.readFile, - write: promises.writeFile, - } + cachingOptions ) .then((blocker) => { if (session) { @@ -27,6 +39,7 @@ const loadAdBlockerEngine = (session = undefined) => } }) .catch((err) => console.log("Error loading adBlocker engine", err)); +}; module.exports = { loadAdBlockerEngine }; if (require.main === module) { diff --git a/plugins/downloader/front.js b/plugins/downloader/front.js index a342c77f3e..0045928173 100644 --- a/plugins/downloader/front.js +++ b/plugins/downloader/front.js @@ -7,6 +7,7 @@ let progress = null; const downloadButton = ElementFromFile( templatePath(__dirname, "download.html") ); +let pluginOptions = {}; const observer = new MutationObserver((mutations, observer) => { if (!menu) { @@ -43,11 +44,13 @@ global.download = () => { triggerAction(CHANNEL, ACTIONS.ERROR, error); reinit(); }, - reinit + reinit, + pluginOptions ); }; -function observeMenu() { +function observeMenu(options) { + pluginOptions = { ...pluginOptions, ...options }; observer.observe(document, { childList: true, subtree: true, diff --git a/plugins/downloader/youtube-dl.js b/plugins/downloader/youtube-dl.js index 540f24d855..3b0c7688e8 100644 --- a/plugins/downloader/youtube-dl.js +++ b/plugins/downloader/youtube-dl.js @@ -19,7 +19,13 @@ const ffmpeg = createFFmpeg({ progress: () => {}, // console.log, }); -const downloadVideoToMP3 = (videoUrl, sendFeedback, sendError, reinit) => { +const downloadVideoToMP3 = ( + videoUrl, + sendFeedback, + sendError, + reinit, + options +) => { sendFeedback("Downloading…"); let videoName = "YouTube Music - Unknown title"; @@ -54,11 +60,18 @@ const downloadVideoToMP3 = (videoUrl, sendFeedback, sendError, reinit) => { .on("error", sendError) .on("end", () => { const buffer = Buffer.concat(chunks); - toMP3(videoName, buffer, sendFeedback, sendError, reinit); + toMP3(videoName, buffer, sendFeedback, sendError, reinit, options); }); }; -const toMP3 = async (videoName, buffer, sendFeedback, sendError, reinit) => { +const toMP3 = async ( + videoName, + buffer, + sendFeedback, + sendError, + reinit, + options +) => { const safeVideoName = randomBytes(32).toString("hex"); try { @@ -71,11 +84,17 @@ const toMP3 = async (videoName, buffer, sendFeedback, sendError, reinit) => { ffmpeg.FS("writeFile", safeVideoName, buffer); sendFeedback("Converting…"); - await ffmpeg.run("-i", safeVideoName, safeVideoName + ".mp3"); + await ffmpeg.run( + "-i", + safeVideoName, + ...options.ffmpegArgs, + safeVideoName + ".mp3" + ); + const folder = options.downloadFolder || downloadsFolder(); const filename = filenamify(videoName + ".mp3", { replacement: "_" }); writeFileSync( - join(downloadsFolder(), filename), + join(folder, filename), ffmpeg.FS("readFile", safeVideoName + ".mp3") ); diff --git a/preload.js b/preload.js index add2597977..362c917032 100644 --- a/preload.js +++ b/preload.js @@ -2,31 +2,31 @@ const path = require("path"); const { remote } = require("electron"); -const { getEnabledPlugins, store } = require("./store"); -const { fileExists } = require("./plugins/utils"); +const config = require("./config"); +const { fileExists } = require("./plugins/utils"); -const plugins = getEnabledPlugins(); +const plugins = config.plugins.getEnabled(); -plugins.forEach(plugin => { +plugins.forEach(([plugin, options]) => { const pluginPath = path.join(__dirname, "plugins", plugin, "actions.js"); fileExists(pluginPath, () => { const actions = require(pluginPath).global || {}; - Object.keys(actions).forEach(actionName => { + Object.keys(actions).forEach((actionName) => { global[actionName] = actions[actionName]; }); }); }); document.addEventListener("DOMContentLoaded", () => { - plugins.forEach(plugin => { + plugins.forEach(([plugin, options]) => { const pluginPath = path.join(__dirname, "plugins", plugin, "front.js"); fileExists(pluginPath, () => { const run = require(pluginPath); - run(); + run(options); }); }); // Add action for reloading global.reload = () => - remote.getCurrentWindow().webContents.loadURL(store.get("url")); + remote.getCurrentWindow().webContents.loadURL(config.get("url")); }); diff --git a/store/index.js b/store/index.js deleted file mode 100644 index b49681cb4e..0000000000 --- a/store/index.js +++ /dev/null @@ -1,40 +0,0 @@ -const Store = require("electron-store"); -const plugins = require("./plugins"); - -const store = new Store({ - defaults: { - "window-size": { - width: 1100, - height: 550, - }, - url: "https://music.youtube.com", - plugins: ["navigation", "shortcuts", "adblocker"], - options: { - tray: false, - appVisible: true, - autoUpdates: true, - hideMenu: false, - startAtLogin: false, - disableHardwareAcceleration: false, - }, - }, -}); - -module.exports = { - store: store, - // Plugins - isPluginEnabled: plugin => plugins.isEnabled(store, plugin), - getEnabledPlugins: () => plugins.getEnabledPlugins(store), - enablePlugin: plugin => plugins.enablePlugin(store, plugin), - disablePlugin: plugin => plugins.disablePlugin(store, plugin), - // Options - setOptions: options => - store.set("options", { ...store.get("options"), ...options }), - isTrayEnabled: () => store.get("options.tray"), - isAppVisible: () => store.get("options.appVisible"), - autoUpdate: () => store.get("options.autoUpdates"), - hideMenu: () => store.get("options.hideMenu"), - startAtLogin: () => store.get("options.startAtLogin"), - disableHardwareAcceleration: () => - store.get("options.disableHardwareAcceleration"), -}; diff --git a/store/plugins.js b/store/plugins.js deleted file mode 100644 index 28eef1fc3c..0000000000 --- a/store/plugins.js +++ /dev/null @@ -1,31 +0,0 @@ -function getEnabledPlugins(store) { - return store.get("plugins"); -} - -function isEnabled(store, plugin) { - return store.get("plugins").indexOf(plugin) > -1; -} - -function enablePlugin(store, plugin) { - let plugins = getEnabledPlugins(store); - if (plugins.indexOf(plugin) === -1) { - plugins.push(plugin); - store.set("plugins", plugins); - } -} - -function disablePlugin(store, plugin) { - let plugins = getEnabledPlugins(store); - let index = plugins.indexOf(plugin); - if (index > -1) { - plugins.splice(index, 1); - store.set("plugins", plugins); - } -} - -module.exports = { - isEnabled : isEnabled, - getEnabledPlugins: getEnabledPlugins, - enablePlugin : enablePlugin, - disablePlugin : disablePlugin -}; diff --git a/tray.js b/tray.js index e3ae9bb3c6..ff5f91eb07 100644 --- a/tray.js +++ b/tray.js @@ -2,15 +2,15 @@ const path = require("path"); const { Menu, nativeImage, Tray } = require("electron"); +const config = require("./config"); const { mainMenuTemplate } = require("./menu"); -const { isTrayEnabled } = require("./store"); const { clickInYoutubeMusic } = require("./utils/youtube-music"); // Prevent tray being garbage collected let tray; module.exports.setUpTray = (app, win) => { - if (!isTrayEnabled()) { + if (!config.get("options.tray")) { tray = undefined; return; }