diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e5c7e8a0..962b4adb 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,7 +1,9 @@
name: Build YouTube Music
on:
- - push
+ push:
+ branches: [ master ]
+ pull_request:
env:
NODE_VERSION: "16.x"
diff --git a/.github/workflows/winget-submission.yml b/.github/workflows/winget-submission.yml
new file mode 100644
index 00000000..89d5e4c1
--- /dev/null
+++ b/.github/workflows/winget-submission.yml
@@ -0,0 +1,26 @@
+name: Submit to Windows Package Manager Community Repository
+
+on:
+ release:
+ types: [released]
+ workflow_dispatch:
+ inputs:
+ tag_name:
+ description: "Specific tag name"
+ required: true
+ type: string
+
+jobs:
+ winget:
+ name: Publish winget package
+ runs-on: windows-latest
+ steps:
+ - name: Submit package to Windows Package Manager Community Repository
+ uses: vedantmgoyal2009/winget-releaser@v2
+ with:
+ identifier: th-ch.YouTubeMusic
+ installers-regex: '^YouTube-Music-Setup-[\d\.]+\.exe$'
+ version: ${{ inputs.tag_name || github.event.release.tag_name }}
+ release-tag: ${{ inputs.tag_name || github.event.release.tag_name }}
+ token: ${{ secrets.WINGET_ACC_TOKEN }}
+ fork-user: youtube-music-winget
diff --git a/plugins/taskbar-mediacontrol/assets/forward.png b/assets/media-icons-black/next.png
similarity index 100%
rename from plugins/taskbar-mediacontrol/assets/forward.png
rename to assets/media-icons-black/next.png
diff --git a/plugins/taskbar-mediacontrol/assets/pause.png b/assets/media-icons-black/pause.png
similarity index 100%
rename from plugins/taskbar-mediacontrol/assets/pause.png
rename to assets/media-icons-black/pause.png
diff --git a/plugins/taskbar-mediacontrol/assets/play.png b/assets/media-icons-black/play.png
similarity index 100%
rename from plugins/taskbar-mediacontrol/assets/play.png
rename to assets/media-icons-black/play.png
diff --git a/plugins/taskbar-mediacontrol/assets/backward.png b/assets/media-icons-black/previous.png
similarity index 100%
rename from plugins/taskbar-mediacontrol/assets/backward.png
rename to assets/media-icons-black/previous.png
diff --git a/config/defaults.js b/config/defaults.js
index 329f3851..d01d059a 100644
--- a/config/defaults.js
+++ b/config/defaults.js
@@ -55,8 +55,13 @@ const defaultConfig = {
notifications: {
enabled: false,
unpauseNotification: false,
- urgency: "normal", //has effect only on Linux
- interactive: false //has effect only on Windows
+ urgency: "normal", //has effect only on Linux
+ // the following has effect only on Windows
+ interactive: true,
+ toastStyle: 1, // see plugins/notifications/utils for more info
+ refreshOnPlayPause: false,
+ trayControls: true,
+ hideButtonText: false
},
"precise-volume": {
enabled: false,
diff --git a/config/dynamic.js b/config/dynamic.js
new file mode 100644
index 00000000..34cb0ac3
--- /dev/null
+++ b/config/dynamic.js
@@ -0,0 +1,139 @@
+const { ipcRenderer, ipcMain } = require("electron");
+
+const defaultConfig = require("./defaults");
+const { getOptions, setOptions, setMenuOptions } = require("./plugins");
+
+const activePlugins = {};
+/**
+ * [!IMPORTANT!]
+ * The method is **sync** in the main process and **async** in the renderer process.
+ */
+module.exports.getActivePlugins =
+ process.type === "renderer"
+ ? async () => ipcRenderer.invoke("get-active-plugins")
+ : () => activePlugins;
+
+if (process.type === "browser") {
+ ipcMain.handle("get-active-plugins", this.getActivePlugins);
+}
+
+/**
+ * [!IMPORTANT!]
+ * The method is **sync** in the main process and **async** in the renderer process.
+ */
+module.exports.isActive =
+ process.type === "renderer"
+ ? async (plugin) =>
+ plugin in (await ipcRenderer.invoke("get-active-plugins"))
+ : (plugin) => plugin in activePlugins;
+
+/**
+ * This class is used to create a dynamic synced config for plugins.
+ *
+ * [!IMPORTANT!]
+ * The methods are **sync** in the main process and **async** in the renderer process.
+ *
+ * @param {string} name - The name of the plugin.
+ * @param {boolean} [options.enableFront] - Whether the config should be available in front.js. Default: false.
+ * @param {object} [options.initialOptions] - The initial options for the plugin. Default: loaded from store.
+ *
+ * @example
+ * const { PluginConfig } = require("../../config/dynamic");
+ * const config = new PluginConfig("plugin-name", { enableFront: true });
+ * module.exports = { ...config };
+ *
+ * // or
+ *
+ * module.exports = (win, options) => {
+ * const config = new PluginConfig("plugin-name", {
+ * enableFront: true,
+ * initialOptions: options,
+ * });
+ * setupMyPlugin(win, config);
+ * };
+ */
+module.exports.PluginConfig = class PluginConfig {
+ #name;
+ #config;
+ #defaultConfig;
+ #enableFront;
+
+ constructor(name, { enableFront = false, initialOptions = undefined } = {}) {
+ const pluginDefaultConfig = defaultConfig.plugins[name] || {};
+ const pluginConfig = initialOptions || getOptions(name) || {};
+
+ this.#name = name;
+ this.#enableFront = enableFront;
+ this.#defaultConfig = pluginDefaultConfig;
+ this.#config = { ...pluginDefaultConfig, ...pluginConfig };
+
+ if (this.#enableFront) {
+ this.#setupFront();
+ }
+
+ activePlugins[name] = this;
+ }
+
+ get = (option) => {
+ return this.#config[option];
+ };
+
+ set = (option, value) => {
+ this.#config[option] = value;
+ this.#save();
+ };
+
+ toggle = (option) => {
+ this.#config[option] = !this.#config[option];
+ this.#save();
+ };
+
+ getAll = () => {
+ return { ...this.#config };
+ };
+
+ setAll = (options) => {
+ this.#config = { ...this.#config, ...options };
+ this.#save();
+ };
+
+ getDefaultConfig = () => {
+ return this.#defaultConfig;
+ };
+
+ /**
+ * Use this method to set an option and restart the app if `appConfig.restartOnConfigChange === true`
+ *
+ * Used for options that require a restart to take effect.
+ */
+ setAndMaybeRestart = (option, value) => {
+ this.#config[option] = value;
+ setMenuOptions(this.#name, this.#config);
+ };
+
+ /** Called only from back */
+ #save() {
+ setOptions(this.#name, this.#config);
+ }
+
+ #setupFront() {
+ if (process.type === "renderer") {
+ for (const [fnName, fn] of Object.entries(this)) {
+ if (typeof fn !== "function") return;
+ this[fnName] = async (...args) => {
+ return await ipcRenderer.invoke(
+ `${this.name}-config-${fnName}`,
+ ...args,
+ );
+ };
+ }
+ } else if (process.type === "browser") {
+ for (const [fnName, fn] of Object.entries(this)) {
+ if (typeof fn !== "function") return;
+ ipcMain.handle(`${this.name}-config-${fnName}`, (_, ...args) => {
+ return fn(...args);
+ });
+ }
+ }
+ }
+};
diff --git a/config/store.js b/config/store.js
index c528d6a4..07984820 100644
--- a/config/store.js
+++ b/config/store.js
@@ -11,6 +11,14 @@ const setDefaultPluginOptions = (store, plugin) => {
const migrations = {
">=1.20.0": (store) => {
setDefaultPluginOptions(store, "visualizer");
+
+ if (store.get("plugins.notifications.toastStyle") === undefined) {
+ const pluginOptions = store.get("plugins.notifications") || {};
+ store.set("plugins.notifications", {
+ ...defaults.plugins.notifications,
+ ...pluginOptions,
+ });
+ }
},
">=1.17.0": (store) => {
setDefaultPluginOptions(store, "picture-in-picture");
diff --git a/index.js b/index.js
index 19a2e2f9..dcc23eb7 100644
--- a/index.js
+++ b/index.js
@@ -14,6 +14,7 @@ const { isTesting } = require("./utils/testing");
const { setUpTray } = require("./tray");
const { setupSongInfo } = require("./providers/song-info");
const { setupAppControls, restart } = require("./providers/app-controls");
+const { APP_PROTOCOL, setupProtocolHandler, handleProtocol } = require("./providers/protocol-handler");
// Catch errors and log them
unhandled({
@@ -29,17 +30,9 @@ const app = electron.app;
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();
- });
-}
+const gotTheLock = app.requestSingleInstanceLock();
+if (!gotTheLock) app.exit();
app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer"); // Required for downloader
app.allowRendererProcessReuse = true; // https://github.com/electron/electron/issues/18397
@@ -77,6 +70,7 @@ function onClosed() {
mainWindow = null;
}
+/** @param {Electron.BrowserWindow} win */
function loadPlugins(win) {
injectCSS(win.webContents, path.join(__dirname, "youtube-music.css"));
// Load user CSS
@@ -347,9 +341,6 @@ app.on("ready", () => {
// Register appID on windows
if (is.windows()) {
- // Depends on SnoreToast version https://github.com/KDE/snoretoast/blob/master/CMakeLists.txt#L5
- const toastActivatorClsid = "eb1fdd5b-8f70-4b5a-b230-998a2dc19303";
-
const appID = "com.github.th-ch.youtube-music";
app.setAppUserModelId(appID);
const appLocation = process.execPath;
@@ -361,8 +352,7 @@ app.on("ready", () => {
const shortcutDetails = electron.shell.readShortcutLink(shortcutPath); // throw error if doesn't exist yet
if (
shortcutDetails.target !== appLocation ||
- shortcutDetails.appUserModelId !== appID ||
- shortcutDetails.toastActivatorClsid !== toastActivatorClsid
+ shortcutDetails.appUserModelId !== appID
) {
throw "needUpdate";
}
@@ -375,7 +365,6 @@ app.on("ready", () => {
cwd: path.dirname(appLocation),
description: "YouTube Music Desktop App - including custom plugins",
appUserModelId: appID,
- toastActivatorClsid
}
);
}
@@ -386,6 +375,23 @@ app.on("ready", () => {
setApplicationMenu(mainWindow);
setUpTray(app, mainWindow);
+ setupProtocolHandler(mainWindow);
+
+ app.on('second-instance', (_event, commandLine, _workingDirectory) => {
+ const uri = `${APP_PROTOCOL}://`;
+ const protocolArgv = commandLine.find(arg => arg.startsWith(uri));
+ if (protocolArgv) {
+ const command = protocolArgv.slice(uri.length, -1);
+ if (is.dev()) console.debug(`Received command over protocol: "${command}"`);
+ handleProtocol(command);
+ return;
+ }
+ if (!mainWindow) return;
+ if (mainWindow.isMinimized()) mainWindow.restore();
+ if (!mainWindow.isVisible()) mainWindow.show();
+ mainWindow.focus();
+ });
+
// Autostart at login
app.setLoginItemSettings({
openAtLogin: config.get("options.startAtLogin"),
diff --git a/menu.js b/menu.js
index 068393f6..a741cd5d 100644
--- a/menu.js
+++ b/menu.js
@@ -133,14 +133,12 @@ const mainMenuTemplate = (win) => {
{
label: "Single instance lock",
type: "checkbox",
- checked: config.get("options.singleInstanceLock"),
+ checked: false,
click: (item) => {
- config.setMenuOption("options.singleInstanceLock", item.checked);
- if (item.checked && !app.hasSingleInstanceLock()) {
- app.requestSingleInstanceLock();
- } else if (!item.checked && app.hasSingleInstanceLock()) {
+ if (item.checked && app.hasSingleInstanceLock())
app.releaseSingleInstanceLock();
- }
+ else if (!item.checked && !app.hasSingleInstanceLock())
+ app.requestSingleInstanceLock();
},
},
{
@@ -163,7 +161,7 @@ const mainMenuTemplate = (win) => {
if (item.checked && !config.get("options.hideMenuWarned")) {
dialog.showMessageBox(win, {
type: 'info', title: 'Hide Menu Enabled',
- message: "Menu will be hidden on next launch, use 'Alt' to show it (or 'Escape' if using in-app-menu)"
+ message: "Menu will be hidden on next launch, use [Alt] to show it (or backtick [`] if using in-app-menu)"
});
}
},
diff --git a/package.json b/package.json
index f8f40050..8ffd68d8 100644
--- a/package.json
+++ b/package.json
@@ -87,11 +87,11 @@
"generate:package": "node utils/generate-package-json.js",
"postinstall": "yarn run icon && yarn run plugins",
"clean": "del-cli dist",
- "build": "yarn run clean && electron-builder --win --mac --linux",
- "build:linux": "yarn run clean && electron-builder --linux",
- "build:mac": "yarn run clean && electron-builder --mac dmg:x64",
- "build:mac:arm64": "yarn run clean && electron-builder --mac dmg:arm64",
- "build:win": "yarn run clean && electron-builder --win",
+ "build": "yarn run clean && electron-builder --win --mac --linux -p never",
+ "build:linux": "yarn run clean && electron-builder --linux -p never",
+ "build:mac": "yarn run clean && electron-builder --mac dmg:x64 -p never",
+ "build:mac:arm64": "yarn run clean && electron-builder --mac dmg:arm64 -p never",
+ "build:win": "yarn run clean && electron-builder --win -p never",
"lint": "xo",
"changelog": "auto-changelog",
"plugins": "yarn run plugin:adblocker && yarn run plugin:bypass-age-restrictions",
@@ -115,8 +115,8 @@
"browser-id3-writer": "^4.4.0",
"butterchurn": "^2.6.7",
"butterchurn-presets": "^2.4.7",
- "custom-electron-prompt": "^1.5.1",
- "custom-electron-titlebar": "^4.1.5",
+ "custom-electron-prompt": "^1.5.4",
+ "custom-electron-titlebar": "^4.1.6",
"electron-better-web-request": "^1.0.1",
"electron-debug": "^3.2.0",
"electron-is": "^3.0.0",
@@ -126,13 +126,12 @@
"electron-updater": "^5.3.0",
"filenamify": "^4.3.0",
"howler": "^2.2.3",
- "html-to-text": "^9.0.3",
+ "html-to-text": "^9.0.4",
"keyboardevent-from-electron-accelerator": "^2.0.0",
"keyboardevents-areequal": "^0.2.2",
"md5": "^2.3.0",
"mpris-service": "^2.1.2",
"node-fetch": "^2.6.8",
- "node-notifier": "^10.0.1",
"simple-youtube-age-restriction-bypass": "https://gitpkg.now.sh/api/pkg.tgz?url=zerodytrash/Simple-YouTube-Age-Restriction-Bypass&commit=v2.5.4",
"vudio": "^2.1.1",
"youtubei.js": "^3.1.1",
diff --git a/plugins/captions-selector/front.js b/plugins/captions-selector/front.js
index 6ed83d90..bd5203bb 100644
--- a/plugins/captions-selector/front.js
+++ b/plugins/captions-selector/front.js
@@ -11,28 +11,32 @@ module.exports = (options) => {
document.addEventListener('apiLoaded', (event) => setup(event, options), { once: true, passive: true });
}
-/**
- * If captions are disabled by default,
- * unload "captions" module when video changes.
- */
-const videoChanged = (api, options) => {
- if (options.disableCaptions) {
- setTimeout(() => api.unloadModule("captions"), 100);
- }
-}
-
function setup(event, options) {
const api = event.detail;
- $("video").addEventListener("srcChanged", () => videoChanged(api, options));
-
$(".right-controls-buttons").append(captionsSettingsButton);
+ let captionTrackList = api.getOption("captions", "tracklist");
+
+ $("video").addEventListener("srcChanged", () => {
+ if (options.disableCaptions) {
+ setTimeout(() => api.unloadModule("captions"), 100);
+ captionsSettingsButton.style.display = "none";
+ return;
+ }
+
+ api.loadModule("captions");
+
+ setTimeout(() => {
+ captionTrackList = api.getOption("captions", "tracklist");
+
+ captionsSettingsButton.style.display = captionTrackList?.length
+ ? "inline-block"
+ : "none";
+ }, 250);
+ });
+
captionsSettingsButton.onclick = async () => {
- api.loadModule("captions");
-
- const captionTrackList = api.getOption("captions", "tracklist");
-
if (captionTrackList?.length) {
const currentCaptionTrack = api.getOption("captions", "track");
let currentIndex = !currentCaptionTrack ?
diff --git a/plugins/in-app-menu/back.js b/plugins/in-app-menu/back.js
index b75e8a32..d0a54605 100644
--- a/plugins/in-app-menu/back.js
+++ b/plugins/in-app-menu/back.js
@@ -2,14 +2,12 @@ const path = require("path");
const electronLocalshortcut = require("electron-localshortcut");
-const config = require("../../config");
const { injectCSS } = require("../utils");
const { setupTitlebar, attachTitlebarToWindow } = require('custom-electron-titlebar/main');
setupTitlebar();
//tracks menu visibility
-let visible = !config.get("options.hideMenu");
module.exports = (win) => {
// css for custom scrollbar + disable drag area(was causing bugs)
@@ -18,16 +16,8 @@ module.exports = (win) => {
win.once("ready-to-show", () => {
attachTitlebarToWindow(win);
- //register keyboard shortcut && hide menu if hideMenu is enabled
- if (config.get("options.hideMenu")) {
- electronLocalshortcut.register(win, "Esc", () => {
- setMenuVisibility(!visible);
- });
- }
+ electronLocalshortcut.register(win, "`", () => {
+ win.webContents.send("toggleMenu");
+ });
});
-
- function setMenuVisibility(value) {
- visible = value;
- win.webContents.send("refreshMenu", visible);
- }
};
diff --git a/plugins/in-app-menu/front.js b/plugins/in-app-menu/front.js
index 17c89d4e..e0fcfded 100644
--- a/plugins/in-app-menu/front.js
+++ b/plugins/in-app-menu/front.js
@@ -5,28 +5,31 @@ const { isEnabled } = require("../../config/plugins");
function $(selector) { return document.querySelector(selector); }
module.exports = (options) => {
- let visible = !config.get("options.hideMenu");
+ let visible = () => !!$('.cet-menubar').firstChild;
const bar = new Titlebar({
+ icon: "https://cdn-icons-png.flaticon.com/512/5358/5358672.png",
backgroundColor: Color.fromHex("#050505"),
itemBackgroundColor: Color.fromHex("#1d1d1d"),
svgColor: Color.WHITE,
- menu: visible ? undefined : null
+ menu: config.get("options.hideMenu") ? null : undefined
});
bar.updateTitle(" ");
document.title = "Youtube Music";
- const hideIcon = hide => $('.cet-window-icon').style.display = hide ? 'none' : 'flex';
-
- if (options.hideIcon) hideIcon(true);
-
- ipcRenderer.on("refreshMenu", (_, showMenu) => {
- if (showMenu === undefined && !visible) return;
- if (showMenu === false) {
+ const toggleMenu = () => {
+ if (visible()) {
bar.updateMenu(null);
- visible = false;
} else {
bar.refreshMenu();
- visible = true;
+ }
+ };
+
+ $('.cet-window-icon').addEventListener('click', toggleMenu);
+ ipcRenderer.on("toggleMenu", toggleMenu);
+
+ ipcRenderer.on("refreshMenu", () => {
+ if (visible()) {
+ bar.refreshMenu();
}
});
@@ -36,14 +39,13 @@ module.exports = (options) => {
});
}
- ipcRenderer.on("hideIcon", (_, hide) => hideIcon(hide));
-
// Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it)
document.addEventListener('apiLoaded', () => {
setNavbarMargin();
const playPageObserver = new MutationObserver(setNavbarMargin);
playPageObserver.observe($('ytmusic-app-layout'), { attributeFilter: ['player-page-open_', 'playerPageOpen_'] })
setupSearchOpenObserver();
+ setupMenuOpenObserver();
}, { once: true, passive: true })
};
@@ -55,6 +57,15 @@ function setupSearchOpenObserver() {
searchOpenObserver.observe($('ytmusic-search-box'), { attributeFilter: ["opened"] })
}
+function setupMenuOpenObserver() {
+ const menuOpenObserver = new MutationObserver(mutations => {
+ $('#nav-bar-background').style.webkitAppRegion =
+ Array.from($('.cet-menubar').childNodes).some(c => c.classList.contains('open')) ?
+ 'no-drag' : 'drag';
+ });
+ menuOpenObserver.observe($('.cet-menubar'), { subtree: true, attributeFilter: ["class"] })
+}
+
function setNavbarMargin() {
$('#nav-bar-background').style.right =
$('ytmusic-app-layout').playerPageOpen_ ?
diff --git a/plugins/in-app-menu/menu.js b/plugins/in-app-menu/menu.js
deleted file mode 100644
index dbfd88a3..00000000
--- a/plugins/in-app-menu/menu.js
+++ /dev/null
@@ -1,14 +0,0 @@
-const { setOptions } = require("../../config/plugins");
-
-module.exports = (win, options) => [
- {
- label: "Hide Icon",
- type: "checkbox",
- checked: options.hideIcon,
- click: (item) => {
- win.webContents.send("hideIcon", item.checked);
- options.hideIcon = item.checked;
- setOptions("in-app-menu", options);
- },
- }
-];
diff --git a/plugins/in-app-menu/style.css b/plugins/in-app-menu/style.css
index c27b2d57..0148b38a 100644
--- a/plugins/in-app-menu/style.css
+++ b/plugins/in-app-menu/style.css
@@ -12,11 +12,17 @@
height: 75px !important;
}
-/* fixes top gap between nav-bar and browse-page */
+/* fix top gap between nav-bar and browse-page */
#browse-page {
padding-top: 0 !important;
}
+/* fix navbar hiding library items */
+ytmusic-section-list-renderer[page-type="MUSIC_PAGE_TYPE_LIBRARY_CONTENT_LANDING_PAGE"] {
+ top: 50px;
+ position: relative;
+}
+
/* remove window dragging for nav bar (conflict with titlebar drag) */
ytmusic-nav-bar,
.tab-titleiron-icon,
@@ -46,7 +52,7 @@ yt-page-navigation-progress,
top: 30px !important;
}
-/* Custom scrollbar */
+/* custom scrollbar */
::-webkit-scrollbar {
width: 12px;
background-color: #030303;
@@ -59,7 +65,7 @@ yt-page-navigation-progress,
background-color: rgba(15, 15, 15, 0.699);
}
-/* The scrollbar 'thumb' ...that marque oval shape in a scrollbar */
+/* the scrollbar 'thumb' ...that marque oval shape in a scrollbar */
::-webkit-scrollbar-thumb:vertical {
border: 2px solid rgba(0, 0, 0, 0);
@@ -70,7 +76,7 @@ yt-page-navigation-progress,
-webkit-border-radius: 100px;
}
::-webkit-scrollbar-thumb:vertical:active {
- background: #4d4c4c; /* Some darker color when you click it */
+ background: #4d4c4c; /* some darker color when you click it */
border-radius: 100px;
-moz-border-radius: 100px;
-webkit-border-radius: 100px;
@@ -80,6 +86,17 @@ yt-page-navigation-progress,
background-color: inherit
}
+/** hideMenu toggler **/
+.cet-window-icon {
+ -webkit-app-region: no-drag;
+}
+
+.cet-window-icon img {
+ -webkit-user-drag: none;
+ filter: invert(50%);
+}
+
+/** make navbar draggable **/
#nav-bar-background {
-webkit-app-region: drag;
}
diff --git a/plugins/lyrics-genius/back.js b/plugins/lyrics-genius/back.js
index 05548648..cb781ca8 100644
--- a/plugins/lyrics-genius/back.js
+++ b/plugins/lyrics-genius/back.js
@@ -7,8 +7,13 @@ const fetch = require("node-fetch");
const { cleanupName } = require("../../providers/song-info");
const { injectCSS } = require("../utils");
+let eastAsianChars = /\p{Script=Han}|\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}|\p{Script=Han}/u;
+let revRomanized = false;
-module.exports = async (win) => {
+module.exports = async (win, options) => {
+ if(options.romanizedLyrics) {
+ revRomanized = true;
+ }
injectCSS(win.webContents, join(__dirname, "style.css"));
ipcMain.on("search-genius-lyrics", async (event, extractedSongInfo) => {
@@ -17,17 +22,51 @@ module.exports = async (win) => {
});
};
+const toggleRomanized = () => {
+ revRomanized = !revRomanized;
+};
+
const fetchFromGenius = async (metadata) => {
- const queryString = `${cleanupName(metadata.artist)} ${cleanupName(
- metadata.title
- )}`;
+ const songTitle = `${cleanupName(metadata.title)}`;
+ const songArtist = `${cleanupName(metadata.artist)}`;
+ let lyrics;
+
+ /* Uses Regex to test the title and artist first for said characters if romanization is enabled. Otherwise normal
+ Genius Lyrics behavior is observed.
+ */
+ let hasAsianChars = false;
+ if (revRomanized && (eastAsianChars.test(songTitle) || eastAsianChars.test(songArtist))) {
+ lyrics = await getLyricsList(`${songArtist} ${songTitle} Romanized`);
+ hasAsianChars = true;
+ } else {
+ lyrics = await getLyricsList(`${songArtist} ${songTitle}`);
+ }
+
+ /* If the romanization toggle is on, and we did not detect any characters in the title or artist, we do a check
+ for characters in the lyrics themselves. If this check proves true, we search for Romanized lyrics.
+ */
+ if(revRomanized && !hasAsianChars && eastAsianChars.test(lyrics)) {
+ lyrics = await getLyricsList(`${songArtist} ${songTitle} Romanized`);
+ }
+ return lyrics;
+};
+
+/**
+ * Fetches a JSON of songs which is then parsed and passed into getLyrics to get the lyrical content of the first song
+ * @param {*} queryString
+ * @returns The lyrics of the first song found using the Genius-Lyrics API
+ */
+const getLyricsList = async (queryString) => {
let response = await fetch(
- `https://genius.com/api/search/multi?per_page=5&q=${encodeURI(queryString)}`
+ `https://genius.com/api/search/multi?per_page=5&q=${encodeURIComponent(queryString)}`
);
if (!response.ok) {
return null;
}
+ /* Fetch the first URL with the api, giving a collection of song results.
+ Pick the first song, parsing the json given by the API.
+ */
const info = await response.json();
let url = "";
try {
@@ -36,16 +75,23 @@ const fetchFromGenius = async (metadata) => {
} catch {
return null;
}
+ let lyrics = await getLyrics(url);
+ return lyrics;
+}
- if (is.dev()) {
- console.log("Fetching lyrics from Genius:", url);
- }
-
+/**
+ *
+ * @param {*} url
+ * @returns The lyrics of the song URL provided, null if none
+ */
+const getLyrics = async (url) => {
response = await fetch(url);
if (!response.ok) {
return null;
}
-
+ if (is.dev()) {
+ console.log("Fetching lyrics from Genius:", url);
+ }
const html = await response.text();
const lyrics = convert(html, {
baseElements: {
@@ -64,8 +110,8 @@ const fetchFromGenius = async (metadata) => {
},
},
});
-
return lyrics;
};
-module.exports.fetchFromGenius = fetchFromGenius;
+module.exports.toggleRomanized = toggleRomanized;
+module.exports.fetchFromGenius = fetchFromGenius;
\ No newline at end of file
diff --git a/plugins/lyrics-genius/front.js b/plugins/lyrics-genius/front.js
index c62bfb0c..5725d1e1 100644
--- a/plugins/lyrics-genius/front.js
+++ b/plugins/lyrics-genius/front.js
@@ -2,7 +2,7 @@ const { ipcRenderer } = require("electron");
const is = require("electron-is");
module.exports = () => {
- ipcRenderer.on("update-song-info", (_, extractedSongInfo) => {
+ ipcRenderer.on("update-song-info", (_, extractedSongInfo) => setTimeout(() => {
const tabList = document.querySelectorAll("tp-yt-paper-tab");
const tabs = {
upNext: tabList[0],
@@ -90,5 +90,5 @@ module.exports = () => {
tabs.lyrics.removeAttribute("disabled");
tabs.lyrics.removeAttribute("aria-disabled");
}
- });
+ }, 500));
};
diff --git a/plugins/lyrics-genius/menu.js b/plugins/lyrics-genius/menu.js
new file mode 100644
index 00000000..5d8c390e
--- /dev/null
+++ b/plugins/lyrics-genius/menu.js
@@ -0,0 +1,17 @@
+const { setOptions } = require("../../config/plugins");
+const { toggleRomanized } = require("./back");
+
+module.exports = (win, options, refreshMenu) => {
+ return [
+ {
+ label: "Romanized Lyrics",
+ type: "checkbox",
+ checked: options.romanizedLyrics,
+ click: (item) => {
+ options.romanizedLyrics = item.checked;
+ setOptions('lyrics-genius', options);
+ toggleRomanized();
+ },
+ },
+ ];
+};
\ No newline at end of file
diff --git a/plugins/notifications/back.js b/plugins/notifications/back.js
index f3282ed3..385ecca7 100644
--- a/plugins/notifications/back.js
+++ b/plugins/notifications/back.js
@@ -2,8 +2,9 @@ const { Notification } = require("electron");
const is = require("electron-is");
const registerCallback = require("../../providers/song-info");
const { notificationImage } = require("./utils");
+const config = require("./config");
-const notify = (info, options) => {
+const notify = (info) => {
// Fill the notification with content
const notification = {
@@ -11,7 +12,7 @@ const notify = (info, options) => {
body: info.artist,
icon: notificationImage(info),
silent: true,
- urgency: options.urgency,
+ urgency: config.get('urgency'),
};
// Send the notification
@@ -21,24 +22,25 @@ const notify = (info, options) => {
return currentNotification;
};
-const setup = (options) => {
+const setup = () => {
let oldNotification;
let currentUrl;
registerCallback(songInfo => {
- if (!songInfo.isPaused && (songInfo.url !== currentUrl || options.unpauseNotification)) {
+ if (!songInfo.isPaused && (songInfo.url !== currentUrl || config.get('unpauseNotification'))) {
// Close the old notification
oldNotification?.close();
currentUrl = songInfo.url;
// This fixes a weird bug that would cause the notification to be updated instead of showing
- setTimeout(() => { oldNotification = notify(songInfo, options) }, 10);
+ setTimeout(() => { oldNotification = notify(songInfo) }, 10);
}
});
}
+/** @param {Electron.BrowserWindow} win */
module.exports = (win, options) => {
// Register the callback for new song information
is.windows() && options.interactive ?
- require("./interactive")(win, options.unpauseNotification) :
- setup(options);
+ require("./interactive")(win) :
+ setup();
};
diff --git a/plugins/notifications/config.js b/plugins/notifications/config.js
new file mode 100644
index 00000000..d0898dc3
--- /dev/null
+++ b/plugins/notifications/config.js
@@ -0,0 +1,5 @@
+const { PluginConfig } = require("../../config/dynamic");
+
+const config = new PluginConfig("notifications");
+
+module.exports = { ...config };
diff --git a/plugins/notifications/interactive.js b/plugins/notifications/interactive.js
index b7535ea0..7bda6944 100644
--- a/plugins/notifications/interactive.js
+++ b/plugins/notifications/interactive.js
@@ -1,106 +1,235 @@
-const { notificationImage, icons } = require("./utils");
+const { notificationImage, icons, save_temp_icons, secondsToMinutes, ToastStyles } = require("./utils");
const getSongControls = require('../../providers/song-controls');
const registerCallback = require("../../providers/song-info");
-const is = require("electron-is");
-const WindowsToaster = require('node-notifier').WindowsToaster;
+const { changeProtocolHandler } = require("../../providers/protocol-handler");
+const { setTrayOnClick, setTrayOnDoubleClick } = require("../../tray");
-const notifier = new WindowsToaster({ withFallback: true });
+const { Notification, app, ipcMain } = require("electron");
+const path = require('path');
-//store song controls reference on launch
-let controls;
-let notificationOnUnpause;
+const config = require("./config");
-module.exports = (win, unpauseNotification) => {
- //Save controls and onPause option
- const { playPause, next, previous } = getSongControls(win);
- controls = { playPause, next, previous };
- notificationOnUnpause = unpauseNotification;
+let songControls;
+let savedNotification;
- let currentUrl;
+/** @param {Electron.BrowserWindow} win */
+module.exports = (win) => {
+ songControls = getSongControls(win);
+
+ let currentSeconds = 0;
+ ipcMain.on('apiLoaded', () => win.webContents.send('setupTimeChangedListener'));
+
+ ipcMain.on('timeChanged', (_, t) => currentSeconds = t);
+
+ if (app.isPackaged) save_temp_icons();
+
+ let savedSongInfo;
+ let lastUrl;
// Register songInfoCallback
registerCallback(songInfo => {
- if (!songInfo.isPaused && (songInfo.url !== currentUrl || notificationOnUnpause)) {
- currentUrl = songInfo.url;
- sendToaster(songInfo);
+ if (!songInfo.artist && !songInfo.title) return;
+ savedSongInfo = { ...songInfo };
+ if (!songInfo.isPaused &&
+ (songInfo.url !== lastUrl || config.get("unpauseNotification"))
+ ) {
+ lastUrl = songInfo.url
+ sendNotification(songInfo);
}
});
- win.webContents.once("closed", () => {
- deleteNotification()
+ if (config.get("trayControls")) {
+ setTrayOnClick(() => {
+ if (savedNotification) {
+ savedNotification.close();
+ savedNotification = undefined;
+ } else if (savedSongInfo) {
+ sendNotification({
+ ...savedSongInfo,
+ elapsedSeconds: currentSeconds
+ })
+ }
+ });
+
+ setTrayOnDoubleClick(() => {
+ if (win.isVisible()) {
+ win.hide();
+ } else win.show();
+ })
+ }
+
+
+ app.once("before-quit", () => {
+ savedNotification?.close();
});
+
+
+ changeProtocolHandler(
+ (cmd) => {
+ if (Object.keys(songControls).includes(cmd)) {
+ songControls[cmd]();
+ if (config.get("refreshOnPlayPause") && (
+ cmd === 'pause' ||
+ (cmd === 'play' && !config.get("unpauseNotification"))
+ )
+ ) {
+ setImmediate(() =>
+ sendNotification({
+ ...savedSongInfo,
+ isPaused: cmd === 'pause',
+ elapsedSeconds: currentSeconds
+ })
+ );
+ }
+ }
+ }
+ )
}
-//delete old notification
-let toDelete;
-function deleteNotification() {
- if (toDelete !== undefined) {
- // To remove the notification it has to be done this way
- const removeNotif = Object.assign(toDelete, {
- remove: toDelete.id
- })
- notifier.notify(removeNotif)
+function sendNotification(songInfo) {
+ const iconSrc = notificationImage(songInfo);
- toDelete = undefined;
+ savedNotification?.close();
+
+ savedNotification = new Notification({
+ title: songInfo.title || "Playing",
+ body: songInfo.artist,
+ icon: iconSrc,
+ silent: true,
+ // https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root
+ // https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/toast-schema
+ // https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=xml
+ // https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.toasttemplatetype
+ toastXml: get_xml(songInfo, iconSrc),
+ });
+
+ savedNotification.on("close", (_) => {
+ savedNotification = undefined;
+ });
+
+ savedNotification.show();
+}
+
+const get_xml = (songInfo, iconSrc) => {
+ switch (config.get("toastStyle")) {
+ default:
+ case ToastStyles.logo:
+ case ToastStyles.legacy:
+ return xml_logo(songInfo, iconSrc);
+ case ToastStyles.banner_top_custom:
+ return xml_banner_top_custom(songInfo, iconSrc);
+ case ToastStyles.hero:
+ return xml_hero(songInfo, iconSrc);
+ case ToastStyles.banner_bottom:
+ return xml_banner_bottom(songInfo, iconSrc);
+ case ToastStyles.banner_centered_bottom:
+ return xml_banner_centered_bottom(songInfo, iconSrc);
+ case ToastStyles.banner_centered_top:
+ return xml_banner_centered_top(songInfo, iconSrc);
+ };
+}
+
+const iconLocation = app.isPackaged ?
+ path.resolve(app.getPath("userData"), 'icons') :
+ path.resolve(__dirname, '..', '..', 'assets/media-icons-black');
+
+const display = (kind) => {
+ if (config.get("toastStyle") === ToastStyles.legacy) {
+ return `content="${icons[kind]}"`;
+ } else {
+ return `\
+ content="${config.get("hideButtonText") ? "" : kind.charAt(0).toUpperCase() + kind.slice(1)}"\
+ imageUri="file:///${path.resolve(__dirname, iconLocation, `${kind}.png`)}"
+ `;
}
}
-//New notification
-function sendToaster(songInfo) {
- deleteNotification();
- //download image and get path
- let imgSrc = notificationImage(songInfo, true);
- toDelete = {
- appID: "com.github.th-ch.youtube-music",
- title: songInfo.title || "Playing",
- message: songInfo.artist,
- id: parseInt(Math.random() * 1000000, 10),
- icon: imgSrc,
- actions: [
- icons.previous,
- songInfo.isPaused ? icons.play : icons.pause,
- icons.next
- ],
- sound: false,
- };
- //send notification
- notifier.notify(
- toDelete,
- (err, data) => {
- // Will also wait until notification is closed.
- if (err) {
- console.log(`ERROR = ${err.toString()}\n DATA = ${data}`);
- }
- switch (data) {
- //buttons
- case icons.previous.normalize():
- controls.previous();
- return;
- case icons.next.normalize():
- controls.next();
- return;
- case icons.play.normalize():
- controls.playPause();
- // dont delete notification on play/pause
- toDelete = undefined;
- //manually send notification if not sending automatically
- if (!notificationOnUnpause) {
- songInfo.isPaused = false;
- sendToaster(songInfo);
- }
- return;
- case icons.pause.normalize():
- controls.playPause();
- songInfo.isPaused = true;
- toDelete = undefined;
- sendToaster(songInfo);
- return;
- //Native datatype
- case "dismissed":
- case "timeout":
- deleteNotification();
- }
- }
+const getButton = (kind) =>
+ ``;
- );
+const getButtons = (isPaused) => `\
+
+ ${getButton('previous')}
+ ${isPaused ? getButton('play') : getButton('pause')}
+ ${getButton('next')}
+ \
+`;
+
+const toast = (content, isPaused) => `\
+
+
+
+
+ ${content}
+
+
+
+ ${getButtons(isPaused)}
+`;
+
+const xml_image = ({ title, artist, isPaused }, imgSrc, placement) => toast(`\
+
+ ${title}
+ ${artist}\
+`, isPaused);
+
+
+const xml_logo = (songInfo, imgSrc) => xml_image(songInfo, imgSrc, 'placement="appLogoOverride"');
+
+const xml_hero = (songInfo, imgSrc) => xml_image(songInfo, imgSrc, 'placement="hero"');
+
+const xml_banner_bottom = (songInfo, imgSrc) => xml_image(songInfo, imgSrc, '');
+
+const xml_banner_top_custom = (songInfo, imgSrc) => toast(`\
+
+ ㅤ
+
+
+ ${songInfo.title}
+ ${songInfo.artist}
+
+ ${xml_more_data(songInfo)}
+ \
+`, songInfo.isPaused);
+
+const xml_more_data = ({ album, elapsedSeconds, songDuration }) => `\
+
+ ${album ?
+ `${album}` : ''}
+ ${secondsToMinutes(elapsedSeconds)} / ${secondsToMinutes(songDuration)}
+\
+`;
+
+const xml_banner_centered_bottom = ({ title, artist, isPaused }, imgSrc) => toast(`\
+ ㅤ
+
+
+ ${title}
+ ${artist}
+
+
+ \
+`, isPaused);
+
+const xml_banner_centered_top = ({ title, artist, isPaused }, imgSrc) => toast(`\
+
+ ㅤ
+
+
+ ${title}
+ ${artist}
+
+ \
+`, isPaused);
+
+const titleFontPicker = (title) => {
+ if (title.length <= 13) {
+ return 'Header';
+ } else if (title.length <= 22) {
+ return 'Subheader';
+ } else if (title.length <= 26) {
+ return 'Title';
+ } else {
+ return 'Subtitle';
+ }
}
diff --git a/plugins/notifications/menu.js b/plugins/notifications/menu.js
index 3f239909..014be184 100644
--- a/plugins/notifications/menu.js
+++ b/plugins/notifications/menu.js
@@ -1,30 +1,80 @@
-const { urgencyLevels, setOption } = require("./utils");
+const { urgencyLevels, ToastStyles, snakeToCamel } = require("./utils");
const is = require("electron-is");
+const config = require("./config");
-module.exports = (win, options) => [
- ...(is.linux() ?
- [{
- label: "Notification Priority",
- submenu: urgencyLevels.map(level => ({
- label: level.name,
- type: "radio",
- checked: options.urgency === level.value,
- click: () => setOption(options, "urgency", level.value)
- })),
- }] :
- []),
- ...(is.windows() ?
- [{
- label: "Interactive Notifications",
- type: "checkbox",
- checked: options.interactive,
- click: (item) => setOption(options, "interactive", item.checked)
- }] :
- []),
+module.exports = (_win, options) => [
+ ...(is.linux()
+ ? [
+ {
+ label: "Notification Priority",
+ submenu: urgencyLevels.map((level) => ({
+ label: level.name,
+ type: "radio",
+ checked: options.urgency === level.value,
+ click: () => config.set("urgency", level.value),
+ })),
+ },
+ ]
+ : []),
+ ...(is.windows()
+ ? [
+ {
+ label: "Interactive Notifications",
+ type: "checkbox",
+ checked: options.interactive,
+ // doesn't update until restart
+ click: (item) => config.setAndMaybeRestart("interactive", item.checked),
+ },
+ {
+ // submenu with settings for interactive notifications (name shouldn't be too long)
+ label: "Interactive Settings",
+ submenu: [
+ {
+ label: "Open/Close on tray click",
+ type: "checkbox",
+ checked: options.trayControls,
+ click: (item) => config.set("trayControls", item.checked),
+ },
+ {
+ label: "Hide Button Text",
+ type: "checkbox",
+ checked: options.hideButtonText,
+ click: (item) => config.set("hideButtonText", item.checked),
+ },
+ {
+ label: "Refresh on Play/Pause",
+ type: "checkbox",
+ checked: options.refreshOnPlayPause,
+ click: (item) => config.set("refreshOnPlayPause", item.checked),
+ }
+ ]
+ },
+ {
+ label: "Style",
+ submenu: getToastStyleMenuItems(options)
+ },
+ ]
+ : []),
{
label: "Show notification on unpause",
type: "checkbox",
checked: options.unpauseNotification,
- click: (item) => setOption(options, "unpauseNotification", item.checked)
+ click: (item) => config.set("unpauseNotification", item.checked),
},
];
+
+function getToastStyleMenuItems(options) {
+ const arr = new Array(Object.keys(ToastStyles).length);
+
+ // ToastStyles index starts from 1
+ for (const [name, index] of Object.entries(ToastStyles)) {
+ arr[index - 1] = {
+ label: snakeToCamel(name),
+ type: "radio",
+ checked: options.toastStyle === index,
+ click: () => config.set("toastStyle", index),
+ };
+ }
+
+ return arr;
+}
diff --git a/plugins/notifications/utils.js b/plugins/notifications/utils.js
index 7cb9e61e..8717b3a1 100644
--- a/plugins/notifications/utils.js
+++ b/plugins/notifications/utils.js
@@ -1,10 +1,22 @@
-const { setMenuOptions } = require("../../config/plugins");
const path = require("path");
const { app } = require("electron");
const fs = require("fs");
+const config = require("./config");
const icon = "assets/youtube-music.png";
-const tempIcon = path.join(app.getPath("userData"), "tempIcon.png");
+const userData = app.getPath("userData");
+const tempIcon = path.join(userData, "tempIcon.png");
+const tempBanner = path.join(userData, "tempBanner.png");
+
+module.exports.ToastStyles = {
+ logo: 1,
+ banner_centered_top: 2,
+ hero: 3,
+ banner_top_custom: 4,
+ banner_centered_bottom: 5,
+ banner_bottom: 6,
+ legacy: 7
+}
module.exports.icons = {
play: "\u{1405}", // ᐅ
@@ -13,38 +25,37 @@ module.exports.icons = {
previous: "\u{1438}" // ᐸ
}
-module.exports.setOption = (options, option, value) => {
- options[option] = value;
- setMenuOptions("notifications", options)
-}
-
module.exports.urgencyLevels = [
{ name: "Low", value: "low" },
{ name: "Normal", value: "normal" },
{ name: "High", value: "critical" },
];
-module.exports.notificationImage = function (songInfo, saveIcon = false) {
- //return local path to temp icon
- if (saveIcon && !!songInfo.image) {
- try {
- fs.writeFileSync(tempIcon,
- centerNativeImage(songInfo.image)
- .toPNG()
- );
- } catch (err) {
- console.log(`Error writing song icon to disk:\n${err.toString()}`)
- return icon;
- }
- return tempIcon;
- }
- //else: return image
- return songInfo.image
- ? centerNativeImage(songInfo.image)
- : icon
+module.exports.notificationImage = (songInfo) => {
+ if (!songInfo.image) return icon;
+ if (!config.get("interactive")) return nativeImageToLogo(songInfo.image);
+
+ switch (config.get("toastStyle")) {
+ case module.exports.ToastStyles.logo:
+ case module.exports.ToastStyles.legacy:
+ return this.saveImage(nativeImageToLogo(songInfo.image), tempIcon);
+ default:
+ return this.saveImage(songInfo.image, tempBanner);
+ };
};
-function centerNativeImage(nativeImage) {
+module.exports.saveImage = (img, save_path) => {
+ try {
+ fs.writeFileSync(save_path, img.toPNG());
+ } catch (err) {
+ console.log(`Error writing song icon to disk:\n${err.toString()}`)
+ return icon;
+ }
+ return save_path;
+}
+
+
+function nativeImageToLogo(nativeImage) {
const tempImage = nativeImage.resize({ height: 256 });
const margin = Math.max((tempImage.getSize().width - 256), 0);
@@ -54,3 +65,27 @@ function centerNativeImage(nativeImage) {
width: 256, height: 256
})
}
+
+module.exports.save_temp_icons = () => {
+ for (const kind of Object.keys(module.exports.icons)) {
+ const destinationPath = path.join(userData, 'icons', `${kind}.png`);
+ if (fs.existsSync(destinationPath)) continue;
+ const iconPath = path.resolve(__dirname, "../../assets/media-icons-black", `${kind}.png`);
+ fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
+ fs.copyFile(iconPath, destinationPath, () => { });
+ }
+};
+
+module.exports.snakeToCamel = (str) => {
+ return str.replace(/([-_][a-z]|^[a-z])/g, (group) =>
+ group.toUpperCase()
+ .replace('-', ' ')
+ .replace('_', ' ')
+ );
+}
+
+module.exports.secondsToMinutes = (seconds) => {
+ const minutes = Math.floor(seconds / 60);
+ const secondsLeft = seconds % 60;
+ return `${minutes}:${secondsLeft < 10 ? '0' : ''}${secondsLeft}`;
+}
diff --git a/plugins/picture-in-picture/front.js b/plugins/picture-in-picture/front.js
index 941a4333..27600641 100644
--- a/plugins/picture-in-picture/front.js
+++ b/plugins/picture-in-picture/front.js
@@ -34,7 +34,7 @@ const observer = new MutationObserver(() => {
menu = getSongMenu();
if (!menu) return;
}
- if (menu.contains(pipButton)) return;
+ if (menu.contains(pipButton) || !menu.parentElement.eventSink_?.matches('ytmusic-menu-renderer.ytmusic-player-bar')) return;
const menuUrl = $(
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint'
)?.href;
diff --git a/plugins/playback-speed/front.js b/plugins/playback-speed/front.js
index 6f972d14..da01182c 100644
--- a/plugins/playback-speed/front.js
+++ b/plugins/playback-speed/front.js
@@ -30,7 +30,7 @@ const observePopupContainer = () => {
menu = getSongMenu();
}
- if (menu && menu.lastElementChild.lastElementChild.innerText.startsWith('Stats') && !menu.contains(slider)) {
+ if (menu && menu.parentElement.eventSink_?.matches('ytmusic-menu-renderer.ytmusic-player-bar') && !menu.contains(slider)) {
menu.prepend(slider);
if (!observingSlider) {
setupSliderListener();
diff --git a/plugins/shortcuts/mpris.js b/plugins/shortcuts/mpris.js
index 90c1406a..e0ab3468 100644
--- a/plugins/shortcuts/mpris.js
+++ b/plugins/shortcuts/mpris.js
@@ -1,5 +1,5 @@
const mpris = require("mpris-service");
-const {ipcMain} = require("electron");
+const { ipcMain } = require("electron");
const registerCallback = require("../../providers/song-info");
const getSongControls = require("../../providers/song-controls");
const config = require("../../config");
@@ -18,9 +18,10 @@ function setupMPRIS() {
return player;
}
+/** @param {Electron.BrowserWindow} win */
function registerMPRIS(win) {
const songControls = getSongControls(win);
- const {playPause, next, previous, volumeMinus10, volumePlus10, shuffle} = songControls;
+ const { playPause, next, previous, volumeMinus10, volumePlus10, shuffle } = songControls;
try {
const secToMicro = n => Math.round(Number(n) * 1e6);
const microToSec = n => Math.round(Number(n) / 1e6);
@@ -30,6 +31,13 @@ function registerMPRIS(win) {
const player = setupMPRIS();
+ ipcMain.on("apiLoaded", () => {
+ win.webContents.send("setupSeekedListener", "mpris");
+ win.webContents.send("setupTimeChangedListener", "mpris");
+ win.webContents.send("setupRepeatChangedListener", "mpris");
+ win.webContents.send("setupVolumeChangedListener", "mpris");
+ });
+
ipcMain.on('seeked', (_, t) => player.seeked(secToMicro(t)));
let currentSeconds = 0;
@@ -109,7 +117,7 @@ function registerMPRIS(win) {
// With precise volume we can set the volume to the exact value.
let newVol = parseInt(newVolume * 100);
if (parseInt(player.volume * 100) !== newVol) {
- if (!autoUpdate){
+ if (!autoUpdate) {
mprisVolNewer = true;
autoUpdate = false;
win.webContents.send('setVolume', newVol);
diff --git a/plugins/taskbar-mediacontrol/back.js b/plugins/taskbar-mediacontrol/back.js
index 26a11732..dce9a6b3 100644
--- a/plugins/taskbar-mediacontrol/back.js
+++ b/plugins/taskbar-mediacontrol/back.js
@@ -32,22 +32,22 @@ function setThumbar(win, songInfo) {
win.setThumbarButtons([
{
tooltip: 'Previous',
- icon: get('backward.png'),
+ icon: get('previous'),
click() { controls.previous(win.webContents); }
}, {
tooltip: 'Play/Pause',
// Update icon based on play state
- icon: songInfo.isPaused ? get('play.png') : get('pause.png'),
+ icon: songInfo.isPaused ? get('play') : get('pause'),
click() { controls.playPause(win.webContents); }
}, {
tooltip: 'Next',
- icon: get('forward.png'),
+ icon: get('next'),
click() { controls.next(win.webContents); }
}
]);
}
// Util
-function get(file) {
- return path.join(__dirname, "assets", file);
+function get(kind) {
+ return path.join(__dirname, "../../assets/media-icons-black", `${kind}.png`);
}
diff --git a/plugins/tuna-obs/back.js b/plugins/tuna-obs/back.js
index 574562f1..5b3eec65 100644
--- a/plugins/tuna-obs/back.js
+++ b/plugins/tuna-obs/back.js
@@ -28,7 +28,9 @@ const post = async (data) => {
fetch(url, { method: 'POST', headers, body: JSON.stringify({ data }) }).catch(e => console.log(`Error: '${e.code || e.errno}' - when trying to access obs-tuna webserver at port ${port}`));
}
+/** @param {Electron.BrowserWindow} win */
module.exports = async (win) => {
+ ipcMain.on('apiLoaded', () => win.webContents.send('setupTimeChangedListener'));
ipcMain.on('timeChanged', async (_, t) => {
if (!data.title) return;
data.progress = secToMilisec(t);
diff --git a/preload.js b/preload.js
index 3dc5aa89..492f3192 100644
--- a/preload.js
+++ b/preload.js
@@ -1,9 +1,9 @@
-require("./providers/front-logger")();
const config = require("./config");
const { fileExists } = require("./plugins/utils");
const setupSongInfo = require("./providers/song-info-front");
const { setupSongControls } = require("./providers/song-controls-front");
const { ipcRenderer } = require("electron");
+const is = require("electron-is");
const plugins = config.plugins.getEnabled();
@@ -69,6 +69,13 @@ document.addEventListener("DOMContentLoaded", () => {
// Blocks the "Are You Still There?" popup by setting the last active time to Date.now every 15min
setInterval(() => window._lact = Date.now(), 900000);
+
+ // setup back to front logger
+ if (is.dev()) {
+ ipcRenderer.on("log", (_event, log) => {
+ console.log(JSON.parse(log));
+ });
+ }
});
function listenForApiLoad() {
@@ -118,6 +125,7 @@ function onApiLoaded() {
);
document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api }));
+ ipcRenderer.send('apiLoaded');
// Remove upgrade button
if (config.get("options.removeUpgradeButton")) {
diff --git a/providers/front-logger.js b/providers/front-logger.js
deleted file mode 100644
index 99da2329..00000000
--- a/providers/front-logger.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const { ipcRenderer } = require("electron");
-
-function logToString(log) {
- return (typeof log === "string") ?
- log :
- JSON.stringify(log, null, "\t");
-}
-
-module.exports = () => {
- ipcRenderer.on("log", (_event, log) => {
- console.log(logToString(log));
- });
-};
diff --git a/providers/protocol-handler.js b/providers/protocol-handler.js
new file mode 100644
index 00000000..6b0d36e1
--- /dev/null
+++ b/providers/protocol-handler.js
@@ -0,0 +1,44 @@
+const { app } = require("electron");
+const path = require("path");
+const getSongControls = require("./song-controls");
+
+const APP_PROTOCOL = "youtubemusic";
+
+let protocolHandler;
+
+function setupProtocolHandler(win) {
+ if (process.defaultApp && process.argv.length >= 2) {
+ app.setAsDefaultProtocolClient(
+ APP_PROTOCOL,
+ process.execPath,
+ [path.resolve(process.argv[1])]
+ );
+ } else {
+ app.setAsDefaultProtocolClient(APP_PROTOCOL)
+ }
+
+ const songControls = getSongControls(win);
+
+ protocolHandler = (cmd) => {
+ if (Object.keys(songControls).includes(cmd)) {
+ songControls[cmd]();
+ }
+ }
+}
+
+function handleProtocol(cmd) {
+ protocolHandler(cmd);
+}
+
+function changeProtocolHandler(f) {
+ protocolHandler = f;
+}
+
+module.exports = {
+ APP_PROTOCOL,
+ setupProtocolHandler,
+ handleProtocol,
+ changeProtocolHandler,
+};
+
+
diff --git a/providers/song-controls-front.js b/providers/song-controls-front.js
index 07c50542..4bc33274 100644
--- a/providers/song-controls-front.js
+++ b/providers/song-controls-front.js
@@ -1,13 +1,8 @@
const { ipcRenderer } = require("electron");
-const config = require("../config");
-const is = require("electron-is");
module.exports.setupSongControls = () => {
document.addEventListener('apiLoaded', e => {
ipcRenderer.on("seekTo", (_, t) => e.detail.seekTo(t));
ipcRenderer.on("seekBy", (_, t) => e.detail.seekBy(t));
- if (is.linux() && config.plugins.isEnabled('shortcuts')) { // MPRIS Enabled
- document.querySelector('video').addEventListener('seeked', v => ipcRenderer.send('seeked', v.target.currentTime));
- }
}, { once: true, passive: true })
};
diff --git a/providers/song-controls.js b/providers/song-controls.js
index a23190eb..93f352d1 100644
--- a/providers/song-controls.js
+++ b/providers/song-controls.js
@@ -8,7 +8,7 @@ const pressKey = (window, key, modifiers = []) => {
};
module.exports = (win) => {
- return {
+ const commands = {
// Playback
previous: () => pressKey(win, "k"),
next: () => pressKey(win, "j"),
@@ -21,8 +21,7 @@ module.exports = (win) => {
go1sForward: () => pressKey(win, "l", ["shift"]),
shuffle: () => pressKey(win, "s"),
switchRepeat: (n = 1) => {
- for (let i = 0; i < n; i++)
- pressKey(win, "r");
+ for (let i = 0; i < n; i++) pressKey(win, "r");
},
// General
volumeMinus10: () => pressKey(win, "-"),
@@ -50,4 +49,9 @@ module.exports = (win) => {
search: () => pressKey(win, "/"),
showShortcuts: () => pressKey(win, "/", ["shift"]),
};
+ return {
+ ...commands,
+ play: commands.playPause,
+ pause: commands.playPause
+ };
};
diff --git a/providers/song-info-front.js b/providers/song-info-front.js
index 958bcd0a..f033847c 100644
--- a/providers/song-info-front.js
+++ b/providers/song-info-front.js
@@ -1,8 +1,5 @@
-const {ipcRenderer} = require("electron");
-const is = require('electron-is');
-const {getImage} = require("./song-info");
-
-const config = require("../config");
+const { ipcRenderer } = require("electron");
+const { getImage } = require("./song-info");
global.songInfo = {};
@@ -17,20 +14,69 @@ ipcRenderer.on("update-song-info", async (_, extractedSongInfo) => {
// used because 'loadeddata' or 'loadedmetadata' weren't firing on song start for some users (https://github.com/th-ch/youtube-music/issues/473)
const srcChangedEvent = new CustomEvent('srcChanged');
+const singleton = (fn) => {
+ let called = false;
+ return (...args) => {
+ if (called) return;
+ called = true;
+ return fn(...args);
+ }
+}
+
+module.exports.setupSeekedListener = singleton(() => {
+ document.querySelector('video')?.addEventListener('seeked', v => ipcRenderer.send('seeked', v.target.currentTime));
+});
+
+module.exports.setupTimeChangedListener = singleton(() => {
+ const progressObserver = new MutationObserver(mutations => {
+ ipcRenderer.send('timeChanged', mutations[0].target.value);
+ global.songInfo.elapsedSeconds = mutations[0].target.value;
+ });
+ progressObserver.observe($('#progress-bar'), { attributeFilter: ["value"] });
+});
+
+module.exports.setupRepeatChangedListener = singleton(() => {
+ const repeatObserver = new MutationObserver(mutations => {
+ ipcRenderer.send('repeatChanged', mutations[0].target.__dataHost.getState().queue.repeatMode);
+ });
+ repeatObserver.observe($('#right-controls .repeat'), { attributeFilter: ["title"] });
+
+ // Emit the initial value as well; as it's persistent between launches.
+ ipcRenderer.send('repeatChanged', $('ytmusic-player-bar').getState().queue.repeatMode);
+});
+
+module.exports.setupVolumeChangedListener = singleton((api) => {
+ $('video').addEventListener('volumechange', (_) => {
+ ipcRenderer.send('volumeChanged', api.getVolume());
+ });
+ // Emit the initial value as well; as it's persistent between launches.
+ ipcRenderer.send('volumeChanged', api.getVolume());
+});
+
module.exports = () => {
document.addEventListener('apiLoaded', apiEvent => {
- if (config.plugins.isEnabled('tuna-obs') ||
- (is.linux() && config.plugins.isEnabled('shortcuts'))) {
- setupTimeChangeListener();
- setupRepeatChangeListener();
- setupVolumeChangeListener(apiEvent.detail);
- }
+ ipcRenderer.on("setupTimeChangedListener", async () => {
+ this.setupTimeChangedListener();
+ });
+
+ ipcRenderer.on("setupRepeatChangedListener", async () => {
+ this.setupRepeatChangedListener();
+ });
+
+ ipcRenderer.on("setupVolumeChangedListener", async () => {
+ this.setupVolumeChangedListener(apiEvent.detail);
+ });
+
+ ipcRenderer.on("setupSeekedListener", async () => {
+ this.setupSeekedListener();
+ });
+
const video = $('video');
// name = "dataloaded" and abit later "dataupdated"
apiEvent.detail.addEventListener('videodatachange', (name, _dataEvent) => {
if (name !== 'dataloaded') return;
video.dispatchEvent(srcChangedEvent);
- sendSongInfo();
+ setTimeout(sendSongInfo());
})
for (const status of ['playing', 'pause']) {
@@ -49,37 +95,14 @@ module.exports = () => {
data.videoDetails.album = $$(
".byline.ytmusic-player-bar > .yt-simple-endpoint"
- ).find(e => e.href?.includes("browse"))?.textContent;
+ ).find(e =>
+ e.href?.includes("browse/FEmusic_library_privately_owned_release")
+ || e.href?.includes("browse/MPREb")
+ )?.textContent;
data.videoDetails.elapsedSeconds = Math.floor(video.currentTime);
data.videoDetails.isPaused = false;
ipcRenderer.send("video-src-changed", JSON.stringify(data));
}
- }, {once: true, passive: true});
+ }, { once: true, passive: true });
};
-
-function setupTimeChangeListener() {
- const progressObserver = new MutationObserver(mutations => {
- ipcRenderer.send('timeChanged', mutations[0].target.value);
- global.songInfo.elapsedSeconds = mutations[0].target.value;
- });
- progressObserver.observe($('#progress-bar'), {attributeFilter: ["value"]})
-}
-
-function setupRepeatChangeListener() {
- const repeatObserver = new MutationObserver(mutations => {
- ipcRenderer.send('repeatChanged', mutations[0].target.__dataHost.getState().queue.repeatMode)
- });
- repeatObserver.observe($('#right-controls .repeat'), {attributeFilter: ["title"]});
-
- // Emit the initial value as well; as it's persistent between launches.
- ipcRenderer.send('repeatChanged', $('ytmusic-player-bar').getState().queue.repeatMode);
-}
-
-function setupVolumeChangeListener(api) {
- $('video').addEventListener('volumechange', (_) => {
- ipcRenderer.send('volumeChanged', api.getVolume());
- });
- // Emit the initial value as well; as it's persistent between launches.
- ipcRenderer.send('volumeChanged', api.getVolume());
-}
diff --git a/providers/song-info.js b/providers/song-info.js
index 88f757a3..1e26b99c 100644
--- a/providers/song-info.js
+++ b/providers/song-info.js
@@ -61,7 +61,8 @@ const handleData = async (responseText, win) => {
songInfo.album = data?.videoDetails?.album; // Will be undefined if video exist
const oldUrl = songInfo.imageSrc;
- songInfo.imageSrc = videoDetails.thumbnail?.thumbnails?.pop()?.url.split("?")[0];
+ const thumbnails = videoDetails.thumbnail?.thumbnails;
+ songInfo.imageSrc = thumbnails[thumbnails.length - 1]?.url.split("?")[0];
if (oldUrl !== songInfo.imageSrc) {
songInfo.image = await getImage(songInfo.imageSrc);
}
@@ -95,7 +96,7 @@ const registerProvider = (win) => {
await handleData(responseText, win);
handlingData = false;
callbacks.forEach((c) => {
- c(songInfo);
+ c(songInfo, "video-src-changed");
});
});
ipcMain.on("playPaused", (_, { isPaused, elapsedSeconds }) => {
@@ -103,7 +104,7 @@ const registerProvider = (win) => {
songInfo.elapsedSeconds = elapsedSeconds;
if (handlingData) return;
callbacks.forEach((c) => {
- c(songInfo);
+ c(songInfo, "playPaused");
});
})
};
diff --git a/readme.md b/readme.md
index bf086f7f..7682c40b 100644
--- a/readme.md
+++ b/readme.md
@@ -118,7 +118,7 @@ winget install th-ch.YouTubeMusic
- **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
-> If using `Hide Menu` option - you can show the menu with the `alt` key (or `escape` if using the in-app-menu plugin)
+> If `Hide Menu` option is on - you can show the menu with the alt key (or \` [backtick] if using the in-app-menu plugin)
## Themes
diff --git a/tray.js b/tray.js
index cf407aa2..46b76f83 100644
--- a/tray.js
+++ b/tray.js
@@ -1,14 +1,29 @@
const path = require("path");
-const { app, Menu, nativeImage, Tray } = require("electron");
+const { Menu, nativeImage, Tray } = require("electron");
const { restart } = require("./providers/app-controls");
const config = require("./config");
const getSongControls = require("./providers/song-controls");
// Prevent tray being garbage collected
+
+/** @type {Electron.Tray} */
let tray;
+module.exports.setTrayOnClick = (fn) => {
+ if (!tray) return;
+ tray.removeAllListeners('click');
+ tray.on("click", fn);
+};
+
+// wont do anything on macos since its disabled
+module.exports.setTrayOnDoubleClick = (fn) => {
+ if (!tray) return;
+ tray.removeAllListeners('double-click');
+ tray.on("double-click", fn);
+};
+
module.exports.setUpTray = (app, win) => {
if (!config.get("options.tray")) {
tray = undefined;
@@ -17,13 +32,19 @@ module.exports.setUpTray = (app, win) => {
const { playPause, next, previous } = getSongControls(win);
const iconPath = path.join(__dirname, "assets", "youtube-music-tray.png");
+
let trayIcon = nativeImage.createFromPath(iconPath).resize({
width: 16,
height: 16,
});
+
tray = new Tray(trayIcon);
- tray.setToolTip("Youtube Music");
+
+ tray.setToolTip("YouTube Music");
+
+ // macOS only
tray.setIgnoreDoubleClickEvents(true);
+
tray.on("click", () => {
if (config.get("options.trayClickPlayPause")) {
playPause();
diff --git a/yarn.lock b/yarn.lock
index 971b652a..6a69ab79 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2382,16 +2382,16 @@ __metadata:
languageName: node
linkType: hard
-"custom-electron-prompt@npm:^1.5.1":
- version: 1.5.1
- resolution: "custom-electron-prompt@npm:1.5.1"
+"custom-electron-prompt@npm:^1.5.4":
+ version: 1.5.4
+ resolution: "custom-electron-prompt@npm:1.5.4"
peerDependencies:
electron: ">=10.0.0"
- checksum: 43a0d72a7a3471135822cb210d580285f70080d9d3a7b03f82cd4be403059fe20ea05ebdd1f9534928c386ab25a353e678f2cfb3f4ca016b41f3366bff700767
+ checksum: 93995b5f0e9d14401a8c4fdd358af32d8b7585b59b111667cfa55f9505109c08914f3140953125b854e5d09e811de8c76c7fec718934c13e8a1ad09fe1b85270
languageName: node
linkType: hard
-"custom-electron-titlebar@npm:^4.1.5":
+"custom-electron-titlebar@npm:^4.1.6":
version: 4.1.6
resolution: "custom-electron-titlebar@npm:4.1.6"
peerDependencies:
@@ -2528,7 +2528,7 @@ __metadata:
languageName: node
linkType: hard
-"deepmerge@npm:^4.2.2":
+"deepmerge@npm:^4.3.0":
version: 4.3.0
resolution: "deepmerge@npm:4.3.0"
checksum: c7980eb5c5be040b371f1df0d566473875cfabed9f672ccc177b81ba8eee5686ce2478de2f1d0076391621cbe729e5eacda397179a59ef0f68901849647db126
@@ -3017,15 +3017,15 @@ __metadata:
linkType: hard
"electron@npm:^22.0.2":
- version: 22.2.0
- resolution: "electron@npm:22.2.0"
+ version: 22.2.1
+ resolution: "electron@npm:22.2.1"
dependencies:
"@electron/get": ^2.0.0
"@types/node": ^16.11.26
extract-zip: ^2.0.1
bin:
electron: cli.js
- checksum: 096434fe95408928c86de4782e87fcad8b933043a9a7b5447e87964c6c597352584d413157cca43a9f1fd4bf669d2e344d6604ff5c499367e3ca6f1008e2fd5f
+ checksum: d7331f1e4fbdaf7cb2e5093c3636cb2b64bd437a31b4664f67d4353caf1d021ab582f88584dd2e170a282ebf11158b17cc2f6846432beae3a4b5bc371555fd6d
languageName: node
linkType: hard
@@ -4122,7 +4122,7 @@ __metadata:
languageName: node
linkType: hard
-"get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3":
+"get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.0":
version: 1.2.0
resolution: "get-intrinsic@npm:1.2.0"
dependencies:
@@ -4355,13 +4355,6 @@ __metadata:
languageName: node
linkType: hard
-"growly@npm:^1.3.0":
- version: 1.3.0
- resolution: "growly@npm:1.3.0"
- checksum: 53cdecd4c16d7d9154a9061a9ccb87d602e957502ca69b529d7d1b2436c2c0b700ec544fc6b3e4cd115d59b81e62e44ce86bd0521403b579d3a2a97d7ce72a44
- languageName: node
- linkType: hard
-
"handlebars@npm:^4.7.7":
version: 4.7.7
resolution: "handlebars@npm:4.7.7"
@@ -4531,16 +4524,16 @@ __metadata:
languageName: node
linkType: hard
-"html-to-text@npm:^9.0.3":
- version: 9.0.3
- resolution: "html-to-text@npm:9.0.3"
+"html-to-text@npm:^9.0.4":
+ version: 9.0.4
+ resolution: "html-to-text@npm:9.0.4"
dependencies:
"@selderee/plugin-htmlparser2": ^0.10.0
- deepmerge: ^4.2.2
+ deepmerge: ^4.3.0
dom-serializer: ^2.0.0
htmlparser2: ^8.0.1
selderee: ^0.10.0
- checksum: 4509edd5c03a9aae163a3362a05b26aa0f9e7deaaa856a5f64c6885d9d9d7235190efb905c6794e5a696bdd2536d6e19432953670e3243b1399048727c232134
+ checksum: 5431f7fa5501ba05cdc7e7eb90b9d3f7607e9779f313abc6a48bf493e144947f3bde63426679ca153e085ca77d7c0983bb2cf160a30b68b1598d1fb174a0ca05
languageName: node
linkType: hard
@@ -4756,13 +4749,13 @@ __metadata:
linkType: hard
"internal-slot@npm:^1.0.4":
- version: 1.0.4
- resolution: "internal-slot@npm:1.0.4"
+ version: 1.0.5
+ resolution: "internal-slot@npm:1.0.5"
dependencies:
- get-intrinsic: ^1.1.3
+ get-intrinsic: ^1.2.0
has: ^1.0.3
side-channel: ^1.0.4
- checksum: 8974588d06bab4f675573a3b52975370facf6486df51bc0567a982c7024fa29495f10b76c0d4dc742dd951d1b72024fdc1e31bb0bedf1678dc7aacacaf5a4f73
+ checksum: 97e84046bf9e7574d0956bd98d7162313ce7057883b6db6c5c7b5e5f05688864b0978ba07610c726d15d66544ffe4b1050107d93f8a39ebc59b15d8b429b497a
languageName: node
linkType: hard
@@ -5652,11 +5645,11 @@ __metadata:
linkType: hard
"locate-path@npm:^7.1.0":
- version: 7.1.1
- resolution: "locate-path@npm:7.1.1"
+ version: 7.2.0
+ resolution: "locate-path@npm:7.2.0"
dependencies:
p-locate: ^6.0.0
- checksum: 1d88af5b512d6e6398026252e17382907126683ab09ae5d6b8918d0bc72ca2642e1ad6e2fe635c5920840e369618e5d748c08deb57ba537fdd3f78e87ca993e0
+ checksum: c1b653bdf29beaecb3d307dfb7c44d98a2a98a02ebe353c9ad055d1ac45d6ed4e1142563d222df9b9efebc2bcb7d4c792b507fad9e7150a04c29530b7db570f8
languageName: node
linkType: hard
@@ -6238,20 +6231,6 @@ __metadata:
languageName: node
linkType: hard
-"node-notifier@npm:^10.0.1":
- version: 10.0.1
- resolution: "node-notifier@npm:10.0.1"
- dependencies:
- growly: ^1.3.0
- is-wsl: ^2.2.0
- semver: ^7.3.5
- shellwords: ^0.1.1
- uuid: ^8.3.2
- which: ^2.0.2
- checksum: ac09456152e433462dd3ca277048de7a60c6d63fc657e00ac72805841baf9bb2573e8d3f64c4b64af73546d1ed39733af6b0036c38b57a83c883aa33fff35a2e
- languageName: node
- linkType: hard
-
"nopt@npm:^6.0.0":
version: 6.0.0
resolution: "nopt@npm:6.0.0"
@@ -7586,13 +7565,6 @@ __metadata:
languageName: node
linkType: hard
-"shellwords@npm:^0.1.1":
- version: 0.1.1
- resolution: "shellwords@npm:0.1.1"
- checksum: 8d73a5e9861f5e5f1068e2cfc39bc0002400fe58558ab5e5fa75630d2c3adf44ca1fac81957609c8320d5533e093802fcafc72904bf1a32b95de3c19a0b1c0d4
- languageName: node
- linkType: hard
-
"side-channel@npm:^1.0.4":
version: 1.0.4
resolution: "side-channel@npm:1.0.4"
@@ -8295,9 +8267,9 @@ __metadata:
linkType: hard
"type-fest@npm:^3.1.0":
- version: 3.5.6
- resolution: "type-fest@npm:3.5.6"
- checksum: ce80d778c35280e967796b99989b796a75f8c2b6c8f1b88eb62990107f5fa90c30fdb1826fe5e2ab192e5c1eef7de6f29d133b443e80c753cbc020b01a32487a
+ version: 3.5.7
+ resolution: "type-fest@npm:3.5.7"
+ checksum: 06358352daa706d6f582d2041945e629fdd236c3c94678c4d87efb5d2e77bab78740337449f44bbd09a736c966e70570e901e2e2576b59b369891ffc1bf87bb6
languageName: node
linkType: hard
@@ -8495,7 +8467,7 @@ __metadata:
languageName: node
linkType: hard
-"uuid@npm:^8.3.1, uuid@npm:^8.3.2":
+"uuid@npm:^8.3.1":
version: 8.3.2
resolution: "uuid@npm:8.3.2"
bin:
@@ -8911,8 +8883,8 @@ __metadata:
browser-id3-writer: ^4.4.0
butterchurn: ^2.6.7
butterchurn-presets: ^2.4.7
- custom-electron-prompt: ^1.5.1
- custom-electron-titlebar: ^4.1.5
+ custom-electron-prompt: ^1.5.4
+ custom-electron-titlebar: ^4.1.6
del-cli: ^5.0.0
electron: ^22.0.2
electron-better-web-request: ^1.0.1
@@ -8927,14 +8899,13 @@ __metadata:
electron-updater: ^5.3.0
filenamify: ^4.3.0
howler: ^2.2.3
- html-to-text: ^9.0.3
+ html-to-text: ^9.0.4
keyboardevent-from-electron-accelerator: ^2.0.0
keyboardevents-areequal: ^0.2.2
md5: ^2.3.0
mpris-service: ^2.1.2
node-fetch: ^2.6.8
node-gyp: ^9.3.1
- node-notifier: ^10.0.1
playwright: ^1.29.2
simple-youtube-age-restriction-bypass: "https://gitpkg.now.sh/api/pkg.tgz?url=zerodytrash/Simple-YouTube-Age-Restriction-Bypass&commit=v2.5.4"
vudio: ^2.1.1
diff --git a/youtube-music.css b/youtube-music.css
index ee48dc3a..c0d3cc4e 100644
--- a/youtube-music.css
+++ b/youtube-music.css
@@ -44,3 +44,9 @@ ytmusic-cast-button {
.ytp-chrome-top-buttons {
display: none !important;
}
+
+/* Make youtube-music logo un-draggable */
+ytmusic-nav-bar>div.left-content>a,
+ytmusic-nav-bar>div.left-content>a>picture>img {
+ -webkit-user-drag: none;
+}