mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-13 19:31:46 +00:00
Compare commits
179 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d775e3d588 | |||
| e7ec15e90f | |||
| 403470be69 | |||
| 6dc0ba74c4 | |||
| 6dcfb336c2 | |||
| 84516b2ac1 | |||
| 57cf2a8cdd | |||
| e3ae97fec4 | |||
| ee76e2cb45 | |||
| de01bb6e75 | |||
| 42668c3e99 | |||
| 05f3c56e47 | |||
| d54977b9ee | |||
| b89fb4dc2f | |||
| a0cf77edfb | |||
| 069f9855d1 | |||
| e3e0775401 | |||
| d255e5ffe1 | |||
| fea460a374 | |||
| 302d3f693f | |||
| 9cc320d74b | |||
| e255777283 | |||
| fe0f213919 | |||
| e888b5c896 | |||
| f27ff52689 | |||
| acbe0ac25d | |||
| c66ff2bf05 | |||
| d089487aa8 | |||
| 6bc1d1606f | |||
| 9df5d921c7 | |||
| 4b1dfa1173 | |||
| f98318e737 | |||
| 7fa1278b31 | |||
| 878ec1f6c1 | |||
| 086048780a | |||
| aff0415816 | |||
| 6040fe1cbd | |||
| 52f4e9d796 | |||
| 09fe80cae7 | |||
| 817b48dc9d | |||
| c6f8c42c45 | |||
| 0535686129 | |||
| 53a77255ca | |||
| c01506dc44 | |||
| 6f5f9386ff | |||
| fddd0607e6 | |||
| 2cb6f56feb | |||
| 46285a5ed0 | |||
| 496836b33b | |||
| af127879a5 | |||
| 38ef452801 | |||
| a9a5d99676 | |||
| e5ab50cebd | |||
| 49194f8141 | |||
| 641ae27efd | |||
| 47a5dec465 | |||
| c93eabb400 | |||
| 492a47321d | |||
| c89f6af8c6 | |||
| 9687c6c8e4 | |||
| ef0a89126a | |||
| 8ce71d628d | |||
| ca95d105c8 | |||
| 12568c2b09 | |||
| 82abb4d4d3 | |||
| 3c0a5dbbe5 | |||
| 0b98eef06f | |||
| 18e69c9f2a | |||
| 8f5d06d420 | |||
| 8a299461a0 | |||
| 0c58bec921 | |||
| e0cb132686 | |||
| 2a192f39f9 | |||
| b7ebb7d499 | |||
| fffeac21b7 | |||
| 4387cb485d | |||
| 2a58dc823a | |||
| 8eb38271ff | |||
| 1987ad1d4f | |||
| cc4dae60ef | |||
| 1943116aa1 | |||
| 3485d26b11 | |||
| 4a60aa9f20 | |||
| cda07c9675 | |||
| ca64a77ed0 | |||
| 30e94d1d6f | |||
| b8c6ebfa53 | |||
| b26748ded8 | |||
| f186da0834 | |||
| c52c2d886a | |||
| e5dc1f8a58 | |||
| 6dbf4134de | |||
| e1cc49a74d | |||
| 4489a400b7 | |||
| 28aa1c0b22 | |||
| c540788d20 | |||
| 4ab07dc875 | |||
| 5033de13ef | |||
| 55a8787a16 | |||
| 3515bf364d | |||
| d8f3246e46 | |||
| cd613aaba2 | |||
| c5f84b568b | |||
| 14dc78984f | |||
| fb61dbfa6c | |||
| 33855f17dd | |||
| 8124623142 | |||
| 177ce5721f | |||
| 4fb0b1dd08 | |||
| bbe5a7d50b | |||
| 5bc8e86353 | |||
| 5b00465558 | |||
| 541c7f34b7 | |||
| 0e8e78362b | |||
| cb6a5a478e | |||
| d615030222 | |||
| cb5ef1d6e5 | |||
| 78a7dcb7e8 | |||
| 8cca9f3eeb | |||
| 93d4d3c976 | |||
| 8284b56075 | |||
| b266037bb4 | |||
| cb743de7fd | |||
| 7cf78c6635 | |||
| 7942efa202 | |||
| 792c2931b0 | |||
| a3778af48a | |||
| 163dc7e1d1 | |||
| 0a59122ac2 | |||
| d2a5110f3b | |||
| cf4bbf94e4 | |||
| d7e42471a4 | |||
| d634c41e75 | |||
| 6b88397f82 | |||
| da3c709ff0 | |||
| ccd320d8ff | |||
| 3831e61d10 | |||
| e46e7b74e2 | |||
| b3da77a6bc | |||
| 96a74f8955 | |||
| 3ea17e6f46 | |||
| a8ac2c3af9 | |||
| 2168cbca30 | |||
| cceb45319a | |||
| e985b78241 | |||
| 090ca828c0 | |||
| 3522925dec | |||
| 5faeddb99b | |||
| 1140c3e2e7 | |||
| 3fb08d27c7 | |||
| 250940d083 | |||
| e00be8f010 | |||
| 8b471c0772 | |||
| 88e738c796 | |||
| c76d8c79d8 | |||
| b396431a8b | |||
| d1795a82f7 | |||
| 9b821a0dfe | |||
| bf89842ee8 | |||
| d274e80f75 | |||
| a98b8945fb | |||
| 6b72599f80 | |||
| 4b6fe78a1a | |||
| d000c03fca | |||
| c0a185ba68 | |||
| ef0813e638 | |||
| 25e9f44260 | |||
| 2d6e858e8f | |||
| 53bf7c5068 | |||
| 13fb686188 | |||
| 61c5494588 | |||
| d96fefbc24 | |||
| e18b7c1013 | |||
| f190b51dcc | |||
| ca41c12f7c | |||
| 844edbe2f4 | |||
| 78974c02e5 | |||
| 4508464fd1 | |||
| dd6455a559 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
|||||||
- name: Setup NodeJS
|
- name: Setup NodeJS
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: "12.x"
|
node-version: "14.x"
|
||||||
|
|
||||||
- name: Get yarn cache directory path
|
- name: Get yarn cache directory path
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
|
|||||||
@ -41,7 +41,6 @@ const defaultConfig = {
|
|||||||
api_root: "http://ws.audioscrobbler.com/2.0/",
|
api_root: "http://ws.audioscrobbler.com/2.0/",
|
||||||
api_key: "04d76faaac8726e60988e14c105d421a", // api key registered by @semvis123
|
api_key: "04d76faaac8726e60988e14c105d421a", // api key registered by @semvis123
|
||||||
secret: "a5d2a36fdf64819290f6982481eaffa2",
|
secret: "a5d2a36fdf64819290f6982481eaffa2",
|
||||||
suffixesToRemove: [' - Topic', 'VEVO'] // removes suffixes of the artist name, for better recognition
|
|
||||||
},
|
},
|
||||||
discord: {
|
discord: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -64,7 +63,19 @@ const defaultConfig = {
|
|||||||
volumeDown: "Shift+PageDown"
|
volumeDown: "Shift+PageDown"
|
||||||
},
|
},
|
||||||
savedVolume: undefined //plugin save volume between session here
|
savedVolume: undefined //plugin save volume between session here
|
||||||
}
|
},
|
||||||
|
sponsorblock: {
|
||||||
|
enabled: false,
|
||||||
|
apiURL: "https://sponsor.ajay.app",
|
||||||
|
categories: [
|
||||||
|
"sponsor",
|
||||||
|
"intro",
|
||||||
|
"outro",
|
||||||
|
"interaction",
|
||||||
|
"selfpromo",
|
||||||
|
"music_offtopic",
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
84
index.js
84
index.js
@ -2,6 +2,7 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const electron = require("electron");
|
const electron = require("electron");
|
||||||
|
const enhanceWebRequest = require("electron-better-web-request").default;
|
||||||
const is = require("electron-is");
|
const is = require("electron-is");
|
||||||
const unhandled = require("electron-unhandled");
|
const unhandled = require("electron-unhandled");
|
||||||
const { autoUpdater } = require("electron-updater");
|
const { autoUpdater } = require("electron-updater");
|
||||||
@ -11,6 +12,7 @@ const { setApplicationMenu } = require("./menu");
|
|||||||
const { fileExists, injectCSS } = require("./plugins/utils");
|
const { fileExists, injectCSS } = require("./plugins/utils");
|
||||||
const { isTesting } = require("./utils/testing");
|
const { isTesting } = require("./utils/testing");
|
||||||
const { setUpTray } = require("./tray");
|
const { setUpTray } = require("./tray");
|
||||||
|
const { setupSongInfo } = require("./providers/song-info");
|
||||||
|
|
||||||
// Catch errors and log them
|
// Catch errors and log them
|
||||||
unhandled({
|
unhandled({
|
||||||
@ -153,10 +155,13 @@ function createMainWindow() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
removeContentSecurityPolicy();
|
||||||
|
|
||||||
return win;
|
return win;
|
||||||
}
|
}
|
||||||
|
|
||||||
app.once("browser-window-created", (event, win) => {
|
app.once("browser-window-created", (event, win) => {
|
||||||
|
setupSongInfo(win);
|
||||||
loadPlugins(win);
|
loadPlugins(win);
|
||||||
|
|
||||||
win.webContents.on("did-fail-load", (
|
win.webContents.on("did-fail-load", (
|
||||||
@ -202,8 +207,18 @@ app.once("browser-window-created", (event, win) => {
|
|||||||
// Force user-agent "Firefox Windows" for Google OAuth to work
|
// Force user-agent "Firefox Windows" for Google OAuth to work
|
||||||
// From https://github.com/firebase/firebase-js-sdk/issues/2478#issuecomment-571356751
|
// From https://github.com/firebase/firebase-js-sdk/issues/2478#issuecomment-571356751
|
||||||
// Only set on accounts.google.com, otherwise querySelectors in preload scripts fail (?)
|
// Only set on accounts.google.com, otherwise querySelectors in preload scripts fail (?)
|
||||||
|
// Uses custom user agent to Google alert with a correct device type (https://github.com/th-ch/youtube-music/issues/327)
|
||||||
|
// User agents are from https://developers.whatismybrowser.com/useragents/explore/
|
||||||
|
const userAgents = {
|
||||||
|
mac: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:70.0) Gecko/20100101 Firefox/70.0",
|
||||||
|
windows: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0",
|
||||||
|
linux: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:90.0) Gecko/20100101 Firefox/90.0",
|
||||||
|
}
|
||||||
|
|
||||||
const userAgent =
|
const userAgent =
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:70.0) Gecko/20100101 Firefox/70.0";
|
is.macOS() ? userAgents.mac :
|
||||||
|
is.windows() ? userAgents.windows :
|
||||||
|
userAgents.linux;
|
||||||
|
|
||||||
win.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => {
|
win.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => {
|
||||||
details.requestHeaders["User-Agent"] = userAgent;
|
details.requestHeaders["User-Agent"] = userAgent;
|
||||||
@ -255,6 +270,35 @@ app.on("ready", () => {
|
|||||||
}, 20000);
|
}, 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();
|
mainWindow = createMainWindow();
|
||||||
setApplicationMenu(mainWindow);
|
setApplicationMenu(mainWindow);
|
||||||
if (config.get("options.restartOnConfigChanges")) {
|
if (config.get("options.restartOnConfigChanges")) {
|
||||||
@ -346,3 +390,41 @@ function showUnresponsiveDialog(win, details) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -2,6 +2,5 @@ module.exports = {
|
|||||||
globals: {
|
globals: {
|
||||||
__APP__: undefined, // A different app will be launched in each test environment
|
__APP__: undefined, // A different app will be launched in each test environment
|
||||||
},
|
},
|
||||||
testEnvironment: "./tests/environment",
|
|
||||||
testTimeout: 30000, // 30s
|
testTimeout: 30000, // 30s
|
||||||
};
|
};
|
||||||
|
|||||||
445
menu.js
445
menu.js
@ -7,12 +7,13 @@ const is = require("electron-is");
|
|||||||
const { getAllPlugins } = require("./plugins/utils");
|
const { getAllPlugins } = require("./plugins/utils");
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
|
|
||||||
const pluginEnabledMenu = (win, plugin, label = "", hasSubmenu = false) => ({
|
// true only if in-app-menu was loaded on launch
|
||||||
|
const inAppMenuActive = config.plugins.isEnabled("in-app-menu");
|
||||||
|
|
||||||
|
const pluginEnabledMenu = (plugin, label = "", hasSubmenu = false, refreshMenu = undefined) => ({
|
||||||
label: label || plugin,
|
label: label || plugin,
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
checked: config.plugins.isEnabled(plugin),
|
checked: config.plugins.isEnabled(plugin),
|
||||||
//Submenu check used in in-app-menu
|
|
||||||
hasSubmenu: hasSubmenu || undefined,
|
|
||||||
click: (item) => {
|
click: (item) => {
|
||||||
if (item.checked) {
|
if (item.checked) {
|
||||||
config.plugins.enable(plugin);
|
config.plugins.enable(plugin);
|
||||||
@ -20,252 +21,226 @@ const pluginEnabledMenu = (win, plugin, label = "", hasSubmenu = false) => ({
|
|||||||
config.plugins.disable(plugin);
|
config.plugins.disable(plugin);
|
||||||
}
|
}
|
||||||
if (hasSubmenu) {
|
if (hasSubmenu) {
|
||||||
this.setApplicationMenu(win);
|
refreshMenu();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const mainMenuTemplate = (win) => [
|
const mainMenuTemplate = (win) => {
|
||||||
{
|
const refreshMenu = () => {
|
||||||
label: "Plugins",
|
this.setApplicationMenu(win);
|
||||||
submenu: [
|
if (inAppMenuActive) {
|
||||||
...getAllPlugins().map((plugin) => {
|
win.webContents.send("updateMenu", true);
|
||||||
const pluginPath = path.join(__dirname, "plugins", plugin, "menu.js")
|
}
|
||||||
if (existsSync(pluginPath)) {
|
}
|
||||||
if (!config.plugins.isEnabled(plugin)) {
|
return [
|
||||||
return pluginEnabledMenu(win, plugin, "", true);
|
{
|
||||||
|
label: "Plugins",
|
||||||
|
submenu: [
|
||||||
|
...getAllPlugins().map((plugin) => {
|
||||||
|
const pluginPath = path.join(__dirname, "plugins", plugin, "menu.js")
|
||||||
|
if (existsSync(pluginPath)) {
|
||||||
|
if (!config.plugins.isEnabled(plugin)) {
|
||||||
|
return pluginEnabledMenu(plugin, "", true, refreshMenu);
|
||||||
|
}
|
||||||
|
const getPluginMenu = require(pluginPath);
|
||||||
|
return {
|
||||||
|
label: plugin,
|
||||||
|
submenu: [
|
||||||
|
pluginEnabledMenu(plugin, "Enabled", true, refreshMenu),
|
||||||
|
...getPluginMenu(win, config.plugins.getOptions(plugin), refreshMenu),
|
||||||
|
],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const getPluginMenu = require(pluginPath);
|
|
||||||
return {
|
|
||||||
label: plugin,
|
|
||||||
submenu: [
|
|
||||||
pluginEnabledMenu(win, plugin, "Enabled", true),
|
|
||||||
...getPluginMenu(win, config.plugins.getOptions(plugin), () =>
|
|
||||||
module.exports.setApplicationMenu(win)
|
|
||||||
),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return pluginEnabledMenu(win, plugin);
|
return pluginEnabledMenu(plugin);
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Options",
|
label: "Options",
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: "Auto-update",
|
label: "Auto-update",
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
checked: config.get("options.autoUpdates"),
|
checked: config.get("options.autoUpdates"),
|
||||||
click: (item) => {
|
click: (item) => {
|
||||||
config.set("options.autoUpdates", item.checked);
|
config.set("options.autoUpdates", item.checked);
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Resume last song when app starts",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: config.get("options.resumeOnStart"),
|
|
||||||
click: (item) => {
|
|
||||||
config.set("options.resumeOnStart", item.checked);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...(is.windows() || is.linux()
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: "Hide menu",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: config.get("options.hideMenu"),
|
|
||||||
click: (item) => {
|
|
||||||
config.set("options.hideMenu", item.checked);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...(is.windows() || is.macOS()
|
|
||||||
? // Only works on Win/Mac
|
|
||||||
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
|
|
||||||
[
|
|
||||||
{
|
|
||||||
label: "Start at login",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: config.get("options.startAtLogin"),
|
|
||||||
click: (item) => {
|
|
||||||
config.set("options.startAtLogin", item.checked);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
label: "Tray",
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: "Disabled",
|
|
||||||
type: "radio",
|
|
||||||
checked: !config.get("options.tray"),
|
|
||||||
click: () => {
|
|
||||||
config.set("options.tray", false);
|
|
||||||
config.set("options.appVisible", true);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Enabled + app visible",
|
|
||||||
type: "radio",
|
|
||||||
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:
|
|
||||||
config.get("options.tray") && !config.get("options.appVisible"),
|
|
||||||
click: () => {
|
|
||||||
config.set("options.tray", true);
|
|
||||||
config.set("options.appVisible", false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ type: "separator" },
|
|
||||||
{
|
|
||||||
label: "Play/Pause on click",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: config.get("options.trayClickPlayPause"),
|
|
||||||
click: (item) => {
|
|
||||||
config.set("options.trayClickPlayPause", item.checked);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ type: "separator" },
|
|
||||||
{
|
|
||||||
label: "Advanced options",
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: "Disable hardware acceleration",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: config.get("options.disableHardwareAcceleration"),
|
|
||||||
click: (item) => {
|
|
||||||
config.set("options.disableHardwareAcceleration", item.checked);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Restart on config changes",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: config.get("options.restartOnConfigChanges"),
|
|
||||||
click: (item) => {
|
|
||||||
config.set("options.restartOnConfigChanges", item.checked);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Reset App cache when app starts",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: config.get("options.autoResetAppCache"),
|
|
||||||
click: (item) => {
|
|
||||||
config.set("options.autoResetAppCache", item.checked);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ type: "separator" },
|
|
||||||
{
|
|
||||||
label: "Toggle DevTools",
|
|
||||||
// Cannot use "toggleDevTools" role in MacOS
|
|
||||||
click: () => {
|
|
||||||
const { webContents } = win;
|
|
||||||
if (webContents.isDevToolsOpened()) {
|
|
||||||
webContents.closeDevTools();
|
|
||||||
} else {
|
|
||||||
const devToolsOptions = {};
|
|
||||||
webContents.openDevTools(devToolsOptions);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Edit config.json",
|
|
||||||
click: () => {
|
|
||||||
config.edit();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "View",
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: "Reload",
|
|
||||||
click: () => {
|
|
||||||
win.webContents.reload();
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
label: "Resume last song when app starts",
|
||||||
label: "Force Reload",
|
type: "checkbox",
|
||||||
click: () => {
|
checked: config.get("options.resumeOnStart"),
|
||||||
win.webContents.reloadIgnoringCache();
|
click: (item) => {
|
||||||
|
config.set("options.resumeOnStart", item.checked);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
...(is.windows() || is.linux()
|
||||||
{ type: "separator" },
|
? [
|
||||||
{
|
{
|
||||||
label: "Zoom In",
|
label: "Hide menu",
|
||||||
click: () => {
|
type: "checkbox",
|
||||||
win.webContents.setZoomLevel(
|
checked: config.get("options.hideMenu"),
|
||||||
win.webContents.getZoomLevel() + 1
|
click: (item) => {
|
||||||
);
|
config.set("options.hideMenu", item.checked);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(is.windows() || is.macOS()
|
||||||
|
? // Only works on Win/Mac
|
||||||
|
// https://www.electronjs.org/docs/api/app#appsetloginitemsettingssettings-macos-windows
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: "Start at login",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: config.get("options.startAtLogin"),
|
||||||
|
click: (item) => {
|
||||||
|
config.set("options.startAtLogin", item.checked);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
label: "Tray",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Disabled",
|
||||||
|
type: "radio",
|
||||||
|
checked: !config.get("options.tray"),
|
||||||
|
click: () => {
|
||||||
|
config.set("options.tray", false);
|
||||||
|
config.set("options.appVisible", true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Enabled + app visible",
|
||||||
|
type: "radio",
|
||||||
|
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:
|
||||||
|
config.get("options.tray") && !config.get("options.appVisible"),
|
||||||
|
click: () => {
|
||||||
|
config.set("options.tray", true);
|
||||||
|
config.set("options.appVisible", false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: "separator" },
|
||||||
|
{
|
||||||
|
label: "Play/Pause on click",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: config.get("options.trayClickPlayPause"),
|
||||||
|
click: (item) => {
|
||||||
|
config.set("options.trayClickPlayPause", item.checked);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
{ type: "separator" },
|
||||||
{
|
{
|
||||||
label: "Zoom Out",
|
label: "Advanced options",
|
||||||
click: () => {
|
submenu: [
|
||||||
win.webContents.setZoomLevel(
|
{
|
||||||
win.webContents.getZoomLevel() - 1
|
label: "Disable hardware acceleration",
|
||||||
);
|
type: "checkbox",
|
||||||
|
checked: config.get("options.disableHardwareAcceleration"),
|
||||||
|
click: (item) => {
|
||||||
|
config.set("options.disableHardwareAcceleration", item.checked);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Restart on config changes",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: config.get("options.restartOnConfigChanges"),
|
||||||
|
click: (item) => {
|
||||||
|
config.set("options.restartOnConfigChanges", item.checked);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reset App cache when app starts",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: config.get("options.autoResetAppCache"),
|
||||||
|
click: (item) => {
|
||||||
|
config.set("options.autoResetAppCache", item.checked);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: "separator" },
|
||||||
|
is.macOS() ?
|
||||||
|
{
|
||||||
|
label: "Toggle DevTools",
|
||||||
|
// Cannot use "toggleDevTools" role in MacOS
|
||||||
|
click: () => {
|
||||||
|
const { webContents } = win;
|
||||||
|
if (webContents.isDevToolsOpened()) {
|
||||||
|
webContents.closeDevTools();
|
||||||
|
} else {
|
||||||
|
const devToolsOptions = {};
|
||||||
|
webContents.openDevTools(devToolsOptions);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} :
|
||||||
|
{ role: "toggleDevTools" },
|
||||||
|
{
|
||||||
|
label: "Edit config.json",
|
||||||
|
click: () => {
|
||||||
|
config.edit();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
{
|
},
|
||||||
label: "Reset Zoom",
|
{
|
||||||
click: () => {
|
label: "View",
|
||||||
win.webContents.setZoomLevel(0);
|
submenu: [
|
||||||
|
{ role: "reload" },
|
||||||
|
{ role: "forceReload" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{ role: "zoomIn" },
|
||||||
|
{ role: "zoomOut" },
|
||||||
|
{ role: "resetZoom" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Navigation",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Go back",
|
||||||
|
click: () => {
|
||||||
|
if (win.webContents.canGoBack()) {
|
||||||
|
win.webContents.goBack();
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
],
|
label: "Go forward",
|
||||||
},
|
click: () => {
|
||||||
{
|
if (win.webContents.canGoForward()) {
|
||||||
label: "Navigation",
|
win.webContents.goForward();
|
||||||
submenu: [
|
}
|
||||||
{
|
},
|
||||||
label: "Go back",
|
|
||||||
click: () => {
|
|
||||||
if (win.webContents.canGoBack()) {
|
|
||||||
win.webContents.goBack();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
label: "Restart App",
|
||||||
label: "Go forward",
|
click: () => {
|
||||||
click: () => {
|
app.relaunch();
|
||||||
if (win.webContents.canGoForward()) {
|
app.quit();
|
||||||
win.webContents.goForward();
|
},
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
{ role: "quit" },
|
||||||
{
|
],
|
||||||
label: "Restart App",
|
},
|
||||||
click: () => {
|
];
|
||||||
app.relaunch();
|
}
|
||||||
app.quit();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Quit App",
|
|
||||||
click: () => {
|
|
||||||
app.quit();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
module.exports.mainMenuTemplate = mainMenuTemplate;
|
module.exports.mainMenuTemplate = mainMenuTemplate;
|
||||||
module.exports.setApplicationMenu = (win) => {
|
module.exports.setApplicationMenu = (win) => {
|
||||||
|
|||||||
34
package.json
34
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "youtube-music",
|
"name": "youtube-music",
|
||||||
"productName": "YouTube Music",
|
"productName": "YouTube Music",
|
||||||
"version": "1.12.0",
|
"version": "1.13.0",
|
||||||
"description": "YouTube Music Desktop App - including custom plugins",
|
"description": "YouTube Music Desktop App - including custom plugins",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "th-ch/youtube-music",
|
"repository": "th-ch/youtube-music",
|
||||||
@ -42,6 +42,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
|
"start:debug": "ELECTRON_ENABLE_LOGGING=1 electron .",
|
||||||
"icon": "rimraf assets/generated && electron-icon-maker --input=assets/youtube-music.png --output=assets/generated",
|
"icon": "rimraf assets/generated && electron-icon-maker --input=assets/youtube-music.png --output=assets/generated",
|
||||||
"generate:package": "node utils/generate-package-json.js",
|
"generate:package": "node utils/generate-package-json.js",
|
||||||
"postinstall": "yarn run icon && yarn run plugins",
|
"postinstall": "yarn run icon && yarn run plugins",
|
||||||
@ -50,6 +51,7 @@
|
|||||||
"build:linux": "yarn run clean && electron-builder --linux",
|
"build:linux": "yarn run clean && electron-builder --linux",
|
||||||
"build:mac": "yarn run clean && electron-builder --mac",
|
"build:mac": "yarn run clean && electron-builder --mac",
|
||||||
"build:win": "yarn run clean && electron-builder --win",
|
"build:win": "yarn run clean && electron-builder --win",
|
||||||
|
"lint": "xo",
|
||||||
"plugins": "yarn run plugin:adblocker && yarn run plugin:autoconfirm",
|
"plugins": "yarn run plugin:adblocker && yarn run plugin:autoconfirm",
|
||||||
"plugin:adblocker": "rimraf plugins/adblocker/ad-blocker-engine.bin && node plugins/adblocker/blocker.js",
|
"plugin:adblocker": "rimraf plugins/adblocker/ad-blocker-engine.bin && node plugins/adblocker/blocker.js",
|
||||||
"plugin:autoconfirm": "yarn run generate:package YoutubeNonStop",
|
"plugin:autoconfirm": "yarn run generate:package YoutubeNonStop",
|
||||||
@ -58,44 +60,48 @@
|
|||||||
"release:win": "yarn run clean && electron-builder --win -p always"
|
"release:win": "yarn run clean && electron-builder --win -p always"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.16.1",
|
"node": ">=14.0.0",
|
||||||
"npm": "Please use yarn and not npm"
|
"npm": "Please use yarn and not npm"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cliqz/adblocker-electron": "^1.20.4",
|
"@cliqz/adblocker-electron": "^1.22.5",
|
||||||
"@ffmpeg/core": "^0.8.5",
|
"@ffmpeg/core": "^0.10.0",
|
||||||
"@ffmpeg/ffmpeg": "^0.9.7",
|
"@ffmpeg/ffmpeg": "^0.10.0",
|
||||||
"YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.9.0",
|
"YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.9.0",
|
||||||
"async-mutex": "^0.3.1",
|
"async-mutex": "^0.3.1",
|
||||||
"browser-id3-writer": "^4.4.0",
|
"browser-id3-writer": "^4.4.0",
|
||||||
"custom-electron-titlebar": "^3.2.6",
|
"chokidar": "^3.5.2",
|
||||||
|
"custom-electron-titlebar": "^3.2.7",
|
||||||
"discord-rpc": "^3.2.0",
|
"discord-rpc": "^3.2.0",
|
||||||
|
"electron-better-web-request": "^1.0.1",
|
||||||
"electron-debug": "^3.2.0",
|
"electron-debug": "^3.2.0",
|
||||||
"electron-is": "^3.0.0",
|
"electron-is": "^3.0.0",
|
||||||
"electron-localshortcut": "^3.2.1",
|
"electron-localshortcut": "^3.2.1",
|
||||||
"electron-store": "^7.0.3",
|
"electron-store": "^7.0.3",
|
||||||
"electron-unhandled": "^3.0.2",
|
"electron-unhandled": "^3.0.2",
|
||||||
"electron-updater": "^4.3.8",
|
"electron-updater": "^4.4.6",
|
||||||
"filenamify": "^4.2.0",
|
"filenamify": "^4.3.0",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
|
"mpris-service": "^2.1.2",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"node-notifier": "^9.0.1",
|
"node-notifier": "^9.0.1",
|
||||||
"open": "^8.0.3",
|
"ytdl-core": "^4.9.1",
|
||||||
"ytdl-core": "^4.5.0",
|
"ytpl": "^2.2.3"
|
||||||
"ytpl": "^2.1.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^11.4.4",
|
"electron": "^12.0.8",
|
||||||
"electron-builder": "^22.10.5",
|
"electron-builder": "^22.10.5",
|
||||||
"electron-devtools-installer": "^3.1.1",
|
"electron-devtools-installer": "^3.1.1",
|
||||||
"electron-icon-maker": "0.0.5",
|
"electron-icon-maker": "0.0.5",
|
||||||
"get-port": "^5.1.1",
|
"get-port": "^5.1.1",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"spectron": "^13.0.0",
|
"spectron": "^14.0.0",
|
||||||
"xo": "^0.38.2"
|
"xo": "^0.40.3"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
"glob-parent": "5.1.2",
|
||||||
|
"minimist": "1.2.5",
|
||||||
"yargs-parser": "18.1.3"
|
"yargs-parser": "18.1.3"
|
||||||
},
|
},
|
||||||
"xo": {
|
"xo": {
|
||||||
|
|||||||
@ -8,7 +8,9 @@ const SOURCES = [
|
|||||||
"https://raw.githubusercontent.com/kbinani/adblock-youtube-ads/master/signed.txt",
|
"https://raw.githubusercontent.com/kbinani/adblock-youtube-ads/master/signed.txt",
|
||||||
// uBlock Origin
|
// uBlock Origin
|
||||||
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt",
|
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt",
|
||||||
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-2020.txt",
|
"https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-2021.txt",
|
||||||
|
// Fanboy Annoyances
|
||||||
|
"https://secure.fanboy.co.nz/fanboy-annoyance_ubo.txt",
|
||||||
];
|
];
|
||||||
|
|
||||||
const loadAdBlockerEngine = (
|
const loadAdBlockerEngine = (
|
||||||
@ -31,7 +33,17 @@ const loadAdBlockerEngine = (
|
|||||||
...additionalBlockLists,
|
...additionalBlockLists,
|
||||||
];
|
];
|
||||||
|
|
||||||
ElectronBlocker.fromLists(fetch, lists, {}, cachingOptions)
|
ElectronBlocker.fromLists(
|
||||||
|
fetch,
|
||||||
|
lists,
|
||||||
|
{
|
||||||
|
// when generating the engine for caching, do not load network filters
|
||||||
|
// So that enhancing the session works as expected
|
||||||
|
// Allowing to define multiple webRequest listeners
|
||||||
|
loadNetworkFilters: session !== undefined,
|
||||||
|
},
|
||||||
|
cachingOptions
|
||||||
|
)
|
||||||
.then((blocker) => {
|
.then((blocker) => {
|
||||||
if (session) {
|
if (session) {
|
||||||
blocker.enableBlockingInSession(session);
|
blocker.enableBlockingInSession(session);
|
||||||
|
|||||||
@ -1,25 +1,10 @@
|
|||||||
let videoElement = null;
|
const { ontimeupdate } = require("../../providers/video-element");
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations, observer) => {
|
module.exports = () => {
|
||||||
if (!videoElement) {
|
ontimeupdate((videoElement) => {
|
||||||
videoElement = document.querySelector("video");
|
if (videoElement.currentTime === 0 && videoElement.duration !== NaN) {
|
||||||
}
|
// auto-confirm-when-paused plugin can interfere here if not disabled!
|
||||||
|
videoElement.pause();
|
||||||
if (videoElement) {
|
}
|
||||||
videoElement.ontimeupdate = () => {
|
|
||||||
if (videoElement.currentTime === 0 && videoElement.duration !== NaN) {
|
|
||||||
// auto-confirm-when-paused plugin can interfere here if not disabled!
|
|
||||||
videoElement.pause();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function observeVideoElement() {
|
|
||||||
observer.observe(document, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
module.exports = observeVideoElement;
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
const Discord = require("discord-rpc");
|
const Discord = require("discord-rpc");
|
||||||
|
|
||||||
const getSongInfo = require("../../providers/song-info");
|
const registerCallback = require("../../providers/song-info");
|
||||||
|
|
||||||
const rpc = new Discord.Client({
|
const rpc = new Discord.Client({
|
||||||
transport: "ipc",
|
transport: "ipc",
|
||||||
@ -12,8 +12,6 @@ const clientId = "790655993809338398";
|
|||||||
let clearActivity;
|
let clearActivity;
|
||||||
|
|
||||||
module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => {
|
module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => {
|
||||||
const registerCallback = getSongInfo(win);
|
|
||||||
|
|
||||||
// If the page is ready, register the callback
|
// If the page is ready, register the callback
|
||||||
win.once("ready-to-show", () => {
|
win.once("ready-to-show", () => {
|
||||||
rpc.once("ready", () => {
|
rpc.once("ready", () => {
|
||||||
@ -31,6 +29,9 @@ module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => {
|
|||||||
songInfo.uploadDate,
|
songInfo.uploadDate,
|
||||||
songInfo.views.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " views"
|
songInfo.views.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " views"
|
||||||
].join(' || '),
|
].join(' || '),
|
||||||
|
buttons: [
|
||||||
|
{ label: "Listen Along", url: songInfo.url },
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
if (songInfo.isPaused) {
|
if (songInfo.isPaused) {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ const CHANNEL = "downloader";
|
|||||||
const ACTIONS = {
|
const ACTIONS = {
|
||||||
ERROR: "error",
|
ERROR: "error",
|
||||||
METADATA: "metadata",
|
METADATA: "metadata",
|
||||||
|
PROGRESS: "progress",
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@ -4,60 +4,73 @@ const { join } = require("path");
|
|||||||
const ID3Writer = require("browser-id3-writer");
|
const ID3Writer = require("browser-id3-writer");
|
||||||
const { dialog, ipcMain } = require("electron");
|
const { dialog, ipcMain } = require("electron");
|
||||||
|
|
||||||
const getSongInfo = require("../../providers/song-info");
|
const registerCallback = require("../../providers/song-info");
|
||||||
const { injectCSS, listenAction } = require("../utils");
|
const { injectCSS, listenAction } = require("../utils");
|
||||||
|
const { cropMaxWidth } = require("./utils");
|
||||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||||
|
const { getImage } = require("../../providers/song-info");
|
||||||
|
|
||||||
const sendError = (win, err) => {
|
const sendError = (win, error) => {
|
||||||
const dialogOpts = {
|
win.setProgressBar(-1); // close progress bar
|
||||||
|
dialog.showMessageBox({
|
||||||
type: "info",
|
type: "info",
|
||||||
buttons: ["OK"],
|
buttons: ["OK"],
|
||||||
title: "Error in download!",
|
title: "Error in download!",
|
||||||
message: "Argh! Apologies, download failed…",
|
message: "Argh! Apologies, download failed…",
|
||||||
detail: err.toString(),
|
detail: error.toString(),
|
||||||
};
|
});
|
||||||
dialog.showMessageBox(dialogOpts);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let metadata = {};
|
let nowPlayingMetadata = {};
|
||||||
|
|
||||||
function handle(win) {
|
function handle(win) {
|
||||||
injectCSS(win.webContents, join(__dirname, "style.css"));
|
injectCSS(win.webContents, join(__dirname, "style.css"));
|
||||||
const registerCallback = getSongInfo(win);
|
|
||||||
registerCallback((info) => {
|
registerCallback((info) => {
|
||||||
metadata = info;
|
nowPlayingMetadata = info;
|
||||||
});
|
});
|
||||||
|
|
||||||
listenAction(CHANNEL, (event, action, error) => {
|
listenAction(CHANNEL, (event, action, arg) => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case ACTIONS.ERROR:
|
case ACTIONS.ERROR: // arg = error
|
||||||
sendError(win, error);
|
sendError(win, arg);
|
||||||
break;
|
break;
|
||||||
case ACTIONS.METADATA:
|
case ACTIONS.METADATA:
|
||||||
event.returnValue = JSON.stringify(metadata);
|
event.returnValue = JSON.stringify(nowPlayingMetadata);
|
||||||
|
break;
|
||||||
|
case ACTIONS.PROGRESS: // arg = progress
|
||||||
|
win.setProgressBar(arg);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log("Unknown action: " + action);
|
console.log("Unknown action: " + action);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on("add-metadata", (event, filePath, songBuffer, currentMetadata) => {
|
ipcMain.on("add-metadata", async (event, filePath, songBuffer, currentMetadata) => {
|
||||||
let fileBuffer = songBuffer;
|
let fileBuffer = songBuffer;
|
||||||
const songMetadata = { ...metadata, ...currentMetadata };
|
const songMetadata = currentMetadata.imageSrcYTPL ? // This means metadata come from ytpl.getInfo();
|
||||||
|
{
|
||||||
|
...currentMetadata,
|
||||||
|
image: cropMaxWidth(await getImage(currentMetadata.imageSrcYTPL))
|
||||||
|
} :
|
||||||
|
{ ...nowPlayingMetadata, ...currentMetadata };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const coverBuffer = songMetadata.image.toPNG();
|
const coverBuffer = songMetadata.image && !songMetadata.image.isEmpty() ?
|
||||||
|
songMetadata.image.toPNG() : null;
|
||||||
|
|
||||||
const writer = new ID3Writer(songBuffer);
|
const writer = new ID3Writer(songBuffer);
|
||||||
|
|
||||||
// Create the metadata tags
|
// Create the metadata tags
|
||||||
writer
|
writer
|
||||||
.setFrame("TIT2", songMetadata.title)
|
.setFrame("TIT2", songMetadata.title)
|
||||||
.setFrame("TPE1", [songMetadata.artist])
|
.setFrame("TPE1", [songMetadata.artist]);
|
||||||
.setFrame("APIC", {
|
if (coverBuffer) {
|
||||||
|
writer.setFrame("APIC", {
|
||||||
type: 3,
|
type: 3,
|
||||||
data: coverBuffer,
|
data: coverBuffer,
|
||||||
description: "",
|
description: ""
|
||||||
});
|
});
|
||||||
|
}
|
||||||
writer.addTag();
|
writer.addTag();
|
||||||
fileBuffer = Buffer.from(writer.arrayBuffer);
|
fileBuffer = Buffer.from(writer.arrayBuffer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -25,6 +25,7 @@ const observer = new MutationObserver((mutations, observer) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const reinit = () => {
|
const reinit = () => {
|
||||||
|
triggerAction(CHANNEL, ACTIONS.PROGRESS, -1); // closes progress bar
|
||||||
if (!progress) {
|
if (!progress) {
|
||||||
console.warn("Cannot update progress");
|
console.warn("Cannot update progress");
|
||||||
} else {
|
} else {
|
||||||
@ -38,22 +39,31 @@ const baseUrl = defaultConfig.url;
|
|||||||
// contextBridge.exposeInMainWorld("downloader", {
|
// contextBridge.exposeInMainWorld("downloader", {
|
||||||
// download: () => {
|
// download: () => {
|
||||||
global.download = () => {
|
global.download = () => {
|
||||||
|
triggerAction(CHANNEL, ACTIONS.PROGRESS, 2); // starts with indefinite progress bar
|
||||||
|
let metadata;
|
||||||
let videoUrl = getSongMenu()
|
let videoUrl = getSongMenu()
|
||||||
.querySelector("ytmusic-menu-navigation-item-renderer")
|
// selector of first button which is always "Start Radio"
|
||||||
.querySelector("#navigation-endpoint")
|
?.querySelector('ytmusic-menu-navigation-item-renderer.iron-selected[tabindex="0"] #navigation-endpoint')
|
||||||
.getAttribute("href");
|
?.getAttribute("href");
|
||||||
videoUrl = !videoUrl
|
if (videoUrl) {
|
||||||
? global.songInfo.url || window.location.href
|
videoUrl = baseUrl + "/" + videoUrl;
|
||||||
: baseUrl + "/" + videoUrl;
|
metadata = null;
|
||||||
|
} else {
|
||||||
|
metadata = global.songInfo;
|
||||||
|
videoUrl = metadata.url || window.location.href;
|
||||||
|
}
|
||||||
|
|
||||||
downloadVideoToMP3(
|
downloadVideoToMP3(
|
||||||
videoUrl,
|
videoUrl,
|
||||||
(feedback) => {
|
(feedback, ratio = undefined) => {
|
||||||
if (!progress) {
|
if (!progress) {
|
||||||
console.warn("Cannot update progress");
|
console.warn("Cannot update progress");
|
||||||
} else {
|
} else {
|
||||||
progress.innerHTML = feedback;
|
progress.innerHTML = feedback;
|
||||||
}
|
}
|
||||||
|
if (ratio) {
|
||||||
|
triggerAction(CHANNEL, ACTIONS.PROGRESS, ratio);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
triggerAction(CHANNEL, ACTIONS.ERROR, error);
|
triggerAction(CHANNEL, ACTIONS.ERROR, error);
|
||||||
@ -61,7 +71,7 @@ global.download = () => {
|
|||||||
},
|
},
|
||||||
reinit,
|
reinit,
|
||||||
pluginOptions,
|
pluginOptions,
|
||||||
global.songInfo
|
metadata
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
// });
|
// });
|
||||||
|
|||||||
@ -2,12 +2,13 @@ const { existsSync, mkdirSync } = require("fs");
|
|||||||
const { join } = require("path");
|
const { join } = require("path");
|
||||||
const { URL } = require("url");
|
const { URL } = require("url");
|
||||||
|
|
||||||
const { dialog, ipcMain } = require("electron");
|
const { dialog } = require("electron");
|
||||||
const is = require("electron-is");
|
const is = require("electron-is");
|
||||||
const ytpl = require("ytpl");
|
const ytpl = require("ytpl");
|
||||||
|
const chokidar = require('chokidar');
|
||||||
|
|
||||||
const { setOptions } = require("../../config/plugins");
|
const { setOptions } = require("../../config/plugins");
|
||||||
const getSongInfo = require("../../providers/song-info");
|
const registerCallback = require("../../providers/song-info");
|
||||||
const { sendError } = require("./back");
|
const { sendError } = require("./back");
|
||||||
const { defaultMenuDownloadLabel, getFolder } = require("./utils");
|
const { defaultMenuDownloadLabel, getFolder } = require("./utils");
|
||||||
|
|
||||||
@ -15,9 +16,8 @@ let downloadLabel = defaultMenuDownloadLabel;
|
|||||||
let metadataURL = undefined;
|
let metadataURL = undefined;
|
||||||
let callbackIsRegistered = false;
|
let callbackIsRegistered = false;
|
||||||
|
|
||||||
module.exports = (win, options, refreshMenu) => {
|
module.exports = (win, options) => {
|
||||||
if (!callbackIsRegistered) {
|
if (!callbackIsRegistered) {
|
||||||
const registerCallback = getSongInfo(win);
|
|
||||||
registerCallback((info) => {
|
registerCallback((info) => {
|
||||||
metadataURL = info.url;
|
metadataURL = info.url;
|
||||||
});
|
});
|
||||||
@ -35,7 +35,16 @@ module.exports = (win, options, refreshMenu) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playlist = await ytpl(playlistID);
|
console.log(`trying to get playlist ID: '${playlistID}'`);
|
||||||
|
let playlist;
|
||||||
|
try {
|
||||||
|
playlist = await ytpl(playlistID, {
|
||||||
|
limit: options.playlistMaxItems || Infinity,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
sendError(win, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const playlistTitle = playlist.title;
|
const playlistTitle = playlist.title;
|
||||||
|
|
||||||
const folder = getFolder(options.downloadFolder);
|
const folder = getFolder(options.downloadFolder);
|
||||||
@ -49,24 +58,40 @@ module.exports = (win, options, refreshMenu) => {
|
|||||||
}
|
}
|
||||||
mkdirSync(playlistFolder, { recursive: true });
|
mkdirSync(playlistFolder, { recursive: true });
|
||||||
|
|
||||||
ipcMain.on("downloader-feedback", (_, feedback) => {
|
dialog.showMessageBox({
|
||||||
downloadLabel = feedback;
|
type: "info",
|
||||||
refreshMenu();
|
buttons: ["OK"],
|
||||||
|
title: "Started Download",
|
||||||
|
message: `Downloading Playlist "${playlistTitle}"`,
|
||||||
|
detail: `(${playlist.items.length} songs)`,
|
||||||
});
|
});
|
||||||
|
|
||||||
downloadLabel = `Downloading "${playlistTitle}"`;
|
|
||||||
refreshMenu();
|
|
||||||
|
|
||||||
if (is.dev()) {
|
if (is.dev()) {
|
||||||
console.log(
|
console.log(
|
||||||
`Downloading playlist "${playlistTitle}" (${playlist.items.length} songs)`
|
`Downloading playlist "${playlistTitle}" (${playlist.items.length} songs)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
playlist.items.slice(0, options.playlistMaxItems).forEach((song) => {
|
const steps = 1 / playlist.items.length;
|
||||||
|
let progress = 0;
|
||||||
|
|
||||||
|
win.setProgressBar(2); // starts with indefinite bar
|
||||||
|
|
||||||
|
let dirWatcher = chokidar.watch(playlistFolder);
|
||||||
|
dirWatcher.on('add', () => {
|
||||||
|
progress += steps;
|
||||||
|
if (progress >= 0.9999) {
|
||||||
|
win.setProgressBar(-1); // close progress bar
|
||||||
|
dirWatcher.close().then(() => dirWatcher = null);
|
||||||
|
} else {
|
||||||
|
win.setProgressBar(progress);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
playlist.items.forEach((song) => {
|
||||||
win.webContents.send(
|
win.webContents.send(
|
||||||
"downloader-download-playlist",
|
"downloader-download-playlist",
|
||||||
song,
|
song.url,
|
||||||
playlistTitle,
|
playlistTitle,
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,8 +6,16 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-item > .yt-simple-endpoint:hover {
|
||||||
|
background-color: var(--ytmusic-menu-item-hover-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
.menu-icon {
|
.menu-icon {
|
||||||
flex: var(--ytmusic-menu-item-icon_-_flex);
|
flex: var(--ytmusic-menu-item-icon_-_flex);
|
||||||
margin: var(--ytmusic-menu-item-icon_-_margin);
|
margin: var(--ytmusic-menu-item-icon_-_margin);
|
||||||
fill: var(--ytmusic-menu-item-icon_-_fill);
|
fill: var(--ytmusic-menu-item-icon_-_fill);
|
||||||
|
stroke: var(--iron-icon-stroke-color, none);
|
||||||
|
width: var(--iron-icon-width, 24px);
|
||||||
|
height: var(--iron-icon-height, 24px);
|
||||||
|
animation: var(--iron-icon_-_animation);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<div
|
<div
|
||||||
class="menu-item ytmusic-menu-popup-renderer"
|
class="style-scope menu-item ytmusic-menu-popup-renderer"
|
||||||
role="option"
|
role="option"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
@ -7,31 +7,39 @@
|
|||||||
onclick="download()"
|
onclick="download()"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="menu-icon yt-icon-container yt-icon ytmusic-toggle-menu-service-item-renderer"
|
id="navigation-endpoint"
|
||||||
|
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<svg
|
<div
|
||||||
viewBox="0 0 24 24"
|
class="icon menu-icon style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
preserveAspectRatio="xMidYMid meet"
|
|
||||||
focusable="false"
|
|
||||||
class="style-scope yt-icon"
|
|
||||||
style="pointer-events: none; display: block; width: 100%; height: 100%;"
|
|
||||||
>
|
>
|
||||||
<g class="style-scope yt-icon">
|
<svg
|
||||||
<path
|
viewBox="0 0 24 24"
|
||||||
d="M25.462,19.105v6.848H4.515v-6.848H0.489v8.861c0,1.111,0.9,2.012,2.016,2.012h24.967c1.115,0,2.016-0.9,2.016-2.012v-8.861H25.462z"
|
preserveAspectRatio="xMidYMid meet"
|
||||||
class="style-scope yt-icon" fill="#aaaaaa"
|
focusable="false"
|
||||||
/>
|
class="style-scope yt-icon"
|
||||||
<path
|
style="pointer-events: none; display: block; width: 100%; height: 100%"
|
||||||
d="M14.62,18.426l-5.764-6.965c0,0-0.877-0.828,0.074-0.828s3.248,0,3.248,0s0-0.557,0-1.416c0-2.449,0-6.906,0-8.723c0,0-0.129-0.494,0.615-0.494c0.75,0,4.035,0,4.572,0c0.536,0,0.524,0.416,0.524,0.416c0,1.762,0,6.373,0,8.742c0,0.768,0,1.266,0,1.266s1.842,0,2.998,0c1.154,0,0.285,0.867,0.285,0.867s-4.904,6.51-5.588,7.193C15.092,18.979,14.62,18.426,14.62,18.426z"
|
>
|
||||||
class="style-scope yt-icon" fill="#aaaaaa"
|
<g class="style-scope yt-icon">
|
||||||
/>
|
<path
|
||||||
</g>
|
d="M25.462,19.105v6.848H4.515v-6.848H0.489v8.861c0,1.111,0.9,2.012,2.016,2.012h24.967c1.115,0,2.016-0.9,2.016-2.012v-8.861H25.462z"
|
||||||
</svg>
|
class="style-scope yt-icon"
|
||||||
</div>
|
fill="#aaaaaa"
|
||||||
<div
|
/>
|
||||||
class="text style-scope ytmusic-toggle-menu-service-item-renderer"
|
<path
|
||||||
id="ytmcustom-download"
|
d="M14.62,18.426l-5.764-6.965c0,0-0.877-0.828,0.074-0.828s3.248,0,3.248,0s0-0.557,0-1.416c0-2.449,0-6.906,0-8.723c0,0-0.129-0.494,0.615-0.494c0.75,0,4.035,0,4.572,0c0.536,0,0.524,0.416,0.524,0.416c0,1.762,0,6.373,0,8.742c0,0.768,0,1.266,0,1.266s1.842,0,2.998,0c1.154,0,0.285,0.867,0.285,0.867s-4.904,6.51-5.588,7.193C15.092,18.979,14.62,18.426,14.62,18.426z"
|
||||||
>
|
class="style-scope yt-icon"
|
||||||
Download
|
fill="#aaaaaa"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
|
id="ytmcustom-download"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,3 +3,29 @@ const electron = require("electron");
|
|||||||
module.exports.getFolder = (customFolder) =>
|
module.exports.getFolder = (customFolder) =>
|
||||||
customFolder || (electron.app || electron.remote.app).getPath("downloads");
|
customFolder || (electron.app || electron.remote.app).getPath("downloads");
|
||||||
module.exports.defaultMenuDownloadLabel = "Download playlist";
|
module.exports.defaultMenuDownloadLabel = "Download playlist";
|
||||||
|
|
||||||
|
const orderedQualityList = ["maxresdefault", "hqdefault", "mqdefault", "sdddefault"];
|
||||||
|
module.exports.urlToJPG = (imgUrl, videoId) => {
|
||||||
|
if (!imgUrl || imgUrl.includes(".jpg")) return imgUrl;
|
||||||
|
//it will almost never get further than hqdefault
|
||||||
|
for (const quality of orderedQualityList) {
|
||||||
|
if (imgUrl.includes(quality)) {
|
||||||
|
return `https://img.youtube.com/vi/${videoId}/${quality}.jpg`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `https://img.youtube.com/vi/${videoId}/default.jpg`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.cropMaxWidth = (image) => {
|
||||||
|
const imageSize = image.getSize();
|
||||||
|
// standart youtube artwork width with margins from both sides is 280 + 720 + 280
|
||||||
|
if (imageSize.width === 1280 && imageSize.height === 720) {
|
||||||
|
return image.crop({
|
||||||
|
x: 280,
|
||||||
|
y: 0,
|
||||||
|
width: 720,
|
||||||
|
height: 720
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|||||||
@ -14,7 +14,8 @@ const ytdl = require("ytdl-core");
|
|||||||
|
|
||||||
const { triggerAction, triggerActionSync } = require("../utils");
|
const { triggerAction, triggerActionSync } = require("../utils");
|
||||||
const { ACTIONS, CHANNEL } = require("./actions.js");
|
const { ACTIONS, CHANNEL } = require("./actions.js");
|
||||||
const { defaultMenuDownloadLabel, getFolder } = require("./utils");
|
const { getFolder, urlToJPG } = require("./utils");
|
||||||
|
const { cleanupName } = require("../../providers/song-info");
|
||||||
|
|
||||||
const { createFFmpeg } = FFmpeg;
|
const { createFFmpeg } = FFmpeg;
|
||||||
const ffmpeg = createFFmpeg({
|
const ffmpeg = createFFmpeg({
|
||||||
@ -24,7 +25,7 @@ const ffmpeg = createFFmpeg({
|
|||||||
});
|
});
|
||||||
const ffmpegMutex = new Mutex();
|
const ffmpegMutex = new Mutex();
|
||||||
|
|
||||||
const downloadVideoToMP3 = (
|
const downloadVideoToMP3 = async (
|
||||||
videoUrl,
|
videoUrl,
|
||||||
sendFeedback,
|
sendFeedback,
|
||||||
sendError,
|
sendError,
|
||||||
@ -35,6 +36,21 @@ const downloadVideoToMP3 = (
|
|||||||
) => {
|
) => {
|
||||||
sendFeedback("Downloading…");
|
sendFeedback("Downloading…");
|
||||||
|
|
||||||
|
if (metadata === null) {
|
||||||
|
const { videoDetails } = await ytdl.getInfo(videoUrl);
|
||||||
|
const thumbnails = videoDetails?.thumbnails;
|
||||||
|
metadata = {
|
||||||
|
artist:
|
||||||
|
videoDetails?.media?.artist ||
|
||||||
|
cleanupName(videoDetails?.author?.name) ||
|
||||||
|
"",
|
||||||
|
title: videoDetails?.media?.song || videoDetails?.title || "",
|
||||||
|
imageSrcYTPL: thumbnails ?
|
||||||
|
urlToJPG(thumbnails[thumbnails.length - 1].url, videoDetails?.videoId)
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let videoName = "YouTube Music - Unknown title";
|
let videoName = "YouTube Music - Unknown title";
|
||||||
let videoReadableStream;
|
let videoReadableStream;
|
||||||
try {
|
try {
|
||||||
@ -54,9 +70,10 @@ const downloadVideoToMP3 = (
|
|||||||
.on("data", (chunk) => {
|
.on("data", (chunk) => {
|
||||||
chunks.push(chunk);
|
chunks.push(chunk);
|
||||||
})
|
})
|
||||||
.on("progress", (chunkLength, downloaded, total) => {
|
.on("progress", (_chunkLength, downloaded, total) => {
|
||||||
const progress = Math.floor((downloaded / total) * 100);
|
const ratio = downloaded / total;
|
||||||
sendFeedback("Download: " + progress + "%");
|
const progress = Math.floor(ratio * 100);
|
||||||
|
sendFeedback("Download: " + progress + "%", ratio);
|
||||||
})
|
})
|
||||||
.on("info", (info, format) => {
|
.on("info", (info, format) => {
|
||||||
videoName = info.videoDetails.title.replace("|", "").toString("ascii");
|
videoName = info.videoDetails.title.replace("|", "").toString("ascii");
|
||||||
@ -101,7 +118,7 @@ const toMP3 = async (
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (!ffmpeg.isLoaded()) {
|
if (!ffmpeg.isLoaded()) {
|
||||||
sendFeedback("Loading…");
|
sendFeedback("Loading…", 2); // indefinite progress bar after download
|
||||||
await ffmpeg.load();
|
await ffmpeg.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,6 +152,7 @@ const toMP3 = async (
|
|||||||
ipcRenderer.send("add-metadata", filePath, fileBuffer, {
|
ipcRenderer.send("add-metadata", filePath, fileBuffer, {
|
||||||
artist: metadata.artist,
|
artist: metadata.artist,
|
||||||
title: metadata.title,
|
title: metadata.title,
|
||||||
|
imageSrcYTPL: metadata.imageSrcYTPL
|
||||||
});
|
});
|
||||||
ipcRenderer.once("add-metadata-done", reinit);
|
ipcRenderer.once("add-metadata-done", reinit);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -165,22 +183,16 @@ module.exports = {
|
|||||||
|
|
||||||
ipcRenderer.on(
|
ipcRenderer.on(
|
||||||
"downloader-download-playlist",
|
"downloader-download-playlist",
|
||||||
(_, songMetadata, playlistFolder, options) => {
|
(_, url, playlistFolder, options) => {
|
||||||
const reinit = () =>
|
|
||||||
ipcRenderer.send("downloader-feedback", defaultMenuDownloadLabel);
|
|
||||||
|
|
||||||
downloadVideoToMP3(
|
downloadVideoToMP3(
|
||||||
songMetadata.url,
|
url,
|
||||||
(feedback) => {
|
() => {},
|
||||||
ipcRenderer.send("downloader-feedback", feedback);
|
|
||||||
},
|
|
||||||
(error) => {
|
(error) => {
|
||||||
triggerAction(CHANNEL, ACTIONS.ERROR, error);
|
triggerAction(CHANNEL, ACTIONS.ERROR, error);
|
||||||
reinit();
|
|
||||||
},
|
},
|
||||||
reinit,
|
() => {},
|
||||||
options,
|
options,
|
||||||
songMetadata,
|
null,
|
||||||
playlistFolder
|
playlistFolder
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,83 +1,33 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const { Menu } = require("electron");
|
|
||||||
const electronLocalshortcut = require("electron-localshortcut");
|
const electronLocalshortcut = require("electron-localshortcut");
|
||||||
|
|
||||||
const config = require("../../config");
|
const config = require("../../config");
|
||||||
const { setApplicationMenu } = require("../../menu");
|
|
||||||
const { injectCSS } = require("../utils");
|
const { injectCSS } = require("../utils");
|
||||||
|
|
||||||
//tracks menu visibility
|
//tracks menu visibility
|
||||||
let visible = true;
|
let visible = true;
|
||||||
// win hook for fixing menu
|
|
||||||
let win;
|
|
||||||
|
|
||||||
const originalBuildMenu = Menu.buildFromTemplate;
|
|
||||||
// This function natively gets called on all submenu so no more reason to use recursion
|
|
||||||
Menu.buildFromTemplate = (template) => {
|
|
||||||
// Fix checkboxes and radio buttons
|
|
||||||
updateTemplate(template);
|
|
||||||
|
|
||||||
// return as normal
|
|
||||||
return originalBuildMenu(template);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = (winImport) => {
|
|
||||||
win = winImport;
|
|
||||||
|
|
||||||
|
module.exports = (win) => {
|
||||||
// css for custom scrollbar + disable drag area(was causing bugs)
|
// css for custom scrollbar + disable drag area(was causing bugs)
|
||||||
injectCSS(win.webContents, path.join(__dirname, "style.css"));
|
injectCSS(win.webContents, path.join(__dirname, "style.css"));
|
||||||
|
|
||||||
win.once("ready-to-show", () => {
|
win.once("ready-to-show", () => {
|
||||||
|
|
||||||
setApplicationMenu(win);
|
|
||||||
|
|
||||||
//register keyboard shortcut && hide menu if hideMenu is enabled
|
//register keyboard shortcut && hide menu if hideMenu is enabled
|
||||||
if (config.get("options.hideMenu")) {
|
if (config.get("options.hideMenu")) {
|
||||||
electronLocalshortcut.register(win, "Esc", () => {
|
electronLocalshortcut.register(win, "Esc", () => {
|
||||||
switchMenuVisibility();
|
setMenuVisibility(!visible);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//set menu visibility on load
|
|
||||||
win.webContents.once("did-finish-load", () => {
|
win.webContents.once("did-finish-load", () => {
|
||||||
// fix bug with menu not applying on start when no internet connection available
|
// fix bug with menu not applying on start when no internet connection available
|
||||||
setMenuVisibility(!config.get("options.hideMenu"));
|
setMenuVisibility(!config.get("options.hideMenu"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function setMenuVisibility(value) {
|
||||||
|
visible = value;
|
||||||
|
win.webContents.send("updateMenu", visible);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function switchMenuVisibility() {
|
|
||||||
setMenuVisibility(!visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMenuVisibility(value) {
|
|
||||||
visible = value;
|
|
||||||
win.webContents.send("updateMenu", visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCheckboxesAndRadioButtons(item, isRadio, hasSubmenu) {
|
|
||||||
if (!isRadio) {
|
|
||||||
//fix checkbox
|
|
||||||
item.checked = !item.checked;
|
|
||||||
}
|
|
||||||
//update menu if radio / hasSubmenu
|
|
||||||
if (isRadio || hasSubmenu) {
|
|
||||||
win.webContents.send("updateMenu", true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update checkboxes/radio buttons
|
|
||||||
function updateTemplate(template) {
|
|
||||||
for (let item of template) {
|
|
||||||
// Change onClick of checkbox+radio
|
|
||||||
if ((item.type === "checkbox" || item.type === "radio") && !item.fixed) {
|
|
||||||
const originalOnclick = item.click;
|
|
||||||
item.click = (itemClicked) => {
|
|
||||||
originalOnclick(itemClicked);
|
|
||||||
updateCheckboxesAndRadioButtons(itemClicked, item.type === "radio", item.hasSubmenu);
|
|
||||||
};
|
|
||||||
item.fixed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -10,15 +10,7 @@ module.exports = () => {
|
|||||||
bar.updateTitle(" ");
|
bar.updateTitle(" ");
|
||||||
document.title = "Youtube Music";
|
document.title = "Youtube Music";
|
||||||
|
|
||||||
ipcRenderer.on("updateMenu", function (event, menu) {
|
ipcRenderer.on("updateMenu", function (_event, showMenu) {
|
||||||
if (menu) {
|
bar.updateMenu(showMenu ? remote.Menu.getApplicationMenu() : null);
|
||||||
bar.updateMenu(remote.Menu.getApplicationMenu());
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
bar.updateMenu(null);
|
|
||||||
} catch (e) {
|
|
||||||
//will always throw type error - null isn't menu, but it works
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,11 +4,6 @@
|
|||||||
font-size: 14px !important;
|
font-size: 14px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* allow submenu's to show correctly */
|
|
||||||
.menubar-menu-container {
|
|
||||||
overflow-y: visible !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* fixes scrollbar positioning relative to nav bar */
|
/* fixes scrollbar positioning relative to nav bar */
|
||||||
#nav-bar-background.ytmusic-app-layout {
|
#nav-bar-background.ytmusic-app-layout {
|
||||||
right: 15px !important;
|
right: 15px !important;
|
||||||
|
|||||||
@ -1,25 +1,14 @@
|
|||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const md5 = require('md5');
|
const md5 = require('md5');
|
||||||
const open = require("open");
|
const { shell } = require('electron');
|
||||||
const { setOptions } = require('../../config/plugins');
|
const { setOptions } = require('../../config/plugins');
|
||||||
const getSongInfo = require('../../providers/song-info');
|
const registerCallback = require('../../providers/song-info');
|
||||||
const defaultConfig = require('../../config/defaults');
|
const defaultConfig = require('../../config/defaults');
|
||||||
|
|
||||||
const cleanupArtistName = (config, artist) => {
|
|
||||||
// removes the suffixes of the artist name for more recognition by last.fm
|
|
||||||
const { suffixesToRemove } = config;
|
|
||||||
if (suffixesToRemove === undefined) return artist;
|
|
||||||
|
|
||||||
for (suffix of suffixesToRemove) {
|
|
||||||
artist = artist.replace(suffix, '');
|
|
||||||
}
|
|
||||||
return artist;
|
|
||||||
}
|
|
||||||
|
|
||||||
const createFormData = params => {
|
const createFormData = params => {
|
||||||
// creates the body for in the post request
|
// creates the body for in the post request
|
||||||
const formData = new URLSearchParams();
|
const formData = new URLSearchParams();
|
||||||
for (key in params) {
|
for (const key in params) {
|
||||||
formData.append(key, params[key]);
|
formData.append(key, params[key]);
|
||||||
}
|
}
|
||||||
return formData;
|
return formData;
|
||||||
@ -28,7 +17,7 @@ const createQueryString = (params, api_sig) => {
|
|||||||
// creates a querystring
|
// creates a querystring
|
||||||
const queryData = [];
|
const queryData = [];
|
||||||
params.api_sig = api_sig;
|
params.api_sig = api_sig;
|
||||||
for (key in params) {
|
for (const key in params) {
|
||||||
queryData.push(`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`);
|
queryData.push(`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`);
|
||||||
}
|
}
|
||||||
return '?'+queryData.join('&');
|
return '?'+queryData.join('&');
|
||||||
@ -37,12 +26,12 @@ const createQueryString = (params, api_sig) => {
|
|||||||
const createApiSig = (params, secret) => {
|
const createApiSig = (params, secret) => {
|
||||||
// this function creates the api signature, see: https://www.last.fm/api/authspec
|
// this function creates the api signature, see: https://www.last.fm/api/authspec
|
||||||
const keys = [];
|
const keys = [];
|
||||||
for (key in params) {
|
for (const key in params) {
|
||||||
keys.push(key);
|
keys.push(key);
|
||||||
}
|
}
|
||||||
keys.sort();
|
keys.sort();
|
||||||
let sig = '';
|
let sig = '';
|
||||||
for (key of keys) {
|
for (const key of keys) {
|
||||||
if (String(key) === 'format')
|
if (String(key) === 'format')
|
||||||
continue
|
continue
|
||||||
sig += `${key}${params[key]}`;
|
sig += `${key}${params[key]}`;
|
||||||
@ -69,7 +58,7 @@ const authenticate = async config => {
|
|||||||
// asks the user for authentication
|
// asks the user for authentication
|
||||||
config.token = await createToken(config);
|
config.token = await createToken(config);
|
||||||
setOptions('last-fm', config);
|
setOptions('last-fm', config);
|
||||||
open(`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`);
|
shell.openExternal(`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`);
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,10 +128,8 @@ const setNowPlaying = (songInfo, config) => {
|
|||||||
// this will store the timeout that will trigger addScrobble
|
// this will store the timeout that will trigger addScrobble
|
||||||
let scrobbleTimer = undefined;
|
let scrobbleTimer = undefined;
|
||||||
|
|
||||||
const lastfm = async (win, config) => {
|
const lastfm = async (_win, config) => {
|
||||||
const registerCallback = getSongInfo(win);
|
if (!config.api_root) {
|
||||||
|
|
||||||
if (!config.api_root || !config.suffixesToRemove) {
|
|
||||||
// settings are not present, creating them with the default values
|
// settings are not present, creating them with the default values
|
||||||
config = defaultConfig.plugins['last-fm'];
|
config = defaultConfig.plugins['last-fm'];
|
||||||
config.enabled = true;
|
config.enabled = true;
|
||||||
@ -157,8 +144,6 @@ const lastfm = async (win, config) => {
|
|||||||
registerCallback( songInfo => {
|
registerCallback( songInfo => {
|
||||||
// set remove the old scrobble timer
|
// set remove the old scrobble timer
|
||||||
clearTimeout(scrobbleTimer);
|
clearTimeout(scrobbleTimer);
|
||||||
// make the artist name a bit cleaner
|
|
||||||
songInfo.artist = cleanupArtistName(config, songInfo.artist);
|
|
||||||
if (!songInfo.isPaused) {
|
if (!songInfo.isPaused) {
|
||||||
setNowPlaying(songInfo, config);
|
setNowPlaying(songInfo, config);
|
||||||
// scrobble when the song is half way through, or has passed the 4 minute mark
|
// scrobble when the song is half way through, or has passed the 4 minute mark
|
||||||
|
|||||||
52
plugins/lyrics-genius/back.js
Normal file
52
plugins/lyrics-genius/back.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
const { join } = require("path");
|
||||||
|
|
||||||
|
const { ipcMain } = require("electron");
|
||||||
|
const is = require("electron-is");
|
||||||
|
const fetch = require("node-fetch");
|
||||||
|
|
||||||
|
const { cleanupName } = require("../../providers/song-info");
|
||||||
|
const { injectCSS } = require("../utils");
|
||||||
|
|
||||||
|
module.exports = async (win) => {
|
||||||
|
injectCSS(win.webContents, join(__dirname, "style.css"));
|
||||||
|
|
||||||
|
ipcMain.on("search-genius-lyrics", async (event, extractedSongInfo) => {
|
||||||
|
const metadata = JSON.parse(extractedSongInfo);
|
||||||
|
const queryString = `${cleanupName(metadata.artist)} ${cleanupName(
|
||||||
|
metadata.title
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
let response = await fetch(
|
||||||
|
`https://genius.com/api/search/multi?per_page=5&q=${encodeURI(
|
||||||
|
queryString
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
event.returnValue = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = await response.json();
|
||||||
|
let url = "";
|
||||||
|
try {
|
||||||
|
url = info.response.sections.filter(
|
||||||
|
(section) => section.type === "song"
|
||||||
|
)[0].hits[0].result.url;
|
||||||
|
} catch {
|
||||||
|
event.returnValue = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is.dev()) {
|
||||||
|
console.log("Fetching lyrics from Genius:", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
event.returnValue = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.returnValue = await response.text();
|
||||||
|
});
|
||||||
|
};
|
||||||
65
plugins/lyrics-genius/front.js
Normal file
65
plugins/lyrics-genius/front.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
const { ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
ipcRenderer.on("update-song-info", (_, extractedSongInfo) => {
|
||||||
|
const lyricsTab = document.querySelector('tp-yt-paper-tab[tabindex="-1"]');
|
||||||
|
|
||||||
|
// Check if disabled
|
||||||
|
if (!lyricsTab || !lyricsTab.hasAttribute("disabled")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = ipcRenderer.sendSync(
|
||||||
|
"search-genius-lyrics",
|
||||||
|
extractedSongInfo
|
||||||
|
);
|
||||||
|
if (!html) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.innerHTML = html;
|
||||||
|
const lyricsSelector1 = wrapper.querySelector(".lyrics");
|
||||||
|
const lyricsSelector2 = wrapper.querySelector(
|
||||||
|
'[class^="Lyrics__Container"]'
|
||||||
|
);
|
||||||
|
const lyrics = lyricsSelector1
|
||||||
|
? lyricsSelector1.innerHTML
|
||||||
|
: lyricsSelector2
|
||||||
|
? lyricsSelector2.innerHTML
|
||||||
|
: null;
|
||||||
|
if (!lyrics) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lyricsTab.removeAttribute("disabled");
|
||||||
|
lyricsTab.removeAttribute("aria-disabled");
|
||||||
|
document.querySelector("tp-yt-paper-tab").onclick = () => {
|
||||||
|
lyricsTab.removeAttribute("disabled");
|
||||||
|
lyricsTab.removeAttribute("aria-disabled");
|
||||||
|
};
|
||||||
|
|
||||||
|
lyricsTab.onclick = () => {
|
||||||
|
const tabContainer = document.querySelector("ytmusic-tab-renderer");
|
||||||
|
console.log("tabContainer", tabContainer);
|
||||||
|
const observer = new MutationObserver((_, observer) => {
|
||||||
|
const lyricsContainer = document.querySelector(
|
||||||
|
'[page-type="MUSIC_PAGE_TYPE_TRACK_LYRICS"] > ytmusic-message-renderer'
|
||||||
|
);
|
||||||
|
if (lyricsContainer) {
|
||||||
|
lyricsContainer.innerHTML = `<div id="contents" class="style-scope ytmusic-section-list-renderer genius-lyrics">
|
||||||
|
${lyrics}
|
||||||
|
|
||||||
|
<yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer">Source : Genius</yt-formatted-string>
|
||||||
|
</div>`;
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(tabContainer, {
|
||||||
|
attributes: true,
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
7
plugins/lyrics-genius/style.css
Normal file
7
plugins/lyrics-genius/style.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/* Disable links in Genius lyrics */
|
||||||
|
.genius-lyrics a {
|
||||||
|
color: var(--ytmusic-text-primary);
|
||||||
|
display: inline-block;
|
||||||
|
pointer-events: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
const { Notification } = require("electron");
|
const { Notification } = require("electron");
|
||||||
const is = require("electron-is");
|
const is = require("electron-is");
|
||||||
const getSongInfo = require("../../providers/song-info");
|
const registerCallback = require("../../providers/song-info");
|
||||||
const { notificationImage } = require("./utils");
|
const { notificationImage } = require("./utils");
|
||||||
|
|
||||||
const { setupInteractive, notifyInteractive } = require("./interactive")
|
const setupInteractive = require("./interactive")
|
||||||
|
|
||||||
const notify = (info, options) => {
|
const notify = (info, options) => {
|
||||||
|
|
||||||
@ -23,38 +23,24 @@ const notify = (info, options) => {
|
|||||||
return currentNotification;
|
return currentNotification;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = (win, options) => {
|
const setup = (options) => {
|
||||||
const isInteractive = is.windows() && options.interactive;
|
|
||||||
//setup interactive notifications for windows
|
|
||||||
if (isInteractive) {
|
|
||||||
setupInteractive(win, options.unpauseNotification);
|
|
||||||
}
|
|
||||||
const registerCallback = getSongInfo(win);
|
|
||||||
let oldNotification;
|
let oldNotification;
|
||||||
let oldURL = "";
|
let currentUrl;
|
||||||
win.once("ready-to-show", () => {
|
|
||||||
// Register the callback for new song information
|
registerCallback(songInfo => {
|
||||||
registerCallback(songInfo => {
|
if (!songInfo.isPaused && (songInfo.url !== currentUrl || options.unpauseNotification)) {
|
||||||
// on pause - reset url? and skip notification
|
// Close the old notification
|
||||||
if (songInfo.isPaused) {
|
oldNotification?.close();
|
||||||
//reset oldURL if unpause notification option is on
|
currentUrl = songInfo.url;
|
||||||
if (options.unpauseNotification) {
|
// This fixes a weird bug that would cause the notification to be updated instead of showing
|
||||||
oldURL = "";
|
setTimeout(() => { oldNotification = notify(songInfo, options) }, 10);
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
// If url isn't the same as last one - send notification
|
|
||||||
if (songInfo.url !== oldURL) {
|
|
||||||
oldURL = songInfo.url;
|
|
||||||
if (isInteractive) {
|
|
||||||
notifyInteractive(songInfo);
|
|
||||||
} else {
|
|
||||||
// Close the old notification
|
|
||||||
oldNotification?.close();
|
|
||||||
// This fixes a weird bug that would cause the notification to be updated instead of showing
|
|
||||||
setTimeout(() => { oldNotification = notify(songInfo, options) }, 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = (win, options) => {
|
||||||
|
// Register the callback for new song information
|
||||||
|
is.windows() && options.interactive ?
|
||||||
|
setupInteractive(win, options.unpauseNotification) :
|
||||||
|
setup(options);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,17 +1,27 @@
|
|||||||
const { notificationImage, icons } = require("./utils");
|
const { notificationImage, icons } = require("./utils");
|
||||||
const getSongControls = require('../../providers/song-controls');
|
const getSongControls = require('../../providers/song-controls');
|
||||||
|
const registerCallback = require("../../providers/song-info");
|
||||||
const notifier = require("node-notifier");
|
const notifier = require("node-notifier");
|
||||||
|
|
||||||
//store song controls reference on launch
|
//store song controls reference on launch
|
||||||
let controls;
|
let controls;
|
||||||
let notificationOnPause;
|
let notificationOnUnpause;
|
||||||
|
|
||||||
//Save controls and onPause option
|
module.exports = (win, unpauseNotification) => {
|
||||||
module.exports.setupInteractive = (win, unpauseNotification) => {
|
//Save controls and onPause option
|
||||||
const { playPause, next, previous } = getSongControls(win);
|
const { playPause, next, previous } = getSongControls(win);
|
||||||
controls = { playPause, next, previous };
|
controls = { playPause, next, previous };
|
||||||
|
notificationOnUnpause = unpauseNotification;
|
||||||
|
|
||||||
notificationOnPause = unpauseNotification;
|
let currentUrl;
|
||||||
|
|
||||||
|
// Register songInfoCallback
|
||||||
|
registerCallback(songInfo => {
|
||||||
|
if (!songInfo.isPaused && (songInfo.url !== currentUrl || notificationOnUnpause)) {
|
||||||
|
currentUrl = songInfo.url;
|
||||||
|
sendToaster(songInfo);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
win.webContents.once("closed", () => {
|
win.webContents.once("closed", () => {
|
||||||
deleteNotification()
|
deleteNotification()
|
||||||
@ -33,7 +43,7 @@ function deleteNotification() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//New notification
|
//New notification
|
||||||
module.exports.notifyInteractive = function sendToaster(songInfo) {
|
function sendToaster(songInfo) {
|
||||||
deleteNotification();
|
deleteNotification();
|
||||||
//download image and get path
|
//download image and get path
|
||||||
let imgSrc = notificationImage(songInfo, true);
|
let imgSrc = notificationImage(songInfo, true);
|
||||||
@ -71,7 +81,7 @@ module.exports.notifyInteractive = function sendToaster(songInfo) {
|
|||||||
// dont delete notification on play/pause
|
// dont delete notification on play/pause
|
||||||
toDelete = undefined;
|
toDelete = undefined;
|
||||||
//manually send notification if not sending automatically
|
//manually send notification if not sending automatically
|
||||||
if (!notificationOnPause) {
|
if (!notificationOnUnpause) {
|
||||||
songInfo.isPaused = false;
|
songInfo.isPaused = false;
|
||||||
sendToaster(songInfo);
|
sendToaster(songInfo);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,79 +1,87 @@
|
|||||||
<div
|
<div
|
||||||
class="menu-item ytmusic-menu-popup-renderer"
|
class="style-scope menu-item ytmusic-menu-popup-renderer"
|
||||||
role="option"
|
role="option"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
aria-selected="false"
|
aria-selected="false"
|
||||||
>
|
>
|
||||||
<tp-yt-paper-slider
|
<div
|
||||||
id="playback-speed-slider"
|
id="navigation-endpoint"
|
||||||
class="volume-slider style-scope ytmusic-player-bar on-hover"
|
class="yt-simple-endpoint style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
max="100"
|
tabindex="-1"
|
||||||
min="0"
|
>
|
||||||
step="5"
|
<tp-yt-paper-slider
|
||||||
dir="ltr"
|
id="playback-speed-slider"
|
||||||
title="Playback speed"
|
class="volume-slider style-scope ytmusic-player-bar on-hover"
|
||||||
aria-label="Playback speed"
|
max="100"
|
||||||
role="slider"
|
min="0"
|
||||||
tabindex="0"
|
step="5"
|
||||||
aria-valuemin="0"
|
dir="ltr"
|
||||||
aria-valuemax="100"
|
title="Playback speed"
|
||||||
aria-valuenow="50"
|
aria-label="Playback speed"
|
||||||
aria-disabled="false"
|
role="slider"
|
||||||
value="50"
|
tabindex="0"
|
||||||
><!--css-build:shady-->
|
aria-valuemin="0"
|
||||||
<div id="sliderContainer" class="style-scope tp-yt-paper-slider">
|
aria-valuemax="100"
|
||||||
<div class="bar-container style-scope tp-yt-paper-slider">
|
aria-valuenow="50"
|
||||||
<tp-yt-paper-progress
|
aria-disabled="false"
|
||||||
id="sliderBar"
|
value="50"
|
||||||
aria-hidden="true"
|
><!--css-build:shady-->
|
||||||
class="style-scope tp-yt-paper-slider"
|
<div id="sliderContainer" class="style-scope tp-yt-paper-slider">
|
||||||
role="progressbar"
|
<div class="bar-container style-scope tp-yt-paper-slider">
|
||||||
value="50"
|
<tp-yt-paper-progress
|
||||||
aria-valuenow="50"
|
id="sliderBar"
|
||||||
aria-valuemin="0"
|
aria-hidden="true"
|
||||||
aria-valuemax="100"
|
class="style-scope tp-yt-paper-slider"
|
||||||
aria-disabled="false"
|
role="progressbar"
|
||||||
style="touch-action: none"
|
value="50"
|
||||||
><!--css-build:shady-->
|
aria-valuenow="50"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
|
aria-disabled="false"
|
||||||
|
style="touch-action: none"
|
||||||
|
><!--css-build:shady-->
|
||||||
|
|
||||||
<div id="progressContainer" class="style-scope tp-yt-paper-progress">
|
|
||||||
<div
|
<div
|
||||||
id="secondaryProgress"
|
id="progressContainer"
|
||||||
class="style-scope tp-yt-paper-progress"
|
class="style-scope tp-yt-paper-progress"
|
||||||
hidden="true"
|
>
|
||||||
style="transform: scaleX(0)"
|
<div
|
||||||
></div>
|
id="secondaryProgress"
|
||||||
<div
|
class="style-scope tp-yt-paper-progress"
|
||||||
id="primaryProgress"
|
hidden="true"
|
||||||
class="style-scope tp-yt-paper-progress"
|
style="transform: scaleX(0)"
|
||||||
style="transform: scaleX(0.5)"
|
></div>
|
||||||
></div>
|
<div
|
||||||
</div>
|
id="primaryProgress"
|
||||||
</tp-yt-paper-progress>
|
class="style-scope tp-yt-paper-progress"
|
||||||
|
style="transform: scaleX(0.5)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</tp-yt-paper-progress>
|
||||||
|
</div>
|
||||||
|
<dom-if class="style-scope tp-yt-paper-slider"
|
||||||
|
><template is="dom-if"></template
|
||||||
|
></dom-if>
|
||||||
|
<div
|
||||||
|
id="sliderKnob"
|
||||||
|
class="slider-knob style-scope tp-yt-paper-slider"
|
||||||
|
style="left: 50%; touch-action: none"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="slider-knob-inner style-scope tp-yt-paper-slider"
|
||||||
|
value="50"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<dom-if class="style-scope tp-yt-paper-slider"
|
<dom-if class="style-scope tp-yt-paper-slider"
|
||||||
><template is="dom-if"></template
|
><template is="dom-if"></template></dom-if
|
||||||
></dom-if>
|
></tp-yt-paper-slider>
|
||||||
<div
|
<div
|
||||||
id="sliderKnob"
|
class="text style-scope ytmusic-menu-navigation-item-renderer"
|
||||||
class="slider-knob style-scope tp-yt-paper-slider"
|
id="ytmcustom-playback-speed"
|
||||||
style="left: 50%; touch-action: none"
|
>
|
||||||
>
|
Speed (<span id="playback-speed-value">1</span>)
|
||||||
<div
|
|
||||||
class="slider-knob-inner style-scope tp-yt-paper-slider"
|
|
||||||
value="50"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<dom-if class="style-scope tp-yt-paper-slider"
|
|
||||||
><template is="dom-if"></template></dom-if
|
|
||||||
></tp-yt-paper-slider>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="text style-scope ytmusic-toggle-menu-service-item-renderer"
|
|
||||||
id="ytmcustom-playback-speed"
|
|
||||||
>
|
|
||||||
Speed (<span id="playback-speed-value">1</span>)
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -194,12 +194,13 @@ function setupLocalArrowShortcuts(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function callback(event) {
|
function callback(event) {
|
||||||
event.preventDefault();
|
|
||||||
switch (event.code) {
|
switch (event.code) {
|
||||||
case "ArrowUp":
|
case "ArrowUp":
|
||||||
|
event.preventDefault();
|
||||||
changeVolume(true, options);
|
changeVolume(true, options);
|
||||||
break;
|
break;
|
||||||
case "ArrowDown":
|
case "ArrowDown":
|
||||||
|
event.preventDefault();
|
||||||
changeVolume(false, options);
|
changeVolume(false, options);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,28 +1,33 @@
|
|||||||
const { ipcRenderer } = require("electron");
|
const { ipcRenderer } = require("electron");
|
||||||
|
const is = require("electron-is");
|
||||||
|
|
||||||
|
let ignored = {
|
||||||
|
id: ["volume-slider", "expand-volume-slider"],
|
||||||
|
types: ["mousewheel", "keydown", "keyup"]
|
||||||
|
};
|
||||||
|
|
||||||
// Override specific listeners of volume-slider by modifying Element.prototype
|
|
||||||
function overrideAddEventListener() {
|
function overrideAddEventListener() {
|
||||||
// Events to ignore
|
|
||||||
const nativeEvents = ["mousewheel", "keydown", "keyup"];
|
|
||||||
// Save native addEventListener
|
// Save native addEventListener
|
||||||
Element.prototype._addEventListener = Element.prototype.addEventListener;
|
Element.prototype._addEventListener = Element.prototype.addEventListener;
|
||||||
// Override addEventListener to Ignore specific events in volume-slider
|
// Override addEventListener to Ignore specific events in volume-slider
|
||||||
Element.prototype.addEventListener = function (type, listener, useCapture = false) {
|
Element.prototype.addEventListener = function (type, listener, useCapture = false) {
|
||||||
if (this.tagName === "TP-YT-PAPER-SLIDER") { // tagName of #volume-slider
|
if (!(
|
||||||
for (const eventType of nativeEvents) {
|
ignored.id.includes(this.id) &&
|
||||||
if (eventType === type) {
|
ignored.types.includes(type)
|
||||||
return;
|
)) {
|
||||||
}
|
this._addEventListener(type, listener, useCapture);
|
||||||
}
|
} else if (is.dev()) {
|
||||||
}//else
|
console.log(`Ignoring event: "${this.id}.${type}()"`);
|
||||||
this._addEventListener(type, listener, useCapture);
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
overrideAddEventListener();
|
overrideAddEventListener();
|
||||||
// Restore original function after did-finish-load to avoid keeping Element.prototype altered
|
// Restore original function after did-finish-load to avoid keeping Element.prototype altered
|
||||||
ipcRenderer.once("restoreAddEventListener", () => { //called from Main to make sure page is completly loaded
|
ipcRenderer.once("restoreAddEventListener", () => { // Called from main to make sure page is completly loaded
|
||||||
Element.prototype.addEventListener = Element.prototype._addEventListener;
|
Element.prototype.addEventListener = Element.prototype._addEventListener;
|
||||||
|
Element.prototype._addEventListener = undefined;
|
||||||
|
ignored = undefined;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
const { globalShortcut } = require("electron");
|
const { globalShortcut } = require("electron");
|
||||||
|
const is = require("electron-is");
|
||||||
const electronLocalshortcut = require("electron-localshortcut");
|
const electronLocalshortcut = require("electron-localshortcut");
|
||||||
|
|
||||||
const getSongControls = require("../../providers/song-controls");
|
const getSongControls = require("../../providers/song-controls");
|
||||||
|
const { setupMPRIS } = require("./mpris");
|
||||||
|
|
||||||
function _registerGlobalShortcut(webContents, shortcut, action) {
|
function _registerGlobalShortcut(webContents, shortcut, action) {
|
||||||
globalShortcut.register(shortcut, () => {
|
globalShortcut.register(shortcut, () => {
|
||||||
@ -25,6 +27,22 @@ function registerShortcuts(win, options) {
|
|||||||
_registerLocalShortcut(win, "CommandOrControl+F", search);
|
_registerLocalShortcut(win, "CommandOrControl+F", search);
|
||||||
_registerLocalShortcut(win, "CommandOrControl+L", search);
|
_registerLocalShortcut(win, "CommandOrControl+L", search);
|
||||||
|
|
||||||
|
if (is.linux()) {
|
||||||
|
try {
|
||||||
|
const player = setupMPRIS();
|
||||||
|
|
||||||
|
player.on("raise", () => {
|
||||||
|
win.setSkipTaskbar(false);
|
||||||
|
win.show();
|
||||||
|
});
|
||||||
|
player.on("playpause", playPause);
|
||||||
|
player.on("next", next);
|
||||||
|
player.on("previous", previous);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Error in MPRIS", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { global, local } = options;
|
const { global, local } = options;
|
||||||
(global || []).forEach(({ shortcut, action }) => {
|
(global || []).forEach(({ shortcut, action }) => {
|
||||||
console.debug("Registering global shortcut", shortcut, ":", action);
|
console.debug("Registering global shortcut", shortcut, ":", action);
|
||||||
|
|||||||
19
plugins/shortcuts/mpris.js
Normal file
19
plugins/shortcuts/mpris.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const mpris = require("mpris-service");
|
||||||
|
|
||||||
|
function setupMPRIS() {
|
||||||
|
const player = mpris({
|
||||||
|
name: "youtube-music",
|
||||||
|
identity: "YouTube Music",
|
||||||
|
canRaise: true,
|
||||||
|
supportedUriSchemes: ["https"],
|
||||||
|
supportedMimeTypes: ["audio/mpeg"],
|
||||||
|
supportedInterfaces: ["player"],
|
||||||
|
desktopEntry: "youtube-music",
|
||||||
|
});
|
||||||
|
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
setupMPRIS,
|
||||||
|
};
|
||||||
51
plugins/sponsorblock/back.js
Normal file
51
plugins/sponsorblock/back.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
const fetch = require("node-fetch");
|
||||||
|
|
||||||
|
const defaultConfig = require("../../config/defaults");
|
||||||
|
const registerCallback = require("../../providers/song-info");
|
||||||
|
const { sortSegments } = require("./segments");
|
||||||
|
|
||||||
|
let videoID;
|
||||||
|
|
||||||
|
module.exports = (win, options) => {
|
||||||
|
const { apiURL, categories } = {
|
||||||
|
...defaultConfig.plugins.sponsorblock,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
registerCallback(async (info) => {
|
||||||
|
const newURL = info.url || win.webContents.getURL();
|
||||||
|
const newVideoID = new URL(newURL).searchParams.get("v");
|
||||||
|
|
||||||
|
if (videoID !== newVideoID) {
|
||||||
|
videoID = newVideoID;
|
||||||
|
const segments = await fetchSegments(apiURL, categories);
|
||||||
|
win.webContents.send("sponsorblock-skip", segments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchSegments = async (apiURL, categories) => {
|
||||||
|
const sponsorBlockURL = `${apiURL}/api/skipSegments?videoID=${videoID}&categories=${JSON.stringify(
|
||||||
|
categories
|
||||||
|
)}`;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(sponsorBlockURL, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
redirect: "follow",
|
||||||
|
});
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const segments = await resp.json();
|
||||||
|
const sortedSegments = sortSegments(
|
||||||
|
segments.map((submission) => submission.segment)
|
||||||
|
);
|
||||||
|
|
||||||
|
return sortedSegments;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
27
plugins/sponsorblock/front.js
Normal file
27
plugins/sponsorblock/front.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const { ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
const is = require("electron-is");
|
||||||
|
|
||||||
|
const { ontimeupdate } = require("../../providers/video-element");
|
||||||
|
|
||||||
|
let currentSegments = [];
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
ipcRenderer.on("sponsorblock-skip", (_, segments) => {
|
||||||
|
currentSegments = segments;
|
||||||
|
});
|
||||||
|
|
||||||
|
ontimeupdate((videoElement) => {
|
||||||
|
currentSegments.forEach((segment) => {
|
||||||
|
if (
|
||||||
|
videoElement.currentTime >= segment[0] &&
|
||||||
|
videoElement.currentTime <= segment[1]
|
||||||
|
) {
|
||||||
|
videoElement.currentTime = segment[1];
|
||||||
|
if (is.dev()) {
|
||||||
|
console.log("SponsorBlock: skipping segment", segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
29
plugins/sponsorblock/segments.js
Normal file
29
plugins/sponsorblock/segments.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Segments are an array [ [start, end], … ]
|
||||||
|
module.exports.sortSegments = (segments) => {
|
||||||
|
segments.sort((segment1, segment2) =>
|
||||||
|
segment1[0] === segment2[0]
|
||||||
|
? segment1[1] - segment2[1]
|
||||||
|
: segment1[0] - segment2[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
const compiledSegments = [];
|
||||||
|
let currentSegment;
|
||||||
|
|
||||||
|
segments.forEach((segment) => {
|
||||||
|
if (!currentSegment) {
|
||||||
|
currentSegment = segment;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSegment[1] < segment[0]) {
|
||||||
|
compiledSegments.push(currentSegment);
|
||||||
|
currentSegment = segment;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSegment[1] = Math.max(currentSegment[1], segment[1]);
|
||||||
|
});
|
||||||
|
compiledSegments.push(currentSegment);
|
||||||
|
|
||||||
|
return compiledSegments;
|
||||||
|
};
|
||||||
34
plugins/sponsorblock/tests/segments.test.js
Normal file
34
plugins/sponsorblock/tests/segments.test.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
const { sortSegments } = require("../segments");
|
||||||
|
|
||||||
|
test("Segment sorting", () => {
|
||||||
|
expect(
|
||||||
|
sortSegments([
|
||||||
|
[0, 3],
|
||||||
|
[7, 8],
|
||||||
|
[5, 6],
|
||||||
|
])
|
||||||
|
).toEqual([
|
||||||
|
[0, 3],
|
||||||
|
[5, 6],
|
||||||
|
[7, 8],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
sortSegments([
|
||||||
|
[0, 5],
|
||||||
|
[6, 8],
|
||||||
|
[4, 6],
|
||||||
|
])
|
||||||
|
).toEqual([[0, 8]]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
sortSegments([
|
||||||
|
[0, 6],
|
||||||
|
[7, 8],
|
||||||
|
[4, 6],
|
||||||
|
])
|
||||||
|
).toEqual([
|
||||||
|
[0, 6],
|
||||||
|
[7, 8],
|
||||||
|
]);
|
||||||
|
});
|
||||||
@ -1,12 +1,11 @@
|
|||||||
const getSongControls = require('../../providers/song-controls');
|
const getSongControls = require('../../providers/song-controls');
|
||||||
const getSongInfo = require('../../providers/song-info');
|
const registerCallback = require('../../providers/song-info');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
let controls;
|
let controls;
|
||||||
let currentSongInfo;
|
let currentSongInfo;
|
||||||
|
|
||||||
module.exports = win => {
|
module.exports = win => {
|
||||||
const registerCallback = getSongInfo(win);
|
|
||||||
const { playPause, next, previous } = getSongControls(win);
|
const { playPause, next, previous } = getSongControls(win);
|
||||||
controls = { playPause, next, previous };
|
controls = { playPause, next, previous };
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ const {
|
|||||||
TouchBarScrubber,
|
TouchBarScrubber,
|
||||||
} = TouchBar;
|
} = TouchBar;
|
||||||
|
|
||||||
const getSongInfo = require("../../providers/song-info");
|
const registerCallback = require("../../providers/song-info");
|
||||||
const getSongControls = require("../../providers/song-controls");
|
const getSongControls = require("../../providers/song-controls");
|
||||||
|
|
||||||
// Songtitle label
|
// Songtitle label
|
||||||
@ -59,7 +59,6 @@ const touchBar = new TouchBar({
|
|||||||
});
|
});
|
||||||
|
|
||||||
module.exports = (win) => {
|
module.exports = (win) => {
|
||||||
const registerCallback = getSongInfo(win);
|
|
||||||
const { playPause, next, previous, like, dislike } = getSongControls(win);
|
const { playPause, next, previous, like, dislike } = getSongControls(win);
|
||||||
|
|
||||||
// If the page is ready, register the callback
|
// If the page is ready, register the callback
|
||||||
|
|||||||
33
plugins/tuna-obs/back.js
Normal file
33
plugins/tuna-obs/back.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
const { ipcRenderer } = require("electron");
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
const registerCallback = require("../../providers/song-info");
|
||||||
|
|
||||||
|
const post = (data) => {
|
||||||
|
const port = 1608;
|
||||||
|
headers = {'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Access-Control-Allow-Headers': '*',
|
||||||
|
'Access-Control-Allow-Origin': '*'}
|
||||||
|
const url = `http://localhost:${port}/`;
|
||||||
|
fetch(url, {method: 'POST', headers, body:JSON.stringify({data})});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async (win) => {
|
||||||
|
registerCallback((songInfo) => {
|
||||||
|
|
||||||
|
// Register the callback
|
||||||
|
if (songInfo.title.length === 0 && songInfo.artist.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = Number(songInfo.songDuration)*1000
|
||||||
|
const progress = Number(songInfo.elapsedSeconds)*1000
|
||||||
|
const cover_url = songInfo.imageSrc
|
||||||
|
const album_url = songInfo.imageSrc
|
||||||
|
const title = songInfo.title
|
||||||
|
const artists = [songInfo.artist]
|
||||||
|
const status = !songInfo.isPaused ? 'Playing': 'Paused'
|
||||||
|
post({ cover_url, title, artists, status, progress, duration, album_url});
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -43,7 +43,7 @@ module.exports.fileExists = (path, callbackIfExists) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
module.exports.injectCSS = (webContents, filepath, cb = undefined) => {
|
module.exports.injectCSS = (webContents, filepath, cb = undefined) => {
|
||||||
webContents.once("did-finish-load", async () => {
|
webContents.on("did-finish-load", async () => {
|
||||||
await webContents.insertCSS(fs.readFileSync(filepath, "utf8"));
|
await webContents.insertCSS(fs.readFileSync(filepath, "utf8"));
|
||||||
if (cb) {
|
if (cb) {
|
||||||
cb();
|
cb();
|
||||||
|
|||||||
@ -5,6 +5,8 @@ const { remote } = require("electron");
|
|||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
const { fileExists } = require("./plugins/utils");
|
const { fileExists } = require("./plugins/utils");
|
||||||
const setupFrontLogger = require("./providers/front-logger");
|
const setupFrontLogger = require("./providers/front-logger");
|
||||||
|
const setupSongControl = require("./providers/song-controls-front");
|
||||||
|
const setupSongInfo = require("./providers/song-info-front");
|
||||||
|
|
||||||
const plugins = config.plugins.getEnabled();
|
const plugins = config.plugins.getEnabled();
|
||||||
|
|
||||||
@ -37,8 +39,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// inject song-info provider
|
// inject song-info provider
|
||||||
const songInfoProviderPath = path.join(__dirname, "providers", "song-info-front.js")
|
setupSongInfo();
|
||||||
fileExists(songInfoProviderPath, require(songInfoProviderPath));
|
|
||||||
|
// inject song-control provider
|
||||||
|
setupSongControl();
|
||||||
|
|
||||||
// inject front logger
|
// inject front logger
|
||||||
setupFrontLogger();
|
setupFrontLogger();
|
||||||
|
|||||||
18
providers/song-controls-front.js
Normal file
18
providers/song-controls-front.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
const { ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
let videoStream = document.querySelector(".video-stream");
|
||||||
|
module.exports = () => {
|
||||||
|
ipcRenderer.on("playPause", () => {
|
||||||
|
if (!videoStream) {
|
||||||
|
videoStream = document.querySelector(".video-stream");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoStream.paused) {
|
||||||
|
videoStream.play();
|
||||||
|
} else {
|
||||||
|
videoStream.yns_pause ?
|
||||||
|
videoStream.yns_pause() :
|
||||||
|
videoStream.pause();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -12,7 +12,7 @@ module.exports = (win) => {
|
|||||||
// Playback
|
// Playback
|
||||||
previous: () => pressKey(win, "k"),
|
previous: () => pressKey(win, "k"),
|
||||||
next: () => pressKey(win, "j"),
|
next: () => pressKey(win, "j"),
|
||||||
playPause: () => pressKey(win, "space"),
|
playPause: () => win.webContents.send("playPause"),
|
||||||
like: () => pressKey(win, "_"),
|
like: () => pressKey(win, "_"),
|
||||||
dislike: () => pressKey(win, "+"),
|
dislike: () => pressKey(win, "+"),
|
||||||
go10sBack: () => pressKey(win, "h"),
|
go10sBack: () => pressKey(win, "h"),
|
||||||
|
|||||||
@ -10,17 +10,16 @@ ipcRenderer.on("update-song-info", async (_, extractedSongInfo) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const injectListener = () => {
|
const injectListener = () => {
|
||||||
var oldXHR = window.XMLHttpRequest;
|
const oldXHR = window.XMLHttpRequest;
|
||||||
function newXHR() {
|
function newXHR() {
|
||||||
var realXHR = new oldXHR();
|
const realXHR = new oldXHR();
|
||||||
realXHR.addEventListener(
|
realXHR.addEventListener(
|
||||||
"readystatechange",
|
"readystatechange",
|
||||||
() => {
|
() => {
|
||||||
if (realXHR.readyState == 4 && realXHR.status == 200) {
|
if (realXHR.readyState === 4 && realXHR.status === 200
|
||||||
if (realXHR.responseURL.includes("/player")) {
|
&& realXHR.responseURL.includes("/player")) {
|
||||||
// if the request contains the song info, send the response to ipcMain
|
// if the request contains the song info, send the response to ipcMain
|
||||||
ipcRenderer.send("song-info-request", realXHR.responseText);
|
ipcRenderer.send("song-info-request", realXHR.responseText);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
false
|
false
|
||||||
|
|||||||
@ -9,18 +9,21 @@ const progressSelector = "#progress-bar";
|
|||||||
// Grab the progress using the selector
|
// Grab the progress using the selector
|
||||||
const getProgress = async (win) => {
|
const getProgress = async (win) => {
|
||||||
// Get current value of the progressbar element
|
// Get current value of the progressbar element
|
||||||
const elapsedSeconds = await win.webContents.executeJavaScript(
|
return win.webContents.executeJavaScript(
|
||||||
'document.querySelector("' + progressSelector + '").value'
|
'document.querySelector("' + progressSelector + '").value'
|
||||||
);
|
);
|
||||||
|
|
||||||
return elapsedSeconds;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Grab the native image using the src
|
// Grab the native image using the src
|
||||||
const getImage = async (src) => {
|
const getImage = async (src) => {
|
||||||
const result = await fetch(src);
|
const result = await fetch(src);
|
||||||
const buffer = await result.buffer();
|
const buffer = await result.buffer();
|
||||||
return nativeImage.createFromBuffer(buffer);
|
const output = nativeImage.createFromBuffer(buffer);
|
||||||
|
if (output.isEmpty() && !src.endsWith(".jpg") && src.includes(".jpg")) { // fix hidden webp files (https://github.com/th-ch/youtube-music/issues/315)
|
||||||
|
return getImage(src.slice(0, src.lastIndexOf(".jpg")+4));
|
||||||
|
} else {
|
||||||
|
return output;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// To find the paused status, we check if the title contains `-`
|
// To find the paused status, we check if the title contains `-`
|
||||||
@ -30,15 +33,10 @@ const getPausedStatus = async (win) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getArtist = async (win) => {
|
const getArtist = async (win) => {
|
||||||
return await win.webContents.executeJavaScript(
|
return win.webContents.executeJavaScript(`
|
||||||
`
|
document.querySelector(".subtitle.ytmusic-player-bar .yt-formatted-string")
|
||||||
var bar = document.getElementsByClassName('subtitle ytmusic-player-bar')[0];
|
?.textContent
|
||||||
var artistName = (bar.getElementsByClassName('yt-formatted-string')[0]) || (bar.getElementsByClassName('byline ytmusic-player-bar')[0]);
|
`);
|
||||||
if (artistName) {
|
|
||||||
artistName.textContent;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill songInfo with empty values
|
// Fill songInfo with empty values
|
||||||
@ -57,8 +55,9 @@ const songInfo = {
|
|||||||
|
|
||||||
const handleData = async (responseText, win) => {
|
const handleData = async (responseText, win) => {
|
||||||
let data = JSON.parse(responseText);
|
let data = JSON.parse(responseText);
|
||||||
songInfo.title = data?.videoDetails?.title;
|
songInfo.title = cleanupName(data?.videoDetails?.title);
|
||||||
songInfo.artist = await getArtist(win) || data?.videoDetails?.author;
|
songInfo.artist =
|
||||||
|
(await getArtist(win)) || cleanupName(data?.videoDetails?.author);
|
||||||
songInfo.views = data?.videoDetails?.viewCount;
|
songInfo.views = data?.videoDetails?.viewCount;
|
||||||
songInfo.imageSrc = data?.videoDetails?.thumbnail?.thumbnails?.pop()?.url;
|
songInfo.imageSrc = data?.videoDetails?.thumbnail?.thumbnails?.pop()?.url;
|
||||||
songInfo.songDuration = data?.videoDetails?.lengthSeconds;
|
songInfo.songDuration = data?.videoDetails?.lengthSeconds;
|
||||||
@ -69,15 +68,15 @@ const handleData = async (responseText, win) => {
|
|||||||
win.webContents.send("update-song-info", JSON.stringify(songInfo));
|
win.webContents.send("update-song-info", JSON.stringify(songInfo));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// This variable will be filled with the callbacks once they register
|
||||||
|
const callbacks = [];
|
||||||
|
|
||||||
|
// This function will allow plugins to register callback that will be triggered when data changes
|
||||||
|
const registerCallback = (callback) => {
|
||||||
|
callbacks.push(callback);
|
||||||
|
};
|
||||||
|
|
||||||
const registerProvider = (win) => {
|
const registerProvider = (win) => {
|
||||||
// This variable will be filled with the callbacks once they register
|
|
||||||
const callbacks = [];
|
|
||||||
|
|
||||||
// This function will allow plugins to register callback that will be triggered when data changes
|
|
||||||
const registerCallback = (callback) => {
|
|
||||||
callbacks.push(callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
win.on("page-title-updated", async () => {
|
win.on("page-title-updated", async () => {
|
||||||
// Get and set the new data
|
// Get and set the new data
|
||||||
songInfo.isPaused = await getPausedStatus(win);
|
songInfo.isPaused = await getPausedStatus(win);
|
||||||
@ -98,9 +97,29 @@ const registerProvider = (win) => {
|
|||||||
c(songInfo);
|
c(songInfo);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return registerCallback;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = registerProvider;
|
const suffixesToRemove = [
|
||||||
|
" - Topic",
|
||||||
|
"VEVO",
|
||||||
|
" (Performance Video)",
|
||||||
|
" (Official Music Video)",
|
||||||
|
" (Official Video)",
|
||||||
|
" (Clip officiel)",
|
||||||
|
];
|
||||||
|
function cleanupName(artist) {
|
||||||
|
if (!artist) {
|
||||||
|
return artist;
|
||||||
|
}
|
||||||
|
for (const suffix of suffixesToRemove) {
|
||||||
|
if (artist.endsWith(suffix)) {
|
||||||
|
return artist.slice(0, -suffix.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return artist;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerCallback;
|
||||||
|
module.exports.setupSongInfo = registerProvider;
|
||||||
module.exports.getImage = getImage;
|
module.exports.getImage = getImage;
|
||||||
|
module.exports.cleanupName = cleanupName;
|
||||||
|
|||||||
22
providers/video-element.js
Normal file
22
providers/video-element.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
let videoElement = null;
|
||||||
|
|
||||||
|
module.exports.ontimeupdate = (cb) => {
|
||||||
|
const observer = new MutationObserver((mutations, observer) => {
|
||||||
|
if (!videoElement) {
|
||||||
|
videoElement = document.querySelector("video");
|
||||||
|
if (videoElement) {
|
||||||
|
observer.disconnect();
|
||||||
|
videoElement.ontimeupdate = () => cb(videoElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!videoElement) {
|
||||||
|
observer.observe(document, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
videoElement.ontimeupdate = () => cb(videoElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
21
readme.md
21
readme.md
@ -34,15 +34,22 @@ You can check out the [latest release](https://github.com/th-ch/youtube-music/re
|
|||||||
Install the `youtube-music-bin` package from the AUR. For AUR installation instructions, take a look at this [wiki page](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages).
|
Install the `youtube-music-bin` package from the AUR. For AUR installation instructions, take a look at this [wiki page](https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages).
|
||||||
|
|
||||||
## Available plugins:
|
## Available plugins:
|
||||||
|
|
||||||
- **Ad Blocker**: block all ads and tracking out of the box
|
- **Ad Blocker**: block all ads and tracking out of the box
|
||||||
- **Downloader**: download to MP3 directly from the interface (youtube-dl)
|
- **Auto confirm when paused**: when the ["Continue Watching?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png) modal appears, automatically click "Yes"
|
||||||
- **No Google Login**: remove Google login buttons and links from the interface
|
- **Disable autoplay**: makes every song start in "paused" mode
|
||||||
- **Shortcuts**: use your usual shortcuts (media keys, Ctrl/CMD + F…) to control YouTube Music
|
- [**Discord**](https://discord.com/): show your friends what you listen to with [Rich Presence](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png)
|
||||||
- **Navigation**: next/back navigation arrows directly integrated in the interface, like in your favorite browser
|
- **Downloader**: downloads MP3 [directly from the interface](https://user-images.githubusercontent.com/61631665/129977677-83a7d067-c192-45e1-98ae-b5a4927393be.png) [(youtube-dl)](https://github.com/ytdl-org/youtube-dl)
|
||||||
- **Auto confirm when paused**: when the "Continue Watching?" modal appears, automatically click "Yes"
|
|
||||||
- **Hide video player**: no video in the interface when playing music
|
- **Hide video player**: no video in the interface when playing music
|
||||||
- **Notifications**: display a notification when a song starts playing
|
- **In-app menu**: [gives bars a fancy, dark look](https://user-images.githubusercontent.com/78568641/112215894-923dbf00-8c29-11eb-95c3-3ce15db27eca.png)
|
||||||
|
- [**Last.fm**](https://www.last.fm/): scrobbles support
|
||||||
|
- **Navigation**: next/back navigation arrows directly integrated in the interface, like in your favorite browser
|
||||||
|
- **No Google Login**: remove Google login buttons and links from the interface
|
||||||
|
- **Notifications**: display a [notification](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png) when a song starts playing
|
||||||
|
- **Playback speed**: listen fast, listen slow! [Adds a slider that controls song speed](https://user-images.githubusercontent.com/61631665/129976003-e55db5ba-bf42-448c-a059-26a009775e68.png)
|
||||||
|
- **Precise volume**: customizable volume steps for more comfort, allows controlling the volume precisely using mousewheel
|
||||||
|
- **Shortcuts**: use your usual shortcuts (media keys, Ctrl/CMD + F…) to control YouTube Music, you may setup custom global hotkeys for play/pause/next/previous song
|
||||||
|
- [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock): skips non-music parts
|
||||||
|
- **Taskbar media control**: control app from your [Windows taskbar](https://user-images.githubusercontent.com/78568641/111916130-24a35e80-8a82-11eb-80c8-5021c1aa27f4.png)
|
||||||
- **Touchbar**: custom TouchBar layout for macOS
|
- **Touchbar**: custom TouchBar layout for macOS
|
||||||
|
|
||||||
## Dev
|
## Dev
|
||||||
|
|||||||
@ -1,3 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment ./tests/environment
|
||||||
|
*/
|
||||||
|
|
||||||
describe("YouTube Music App", () => {
|
describe("YouTube Music App", () => {
|
||||||
const app = global.__APP__;
|
const app = global.__APP__;
|
||||||
|
|
||||||
|
|||||||
7
tray.js
7
tray.js
@ -63,12 +63,7 @@ module.exports.setUpTray = (app, win) => {
|
|||||||
app.quit();
|
app.quit();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{ role: "quit" }
|
||||||
label: "Quit",
|
|
||||||
click: () => {
|
|
||||||
app.quit();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const trayMenu = Menu.buildFromTemplate(template);
|
const trayMenu = Menu.buildFromTemplate(template);
|
||||||
|
|||||||
Reference in New Issue
Block a user