mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 18:41:47 +00:00
Compare commits
294 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f44b6f0c33 | |||
| c45e4e50fc | |||
| 9839a973f7 | |||
| 9ea967f03b | |||
| 9d6765125b | |||
| 8d66735585 | |||
| 14b4c55ce7 | |||
| 1d1f4bbcc3 | |||
| bd520c7eff | |||
| 73e201bb2c | |||
| 81b08917ae | |||
| 81c2ab34d9 | |||
| 33faa2deb3 | |||
| 4d4ac56486 | |||
| 56ac2b3b06 | |||
| c72ea4bad5 | |||
| d60069555e | |||
| ed7025b4a2 | |||
| 5fbc0f8122 | |||
| 02a989ca07 | |||
| 7c6fe6748e | |||
| 8f2ed3039a | |||
| baeebd1959 | |||
| 49edbf723f | |||
| 3764ce9a7c | |||
| 46943520bd | |||
| 1048b3f99a | |||
| b1e40271e6 | |||
| 11429978c9 | |||
| 47ca6e0b1f | |||
| a273f6f73c | |||
| c8ba85be76 | |||
| 6633243628 | |||
| 2fb47933ac | |||
| 4fd683ed23 | |||
| e1e9748002 | |||
| dd122666c5 | |||
| 5483f0ee36 | |||
| 2c6c80d829 | |||
| 584d3e83c6 | |||
| 58d5256dd2 | |||
| 920d61a1c6 | |||
| c5c2d5b74c | |||
| 2daee01ff7 | |||
| d13c9b7ca6 | |||
| 5296a88525 | |||
| 8ce4b5b297 | |||
| 2c39c0efed | |||
| e917abaec9 | |||
| bdd0a2e8db | |||
| 362003e10e | |||
| 4e4b557413 | |||
| 3a068af925 | |||
| 44ca812330 | |||
| 74a69e1c7a | |||
| c3ef16c3dd | |||
| 8c5ac17cdf | |||
| c99b95e611 | |||
| 4362101c0a | |||
| 7ba205cc6c | |||
| abc1712cf7 | |||
| 92452f804f | |||
| c76df84ce3 | |||
| 185ebbf417 | |||
| 6726e2600b | |||
| 93e0664f95 | |||
| bf45ed10aa | |||
| 8da78d50c4 | |||
| b27a959c2b | |||
| cfe719b6bd | |||
| 071799c435 | |||
| 87ee7ed83d | |||
| 08fdd07969 | |||
| 02d5b78f55 | |||
| 5492afe5f6 | |||
| 9a7baeac23 | |||
| ccfe7434bf | |||
| 6dbed73e6b | |||
| 895136af0a | |||
| 72b4398024 | |||
| 65ce62adc1 | |||
| eafdd5046d | |||
| bbece751c0 | |||
| 719c244e32 | |||
| e70b41b256 | |||
| f4b6fd53f3 | |||
| f40ed04899 | |||
| c897323be0 | |||
| d7c4716a6e | |||
| 461cac741b | |||
| cee2e066b9 | |||
| 953d6fe3e4 | |||
| c592a26e42 | |||
| 62bacf76d0 | |||
| fc254db010 | |||
| 0287b69424 | |||
| ceebd99927 | |||
| 41285ac9fc | |||
| 7b9415033f | |||
| b1ffd93bc2 | |||
| 64637a6ac5 | |||
| b83afa22d9 | |||
| c68daabeab | |||
| 9ec0830a65 | |||
| 48943ee74b | |||
| 29d5b3c7db | |||
| 3de574a2e4 | |||
| e765d18ab0 | |||
| 4629759eec | |||
| a14d27da70 | |||
| 5256ffcf77 | |||
| e2bbc6abbc | |||
| 6243e6fd48 | |||
| 1434849142 | |||
| 9bd089adb0 | |||
| 6c1a4c0ac2 | |||
| d35fef82fe | |||
| bca22d8e24 | |||
| 754eac6ee0 | |||
| 762566cce6 | |||
| 286bc0113e | |||
| 62e8e673eb | |||
| 68996809f0 | |||
| 48a2a13163 | |||
| 6e94422b15 | |||
| 713e005aa8 | |||
| 3f3ab766ce | |||
| 00e1bbf994 | |||
| b45adac847 | |||
| 3d9b495863 | |||
| a70364facf | |||
| 02cb39602f | |||
| 12c31725fe | |||
| e0841060df | |||
| b7b55b5c83 | |||
| 43a9093eb7 | |||
| 67e43bc0e3 | |||
| a47c906ab2 | |||
| 41a01ba58a | |||
| ca2bd011e2 | |||
| 2106914aff | |||
| 1c11ddbb7d | |||
| fc1211f7a1 | |||
| 18f87c7b0d | |||
| f9a4bffa55 | |||
| 02f4aabead | |||
| f97dade168 | |||
| 8c3a6472f8 | |||
| 315f9783f5 | |||
| ada78837ce | |||
| 58c6a12e53 | |||
| 005c930d58 | |||
| 7c2891b732 | |||
| 8b36139dab | |||
| a045d65e58 | |||
| 7b064c1e6f | |||
| 6a2e3ab6c1 | |||
| 362da8c308 | |||
| 5658765f54 | |||
| 38449f003a | |||
| df75e480a6 | |||
| 79a95f133b | |||
| 9b1a5b8d26 | |||
| bb2e1bd616 | |||
| 2d518abc19 | |||
| 978aca1f9a | |||
| 2224786478 | |||
| 51364b63e7 | |||
| c897bedd90 | |||
| 831b1ea8e1 | |||
| 4d4dacbc71 | |||
| 1cd4f53657 | |||
| 2eda0e4948 | |||
| f2e04f9170 | |||
| c92b3915d9 | |||
| 6118a17b08 | |||
| 1490c0f179 | |||
| fdf203e70a | |||
| 8114a28964 | |||
| 663507b3f8 | |||
| 79d0c7b666 | |||
| ce4580605d | |||
| dda18a72af | |||
| f7a1de05c8 | |||
| 361606427a | |||
| 81fb5118aa | |||
| a76f12c01c | |||
| 88ee0fb989 | |||
| 3ec49bca74 | |||
| 1908921ae6 | |||
| b9dbd8bd4d | |||
| 587818b91e | |||
| 157ae05f80 | |||
| f2039e29e7 | |||
| ea2d33c3cf | |||
| d775e3d588 | |||
| e7ec15e90f | |||
| 403470be69 | |||
| 6dc0ba74c4 | |||
| 6dcfb336c2 | |||
| 84516b2ac1 | |||
| 57cf2a8cdd | |||
| e3ae97fec4 | |||
| ee76e2cb45 | |||
| de01bb6e75 | |||
| 42668c3e99 | |||
| 05f3c56e47 | |||
| d54977b9ee | |||
| b89fb4dc2f | |||
| a0cf77edfb | |||
| 069f9855d1 | |||
| e3e0775401 | |||
| d255e5ffe1 | |||
| fea460a374 | |||
| 302d3f693f | |||
| 9cc320d74b | |||
| e255777283 | |||
| ef66612cc8 | |||
| 4bed835347 | |||
| b5fd6b4969 | |||
| fe0f213919 | |||
| e888b5c896 | |||
| f27ff52689 | |||
| acbe0ac25d | |||
| c66ff2bf05 | |||
| d089487aa8 | |||
| 6bc1d1606f | |||
| 9df5d921c7 | |||
| 4b1dfa1173 | |||
| f98318e737 | |||
| 7fa1278b31 | |||
| 878ec1f6c1 | |||
| 086048780a | |||
| 65eaaecae5 | |||
| aff0415816 | |||
| 6040fe1cbd | |||
| 36bc9c62b0 | |||
| 3901457218 | |||
| 52f4e9d796 | |||
| 183bad43f6 | |||
| 09fe80cae7 | |||
| 817b48dc9d | |||
| c6f8c42c45 | |||
| 0535686129 | |||
| 53a77255ca | |||
| c01506dc44 | |||
| a49817fdc3 | |||
| 52a4608d76 | |||
| 6f5f9386ff | |||
| fddd0607e6 | |||
| 664be51de2 | |||
| e99c91ce6e | |||
| 177ad2ce7c | |||
| 9b88769585 | |||
| fd044072a1 | |||
| bae5155e19 | |||
| 1e2085b990 | |||
| e5473cdfe4 | |||
| 9c7a70e056 | |||
| 28b70f6459 | |||
| 6961cdee95 | |||
| 58557505ae | |||
| 65178b259f | |||
| b2c209837c | |||
| 355f61188a | |||
| ea672c2423 | |||
| 71ba6b8e55 | |||
| 8a07fccf8f | |||
| 7bc35f4cee | |||
| 002081bcb9 | |||
| e43c01da64 | |||
| 580caeffb9 | |||
| 0eca30367f | |||
| 36317c953a | |||
| f910593fb6 | |||
| 5418ef7ae2 | |||
| 6b147b098a | |||
| 834f8674a3 | |||
| 5cee331abe | |||
| 98c00f7a60 | |||
| db8d946178 | |||
| b97a86f6dc | |||
| 34a4e6be3d | |||
| 22c5ea5000 | |||
| 79acf6c0ba | |||
| ebaa01896f | |||
| ec981ac547 | |||
| d0d4ada7c2 | |||
| 54cbe3faa4 | |||
| 49e51de274 | |||
| e456035f29 | |||
| 964974c142 | |||
| a229ba9c15 | |||
| e4eed2e519 |
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
|
||||||
|
|||||||
@ -30,11 +30,13 @@ const defaultConfig = {
|
|||||||
// Disabled plugins
|
// Disabled plugins
|
||||||
shortcuts: {
|
shortcuts: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
overrideMediaKeys: false,
|
||||||
},
|
},
|
||||||
downloader: {
|
downloader: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
ffmpegArgs: [], // e.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s
|
ffmpegArgs: [], // e.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s
|
||||||
downloadFolder: undefined, // Custom download folder (absolute path)
|
downloadFolder: undefined, // Custom download folder (absolute path)
|
||||||
|
preset: "mp3",
|
||||||
},
|
},
|
||||||
"last-fm": {
|
"last-fm": {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -45,7 +47,8 @@ const defaultConfig = {
|
|||||||
discord: {
|
discord: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
activityTimoutEnabled: true, // if enabled, the discord rich presence gets cleared when music paused after the time specified below
|
activityTimoutEnabled: true, // if enabled, the discord rich presence gets cleared when music paused after the time specified below
|
||||||
activityTimoutTime: 10 * 60 * 1000 // 10 minutes
|
activityTimoutTime: 10 * 60 * 1000, // 10 minutes
|
||||||
|
listenAlong: true, // add a "listen along" button to rich presence
|
||||||
},
|
},
|
||||||
notifications: {
|
notifications: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -58,9 +61,8 @@ const defaultConfig = {
|
|||||||
steps: 1, //percentage of volume to change
|
steps: 1, //percentage of volume to change
|
||||||
arrowsShortcut: true, //enable ArrowUp + ArrowDown local shortcuts
|
arrowsShortcut: true, //enable ArrowUp + ArrowDown local shortcuts
|
||||||
globalShortcuts: {
|
globalShortcuts: {
|
||||||
enabled: false, // enable global shortcuts
|
volumeUp: "",
|
||||||
volumeUp: "Shift+PageUp", // Keybind default can be changed
|
volumeDown: ""
|
||||||
volumeDown: "Shift+PageDown"
|
|
||||||
},
|
},
|
||||||
savedVolume: undefined //plugin save volume between session here
|
savedVolume: undefined //plugin save volume between session here
|
||||||
},
|
},
|
||||||
@ -76,6 +78,10 @@ const defaultConfig = {
|
|||||||
"music_offtopic",
|
"music_offtopic",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
"video-toggle": {
|
||||||
|
enabled: false,
|
||||||
|
forceHide: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,44 @@ const Store = require("electron-store");
|
|||||||
const defaults = require("./defaults");
|
const defaults = require("./defaults");
|
||||||
|
|
||||||
const migrations = {
|
const migrations = {
|
||||||
|
">=1.14.0": (store) => {
|
||||||
|
if (
|
||||||
|
typeof store.get("plugins.precise-volume.globalShortcuts") !== "object"
|
||||||
|
) {
|
||||||
|
store.set("plugins.precise-volume.globalShortcuts", {});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (store.get("plugins.hide-video-player.enabled")) {
|
||||||
|
store.delete("plugins.hide-video-player");
|
||||||
|
store.set("plugins.video-toggle.enabled", true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
">=1.13.0": (store) => {
|
||||||
|
if (store.get("plugins.discord.listenAlong") === undefined) {
|
||||||
|
store.set("plugins.discord.listenAlong", true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
">=1.12.0": (store) => {
|
||||||
|
const options = store.get("plugins.shortcuts");
|
||||||
|
let updated = false;
|
||||||
|
for (const optionType of ["global", "local"]) {
|
||||||
|
if (Array.isArray(options[optionType])) {
|
||||||
|
const updatedOptions = {};
|
||||||
|
for (const optionObject of options[optionType]) {
|
||||||
|
if (optionObject.action && optionObject.shortcut) {
|
||||||
|
updatedOptions[optionObject.action] = optionObject.shortcut;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options[optionType] = updatedOptions;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
store.set("plugins.shortcuts", options);
|
||||||
|
}
|
||||||
|
},
|
||||||
">=1.11.0": (store) => {
|
">=1.11.0": (store) => {
|
||||||
if (store.get("options.resumeOnStart") === undefined) {
|
if (store.get("options.resumeOnStart") === undefined) {
|
||||||
store.set("options.resumeOnStart", true);
|
store.set("options.resumeOnStart", true);
|
||||||
|
|||||||
68
index.js
68
index.js
@ -2,6 +2,8 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const electron = require("electron");
|
const electron = require("electron");
|
||||||
|
const remote = require('@electron/remote/main');
|
||||||
|
remote.initialize();
|
||||||
const enhanceWebRequest = require("electron-better-web-request").default;
|
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");
|
||||||
@ -24,8 +26,9 @@ const app = electron.app;
|
|||||||
app.commandLine.appendSwitch(
|
app.commandLine.appendSwitch(
|
||||||
"js-flags",
|
"js-flags",
|
||||||
// WebAssembly flags
|
// WebAssembly flags
|
||||||
"--experimental-wasm-threads --experimental-wasm-bulk-memory"
|
"--experimental-wasm-threads"
|
||||||
);
|
);
|
||||||
|
app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer"); // Required for downloader
|
||||||
app.allowRendererProcessReuse = true; // https://github.com/electron/electron/issues/18397
|
app.allowRendererProcessReuse = true; // https://github.com/electron/electron/issues/18397
|
||||||
if (config.get("options.disableHardwareAcceleration")) {
|
if (config.get("options.disableHardwareAcceleration")) {
|
||||||
if (is.dev()) {
|
if (is.dev()) {
|
||||||
@ -39,7 +42,9 @@ if (config.get("options.proxy")) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Adds debug features like hotkeys for triggering dev tools and reload
|
// Adds debug features like hotkeys for triggering dev tools and reload
|
||||||
require("electron-debug")();
|
require("electron-debug")({
|
||||||
|
showDevTools: false //disable automatic devTools on new window
|
||||||
|
});
|
||||||
|
|
||||||
// Prevent window being garbage collected
|
// Prevent window being garbage collected
|
||||||
let mainWindow;
|
let mainWindow;
|
||||||
@ -60,7 +65,7 @@ function onClosed() {
|
|||||||
|
|
||||||
function loadPlugins(win) {
|
function loadPlugins(win) {
|
||||||
injectCSS(win.webContents, path.join(__dirname, "youtube-music.css"));
|
injectCSS(win.webContents, path.join(__dirname, "youtube-music.css"));
|
||||||
win.webContents.on("did-finish-load", () => {
|
win.webContents.once("did-finish-load", () => {
|
||||||
if (is.dev()) {
|
if (is.dev()) {
|
||||||
console.log("did finish load");
|
console.log("did finish load");
|
||||||
win.webContents.openDevTools();
|
win.webContents.openDevTools();
|
||||||
@ -96,7 +101,6 @@ function createMainWindow() {
|
|||||||
preload: path.join(__dirname, "preload.js"),
|
preload: path.join(__dirname, "preload.js"),
|
||||||
nodeIntegrationInSubFrames: true,
|
nodeIntegrationInSubFrames: true,
|
||||||
nativeWindowOpen: true, // window.open return Window object(like in regular browsers), not BrowserWindowProxy
|
nativeWindowOpen: true, // window.open return Window object(like in regular browsers), not BrowserWindowProxy
|
||||||
enableRemoteModule: true,
|
|
||||||
affinity: "main-window", // main window, and addition windows should work in one process
|
affinity: "main-window", // main window, and addition windows should work in one process
|
||||||
...(isTesting()
|
...(isTesting()
|
||||||
? {
|
? {
|
||||||
@ -114,6 +118,7 @@ function createMainWindow() {
|
|||||||
: "default",
|
: "default",
|
||||||
autoHideMenuBar: config.get("options.hideMenu"),
|
autoHideMenuBar: config.get("options.hideMenu"),
|
||||||
});
|
});
|
||||||
|
remote.enable(win.webContents);
|
||||||
if (windowPosition) {
|
if (windowPosition) {
|
||||||
const { x, y } = windowPosition;
|
const { x, y } = windowPosition;
|
||||||
win.setPosition(x, y);
|
win.setPosition(x, y);
|
||||||
@ -161,6 +166,31 @@ function createMainWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.once("browser-window-created", (event, win) => {
|
app.once("browser-window-created", (event, win) => {
|
||||||
|
// User agents are from https://developers.whatismybrowser.com/useragents/explore/
|
||||||
|
const originalUserAgent = win.webContents.userAgent;
|
||||||
|
const userAgents = {
|
||||||
|
mac: "Mozilla/5.0 (Macintosh; Intel Mac OS X 12.1; rv:95.0) Gecko/20100101 Firefox/95.0",
|
||||||
|
windows: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0",
|
||||||
|
linux: "Mozilla/5.0 (Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedUserAgent =
|
||||||
|
is.macOS() ? userAgents.mac :
|
||||||
|
is.windows() ? userAgents.windows :
|
||||||
|
userAgents.linux;
|
||||||
|
|
||||||
|
win.webContents.userAgent = updatedUserAgent;
|
||||||
|
app.userAgentFallback = updatedUserAgent;
|
||||||
|
|
||||||
|
win.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => {
|
||||||
|
// this will only happen if login failed, and "retry" was pressed
|
||||||
|
if (win.webContents.getURL().startsWith("https://accounts.google.com") && details.url.startsWith("https://accounts.google.com")){
|
||||||
|
details.requestHeaders["User-Agent"] = originalUserAgent;
|
||||||
|
}
|
||||||
|
cb({ requestHeaders: details.requestHeaders });
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
setupSongInfo(win);
|
setupSongInfo(win);
|
||||||
loadPlugins(win);
|
loadPlugins(win);
|
||||||
|
|
||||||
@ -195,28 +225,6 @@ app.once("browser-window-created", (event, win) => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
win.webContents.on("did-navigate-in-page", () => {
|
|
||||||
const url = win.webContents.getURL();
|
|
||||||
if (url.startsWith("https://music.youtube.com")) {
|
|
||||||
config.set("url", url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
win.webContents.on("will-navigate", (_, url) => {
|
|
||||||
if (url.startsWith("https://accounts.google.com")) {
|
|
||||||
// Force user-agent "Firefox Windows" for Google OAuth to work
|
|
||||||
// From https://github.com/firebase/firebase-js-sdk/issues/2478#issuecomment-571356751
|
|
||||||
// Only set on accounts.google.com, otherwise querySelectors in preload scripts fail (?)
|
|
||||||
const userAgent =
|
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:70.0) Gecko/20100101 Firefox/70.0";
|
|
||||||
|
|
||||||
win.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => {
|
|
||||||
details.requestHeaders["User-Agent"] = userAgent;
|
|
||||||
cb({ requestHeaders: details.requestHeaders });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
win.webContents.on(
|
win.webContents.on(
|
||||||
"new-window",
|
"new-window",
|
||||||
(e, url, frameName, disposition, options) => {
|
(e, url, frameName, disposition, options) => {
|
||||||
@ -336,6 +344,14 @@ app.on("ready", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.get("options.hideMenu") && !config.get("options.hideMenuWarned")) {
|
||||||
|
electron.dialog.showMessageBox(mainWindow, {
|
||||||
|
type: 'info', title: 'Hide Menu Enabled',
|
||||||
|
message: "Menu is hidden, use 'Alt' to show it (or 'Escape' if using in-app-menu)"
|
||||||
|
});
|
||||||
|
config.set("options.hideMenuWarned", true);
|
||||||
|
}
|
||||||
|
|
||||||
// Optimized for Mac OS X
|
// Optimized for Mac OS X
|
||||||
if (is.macOS() && !config.get("options.appVisible")) {
|
if (is.macOS() && !config.get("options.appVisible")) {
|
||||||
app.dock.hide();
|
app.dock.hide();
|
||||||
|
|||||||
49
menu.js
49
menu.js
@ -1,12 +1,15 @@
|
|||||||
const { existsSync } = require("fs");
|
const { existsSync } = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const { app, Menu } = require("electron");
|
const { app, Menu, dialog } = require("electron");
|
||||||
const is = require("electron-is");
|
const is = require("electron-is");
|
||||||
|
|
||||||
const { getAllPlugins } = require("./plugins/utils");
|
const { getAllPlugins } = require("./plugins/utils");
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
|
|
||||||
|
const prompt = require("custom-electron-prompt");
|
||||||
|
const promptOptions = require("./providers/prompt-options");
|
||||||
|
|
||||||
// true only if in-app-menu was loaded on launch
|
// true only if in-app-menu was loaded on launch
|
||||||
const inAppMenuActive = config.plugins.isEnabled("in-app-menu");
|
const inAppMenuActive = config.plugins.isEnabled("in-app-menu");
|
||||||
|
|
||||||
@ -76,6 +79,14 @@ const mainMenuTemplate = (win) => {
|
|||||||
config.set("options.resumeOnStart", item.checked);
|
config.set("options.resumeOnStart", item.checked);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Remove upgrade button",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: config.get("options.removeUpgradeButton"),
|
||||||
|
click: (item) => {
|
||||||
|
config.set("options.removeUpgradeButton", item.checked);
|
||||||
|
},
|
||||||
|
},
|
||||||
...(is.windows() || is.linux()
|
...(is.windows() || is.linux()
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@ -84,6 +95,12 @@ const mainMenuTemplate = (win) => {
|
|||||||
checked: config.get("options.hideMenu"),
|
checked: config.get("options.hideMenu"),
|
||||||
click: (item) => {
|
click: (item) => {
|
||||||
config.set("options.hideMenu", item.checked);
|
config.set("options.hideMenu", item.checked);
|
||||||
|
if (item.checked && !config.get("options.hideMenuWarned")) {
|
||||||
|
dialog.showMessageBox(win, {
|
||||||
|
type: 'info', title: 'Hide Menu Enabled',
|
||||||
|
message: "Menu will be hidden on next launch, use 'Alt' to show it (or 'Escape' if using in-app-menu)"
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -149,6 +166,14 @@ const mainMenuTemplate = (win) => {
|
|||||||
{
|
{
|
||||||
label: "Advanced options",
|
label: "Advanced options",
|
||||||
submenu: [
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Proxy",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: !!config.get("options.proxy"),
|
||||||
|
click: (item) => {
|
||||||
|
setProxy(item, win);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Disable hardware acceleration",
|
label: "Disable hardware acceleration",
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
@ -275,3 +300,25 @@ module.exports.setApplicationMenu = (win) => {
|
|||||||
const menu = Menu.buildFromTemplate(menuTemplate);
|
const menu = Menu.buildFromTemplate(menuTemplate);
|
||||||
Menu.setApplicationMenu(menu);
|
Menu.setApplicationMenu(menu);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function setProxy(item, win) {
|
||||||
|
const output = await prompt({
|
||||||
|
title: 'Set Proxy',
|
||||||
|
label: 'Enter Proxy Address: (leave empty to disable)',
|
||||||
|
value: config.get("options.proxy"),
|
||||||
|
type: 'input',
|
||||||
|
inputAttrs: {
|
||||||
|
type: 'url',
|
||||||
|
placeholder: "Example: 'socks5://127.0.0.1:9999"
|
||||||
|
},
|
||||||
|
width: 450,
|
||||||
|
...promptOptions()
|
||||||
|
}, win);
|
||||||
|
|
||||||
|
if (typeof output === "string") {
|
||||||
|
config.set("options.proxy", output);
|
||||||
|
item.checked = output !== "";
|
||||||
|
} else { //user pressed cancel
|
||||||
|
item.checked = !item.checked; //reset checkbox
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
48
package.json
48
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "youtube-music",
|
"name": "youtube-music",
|
||||||
"productName": "YouTube Music",
|
"productName": "YouTube Music",
|
||||||
"version": "1.12.2",
|
"version": "1.15.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",
|
||||||
@ -37,11 +37,20 @@
|
|||||||
"deb",
|
"deb",
|
||||||
"rpm"
|
"rpm"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"snap": {
|
||||||
|
"slots": [
|
||||||
|
{
|
||||||
|
"mpris": {
|
||||||
|
"interface": "mpris"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"start": "electron .",
|
"start": "NODE_OPTIONS= electron .",
|
||||||
"start:debug": "ELECTRON_ENABLE_LOGGING=1 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",
|
||||||
@ -52,26 +61,26 @@
|
|||||||
"build:mac": "yarn run clean && electron-builder --mac",
|
"build:mac": "yarn run clean && electron-builder --mac",
|
||||||
"build:win": "yarn run clean && electron-builder --win",
|
"build:win": "yarn run clean && electron-builder --win",
|
||||||
"lint": "xo",
|
"lint": "xo",
|
||||||
"plugins": "yarn run plugin:adblocker && yarn run plugin:autoconfirm",
|
"plugins": "yarn run plugin:adblocker",
|
||||||
"plugin:adblocker": "rimraf plugins/adblocker/ad-blocker-engine.bin && node plugins/adblocker/blocker.js",
|
"plugin:adblocker": "rimraf plugins/adblocker/ad-blocker-engine.bin && node plugins/adblocker/blocker.js",
|
||||||
"plugin:autoconfirm": "yarn run generate:package YoutubeNonStop",
|
|
||||||
"release:linux": "yarn run clean && electron-builder --linux -p always -c.snap.publish=github",
|
"release:linux": "yarn run clean && electron-builder --linux -p always -c.snap.publish=github",
|
||||||
"release:mac": "yarn run clean && electron-builder --mac -p always",
|
"release:mac": "yarn run clean && electron-builder --mac -p always",
|
||||||
"release:win": "yarn run clean && electron-builder --win -p always"
|
"release:win": "yarn run clean && electron-builder --win -p always"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.20",
|
"node": ">=14.0.0",
|
||||||
"npm": "Please use yarn and not npm"
|
"npm": "Please use yarn and not npm"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cliqz/adblocker-electron": "^1.22.1",
|
"@cliqz/adblocker-electron": "^1.23.1",
|
||||||
|
"@electron/remote": "^2.0.1",
|
||||||
"@ffmpeg/core": "^0.10.0",
|
"@ffmpeg/core": "^0.10.0",
|
||||||
"@ffmpeg/ffmpeg": "^0.10.0",
|
"@ffmpeg/ffmpeg": "^0.10.0",
|
||||||
"YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.9.0",
|
"async-mutex": "^0.3.2",
|
||||||
"async-mutex": "^0.3.1",
|
|
||||||
"browser-id3-writer": "^4.4.0",
|
"browser-id3-writer": "^4.4.0",
|
||||||
"chokidar": "^3.5.1",
|
"chokidar": "^3.5.2",
|
||||||
"custom-electron-titlebar": "^3.2.7",
|
"custom-electron-prompt": "^1.4.0",
|
||||||
|
"custom-electron-titlebar": "^3.2.9",
|
||||||
"discord-rpc": "^3.2.0",
|
"discord-rpc": "^3.2.0",
|
||||||
"electron-better-web-request": "^1.0.1",
|
"electron-better-web-request": "^1.0.1",
|
||||||
"electron-debug": "^3.2.0",
|
"electron-debug": "^3.2.0",
|
||||||
@ -79,24 +88,25 @@
|
|||||||
"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.9",
|
"electron-updater": "^4.6.3",
|
||||||
"filenamify": "^4.3.0",
|
"filenamify": "^4.3.0",
|
||||||
|
"hark": "^1.2.3",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"node-fetch": "^2.6.1",
|
"mpris-service": "^2.1.2",
|
||||||
|
"node-fetch": "^2.6.6",
|
||||||
"node-notifier": "^9.0.1",
|
"node-notifier": "^9.0.1",
|
||||||
"ytdl-core": "^4.8.3",
|
"ytdl-core": "^4.9.2",
|
||||||
"ytpl": "^2.2.1"
|
"ytpl": "^2.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^12.0.8",
|
"electron": "^16.0.5",
|
||||||
"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",
|
"jest": "^27.3.1",
|
||||||
"jest": "^26.6.3",
|
"playwright": "^1.17.1",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"spectron": "^14.0.0",
|
"xo": "^0.45.0"
|
||||||
"xo": "^0.40.3"
|
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"glob-parent": "5.1.2",
|
"glob-parent": "5.1.2",
|
||||||
|
|||||||
17
plugins/audio-compressor/front.js
Normal file
17
plugins/audio-compressor/front.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
const applyCompressor = () => {
|
||||||
|
const audioContext = new AudioContext();
|
||||||
|
|
||||||
|
const compressor = audioContext.createDynamicsCompressor();
|
||||||
|
compressor.threshold.value = -50;
|
||||||
|
compressor.ratio.value = 12;
|
||||||
|
compressor.knee.value = 40;
|
||||||
|
compressor.attack.value = 0;
|
||||||
|
compressor.release.value = 0.25;
|
||||||
|
|
||||||
|
const source = audioContext.createMediaElementSource(document.querySelector("video"));
|
||||||
|
|
||||||
|
source.connect(compressor);
|
||||||
|
compressor.connect(audioContext.destination);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = () => document.addEventListener('apiLoaded', applyCompressor, { once: true, passive: true });
|
||||||
@ -1,12 +0,0 @@
|
|||||||
// Define global chrome object to be compliant with the extension code
|
|
||||||
global.chrome = {
|
|
||||||
runtime: {
|
|
||||||
getManifest: () => ({
|
|
||||||
version: 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = () => {
|
|
||||||
require("YoutubeNonStop/autoconfirm.js");
|
|
||||||
};
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
const { injectCSS } = require("../utils");
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const { injectCSS } = require("../utils");
|
||||||
|
|
||||||
module.exports = win => {
|
module.exports = win => {
|
||||||
injectCSS(win.webContents, path.join(__dirname, "style.css"));
|
injectCSS(win.webContents, path.join(__dirname, "style.css"));
|
||||||
10
plugins/blur-nav-bar/style.css
Normal file
10
plugins/blur-nav-bar/style.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#nav-bar-background,
|
||||||
|
#header.ytmusic-item-section-renderer,
|
||||||
|
ytmusic-tabs {
|
||||||
|
background: rgba(0, 0, 0, 0.3) !important;
|
||||||
|
backdrop-filter: blur(8px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-bar-divider {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
@ -1,10 +1,14 @@
|
|||||||
const { ontimeupdate } = require("../../providers/video-element");
|
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
ontimeupdate((videoElement) => {
|
document.addEventListener('apiLoaded', apiEvent => {
|
||||||
if (videoElement.currentTime === 0 && videoElement.duration !== NaN) {
|
apiEvent.detail.addEventListener('videodatachange', name => {
|
||||||
// auto-confirm-when-paused plugin can interfere here if not disabled!
|
if (name === 'dataloaded') {
|
||||||
videoElement.pause();
|
apiEvent.detail.pauseVideo();
|
||||||
}
|
document.querySelector('video').ontimeupdate = e => {
|
||||||
});
|
e.target.pause();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.querySelector('video').ontimeupdate = null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, { once: true, passive: true })
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,58 +1,148 @@
|
|||||||
const Discord = require("discord-rpc");
|
const Discord = require("discord-rpc");
|
||||||
|
const { dev } = require("electron-is");
|
||||||
|
const { dialog, app } = require("electron");
|
||||||
|
|
||||||
const registerCallback = require("../../providers/song-info");
|
const registerCallback = require("../../providers/song-info");
|
||||||
|
|
||||||
const rpc = new Discord.Client({
|
|
||||||
transport: "ipc",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Application ID registered by @semvis123
|
// Application ID registered by @semvis123
|
||||||
const clientId = "790655993809338398";
|
const clientId = "790655993809338398";
|
||||||
|
|
||||||
let clearActivity;
|
/**
|
||||||
|
* @typedef {Object} Info
|
||||||
|
* @property {import('discord-rpc').Client} rpc
|
||||||
|
* @property {boolean} ready
|
||||||
|
* @property {import('../../providers/song-info').SongInfo} lastSongInfo
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @type {Info}
|
||||||
|
*/
|
||||||
|
const info = {
|
||||||
|
rpc: null,
|
||||||
|
ready: false,
|
||||||
|
lastSongInfo: null,
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* @type {(() => void)[]}
|
||||||
|
*/
|
||||||
|
const refreshCallbacks = [];
|
||||||
|
const resetInfo = () => {
|
||||||
|
info.rpc = null;
|
||||||
|
info.ready = false;
|
||||||
|
clearTimeout(clearActivity);
|
||||||
|
if (dev()) console.log("discord disconnected");
|
||||||
|
refreshCallbacks.forEach(cb => cb());
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = (win, {activityTimoutEnabled, activityTimoutTime}) => {
|
let window;
|
||||||
// If the page is ready, register the callback
|
const connect = (showErr = false) => {
|
||||||
win.once("ready-to-show", () => {
|
if (info.rpc) {
|
||||||
rpc.once("ready", () => {
|
if (dev())
|
||||||
// Register the callback
|
console.log('Attempted to connect with active RPC object');
|
||||||
registerCallback((songInfo) => {
|
return;
|
||||||
if (songInfo.title.length === 0 && songInfo.artist.length === 0) {
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Song information changed, so lets update the rich presence
|
|
||||||
const activityInfo = {
|
|
||||||
details: songInfo.title,
|
|
||||||
state: songInfo.artist,
|
|
||||||
largeImageKey: "logo",
|
|
||||||
largeImageText: [
|
|
||||||
songInfo.uploadDate,
|
|
||||||
songInfo.views.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " views"
|
|
||||||
].join(' || '),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (songInfo.isPaused) {
|
info.rpc = new Discord.Client({
|
||||||
// Add an idle icon to show that the song is paused
|
transport: "ipc",
|
||||||
activityInfo.smallImageKey = "idle";
|
});
|
||||||
activityInfo.smallImageText = "idle/paused";
|
info.ready = false;
|
||||||
// Set start the timer so the activity gets cleared after a while if enabled
|
|
||||||
if (activityTimoutEnabled)
|
|
||||||
clearActivity = setTimeout(()=>rpc.clearActivity(), activityTimoutTime||10000);
|
|
||||||
} else {
|
|
||||||
// stop the clear activity timout
|
|
||||||
clearTimeout(clearActivity);
|
|
||||||
// Add the start and end time of the song
|
|
||||||
const songStartTime = Date.now() - songInfo.elapsedSeconds * 1000;
|
|
||||||
activityInfo.startTimestamp = songStartTime;
|
|
||||||
activityInfo.endTimestamp =
|
|
||||||
songStartTime + songInfo.songDuration * 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc.setActivity(activityInfo);
|
info.rpc.once("connected", () => {
|
||||||
});
|
if (dev()) console.log("discord connected");
|
||||||
});
|
refreshCallbacks.forEach(cb => cb());
|
||||||
|
});
|
||||||
|
info.rpc.once("ready", () => {
|
||||||
|
info.ready = true;
|
||||||
|
if (info.lastSongInfo) updateActivity(info.lastSongInfo)
|
||||||
|
});
|
||||||
|
info.rpc.once("disconnected", resetInfo);
|
||||||
|
|
||||||
// Startup the rpc client
|
// Startup the rpc client
|
||||||
rpc.login({ clientId }).catch(console.error);
|
info.rpc.login({ clientId }).catch(err => {
|
||||||
|
resetInfo();
|
||||||
|
if (dev()) console.error(err);
|
||||||
|
if (showErr) dialog.showMessageBox(window, { title: 'Connection failed', message: err.message || String(err), type: 'error' });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let clearActivity;
|
||||||
|
/**
|
||||||
|
* @type {import('../../providers/song-info').songInfoCallback}
|
||||||
|
*/
|
||||||
|
let updateActivity;
|
||||||
|
|
||||||
|
module.exports = (win, { activityTimoutEnabled, activityTimoutTime, listenAlong }) => {
|
||||||
|
window = win;
|
||||||
|
// We get multiple events
|
||||||
|
// Next song: PAUSE(n), PAUSE(n+1), PLAY(n+1)
|
||||||
|
// Skip time: PAUSE(N), PLAY(N)
|
||||||
|
updateActivity = songInfo => {
|
||||||
|
if (songInfo.title.length === 0 && songInfo.artist.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
info.lastSongInfo = songInfo;
|
||||||
|
|
||||||
|
// stop the clear activity timout
|
||||||
|
clearTimeout(clearActivity);
|
||||||
|
|
||||||
|
// stop early if discord connection is not ready
|
||||||
|
// do this after clearTimeout to avoid unexpected clears
|
||||||
|
if (!info.rpc || !info.ready) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear directly if timeout is 0
|
||||||
|
if (songInfo.isPaused && activityTimoutEnabled && activityTimoutTime === 0) {
|
||||||
|
info.rpc.clearActivity().catch(console.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Song information changed, so lets update the rich presence
|
||||||
|
// @see https://discord.com/developers/docs/topics/gateway#activity-object
|
||||||
|
// not all options are transfered through https://github.com/discordjs/RPC/blob/6f83d8d812c87cb7ae22064acd132600407d7d05/src/client.js#L518-530
|
||||||
|
const activityInfo = {
|
||||||
|
type: 2, // Listening, addressed in https://github.com/discordjs/RPC/pull/149
|
||||||
|
details: songInfo.title,
|
||||||
|
state: songInfo.artist,
|
||||||
|
largeImageKey: songInfo.imageSrc,
|
||||||
|
largeImageText: [
|
||||||
|
songInfo.uploadDate,
|
||||||
|
songInfo.views.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " views",
|
||||||
|
].join(' || '),
|
||||||
|
buttons: listenAlong ? [
|
||||||
|
{ label: "Listen Along", url: songInfo.url },
|
||||||
|
] : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (songInfo.isPaused) {
|
||||||
|
// Add an idle icon to show that the song is paused
|
||||||
|
activityInfo.smallImageKey = "idle";
|
||||||
|
activityInfo.smallImageText = "idle/paused";
|
||||||
|
// Set start the timer so the activity gets cleared after a while if enabled
|
||||||
|
if (activityTimoutEnabled)
|
||||||
|
clearActivity = setTimeout(() => info.rpc.clearActivity().catch(console.error), activityTimoutTime ?? 10000);
|
||||||
|
} else {
|
||||||
|
// Add the start and end time of the song
|
||||||
|
const songStartTime = Date.now() - songInfo.elapsedSeconds * 1000;
|
||||||
|
activityInfo.startTimestamp = songStartTime;
|
||||||
|
activityInfo.endTimestamp =
|
||||||
|
songStartTime + songInfo.songDuration * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
info.rpc.setActivity(activityInfo).catch(console.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the page is ready, register the callback
|
||||||
|
win.once("ready-to-show", () => {
|
||||||
|
registerCallback(updateActivity);
|
||||||
|
connect();
|
||||||
|
});
|
||||||
|
app.on('window-all-closed', module.exports.clear)
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.clear = () => {
|
||||||
|
if (info.rpc) info.rpc.clearActivity();
|
||||||
|
clearTimeout(clearActivity);
|
||||||
|
};
|
||||||
|
module.exports.connect = connect;
|
||||||
|
module.exports.registerRefresh = (cb) => refreshCallbacks.push(cb);
|
||||||
|
module.exports.isConnected = () => info.rpc !== null;
|
||||||
|
|||||||
47
plugins/discord/menu.js
Normal file
47
plugins/discord/menu.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
const { setOptions } = require("../../config/plugins");
|
||||||
|
const { edit } = require("../../config");
|
||||||
|
const { clear, connect, registerRefresh, isConnected } = require("./back");
|
||||||
|
|
||||||
|
let hasRegisterred = false;
|
||||||
|
|
||||||
|
module.exports = (win, options, refreshMenu) => {
|
||||||
|
if (!hasRegisterred) {
|
||||||
|
registerRefresh(refreshMenu);
|
||||||
|
hasRegisterred = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: isConnected() ? "Connected" : "Reconnect",
|
||||||
|
enabled: !isConnected(),
|
||||||
|
click: connect,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Clear activity",
|
||||||
|
click: clear,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Clear activity after timeout",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: options.activityTimoutEnabled,
|
||||||
|
click: (item) => {
|
||||||
|
options.activityTimoutEnabled = item.checked;
|
||||||
|
setOptions('discord', options);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Listen Along",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: options.listenAlong,
|
||||||
|
click: (item) => {
|
||||||
|
options.listenAlong = item.checked;
|
||||||
|
setOptions('discord', options);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Set timeout time in config",
|
||||||
|
// open config.json
|
||||||
|
click: edit,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
@ -1,25 +1,23 @@
|
|||||||
const { existsSync, mkdirSync } = require("fs");
|
const { existsSync, mkdirSync } = require("fs");
|
||||||
const { join } = require("path");
|
const { join } = require("path");
|
||||||
const { URL } = require("url");
|
|
||||||
|
|
||||||
const { dialog } = require("electron");
|
const { dialog, ipcMain } = 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 chokidar = require('chokidar');
|
||||||
|
|
||||||
const { setOptions } = require("../../config/plugins");
|
const { setOptions } = require("../../config/plugins");
|
||||||
const registerCallback = require("../../providers/song-info");
|
|
||||||
const { sendError } = require("./back");
|
const { sendError } = require("./back");
|
||||||
const { defaultMenuDownloadLabel, getFolder } = require("./utils");
|
const { defaultMenuDownloadLabel, getFolder, presets } = require("./utils");
|
||||||
|
|
||||||
let downloadLabel = defaultMenuDownloadLabel;
|
let downloadLabel = defaultMenuDownloadLabel;
|
||||||
let metadataURL = undefined;
|
let playingPlaylistId = undefined;
|
||||||
let callbackIsRegistered = false;
|
let callbackIsRegistered = false;
|
||||||
|
|
||||||
module.exports = (win, options) => {
|
module.exports = (win, options) => {
|
||||||
if (!callbackIsRegistered) {
|
if (!callbackIsRegistered) {
|
||||||
registerCallback((info) => {
|
ipcMain.on("video-src-changed", async (_, data) => {
|
||||||
metadataURL = info.url;
|
playingPlaylistId = JSON.parse(data)?.videoDetails?.playlistId;
|
||||||
});
|
});
|
||||||
callbackIsRegistered = true;
|
callbackIsRegistered = true;
|
||||||
}
|
}
|
||||||
@ -28,17 +26,17 @@ module.exports = (win, options) => {
|
|||||||
{
|
{
|
||||||
label: downloadLabel,
|
label: downloadLabel,
|
||||||
click: async () => {
|
click: async () => {
|
||||||
const currentURL = metadataURL || win.webContents.getURL();
|
const currentPagePlaylistId = new URL(win.webContents.getURL()).searchParams.get("list");
|
||||||
const playlistID = new URL(currentURL).searchParams.get("list");
|
const playlistId = currentPagePlaylistId || playingPlaylistId;
|
||||||
if (!playlistID) {
|
if (!playlistId) {
|
||||||
sendError(win, new Error("No playlist ID found"));
|
sendError(win, new Error("No playlist ID found"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`trying to get playlist ID: '${playlistID}'`);
|
console.log(`trying to get playlist ID: '${playlistId}'`);
|
||||||
let playlist;
|
let playlist;
|
||||||
try {
|
try {
|
||||||
playlist = await ytpl(playlistID, {
|
playlist = await ytpl(playlistId, {
|
||||||
limit: options.playlistMaxItems || Infinity,
|
limit: options.playlistMaxItems || Infinity,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -111,5 +109,17 @@ module.exports = (win, options) => {
|
|||||||
} // else = user pressed cancel
|
} // else = user pressed cancel
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Presets",
|
||||||
|
submenu: Object.keys(presets).map((preset) => ({
|
||||||
|
label: preset,
|
||||||
|
type: "radio",
|
||||||
|
click: () => {
|
||||||
|
options.preset = preset;
|
||||||
|
setOptions("downloader", options);
|
||||||
|
},
|
||||||
|
checked: options.preset === preset || presets[preset] === undefined,
|
||||||
|
})),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
const electron = require("electron");
|
const electron = require("electron");
|
||||||
|
|
||||||
module.exports.getFolder = (customFolder) =>
|
module.exports.getFolder = customFolder => customFolder || electron.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"];
|
const orderedQualityList = ["maxresdefault", "hqdefault", "mqdefault", "sdddefault"];
|
||||||
@ -29,3 +28,12 @@ module.exports.cropMaxWidth = (image) => {
|
|||||||
}
|
}
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Presets for FFmpeg
|
||||||
|
module.exports.presets = {
|
||||||
|
"None (defaults to mp3)": undefined,
|
||||||
|
opus: {
|
||||||
|
extension: "opus",
|
||||||
|
ffmpegArgs: ["-acodec", "libopus"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@ -3,6 +3,7 @@ const { join } = require("path");
|
|||||||
|
|
||||||
const Mutex = require("async-mutex").Mutex;
|
const Mutex = require("async-mutex").Mutex;
|
||||||
const { ipcRenderer } = require("electron");
|
const { ipcRenderer } = require("electron");
|
||||||
|
const remote = require('@electron/remote');
|
||||||
const is = require("electron-is");
|
const is = require("electron-is");
|
||||||
const filenamify = require("filenamify");
|
const filenamify = require("filenamify");
|
||||||
|
|
||||||
@ -14,8 +15,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 { getFolder, urlToJPG } = require("./utils");
|
const { presets, urlToJPG } = require("./utils");
|
||||||
const { cleanupArtistName } = require("../../providers/song-info");
|
const { cleanupName } = require("../../providers/song-info");
|
||||||
|
|
||||||
const { createFFmpeg } = FFmpeg;
|
const { createFFmpeg } = FFmpeg;
|
||||||
const ffmpeg = createFFmpeg({
|
const ffmpeg = createFFmpeg({
|
||||||
@ -40,7 +41,10 @@ const downloadVideoToMP3 = async (
|
|||||||
const { videoDetails } = await ytdl.getInfo(videoUrl);
|
const { videoDetails } = await ytdl.getInfo(videoUrl);
|
||||||
const thumbnails = videoDetails?.thumbnails;
|
const thumbnails = videoDetails?.thumbnails;
|
||||||
metadata = {
|
metadata = {
|
||||||
artist: videoDetails?.media?.artist || cleanupArtistName(videoDetails?.author?.name) || "",
|
artist:
|
||||||
|
videoDetails?.media?.artist ||
|
||||||
|
cleanupName(videoDetails?.author?.name) ||
|
||||||
|
"",
|
||||||
title: videoDetails?.media?.song || videoDetails?.title || "",
|
title: videoDetails?.media?.song || videoDetails?.title || "",
|
||||||
imageSrcYTPL: thumbnails ?
|
imageSrcYTPL: thumbnails ?
|
||||||
urlToJPG(thumbnails[thumbnails.length - 1].url, videoDetails?.videoId)
|
urlToJPG(thumbnails[thumbnails.length - 1].url, videoDetails?.videoId)
|
||||||
@ -109,8 +113,9 @@ const toMP3 = async (
|
|||||||
existingMetadata = undefined,
|
existingMetadata = undefined,
|
||||||
subfolder = ""
|
subfolder = ""
|
||||||
) => {
|
) => {
|
||||||
|
const convertOptions = { ...presets[options.preset], ...options };
|
||||||
const safeVideoName = randomBytes(32).toString("hex");
|
const safeVideoName = randomBytes(32).toString("hex");
|
||||||
const extension = options.extension || "mp3";
|
const extension = convertOptions.extension || "mp3";
|
||||||
const releaseFFmpegMutex = await ffmpegMutex.acquire();
|
const releaseFFmpegMutex = await ffmpegMutex.acquire();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -128,11 +133,11 @@ const toMP3 = async (
|
|||||||
"-i",
|
"-i",
|
||||||
safeVideoName,
|
safeVideoName,
|
||||||
...getFFmpegMetadataArgs(metadata),
|
...getFFmpegMetadataArgs(metadata),
|
||||||
...(options.ffmpegArgs || []),
|
...(convertOptions.ffmpegArgs || []),
|
||||||
safeVideoName + "." + extension
|
safeVideoName + "." + extension
|
||||||
);
|
);
|
||||||
|
|
||||||
const folder = getFolder(options.downloadFolder);
|
const folder = options.downloadFolder || remote.app.getPath("downloads");
|
||||||
const name = metadata.title
|
const name = metadata.title
|
||||||
? `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`
|
? `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`
|
||||||
: videoName;
|
: videoName;
|
||||||
|
|||||||
47
plugins/exponential-volume/front.js
Normal file
47
plugins/exponential-volume/front.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// "Youtube Music fix volume ratio 0.4" by Marco Pfeiffer
|
||||||
|
// https://greasyfork.org/en/scripts/397686-youtube-music-fix-volume-ratio/
|
||||||
|
|
||||||
|
const exponentialVolume = () => {
|
||||||
|
// manipulation exponent, higher value = lower volume
|
||||||
|
// 3 is the value used by pulseaudio, which Barteks2x figured out this gist here: https://gist.github.com/Barteks2x/a4e189a36a10c159bb1644ffca21c02a
|
||||||
|
// 0.05 (or 5%) is the lowest you can select in the UI which with an exponent of 3 becomes 0.000125 or 0.0125%
|
||||||
|
const EXPONENT = 3;
|
||||||
|
|
||||||
|
const storedOriginalVolumes = new WeakMap();
|
||||||
|
const { get, set } = Object.getOwnPropertyDescriptor(
|
||||||
|
HTMLMediaElement.prototype,
|
||||||
|
"volume"
|
||||||
|
);
|
||||||
|
Object.defineProperty(HTMLMediaElement.prototype, "volume", {
|
||||||
|
get() {
|
||||||
|
const lowVolume = get.call(this);
|
||||||
|
const calculatedOriginalVolume = lowVolume ** (1 / EXPONENT);
|
||||||
|
|
||||||
|
// The calculated value has some accuracy issues which can lead to problems for implementations that expect exact values.
|
||||||
|
// To avoid this, I'll store the unmodified volume to return it when read here.
|
||||||
|
// This mostly solves the issue, but the initial read has no stored value and the volume can also change though external influences.
|
||||||
|
// To avoid ill effects, I check if the stored volume is somewhere in the same range as the calculated volume.
|
||||||
|
const storedOriginalVolume = storedOriginalVolumes.get(this);
|
||||||
|
const storedDeviation = Math.abs(
|
||||||
|
storedOriginalVolume - calculatedOriginalVolume
|
||||||
|
);
|
||||||
|
|
||||||
|
const originalVolume =
|
||||||
|
storedDeviation < 0.01
|
||||||
|
? storedOriginalVolume
|
||||||
|
: calculatedOriginalVolume;
|
||||||
|
return originalVolume;
|
||||||
|
},
|
||||||
|
set(originalVolume) {
|
||||||
|
const lowVolume = originalVolume ** EXPONENT;
|
||||||
|
storedOriginalVolumes.set(this, originalVolume);
|
||||||
|
set.call(this, lowVolume);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = () =>
|
||||||
|
document.addEventListener("apiLoaded", exponentialVolume, {
|
||||||
|
once: true,
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
@ -1,6 +1,9 @@
|
|||||||
const { remote, ipcRenderer } = require("electron");
|
const { ipcRenderer } = require("electron");
|
||||||
|
const { Menu } = require("@electron/remote");
|
||||||
|
|
||||||
|
|
||||||
const customTitlebar = require("custom-electron-titlebar");
|
const customTitlebar = require("custom-electron-titlebar");
|
||||||
|
function $(selector) { return document.querySelector(selector); }
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
const bar = new customTitlebar.Titlebar({
|
const bar = new customTitlebar.Titlebar({
|
||||||
@ -11,6 +14,20 @@ module.exports = () => {
|
|||||||
document.title = "Youtube Music";
|
document.title = "Youtube Music";
|
||||||
|
|
||||||
ipcRenderer.on("updateMenu", function (_event, showMenu) {
|
ipcRenderer.on("updateMenu", function (_event, showMenu) {
|
||||||
bar.updateMenu(showMenu ? remote.Menu.getApplicationMenu() : null);
|
bar.updateMenu(showMenu ? Menu.getApplicationMenu() : null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it)
|
||||||
|
document.addEventListener('apiLoaded', () => {
|
||||||
|
setNavbarMargin();
|
||||||
|
const playPageObserver = new MutationObserver(setNavbarMargin);
|
||||||
|
playPageObserver.observe($('ytmusic-app-layout'), { attributeFilter: ['player-page-open_', 'playerPageOpen_'] })
|
||||||
|
}, { once: true, passive: true })
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function setNavbarMargin() {
|
||||||
|
$('#nav-bar-background').style.right =
|
||||||
|
$('ytmusic-app-layout').playerPageOpen_ ?
|
||||||
|
'0px' :
|
||||||
|
'12px';
|
||||||
|
}
|
||||||
|
|||||||
@ -4,9 +4,13 @@
|
|||||||
font-size: 14px !important;
|
font-size: 14px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* fixes scrollbar positioning relative to nav bar */
|
/* fixes nav-bar-background opacity bug, reposition it, and allows clicking scrollbar through it */
|
||||||
#nav-bar-background.ytmusic-app-layout {
|
#nav-bar-background {
|
||||||
right: 15px !important;
|
opacity: 1 !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
position: sticky !important;
|
||||||
|
top: 0 !important;
|
||||||
|
height: 75px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* remove window dragging for nav bar (conflict with titlebar drag) */
|
/* remove window dragging for nav bar (conflict with titlebar drag) */
|
||||||
@ -16,14 +20,10 @@ ytmusic-pivot-bar-item-renderer {
|
|||||||
-webkit-app-region: unset !important;
|
-webkit-app-region: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* navbar background black */
|
/* move up item selection renderers */
|
||||||
.center-content.ytmusic-nav-bar {
|
ytmusic-item-section-renderer.stuck #header.ytmusic-item-section-renderer,
|
||||||
background: #030303;
|
ytmusic-tabs.stuck {
|
||||||
}
|
top: calc(var(--ytmusic-nav-bar-height) - 15px) !important;
|
||||||
|
|
||||||
/* move up item selectrion renderer by 15 px */
|
|
||||||
ytmusic-item-section-renderer[has-item-section-tabbed-header-renderer_] #header.ytmusic-item-section-renderer {
|
|
||||||
top: 75 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* fix weird positioning in search screen*/
|
/* fix weird positioning in search screen*/
|
||||||
@ -32,8 +32,7 @@ ytmusic-header-renderer.ytmusic-search-page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Move navBar downwards */
|
/* Move navBar downwards */
|
||||||
ytmusic-app-layout > [slot="nav-bar"],
|
ytmusic-nav-bar[slot="nav-bar"] {
|
||||||
#nav-bar-background.ytmusic-app-layout {
|
|
||||||
top: 17px !important;
|
top: 17px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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();
|
||||||
|
});
|
||||||
|
};
|
||||||
67
plugins/lyrics-genius/front.js
Normal file
67
plugins/lyrics-genius/front.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
const { ipcRenderer } = require("electron");
|
||||||
|
const is = require("electron-is");
|
||||||
|
|
||||||
|
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;
|
||||||
|
} else if (is.dev()) {
|
||||||
|
console.log("Fetched lyrics from Genius");
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
12
plugins/lyrics-genius/style.css
Normal file
12
plugins/lyrics-genius/style.css
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/* Disable links in Genius lyrics */
|
||||||
|
.genius-lyrics a {
|
||||||
|
color: var(--ytmusic-text-primary);
|
||||||
|
display: inline-block;
|
||||||
|
pointer-events: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#contents.genius-lyrics {
|
||||||
|
font-size: 1vw;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
@ -1,83 +1,93 @@
|
|||||||
const {
|
const { getSongMenu } = require("../../providers/dom-elements");
|
||||||
getSongMenu,
|
|
||||||
watchDOMElement,
|
|
||||||
} = require("../../providers/dom-elements");
|
|
||||||
const { ElementFromFile, templatePath } = require("../utils");
|
const { ElementFromFile, templatePath } = require("../utils");
|
||||||
|
|
||||||
|
function $(selector) { return document.querySelector(selector); }
|
||||||
|
|
||||||
const slider = ElementFromFile(templatePath(__dirname, "slider.html"));
|
const slider = ElementFromFile(templatePath(__dirname, "slider.html"));
|
||||||
|
|
||||||
const MIN_PLAYBACK_SPEED = 0.25;
|
const roundToTwo = n => Math.round(n * 1e2) / 1e2;
|
||||||
const MAX_PLAYBACK_SPEED = 2;
|
|
||||||
|
|
||||||
let videoElement;
|
const MIN_PLAYBACK_SPEED = 0.07;
|
||||||
let playbackSpeedPercentage = 50; // = Playback speed of 1
|
const MAX_PLAYBACK_SPEED = 16;
|
||||||
|
|
||||||
const computePlayBackSpeed = () => {
|
let playbackSpeed = 1;
|
||||||
if (playbackSpeedPercentage <= 50) {
|
|
||||||
// Slow down video by setting a playback speed between MIN_PLAYBACK_SPEED and 1
|
|
||||||
return (
|
|
||||||
MIN_PLAYBACK_SPEED +
|
|
||||||
((1 - MIN_PLAYBACK_SPEED) / 50) * playbackSpeedPercentage
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accelerate video by setting a playback speed between 1 and MAX_PLAYBACK_SPEED
|
|
||||||
return 1 + ((MAX_PLAYBACK_SPEED - 1) / 50) * (playbackSpeedPercentage - 50);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatePlayBackSpeed = () => {
|
const updatePlayBackSpeed = () => {
|
||||||
const playbackSpeed = Math.round(computePlayBackSpeed() * 100) / 100;
|
$('video').playbackRate = playbackSpeed;
|
||||||
|
|
||||||
if (!videoElement || videoElement.playbackRate === playbackSpeed) {
|
const playbackSpeedElement = $("#playback-speed-value");
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
videoElement.playbackRate = playbackSpeed;
|
|
||||||
|
|
||||||
const playbackSpeedElement = document.querySelector("#playback-speed-value");
|
|
||||||
if (playbackSpeedElement) {
|
if (playbackSpeedElement) {
|
||||||
playbackSpeedElement.innerHTML = playbackSpeed;
|
playbackSpeedElement.innerHTML = playbackSpeed;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = () => {
|
let menu;
|
||||||
watchDOMElement(
|
let observingSlider = false;
|
||||||
"video",
|
|
||||||
(document) => document.querySelector("video"),
|
|
||||||
(element) => {
|
|
||||||
videoElement = element;
|
|
||||||
updatePlayBackSpeed();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
watchDOMElement(
|
const observePopupContainer = () => {
|
||||||
"menu",
|
const observer = new MutationObserver(() => {
|
||||||
(document) => getSongMenu(document),
|
if (!menu) {
|
||||||
(menuElement) => {
|
menu = getSongMenu();
|
||||||
if (!menuElement.contains(slider)) {
|
}
|
||||||
menuElement.prepend(slider);
|
|
||||||
|
if (menu && !menu.contains(slider)) {
|
||||||
|
menu.prepend(slider);
|
||||||
|
if (!observingSlider) {
|
||||||
|
setupSliderListener();
|
||||||
|
observingSlider = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playbackSpeedElement = document.querySelector(
|
|
||||||
"#playback-speed-slider #sliderKnob .slider-knob-inner"
|
|
||||||
);
|
|
||||||
|
|
||||||
const playbackSpeedObserver = new MutationObserver((mutations) => {
|
|
||||||
mutations.forEach(function (mutation) {
|
|
||||||
if (mutation.type == "attributes") {
|
|
||||||
const value = playbackSpeedElement.getAttribute("value");
|
|
||||||
playbackSpeedPercentage = parseInt(value, 10);
|
|
||||||
if (isNaN(playbackSpeedPercentage)) {
|
|
||||||
playbackSpeedPercentage = 50;
|
|
||||||
}
|
|
||||||
updatePlayBackSpeed();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
playbackSpeedObserver.observe(playbackSpeedElement, {
|
|
||||||
attributes: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
|
observer.observe($('ytmusic-popup-container'), {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const observeVideo = () => {
|
||||||
|
$('video').addEventListener('ratechange', forcePlaybackRate)
|
||||||
|
$('video').addEventListener('srcChanged', forcePlaybackRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupWheelListener = () => {
|
||||||
|
slider.addEventListener('wheel', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (isNaN(playbackSpeed)) {
|
||||||
|
playbackSpeed = 1;
|
||||||
|
}
|
||||||
|
// e.deltaY < 0 means wheel-up
|
||||||
|
playbackSpeed = roundToTwo(e.deltaY < 0 ?
|
||||||
|
Math.min(playbackSpeed + 0.01, MAX_PLAYBACK_SPEED) :
|
||||||
|
Math.max(playbackSpeed - 0.01, MIN_PLAYBACK_SPEED)
|
||||||
|
);
|
||||||
|
|
||||||
|
updatePlayBackSpeed();
|
||||||
|
// update slider position
|
||||||
|
$('#playback-speed-slider').value = playbackSpeed;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupSliderListener() {
|
||||||
|
$('#playback-speed-slider').addEventListener('immediate-value-changed', e => {
|
||||||
|
playbackSpeed = e.detail.value || MIN_PLAYBACK_SPEED;
|
||||||
|
if (isNaN(playbackSpeed)) {
|
||||||
|
playbackSpeed = 1;
|
||||||
|
}
|
||||||
|
updatePlayBackSpeed();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function forcePlaybackRate(e) {
|
||||||
|
if (e.target.playbackRate !== playbackSpeed) {
|
||||||
|
e.target.playbackRate = playbackSpeed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
document.addEventListener('apiLoaded', e => {
|
||||||
|
observePopupContainer();
|
||||||
|
observeVideo();
|
||||||
|
setupWheelListener();
|
||||||
|
}, { once: true, passive: true })
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,79 +1,88 @@
|
|||||||
<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"
|
style="display: inherit !important"
|
||||||
role="slider"
|
max="2"
|
||||||
tabindex="0"
|
min="0"
|
||||||
aria-valuemin="0"
|
step="0.125"
|
||||||
aria-valuemax="100"
|
dir="ltr"
|
||||||
aria-valuenow="50"
|
title="Playback speed"
|
||||||
aria-disabled="false"
|
aria-label="Playback speed"
|
||||||
value="50"
|
role="slider"
|
||||||
><!--css-build:shady-->
|
tabindex="0"
|
||||||
<div id="sliderContainer" class="style-scope tp-yt-paper-slider">
|
aria-valuemin="0"
|
||||||
<div class="bar-container style-scope tp-yt-paper-slider">
|
aria-valuemax="2"
|
||||||
<tp-yt-paper-progress
|
aria-valuenow="1"
|
||||||
id="sliderBar"
|
aria-disabled="false"
|
||||||
aria-hidden="true"
|
value="1"
|
||||||
class="style-scope tp-yt-paper-slider"
|
><!--css-build:shady-->
|
||||||
role="progressbar"
|
<div id="sliderContainer" class="style-scope tp-yt-paper-slider">
|
||||||
value="50"
|
<div class="bar-container style-scope tp-yt-paper-slider">
|
||||||
aria-valuenow="50"
|
<tp-yt-paper-progress
|
||||||
aria-valuemin="0"
|
id="sliderBar"
|
||||||
aria-valuemax="100"
|
aria-hidden="true"
|
||||||
aria-disabled="false"
|
class="style-scope tp-yt-paper-slider"
|
||||||
style="touch-action: none"
|
role="progressbar"
|
||||||
><!--css-build:shady-->
|
value="1"
|
||||||
|
aria-valuenow="1"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="2"
|
||||||
|
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="1"
|
||||||
|
></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>
|
||||||
|
|||||||
@ -1,23 +1,9 @@
|
|||||||
const { isEnabled } = require("../../config/plugins");
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This is used to determine if plugin is actually active
|
This is used to determine if plugin is actually active
|
||||||
(not if its only enabled in options)
|
(not if its only enabled in options)
|
||||||
*/
|
*/
|
||||||
let enabled = false;
|
let enabled = false;
|
||||||
|
|
||||||
module.exports = (win) => {
|
module.exports = () => enabled = true;
|
||||||
enabled = true;
|
|
||||||
|
|
||||||
// youtube-music register some of the target listeners after DOMContentLoaded
|
module.exports.enabled = () => enabled;
|
||||||
// did-finish-load is called after all elements finished loading, including said listeners
|
|
||||||
// Thats the reason the timing is controlled from main
|
|
||||||
win.webContents.once("did-finish-load", () => {
|
|
||||||
win.webContents.send("restoreAddEventListener");
|
|
||||||
win.webContents.send("setupVideoPlayerVolumeMousewheel", !isEnabled("hide-video-player"));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.enabled = () => {
|
|
||||||
return enabled;
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,29 +1,84 @@
|
|||||||
const { ipcRenderer, remote } = require("electron");
|
const { ipcRenderer } = require("electron");
|
||||||
|
const { globalShortcut } = require('@electron/remote');
|
||||||
|
|
||||||
const { setOptions } = require("../../config/plugins");
|
const { setOptions } = require("../../config/plugins");
|
||||||
|
|
||||||
function $(selector) { return document.querySelector(selector); }
|
function $(selector) { return document.querySelector(selector); }
|
||||||
|
let api;
|
||||||
|
|
||||||
module.exports = (options) => {
|
module.exports = (options) => {
|
||||||
|
document.addEventListener('apiLoaded', e => {
|
||||||
|
api = e.detail;
|
||||||
|
firstRun(options);
|
||||||
|
}, { once: true, passive: true })
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Restore saved volume and setup tooltip */
|
||||||
|
function firstRun(options) {
|
||||||
|
if (typeof options.savedVolume === "number") {
|
||||||
|
// Set saved volume as tooltip
|
||||||
|
setTooltip(options.savedVolume);
|
||||||
|
|
||||||
|
if (api.getVolume() !== options.savedVolume) {
|
||||||
|
api.setVolume(options.savedVolume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setupPlaybar(options);
|
setupPlaybar(options);
|
||||||
|
|
||||||
setupSliderObserver(options);
|
|
||||||
|
|
||||||
setupLocalArrowShortcuts(options);
|
setupLocalArrowShortcuts(options);
|
||||||
|
|
||||||
if (options.globalShortcuts?.enabled) {
|
setupGlobalShortcuts(options);
|
||||||
setupGlobalShortcuts(options);
|
|
||||||
|
const noVid = $("#main-panel")?.computedStyleMap().get("display").value === "none";
|
||||||
|
injectVolumeHud(noVid);
|
||||||
|
if (!noVid) {
|
||||||
|
setupVideoPlayerOnwheel(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
firstRun(options);
|
// Change options from renderer to keep sync
|
||||||
|
ipcRenderer.on("setOptions", (_event, newOptions = {}) => {
|
||||||
// This way the ipc listener gets cleared either way
|
for (option in newOptions) {
|
||||||
ipcRenderer.once("setupVideoPlayerVolumeMousewheel", (_event, toEnable) => {
|
options[option] = newOptions[option];
|
||||||
if (toEnable)
|
}
|
||||||
setupVideoPlayerOnwheel(options);
|
setOptions("precise-volume", options);
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
|
function injectVolumeHud(noVid) {
|
||||||
|
if (noVid) {
|
||||||
|
const position = "top: 18px; right: 60px; z-index: 999; position: absolute;";
|
||||||
|
const mainStyle = "font-size: xx-large; padding: 10px; transition: opacity 1s; pointer-events: none;";
|
||||||
|
|
||||||
|
$(".center-content.ytmusic-nav-bar").insertAdjacentHTML("beforeend",
|
||||||
|
`<span id="volumeHud" style="${position + mainStyle}"></span>`)
|
||||||
|
} else {
|
||||||
|
const position = `top: 10px; left: 10px; z-index: 999; position: absolute;`;
|
||||||
|
const mainStyle = "font-size: xxx-large; padding: 10px; transition: opacity 0.6s; webkit-text-stroke: 1px black; font-weight: 600; pointer-events: none;";
|
||||||
|
|
||||||
|
$("#song-video").insertAdjacentHTML('afterend',
|
||||||
|
`<span id="volumeHud" style="${position + mainStyle}"></span>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let hudFadeTimeout;
|
||||||
|
|
||||||
|
function showVolumeHud(volume) {
|
||||||
|
let volumeHud = $("#volumeHud");
|
||||||
|
if (!volumeHud) return;
|
||||||
|
|
||||||
|
volumeHud.textContent = volume + '%';
|
||||||
|
volumeHud.style.opacity = 1;
|
||||||
|
|
||||||
|
if (hudFadeTimeout) {
|
||||||
|
clearTimeout(hudFadeTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
hudFadeTimeout = setTimeout(() => {
|
||||||
|
volumeHud.style.opacity = 0;
|
||||||
|
hudFadeTimeout = null;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
/** Add onwheel event to video player */
|
/** Add onwheel event to video player */
|
||||||
function setupVideoPlayerOnwheel(options) {
|
function setupVideoPlayerOnwheel(options) {
|
||||||
@ -34,35 +89,20 @@ function setupVideoPlayerOnwheel(options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function toPercent(volume) {
|
|
||||||
return Math.round(Number.parseFloat(volume) * 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveVolume(volume, options) {
|
function saveVolume(volume, options) {
|
||||||
options.savedVolume = volume;
|
options.savedVolume = volume;
|
||||||
setOptions("precise-volume", options);
|
writeOptions(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Restore saved volume and setup tooltip */
|
//without this function it would rewrite config 20 time when volume change by 20
|
||||||
function firstRun(options) {
|
let writeTimeout;
|
||||||
const videoStream = $(".video-stream");
|
function writeOptions(options) {
|
||||||
const slider = $("#volume-slider");
|
if (writeTimeout) clearTimeout(writeTimeout);
|
||||||
// Those elements load abit after DOMContentLoaded
|
|
||||||
if (videoStream && slider) {
|
writeTimeout = setTimeout(() => {
|
||||||
// Set saved volume IF it pass checks
|
setOptions("precise-volume", options);
|
||||||
if (options.savedVolume
|
writeTimeout = null;
|
||||||
&& options.savedVolume >= 0 && options.savedVolume <= 100
|
}, 1000)
|
||||||
&& Math.abs(slider.value - options.savedVolume) < 5
|
|
||||||
// If plugin was disabled and volume changed then diff>4
|
|
||||||
) {
|
|
||||||
videoStream.volume = options.savedVolume / 100;
|
|
||||||
slider.value = options.savedVolume;
|
|
||||||
}
|
|
||||||
// Set current volume as tooltip
|
|
||||||
setTooltip(toPercent(videoStream.volume));
|
|
||||||
} else {
|
|
||||||
setTimeout(firstRun, 500, options); // Try again in 500 milliseconds
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Add onwheel event to play bar and also track if play bar is hovered*/
|
/** Add onwheel event to play bar and also track if play bar is hovered*/
|
||||||
@ -83,32 +123,63 @@ function setupPlaybar(options) {
|
|||||||
playerbar.addEventListener("mouseleave", () => {
|
playerbar.addEventListener("mouseleave", () => {
|
||||||
playerbar.classList.remove("on-hover");
|
playerbar.classList.remove("on-hover");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setupSliderObserver(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Save volume + Update the volume tooltip when volume-slider is manually changed */
|
||||||
|
function setupSliderObserver(options) {
|
||||||
|
const sliderObserver = new MutationObserver(mutations => {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
// This checks that volume-slider was manually set
|
||||||
|
if (mutation.oldValue !== mutation.target.value &&
|
||||||
|
(typeof options.savedVolume !== "number" || Math.abs(options.savedVolume - mutation.target.value) > 4)) {
|
||||||
|
// Diff>4 means it was manually set
|
||||||
|
setTooltip(mutation.target.value);
|
||||||
|
saveVolume(mutation.target.value, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Observing only changes in 'value' of volume-slider
|
||||||
|
sliderObserver.observe($("#volume-slider"), {
|
||||||
|
attributeFilter: ["value"],
|
||||||
|
attributeOldValue: true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** if (toIncrease = false) then volume decrease */
|
/** if (toIncrease = false) then volume decrease */
|
||||||
function changeVolume(toIncrease, options) {
|
function changeVolume(toIncrease, options) {
|
||||||
// Need to change both the actual volume and the slider
|
|
||||||
const videoStream = $(".video-stream");
|
|
||||||
const slider = $("#volume-slider");
|
|
||||||
// Apply volume change if valid
|
// Apply volume change if valid
|
||||||
const steps = (options.steps || 1) / 100;
|
const steps = Number(options.steps || 1);
|
||||||
videoStream.volume = toIncrease ?
|
api.setVolume(toIncrease ?
|
||||||
Math.min(videoStream.volume + steps, 1) :
|
Math.min(api.getVolume() + steps, 100) :
|
||||||
Math.max(videoStream.volume - steps, 0);
|
Math.max(api.getVolume() - steps, 0));
|
||||||
|
|
||||||
// Save the new volume
|
// Save the new volume
|
||||||
saveVolume(toPercent(videoStream.volume), options);
|
saveVolume(api.getVolume(), options);
|
||||||
// Slider value automatically rounds to multiples of 5
|
|
||||||
slider.value = options.savedVolume;
|
// change slider position (important)
|
||||||
|
updateVolumeSlider(options);
|
||||||
|
|
||||||
// Change tooltips to new value
|
// Change tooltips to new value
|
||||||
setTooltip(options.savedVolume);
|
setTooltip(options.savedVolume);
|
||||||
// Show volume slider on volume change
|
// Show volume slider
|
||||||
showVolumeSlider(slider);
|
showVolumeSlider();
|
||||||
|
// Show volume HUD
|
||||||
|
showVolumeHud(options.savedVolume);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVolumeSlider(options) {
|
||||||
|
// Slider value automatically rounds to multiples of 5
|
||||||
|
$("#volume-slider").value = options.savedVolume > 0 && options.savedVolume < 5 ?
|
||||||
|
5 : options.savedVolume;
|
||||||
}
|
}
|
||||||
|
|
||||||
let volumeHoverTimeoutID;
|
let volumeHoverTimeoutID;
|
||||||
|
|
||||||
function showVolumeSlider(slider) {
|
function showVolumeSlider() {
|
||||||
|
const slider = $("#volume-slider");
|
||||||
// This class display the volume slider if not in minimized mode
|
// This class display the volume slider if not in minimized mode
|
||||||
slider.classList.add("on-hover");
|
slider.classList.add("on-hover");
|
||||||
// Reset timeout if previous one hasn't completed
|
// Reset timeout if previous one hasn't completed
|
||||||
@ -124,27 +195,6 @@ function showVolumeSlider(slider) {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Save volume + Update the volume tooltip when volume-slider is manually changed */
|
|
||||||
function setupSliderObserver(options) {
|
|
||||||
const sliderObserver = new MutationObserver(mutations => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
// This checks that volume-slider was manually set
|
|
||||||
if (mutation.oldValue !== mutation.target.value &&
|
|
||||||
(!options.savedVolume || Math.abs(options.savedVolume - mutation.target.value) > 4)) {
|
|
||||||
// Diff>4 means it was manually set
|
|
||||||
setTooltip(mutation.target.value);
|
|
||||||
saveVolume(mutation.target.value, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Observing only changes in 'value' of volume-slider
|
|
||||||
sliderObserver.observe($("#volume-slider"), {
|
|
||||||
attributeFilter: ["value"],
|
|
||||||
attributeOldValue: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set new volume as tooltip for volume slider and icon + expanding slider (appears when window size is small)
|
// Set new volume as tooltip for volume slider and icon + expanding slider (appears when window size is small)
|
||||||
const tooltipTargets = [
|
const tooltipTargets = [
|
||||||
"#volume-slider",
|
"#volume-slider",
|
||||||
@ -161,48 +211,26 @@ function setTooltip(volume) {
|
|||||||
|
|
||||||
function setupGlobalShortcuts(options) {
|
function setupGlobalShortcuts(options) {
|
||||||
if (options.globalShortcuts.volumeUp) {
|
if (options.globalShortcuts.volumeUp) {
|
||||||
remote.globalShortcut.register((options.globalShortcuts.volumeUp), () => changeVolume(true, options));
|
globalShortcut.register((options.globalShortcuts.volumeUp), () => changeVolume(true, options));
|
||||||
}
|
}
|
||||||
if (options.globalShortcuts.volumeDown) {
|
if (options.globalShortcuts.volumeDown) {
|
||||||
remote.globalShortcut.register((options.globalShortcuts.volumeDown), () => changeVolume(false, options));
|
globalShortcut.register((options.globalShortcuts.volumeDown), () => changeVolume(false, options));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupLocalArrowShortcuts(options) {
|
function setupLocalArrowShortcuts(options) {
|
||||||
if (options.arrowsShortcut) {
|
if (options.arrowsShortcut) {
|
||||||
addListener();
|
window.addEventListener('keydown', (event) => {
|
||||||
}
|
switch (event.code) {
|
||||||
|
case "ArrowUp":
|
||||||
// Change options from renderer to keep sync
|
event.preventDefault();
|
||||||
ipcRenderer.on("setArrowsShortcut", (_event, isEnabled) => {
|
changeVolume(true, options);
|
||||||
options.arrowsShortcut = isEnabled;
|
break;
|
||||||
setOptions("precise-volume", options);
|
case "ArrowDown":
|
||||||
// This allows changing this setting without restarting app
|
event.preventDefault();
|
||||||
if (isEnabled) {
|
changeVolume(false, options);
|
||||||
addListener();
|
break;
|
||||||
} else {
|
}
|
||||||
removeListener();
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function addListener() {
|
|
||||||
window.addEventListener('keydown', callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeListener() {
|
|
||||||
window.removeEventListener("keydown", callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function callback(event) {
|
|
||||||
switch (event.code) {
|
|
||||||
case "ArrowUp":
|
|
||||||
event.preventDefault();
|
|
||||||
changeVolume(true, options);
|
|
||||||
break;
|
|
||||||
case "ArrowDown":
|
|
||||||
event.preventDefault();
|
|
||||||
changeVolume(false, options);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,82 @@
|
|||||||
const { enabled } = require("./back");
|
const { enabled } = require("./back");
|
||||||
const { setOptions } = require("../../config/plugins");
|
const { setOptions } = require("../../config/plugins");
|
||||||
|
const prompt = require("custom-electron-prompt");
|
||||||
|
const promptOptions = require("../../providers/prompt-options");
|
||||||
|
|
||||||
|
function changeOptions(changedOptions, options, win) {
|
||||||
|
for (option in changedOptions) {
|
||||||
|
options[option] = changedOptions[option];
|
||||||
|
}
|
||||||
|
// Dynamically change setting if plugin is enabled
|
||||||
|
if (enabled()) {
|
||||||
|
win.webContents.send("setOptions", changedOptions);
|
||||||
|
} else { // Fallback to usual method if disabled
|
||||||
|
setOptions("precise-volume", options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = (win, options) => [
|
module.exports = (win, options) => [
|
||||||
{
|
{
|
||||||
label: "Arrowkeys controls",
|
label: "Local Arrowkeys Controls",
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
checked: !!options.arrowsShortcut,
|
checked: !!options.arrowsShortcut,
|
||||||
click: (item) => {
|
click: item => {
|
||||||
// Dynamically change setting if plugin enabled
|
changeOptions({ arrowsShortcut: item.checked }, options, win);
|
||||||
if (enabled()) {
|
|
||||||
win.webContents.send("setArrowsShortcut", item.checked);
|
|
||||||
} else { // Fallback to usual method if disabled
|
|
||||||
options.arrowsShortcut = item.checked;
|
|
||||||
setOptions("precise-volume", options);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Global Hotkeys",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: !!options.globalShortcuts.volumeUp || !!options.globalShortcuts.volumeDown,
|
||||||
|
click: item => promptGlobalShortcuts(win, options, item)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Set Custom Volume Steps",
|
||||||
|
click: () => promptVolumeSteps(win, options)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Helper function for globalShortcuts prompt
|
||||||
|
const kb = (label_, value_, default_) => { return { value: value_, label: label_, default: default_ || undefined }; };
|
||||||
|
|
||||||
|
async function promptVolumeSteps(win, options) {
|
||||||
|
const output = await prompt({
|
||||||
|
title: "Volume Steps",
|
||||||
|
label: "Choose Volume Increase/Decrease Steps",
|
||||||
|
value: options.steps || 1,
|
||||||
|
type: "counter",
|
||||||
|
counterOptions: { minimum: 0, maximum: 100, multiFire: true },
|
||||||
|
width: 380,
|
||||||
|
...promptOptions()
|
||||||
|
}, win)
|
||||||
|
|
||||||
|
if (output || output === 0) { // 0 is somewhat valid
|
||||||
|
changeOptions({ steps: output}, options, win);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptGlobalShortcuts(win, options, item) {
|
||||||
|
const output = await prompt({
|
||||||
|
title: "Global Volume Keybinds",
|
||||||
|
label: "Choose Global Volume Keybinds:",
|
||||||
|
type: "keybind",
|
||||||
|
keybindOptions: [
|
||||||
|
kb("Increase Volume", "volumeUp", options.globalShortcuts?.volumeUp),
|
||||||
|
kb("Decrease Volume", "volumeDown", options.globalShortcuts?.volumeDown)
|
||||||
|
],
|
||||||
|
...promptOptions()
|
||||||
|
}, win)
|
||||||
|
|
||||||
|
if (output) {
|
||||||
|
let newGlobalShortcuts = {};
|
||||||
|
for (const { value, accelerator } of output) {
|
||||||
|
newGlobalShortcuts[value] = accelerator;
|
||||||
|
}
|
||||||
|
changeOptions({ globalShortcuts: newGlobalShortcuts }, options, win);
|
||||||
|
|
||||||
|
item.checked = !!options.globalShortcuts.volumeUp || !!options.globalShortcuts.volumeDown;
|
||||||
|
} else {
|
||||||
|
// Reset checkbox if prompt was canceled
|
||||||
|
item.checked = !item.checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -24,10 +24,10 @@ function overrideAddEventListener() {
|
|||||||
|
|
||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
overrideAddEventListener();
|
overrideAddEventListener();
|
||||||
// Restore original function after did-finish-load to avoid keeping Element.prototype altered
|
// Restore original function after finished loading to avoid keeping Element.prototype altered
|
||||||
ipcRenderer.once("restoreAddEventListener", () => { // Called from main to make sure page is completly loaded
|
window.addEventListener('load', () => {
|
||||||
Element.prototype.addEventListener = Element.prototype._addEventListener;
|
Element.prototype.addEventListener = Element.prototype._addEventListener;
|
||||||
Element.prototype._addEventListener = undefined;
|
Element.prototype._addEventListener = undefined;
|
||||||
ignored = undefined;
|
ignored = undefined;
|
||||||
});
|
}, { once: true });
|
||||||
};
|
};
|
||||||
|
|||||||
41
plugins/quality-changer/front.js
Normal file
41
plugins/quality-changer/front.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
const { ElementFromFile, templatePath } = require("../utils");
|
||||||
|
const { dialog } = require('@electron/remote');
|
||||||
|
|
||||||
|
function $(selector) { return document.querySelector(selector); }
|
||||||
|
|
||||||
|
const qualitySettingsButton = ElementFromFile(
|
||||||
|
templatePath(__dirname, "qualitySettingsTemplate.html")
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
document.addEventListener('apiLoaded', setup, { once: true, passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup(event) {
|
||||||
|
const api = event.detail;
|
||||||
|
|
||||||
|
$('.top-row-buttons.ytmusic-player').prepend(qualitySettingsButton);
|
||||||
|
|
||||||
|
qualitySettingsButton.onclick = function chooseQuality() {
|
||||||
|
if (api.getPlayerState() === 2) api.playVideo();
|
||||||
|
else if (api.getPlayerState() === 1) api.pauseVideo();
|
||||||
|
|
||||||
|
const currentIndex = api.getAvailableQualityLevels().indexOf(api.getPlaybackQuality())
|
||||||
|
|
||||||
|
dialog.showMessageBox({
|
||||||
|
type: "question",
|
||||||
|
buttons: api.getAvailableQualityLabels(),
|
||||||
|
defaultId: currentIndex,
|
||||||
|
title: "Choose Video Quality",
|
||||||
|
message: "Choose Video Quality:",
|
||||||
|
detail: `Current Quality: ${api.getAvailableQualityLabels()[currentIndex]}`,
|
||||||
|
cancelId: -1
|
||||||
|
}).then((promise) => {
|
||||||
|
if (promise.response === -1) return;
|
||||||
|
const newQuality = api.getAvailableQualityLevels()[promise.response];
|
||||||
|
api.setPlaybackQualityRange(newQuality);
|
||||||
|
api.setPlaybackQuality(newQuality)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
<tp-yt-paper-icon-button class="player-quality-button style-scope ytmusic-player" icon="yt-icons:settings"
|
||||||
|
title="Open player quality changer" aria-label="Open player quality changer" role="button" tabindex="0" aria-disabled="false">
|
||||||
|
<tp-yt-iron-icon id="icon" class="style-scope tp-yt-paper-icon-button"><svg viewBox="0 0 24 24"
|
||||||
|
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">
|
||||||
|
<path
|
||||||
|
d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.1-1.65c.2-.15.25-.42.13-.64l-2-3.46c-.12-.22-.4-.3-.6-.22l-2.5 1c-.52-.4-1.08-.73-1.7-.98l-.37-2.65c-.06-.24-.27-.42-.5-.42h-4c-.27 0-.48.18-.5.42l-.4 2.65c-.6.25-1.17.6-1.7.98l-2.48-1c-.23-.1-.5 0-.6.22l-2 3.46c-.14.22-.08.5.1.64l2.12 1.65c-.04.32-.07.65-.07.98s.02.66.06.98l-2.1 1.65c-.2.15-.25.42-.13.64l2 3.46c.12.22.4.3.6.22l2.5-1c.52.4 1.08.73 1.7.98l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.6-.25 1.17-.6 1.7-.98l2.48 1c.23.1.5 0 .6-.22l2-3.46c.13-.22.08-.5-.1-.64l-2.12-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"
|
||||||
|
class="style-scope yt-icon"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</tp-yt-iron-icon>
|
||||||
|
</tp-yt-paper-icon-button>
|
||||||
@ -1,7 +1,8 @@
|
|||||||
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 registerMPRIS = require("./mpris");
|
||||||
|
|
||||||
function _registerGlobalShortcut(webContents, shortcut, action) {
|
function _registerGlobalShortcut(webContents, shortcut, action) {
|
||||||
globalShortcut.register(shortcut, () => {
|
globalShortcut.register(shortcut, () => {
|
||||||
@ -19,31 +20,43 @@ function registerShortcuts(win, options) {
|
|||||||
const songControls = getSongControls(win);
|
const songControls = getSongControls(win);
|
||||||
const { playPause, next, previous, search } = songControls;
|
const { playPause, next, previous, search } = songControls;
|
||||||
|
|
||||||
_registerGlobalShortcut(win.webContents, "MediaPlayPause", playPause);
|
if (options.overrideMediaKeys) {
|
||||||
_registerGlobalShortcut(win.webContents, "MediaNextTrack", next);
|
_registerGlobalShortcut(win.webContents, "MediaPlayPause", playPause);
|
||||||
_registerGlobalShortcut(win.webContents, "MediaPreviousTrack", previous);
|
_registerGlobalShortcut(win.webContents, "MediaNextTrack", next);
|
||||||
|
_registerGlobalShortcut(win.webContents, "MediaPreviousTrack", previous);
|
||||||
|
}
|
||||||
|
|
||||||
_registerLocalShortcut(win, "CommandOrControl+F", search);
|
_registerLocalShortcut(win, "CommandOrControl+F", search);
|
||||||
_registerLocalShortcut(win, "CommandOrControl+L", search);
|
_registerLocalShortcut(win, "CommandOrControl+L", search);
|
||||||
|
|
||||||
|
if (is.linux()) registerMPRIS(win);
|
||||||
|
|
||||||
const { global, local } = options;
|
const { global, local } = options;
|
||||||
(global || []).forEach(({ shortcut, action }) => {
|
const shortcutOptions = { global, local };
|
||||||
console.debug("Registering global shortcut", shortcut, ":", action);
|
|
||||||
if (!action || !songControls[action]) {
|
|
||||||
console.warn("Invalid action", action);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_registerGlobalShortcut(win.webContents, shortcut, songControls[action]);
|
for (const optionType in shortcutOptions) {
|
||||||
});
|
registerAllShortcuts(shortcutOptions[optionType], optionType);
|
||||||
(local || []).forEach(({ shortcut, action }) => {
|
}
|
||||||
console.debug("Registering local shortcut", shortcut, ":", action);
|
|
||||||
if (!action || !songControls[action]) {
|
|
||||||
console.warn("Invalid action", action);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_registerLocalShortcut(win, shortcut, songControls[action]);
|
function registerAllShortcuts(container, type) {
|
||||||
});
|
for (const action in container) {
|
||||||
|
if (!container[action]) {
|
||||||
|
continue; // Action accelerator is empty
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug(`Registering ${type} shortcut`, container[action], ":", action);
|
||||||
|
if (!songControls[action]) {
|
||||||
|
console.warn("Invalid action", action);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "global") {
|
||||||
|
_registerGlobalShortcut(win.webContents, container[action], songControls[action]);
|
||||||
|
} else { // type === "local"
|
||||||
|
_registerLocalShortcut(win, local[action], songControls[action]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = registerShortcuts;
|
module.exports = registerShortcuts;
|
||||||
|
|||||||
53
plugins/shortcuts/menu.js
Normal file
53
plugins/shortcuts/menu.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
const { setOptions } = require("../../config/plugins");
|
||||||
|
const prompt = require("custom-electron-prompt");
|
||||||
|
const promptOptions = require("../../providers/prompt-options");
|
||||||
|
|
||||||
|
module.exports = (win, options) => [
|
||||||
|
{
|
||||||
|
label: "Set Global Song Controls",
|
||||||
|
click: () => promptKeybind(options, win)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Override MediaKeys",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: options.overrideMediaKeys,
|
||||||
|
click: item => setOption(options, "overrideMediaKeys", item.checked)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function setOption(options, key = null, newValue = null) {
|
||||||
|
if (key && newValue !== null) {
|
||||||
|
options[key] = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOptions("shortcuts", options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for keybind prompt
|
||||||
|
const kb = (label_, value_, default_) => { return { value: value_, label: label_, default: default_ }; };
|
||||||
|
|
||||||
|
async function promptKeybind(options, win) {
|
||||||
|
const output = await prompt({
|
||||||
|
title: "Global Keybinds",
|
||||||
|
label: "Choose Global Keybinds for Songs Control:",
|
||||||
|
type: "keybind",
|
||||||
|
keybindOptions: [ // If default=undefined then no default is used
|
||||||
|
kb("Previous", "previous", options.global?.previous),
|
||||||
|
kb("Play / Pause", "playPause", options.global?.playPause),
|
||||||
|
kb("Next", "next", options.global?.next)
|
||||||
|
],
|
||||||
|
height: 270,
|
||||||
|
...promptOptions()
|
||||||
|
}, win);
|
||||||
|
|
||||||
|
if (output) {
|
||||||
|
if (!options.global) {
|
||||||
|
options.global = {};
|
||||||
|
}
|
||||||
|
for (const { value, accelerator } of output) {
|
||||||
|
options.global[value] = accelerator;
|
||||||
|
}
|
||||||
|
setOption(options);
|
||||||
|
}
|
||||||
|
// else -> pressed cancel
|
||||||
|
}
|
||||||
85
plugins/shortcuts/mpris.js
Normal file
85
plugins/shortcuts/mpris.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
const mpris = require("mpris-service");
|
||||||
|
const { ipcMain } = require("electron");
|
||||||
|
const registerCallback = require("../../providers/song-info");
|
||||||
|
const getSongControls = require("../../providers/song-controls");
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerMPRIS(win) {
|
||||||
|
const songControls = getSongControls(win);
|
||||||
|
const { playPause, next, previous } = songControls;
|
||||||
|
try {
|
||||||
|
const secToMicro = n => Math.round(Number(n) * 1e6);
|
||||||
|
const microToSec = n => Math.round(Number(n) / 1e6);
|
||||||
|
|
||||||
|
const seekTo = e => win.webContents.send("seekTo", microToSec(e.position));
|
||||||
|
const seekBy = o => win.webContents.send("seekBy", microToSec(o));
|
||||||
|
|
||||||
|
const player = setupMPRIS();
|
||||||
|
|
||||||
|
ipcMain.on('seeked', (_, t) => player.seeked(secToMicro(t)));
|
||||||
|
|
||||||
|
let currentSeconds = 0;
|
||||||
|
ipcMain.on('timeChanged', (_, t) => currentSeconds = t);
|
||||||
|
|
||||||
|
player.getPosition = () => secToMicro(currentSeconds)
|
||||||
|
|
||||||
|
player.on("raise", () => {
|
||||||
|
win.setSkipTaskbar(false);
|
||||||
|
win.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
player.on("play", () => {
|
||||||
|
if (player.playbackStatus !== 'Playing') {
|
||||||
|
player.playbackStatus = 'Playing';
|
||||||
|
playPause()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
player.on("pause", () => {
|
||||||
|
if (player.playbackStatus !== 'Paused') {
|
||||||
|
player.playbackStatus = 'Paused';
|
||||||
|
playPause()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
player.on("playpause", playPause);
|
||||||
|
player.on("next", next);
|
||||||
|
player.on("previous", previous);
|
||||||
|
|
||||||
|
player.on('seek', seekBy);
|
||||||
|
player.on('position', seekTo);
|
||||||
|
|
||||||
|
registerCallback(songInfo => {
|
||||||
|
if (player) {
|
||||||
|
const data = {
|
||||||
|
'mpris:length': secToMicro(songInfo.songDuration),
|
||||||
|
'mpris:artUrl': songInfo.imageSrc,
|
||||||
|
'xesam:title': songInfo.title,
|
||||||
|
'xesam:artist': songInfo.artist,
|
||||||
|
'mpris:trackid': '/'
|
||||||
|
};
|
||||||
|
if (songInfo.album) data['xesam:album'] = songInfo.album;
|
||||||
|
player.metadata = data;
|
||||||
|
player.seeked(secToMicro(songInfo.elapsedSeconds))
|
||||||
|
player.playbackStatus = songInfo.isPaused ? "Paused" : "Playing"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Error in MPRIS", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = registerMPRIS;
|
||||||
37
plugins/skip-silences/front.js
Normal file
37
plugins/skip-silences/front.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
const hark = require("hark/hark.bundle.js");
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
let isSilent = false;
|
||||||
|
|
||||||
|
document.addEventListener("apiLoaded", (apiEvent) => {
|
||||||
|
const video = document.querySelector("video");
|
||||||
|
const speechEvents = hark(video, {
|
||||||
|
threshold: -100, // dB (-100 = absolute silence, 0 = loudest)
|
||||||
|
interval: 2, // ms
|
||||||
|
});
|
||||||
|
const skipSilence = () => {
|
||||||
|
if (isSilent && !video.paused) {
|
||||||
|
video.currentTime += 0.2; // in s
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
speechEvents.on("speaking", function () {
|
||||||
|
isSilent = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
speechEvents.on("stopped_speaking", function () {
|
||||||
|
if (!(video.paused || video.seeking || video.ended)) {
|
||||||
|
isSilent = true;
|
||||||
|
skipSilence();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
video.addEventListener("play", function () {
|
||||||
|
skipSilence();
|
||||||
|
});
|
||||||
|
|
||||||
|
video.addEventListener("seeked", function () {
|
||||||
|
skipSilence();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,7 +1,8 @@
|
|||||||
const fetch = require("node-fetch");
|
const fetch = require("node-fetch");
|
||||||
|
const is = require("electron-is");
|
||||||
|
const { ipcMain } = require("electron");
|
||||||
|
|
||||||
const defaultConfig = require("../../config/defaults");
|
const defaultConfig = require("../../config/defaults");
|
||||||
const registerCallback = require("../../providers/song-info");
|
|
||||||
const { sortSegments } = require("./segments");
|
const { sortSegments } = require("./segments");
|
||||||
|
|
||||||
let videoID;
|
let videoID;
|
||||||
@ -12,18 +13,14 @@ module.exports = (win, options) => {
|
|||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
||||||
registerCallback(async (info) => {
|
ipcMain.on("video-src-changed", async (_, data) => {
|
||||||
const newURL = info.url || win.webContents.getURL();
|
videoID = JSON.parse(data)?.videoDetails?.videoId;
|
||||||
const newVideoID = new URL(newURL).searchParams.get("v");
|
const segments = await fetchSegments(apiURL, categories);
|
||||||
|
win.webContents.send("sponsorblock-skip", segments);
|
||||||
if (videoID !== newVideoID) {
|
|
||||||
videoID = newVideoID;
|
|
||||||
const segments = await fetchSegments(apiURL, categories);
|
|
||||||
win.webContents.send("sponsorblock-skip", segments);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const fetchSegments = async (apiURL, categories) => {
|
const fetchSegments = async (apiURL, categories) => {
|
||||||
const sponsorBlockURL = `${apiURL}/api/skipSegments?videoID=${videoID}&categories=${JSON.stringify(
|
const sponsorBlockURL = `${apiURL}/api/skipSegments?videoID=${videoID}&categories=${JSON.stringify(
|
||||||
categories
|
categories
|
||||||
@ -45,7 +42,10 @@ const fetchSegments = async (apiURL, categories) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return sortedSegments;
|
return sortedSegments;
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
if (is.dev()) {
|
||||||
|
console.log('error on sponsorblock request:', e);
|
||||||
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,8 +2,6 @@ const { ipcRenderer } = require("electron");
|
|||||||
|
|
||||||
const is = require("electron-is");
|
const is = require("electron-is");
|
||||||
|
|
||||||
const { ontimeupdate } = require("../../providers/video-element");
|
|
||||||
|
|
||||||
let currentSegments = [];
|
let currentSegments = [];
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
@ -11,17 +9,23 @@ module.exports = () => {
|
|||||||
currentSegments = segments;
|
currentSegments = segments;
|
||||||
});
|
});
|
||||||
|
|
||||||
ontimeupdate((videoElement) => {
|
document.addEventListener('apiLoaded', () => {
|
||||||
currentSegments.forEach((segment) => {
|
const video = document.querySelector('video');
|
||||||
if (
|
|
||||||
videoElement.currentTime >= segment[0] &&
|
video.addEventListener('timeupdate', e => {
|
||||||
videoElement.currentTime <= segment[1]
|
currentSegments.forEach((segment) => {
|
||||||
) {
|
if (
|
||||||
videoElement.currentTime = segment[1];
|
e.target.currentTime >= segment[0] &&
|
||||||
if (is.dev()) {
|
e.target.currentTime < segment[1]
|
||||||
console.log("SponsorBlock: skipping segment", segment);
|
) {
|
||||||
|
e.target.currentTime = segment[1];
|
||||||
|
if (is.dev()) {
|
||||||
|
console.log("SponsorBlock: skipping segment", segment);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
})
|
||||||
});
|
// Reset segments on song end
|
||||||
|
video.addEventListener('emptied', () => currentSegments = []);
|
||||||
|
}, { once: true, passive: true })
|
||||||
};
|
};
|
||||||
|
|||||||
52
plugins/tuna-obs/back.js
Normal file
52
plugins/tuna-obs/back.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
const { ipcMain } = require("electron");
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
const registerCallback = require("../../providers/song-info");
|
||||||
|
|
||||||
|
const secToMilisec = t => Math.round(Number(t) * 1e3);
|
||||||
|
const data = {
|
||||||
|
cover_url: '',
|
||||||
|
title: '',
|
||||||
|
artists: [],
|
||||||
|
status: '',
|
||||||
|
progress: 0,
|
||||||
|
duration: 0,
|
||||||
|
album_url: '',
|
||||||
|
album: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
const post = async (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 }) }).catch(e => console.log(`Error: '${e.code || e.errno}' - when trying to access obs-tuna webserver at port ${port}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async (win) => {
|
||||||
|
ipcMain.on('timeChanged', async (_, t) => {
|
||||||
|
if (!data.title) return;
|
||||||
|
data.progress = secToMilisec(t);
|
||||||
|
post(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
registerCallback((songInfo) => {
|
||||||
|
if (!songInfo.title && !songInfo.artist) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.duration = secToMilisec(songInfo.songDuration)
|
||||||
|
data.progress = secToMilisec(songInfo.elapsedSeconds)
|
||||||
|
data.cover_url = songInfo.imageSrc;
|
||||||
|
data.album_url = songInfo.imageSrc;
|
||||||
|
data.title = songInfo.title;
|
||||||
|
data.artists = [songInfo.artist];
|
||||||
|
data.status = songInfo.isPaused ? 'stopped' : 'playing';
|
||||||
|
data.album = songInfo.album;
|
||||||
|
post(data);
|
||||||
|
})
|
||||||
|
}
|
||||||
10
plugins/video-toggle/back.js
Normal file
10
plugins/video-toggle/back.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
const { injectCSS } = require("../utils");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
module.exports = (win, options) => {
|
||||||
|
if (options.forceHide) {
|
||||||
|
injectCSS(win.webContents, path.join(__dirname, "force-hide.css"));
|
||||||
|
} else {
|
||||||
|
injectCSS(win.webContents, path.join(__dirname, "button-switcher.css"));
|
||||||
|
}
|
||||||
|
};
|
||||||
77
plugins/video-toggle/button-switcher.css
Normal file
77
plugins/video-toggle/button-switcher.css
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
#main-panel.ytmusic-player-page {
|
||||||
|
align-items: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-switch-button {
|
||||||
|
z-index: 999;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-left: 10px;
|
||||||
|
background: rgba(33, 33, 33, 0.4);
|
||||||
|
border-radius: 30px;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 240px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: #fff;
|
||||||
|
padding-right: 120px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-switch-button:before {
|
||||||
|
content: "Video";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 120px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-switch-button-checkbox {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-switch-button-label-span {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-switch-button-checkbox:checked+.video-switch-button-label:before {
|
||||||
|
transform: translateX(120px);
|
||||||
|
transition: transform 300ms linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-switch-button-checkbox+.video-switch-button-label {
|
||||||
|
position: relative;
|
||||||
|
padding: 15px 0;
|
||||||
|
display: block;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-switch-button-checkbox+.video-switch-button-label:before {
|
||||||
|
content: "";
|
||||||
|
background: rgba(60, 60, 60, 0.4);
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
border-radius: 30px;
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: transform 300ms;
|
||||||
|
}
|
||||||
116
plugins/video-toggle/front.js
Normal file
116
plugins/video-toggle/front.js
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
const { ElementFromFile, templatePath } = require("../utils");
|
||||||
|
|
||||||
|
const { setOptions } = require("../../config/plugins");
|
||||||
|
|
||||||
|
function $(selector) { return document.querySelector(selector); }
|
||||||
|
|
||||||
|
let options, player, video, api;
|
||||||
|
|
||||||
|
const switchButtonDiv = ElementFromFile(
|
||||||
|
templatePath(__dirname, "button_template.html")
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = (_options) => {
|
||||||
|
if (_options.forceHide) return;
|
||||||
|
options = _options;
|
||||||
|
document.addEventListener('apiLoaded', setup, { once: true, passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup(e) {
|
||||||
|
api = e.detail;
|
||||||
|
player = $('ytmusic-player');
|
||||||
|
video = $('video');
|
||||||
|
|
||||||
|
$('ytmusic-player-page').prepend(switchButtonDiv);
|
||||||
|
|
||||||
|
$('#song-image.ytmusic-player').style.display = "block";
|
||||||
|
|
||||||
|
if (options.hideVideo) {
|
||||||
|
$('.video-switch-button-checkbox').checked = false;
|
||||||
|
changeDisplay(false);
|
||||||
|
forcePlaybackMode();
|
||||||
|
// fix black video
|
||||||
|
video.style.height = "auto";
|
||||||
|
}
|
||||||
|
|
||||||
|
// button checked = show video
|
||||||
|
switchButtonDiv.addEventListener('change', (e) => {
|
||||||
|
options.hideVideo = !e.target.checked;
|
||||||
|
changeDisplay(e.target.checked);
|
||||||
|
setOptions("video-toggle", options);
|
||||||
|
})
|
||||||
|
|
||||||
|
video.addEventListener('srcChanged', videoStarted);
|
||||||
|
|
||||||
|
observeThumbnail();
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeDisplay(showVideo) {
|
||||||
|
player.style.margin = showVideo ? '' : 'auto 0px';
|
||||||
|
player.setAttribute('playback-mode', showVideo ? 'OMV_PREFERRED' : 'ATV_PREFERRED');
|
||||||
|
$('#song-video.ytmusic-player').style.display = showVideo ? 'unset' : 'none';
|
||||||
|
if (showVideo && !video.style.top) {
|
||||||
|
video.style.top = `${(player.clientHeight - video.clientHeight) / 2}px`;
|
||||||
|
}
|
||||||
|
moveVolumeHud(showVideo);
|
||||||
|
}
|
||||||
|
|
||||||
|
function videoStarted() {
|
||||||
|
if (api.getPlayerResponse().videoDetails.musicVideoType !== 'MUSIC_VIDEO_TYPE_ATV') {
|
||||||
|
// switch to high res thumbnail
|
||||||
|
forceThumbnail($('#song-image img'));
|
||||||
|
// show toggle button
|
||||||
|
switchButtonDiv.style.display = "initial";
|
||||||
|
// change display to video mode if video exist & video is hidden & option.hideVideo = false
|
||||||
|
if (!options.hideVideo && $('#song-video.ytmusic-player').style.display === "none") {
|
||||||
|
changeDisplay(true);
|
||||||
|
} else {
|
||||||
|
moveVolumeHud(!options.hideVideo);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// video doesn't exist -> switch to song mode
|
||||||
|
changeDisplay(false);
|
||||||
|
// hide toggle button
|
||||||
|
switchButtonDiv.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// on load, after a delay, the page overrides the playback-mode to 'OMV_PREFERRED' which causes weird aspect ratio in the image container
|
||||||
|
// this function fix the problem by overriding that override :)
|
||||||
|
function forcePlaybackMode() {
|
||||||
|
const playbackModeObserver = new MutationObserver(mutations => {
|
||||||
|
mutations.forEach(mutation => {
|
||||||
|
if (mutation.target.getAttribute('playback-mode') !== "ATV_PREFERRED") {
|
||||||
|
playbackModeObserver.disconnect();
|
||||||
|
mutation.target.setAttribute('playback-mode', "ATV_PREFERRED");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
playbackModeObserver.observe(player, { attributeFilter: ["playback-mode"] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// if precise volume plugin is enabled, move its hud to be on top of the video
|
||||||
|
function moveVolumeHud(showVideo) {
|
||||||
|
const volumeHud = $('#volumeHud');
|
||||||
|
if (volumeHud)
|
||||||
|
volumeHud.style.top = showVideo ? `${(player.clientHeight - video.clientHeight) / 2}px` : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function observeThumbnail() {
|
||||||
|
const playbackModeObserver = new MutationObserver(mutations => {
|
||||||
|
if (!player.videoMode_) return;
|
||||||
|
|
||||||
|
mutations.forEach(mutation => {
|
||||||
|
if (!mutation.target.src.startsWith('data:')) return;
|
||||||
|
forceThumbnail(mutation.target)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
playbackModeObserver.observe($('#song-image img'), { attributeFilter: ["src"] })
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceThumbnail(img) {
|
||||||
|
const thumbnails = $('#movie_player').getPlayerResponse()?.videoDetails?.thumbnail?.thumbnails;
|
||||||
|
if (thumbnails && thumbnails.length > 0) {
|
||||||
|
img.src = thumbnails[thumbnails.length - 1].url.split("?")[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
13
plugins/video-toggle/menu.js
Normal file
13
plugins/video-toggle/menu.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const { setOptions } = require("../../config/plugins");
|
||||||
|
|
||||||
|
module.exports = (win, options) => [
|
||||||
|
{
|
||||||
|
label: "Force Remove Video Tab",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: options.forceHide,
|
||||||
|
click: item => {
|
||||||
|
options.forceHide = item.checked;
|
||||||
|
setOptions("video-toggle", options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
4
plugins/video-toggle/templates/button_template.html
Normal file
4
plugins/video-toggle/templates/button_template.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<div class="video-switch-button">
|
||||||
|
<input class="video-switch-button-checkbox" type="checkbox" checked="true"></input>
|
||||||
|
<label class="video-switch-button-label" for=""><span class="video-switch-button-label-span">Song</span></label>
|
||||||
|
</div>
|
||||||
46
preload.js
46
preload.js
@ -1,15 +1,17 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const { remote } = require("electron");
|
const remote = require('@electron/remote');
|
||||||
|
|
||||||
const config = require("./config");
|
const config = require("./config");
|
||||||
const { fileExists } = require("./plugins/utils");
|
const { fileExists } = require("./plugins/utils");
|
||||||
const setupFrontLogger = require("./providers/front-logger");
|
const setupFrontLogger = require("./providers/front-logger");
|
||||||
const setupSongControl = require("./providers/song-controls-front");
|
|
||||||
const setupSongInfo = require("./providers/song-info-front");
|
const setupSongInfo = require("./providers/song-info-front");
|
||||||
|
const { setupSongControls } = require("./providers/song-controls-front");
|
||||||
|
|
||||||
const plugins = config.plugins.getEnabled();
|
const plugins = config.plugins.getEnabled();
|
||||||
|
|
||||||
|
let api;
|
||||||
|
|
||||||
plugins.forEach(([plugin, options]) => {
|
plugins.forEach(([plugin, options]) => {
|
||||||
const preloadPath = path.join(__dirname, "plugins", plugin, "preload.js");
|
const preloadPath = path.join(__dirname, "plugins", plugin, "preload.js");
|
||||||
fileExists(preloadPath, () => {
|
fileExists(preloadPath, () => {
|
||||||
@ -38,11 +40,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// wait for complete load of youtube api
|
||||||
|
listenForApiLoad();
|
||||||
|
|
||||||
// inject song-info provider
|
// inject song-info provider
|
||||||
setupSongInfo();
|
setupSongInfo();
|
||||||
|
|
||||||
// inject song-control provider
|
// inject song-controls
|
||||||
setupSongControl();
|
setupSongControls();
|
||||||
|
|
||||||
// inject front logger
|
// inject front logger
|
||||||
setupFrontLogger();
|
setupFrontLogger();
|
||||||
@ -50,4 +55,37 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
// Add action for reloading
|
// Add action for reloading
|
||||||
global.reload = () =>
|
global.reload = () =>
|
||||||
remote.getCurrentWindow().webContents.loadURL(config.get("url"));
|
remote.getCurrentWindow().webContents.loadURL(config.get("url"));
|
||||||
|
|
||||||
|
// Blocks the "Are You Still There?" popup by setting the last active time to Date.now every 15min
|
||||||
|
setInterval(() => window._lact = Date.now(), 900000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function listenForApiLoad() {
|
||||||
|
api = document.querySelector('#movie_player');
|
||||||
|
if (api) {
|
||||||
|
onApiLoaded();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
api = document.querySelector('#movie_player');
|
||||||
|
if (api) {
|
||||||
|
observer.disconnect();
|
||||||
|
onApiLoaded();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
observer.observe(document.documentElement, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function onApiLoaded() {
|
||||||
|
document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api }));
|
||||||
|
|
||||||
|
// Remove upgrade button
|
||||||
|
if (config.get("options.removeUpgradeButton")) {
|
||||||
|
const upgradeButtton = document.querySelector('ytmusic-pivot-bar-item-renderer[tab-id="SPunlimited"]')
|
||||||
|
if (upgradeButtton) {
|
||||||
|
upgradeButtton.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,23 +1,4 @@
|
|||||||
let domElements = {};
|
|
||||||
|
|
||||||
const watchDOMElement = (name, selectorFn, cb) => {
|
|
||||||
const observer = new MutationObserver((mutations, observer) => {
|
|
||||||
if (!domElements[name]) {
|
|
||||||
domElements[name] = selectorFn(document);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (domElements[name]) {
|
|
||||||
cb(domElements[name]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSongMenu = () =>
|
const getSongMenu = () =>
|
||||||
document.querySelector("ytmusic-menu-popup-renderer tp-yt-paper-listbox");
|
document.querySelector("ytmusic-menu-popup-renderer tp-yt-paper-listbox");
|
||||||
|
|
||||||
module.exports = { getSongMenu, watchDOMElement };
|
module.exports = { getSongMenu };
|
||||||
|
|||||||
14
providers/prompt-custom-titlebar.js
Normal file
14
providers/prompt-custom-titlebar.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
const customTitlebar = require("custom-electron-titlebar");
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
new customTitlebar.Titlebar({
|
||||||
|
backgroundColor: customTitlebar.Color.fromHex("#050505"),
|
||||||
|
minimizable: false,
|
||||||
|
maximizable: false,
|
||||||
|
menu: null
|
||||||
|
});
|
||||||
|
const mainStyle = document.querySelector("#container").style;
|
||||||
|
mainStyle.width = "100%";
|
||||||
|
mainStyle.position = "fixed";
|
||||||
|
mainStyle.border = "unset";
|
||||||
|
};
|
||||||
18
providers/prompt-options.js
Normal file
18
providers/prompt-options.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const is = require("electron-is");
|
||||||
|
|
||||||
|
const iconPath = path.join(__dirname, "..", "assets", "youtube-music-tray.png");
|
||||||
|
const customTitlebarPath = path.join(__dirname, "prompt-custom-titlebar.js");
|
||||||
|
|
||||||
|
const promptOptions = is.macOS() ? {
|
||||||
|
customStylesheet: "dark",
|
||||||
|
icon: iconPath
|
||||||
|
} : {
|
||||||
|
customStylesheet: "dark",
|
||||||
|
// The following are used for custom titlebar
|
||||||
|
frame: false,
|
||||||
|
customScript: customTitlebarPath,
|
||||||
|
enableRemoteModule: true
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = () => promptOptions;
|
||||||
@ -1,18 +1,13 @@
|
|||||||
const { ipcRenderer } = require("electron");
|
const { ipcRenderer } = require("electron");
|
||||||
|
const config = require("../config");
|
||||||
|
const is = require("electron-is");
|
||||||
|
|
||||||
let videoStream = document.querySelector(".video-stream");
|
module.exports.setupSongControls = () => {
|
||||||
module.exports = () => {
|
document.addEventListener('apiLoaded', e => {
|
||||||
ipcRenderer.on("playPause", () => {
|
ipcRenderer.on("seekTo", (_, t) => e.detail.seekTo(t));
|
||||||
if (!videoStream) {
|
ipcRenderer.on("seekBy", (_, t) => e.detail.seekBy(t));
|
||||||
videoStream = document.querySelector(".video-stream");
|
if (is.linux() && config.plugins.isEnabled('shortcuts')) { // MPRIS Enabled
|
||||||
|
document.querySelector('video').addEventListener('seeked', v => ipcRenderer.send('seeked', v.target.currentTime));
|
||||||
}
|
}
|
||||||
|
}, { once: true, passive: true })
|
||||||
if (videoStream.paused) {
|
|
||||||
videoStream.play();
|
|
||||||
} else {
|
|
||||||
videoStream.yns_pause ?
|
|
||||||
videoStream.yns_pause() :
|
|
||||||
videoStream.pause();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,9 +12,9 @@ module.exports = (win) => {
|
|||||||
// Playback
|
// Playback
|
||||||
previous: () => pressKey(win, "k"),
|
previous: () => pressKey(win, "k"),
|
||||||
next: () => pressKey(win, "j"),
|
next: () => pressKey(win, "j"),
|
||||||
playPause: () => win.webContents.send("playPause"),
|
playPause: () => pressKey(win, "space"),
|
||||||
like: () => pressKey(win, "_"),
|
like: () => pressKey(win, "+"),
|
||||||
dislike: () => pressKey(win, "+"),
|
dislike: () => pressKey(win, "_"),
|
||||||
go10sBack: () => pressKey(win, "h"),
|
go10sBack: () => pressKey(win, "h"),
|
||||||
go10sForward: () => pressKey(win, "l"),
|
go10sForward: () => pressKey(win, "l"),
|
||||||
go1sBack: () => pressKey(win, "h", ["shift"]),
|
go1sBack: () => pressKey(win, "h", ["shift"]),
|
||||||
@ -24,8 +24,6 @@ module.exports = (win) => {
|
|||||||
// General
|
// General
|
||||||
volumeMinus10: () => pressKey(win, "-"),
|
volumeMinus10: () => pressKey(win, "-"),
|
||||||
volumePlus10: () => pressKey(win, "="),
|
volumePlus10: () => pressKey(win, "="),
|
||||||
dislikeAndNext: () => pressKey(win, "-", ["shift"]),
|
|
||||||
like: () => pressKey(win, "=", ["shift"]),
|
|
||||||
fullscreen: () => pressKey(win, "f"),
|
fullscreen: () => pressKey(win, "f"),
|
||||||
muteUnmute: () => pressKey(win, "m"),
|
muteUnmute: () => pressKey(win, "m"),
|
||||||
maximizeMinimisePlayer: () => pressKey(win, "q"),
|
maximizeMinimisePlayer: () => pressKey(win, "q"),
|
||||||
@ -38,14 +36,14 @@ module.exports = (win) => {
|
|||||||
pressKey(win, "g");
|
pressKey(win, "g");
|
||||||
pressKey(win, "l");
|
pressKey(win, "l");
|
||||||
},
|
},
|
||||||
goToHotlist: () => {
|
|
||||||
pressKey(win, "g");
|
|
||||||
pressKey(win, "t");
|
|
||||||
},
|
|
||||||
goToSettings: () => {
|
goToSettings: () => {
|
||||||
pressKey(win, "g");
|
pressKey(win, "g");
|
||||||
pressKey(win, ",");
|
pressKey(win, ",");
|
||||||
},
|
},
|
||||||
|
goToExplore: () => {
|
||||||
|
pressKey(win, "g");
|
||||||
|
pressKey(win, "e");
|
||||||
|
},
|
||||||
search: () => pressKey(win, "/"),
|
search: () => pressKey(win, "/"),
|
||||||
showShortcuts: () => pressKey(win, "/", ["shift"]),
|
showShortcuts: () => pressKey(win, "/", ["shift"]),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,31 +1,60 @@
|
|||||||
const { ipcRenderer } = require("electron");
|
const { ipcRenderer } = require("electron");
|
||||||
|
const is = require('electron-is');
|
||||||
const { getImage } = require("./song-info");
|
const { getImage } = require("./song-info");
|
||||||
|
|
||||||
|
const config = require("../config");
|
||||||
|
|
||||||
global.songInfo = {};
|
global.songInfo = {};
|
||||||
|
|
||||||
|
function $(selector) { return document.querySelector(selector); }
|
||||||
|
|
||||||
ipcRenderer.on("update-song-info", async (_, extractedSongInfo) => {
|
ipcRenderer.on("update-song-info", async (_, extractedSongInfo) => {
|
||||||
global.songInfo = JSON.parse(extractedSongInfo);
|
global.songInfo = JSON.parse(extractedSongInfo);
|
||||||
global.songInfo.image = await getImage(global.songInfo.imageSrc);
|
global.songInfo.image = await getImage(global.songInfo.imageSrc);
|
||||||
});
|
});
|
||||||
|
|
||||||
const injectListener = () => {
|
// used because 'loadeddata' or 'loadedmetadata' weren't firing on song start for some users (https://github.com/th-ch/youtube-music/issues/473)
|
||||||
const oldXHR = window.XMLHttpRequest;
|
const srcChangedEvent = new CustomEvent('srcChanged');
|
||||||
function newXHR() {
|
|
||||||
const realXHR = new oldXHR();
|
module.exports = () => {
|
||||||
realXHR.addEventListener(
|
document.addEventListener('apiLoaded', apiEvent => {
|
||||||
"readystatechange",
|
if (config.plugins.isEnabled('tuna-obs') ||
|
||||||
() => {
|
(is.linux() && config.plugins.isEnabled('shortcuts'))) {
|
||||||
if (realXHR.readyState === 4 && realXHR.status === 200
|
setupTimeChangeListener();
|
||||||
&& realXHR.responseURL.includes("/player")) {
|
}
|
||||||
// if the request contains the song info, send the response to ipcMain
|
const video = $('video');
|
||||||
ipcRenderer.send("song-info-request", realXHR.responseText);
|
// name = "dataloaded" and abit later "dataupdated"
|
||||||
|
apiEvent.detail.addEventListener('videodatachange', (name, _dataEvent) => {
|
||||||
|
if (name !== 'dataloaded') return;
|
||||||
|
video.dispatchEvent(srcChangedEvent);
|
||||||
|
sendSongInfo();
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const status of ['playing', 'pause']) {
|
||||||
|
video.addEventListener(status, e => {
|
||||||
|
if (Math.round(e.target.currentTime) > 0) {
|
||||||
|
ipcRenderer.send("playPaused", {
|
||||||
|
isPaused: status === 'pause',
|
||||||
|
elapsedSeconds: Math.floor(e.target.currentTime)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
false
|
}
|
||||||
);
|
|
||||||
return realXHR;
|
function sendSongInfo() {
|
||||||
}
|
const data = apiEvent.detail.getPlayerResponse();
|
||||||
window.XMLHttpRequest = newXHR;
|
data.videoDetails.album = $('ytmusic-player-page')?.__data?.playerPageWatchMetadata?.albumName?.runs[0].text
|
||||||
|
data.videoDetails.elapsedSeconds = Math.floor(video.currentTime);
|
||||||
|
data.videoDetails.isPaused = false;
|
||||||
|
ipcRenderer.send("video-src-changed", JSON.stringify(data));
|
||||||
|
}
|
||||||
|
}, { once: true, passive: true });
|
||||||
};
|
};
|
||||||
module.exports = injectListener;
|
|
||||||
|
function setupTimeChangeListener() {
|
||||||
|
const progressObserver = new MutationObserver(mutations => {
|
||||||
|
ipcRenderer.send('timeChanged', mutations[0].target.value);
|
||||||
|
global.songInfo.elapsedSeconds = mutations[0].target.value;
|
||||||
|
});
|
||||||
|
progressObserver.observe($('#progress-bar'), { attributeFilter: ["value"] })
|
||||||
|
}
|
||||||
|
|||||||
@ -2,44 +2,12 @@ const { ipcMain, nativeImage } = require("electron");
|
|||||||
|
|
||||||
const fetch = require("node-fetch");
|
const fetch = require("node-fetch");
|
||||||
|
|
||||||
// This selects the progress bar, used for current progress
|
const config = require("../config");
|
||||||
const progressSelector = "#progress-bar";
|
|
||||||
|
|
||||||
|
|
||||||
// Grab the progress using the selector
|
|
||||||
const getProgress = async (win) => {
|
|
||||||
// Get current value of the progressbar element
|
|
||||||
return win.webContents.executeJavaScript(
|
|
||||||
'document.querySelector("' + progressSelector + '").value'
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Grab the native image using the src
|
|
||||||
const getImage = async (src) => {
|
|
||||||
const result = await fetch(src);
|
|
||||||
const buffer = await result.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 `-`
|
|
||||||
const getPausedStatus = async (win) => {
|
|
||||||
const title = await win.webContents.executeJavaScript("document.title");
|
|
||||||
return !title.includes("-");
|
|
||||||
};
|
|
||||||
|
|
||||||
const getArtist = async (win) => {
|
|
||||||
return win.webContents.executeJavaScript(`
|
|
||||||
document.querySelector(".subtitle.ytmusic-player-bar .yt-formatted-string")
|
|
||||||
?.textContent
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill songInfo with empty values
|
// Fill songInfo with empty values
|
||||||
|
/**
|
||||||
|
* @typedef {songInfo} SongInfo
|
||||||
|
*/
|
||||||
const songInfo = {
|
const songInfo = {
|
||||||
title: "",
|
title: "",
|
||||||
artist: "",
|
artist: "",
|
||||||
@ -51,67 +19,111 @@ const songInfo = {
|
|||||||
songDuration: 0,
|
songDuration: 0,
|
||||||
elapsedSeconds: 0,
|
elapsedSeconds: 0,
|
||||||
url: "",
|
url: "",
|
||||||
|
album: undefined,
|
||||||
|
videoId: "",
|
||||||
|
playlistId: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Grab the native image using the src
|
||||||
|
const getImage = async (src) => {
|
||||||
|
const result = await fetch(src);
|
||||||
|
const buffer = await result.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;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleData = async (responseText, win) => {
|
const handleData = async (responseText, win) => {
|
||||||
let data = JSON.parse(responseText);
|
const data = JSON.parse(responseText);
|
||||||
songInfo.title = data?.videoDetails?.title;
|
if (!data) return;
|
||||||
songInfo.artist = await getArtist(win) || cleanupArtistName(data?.videoDetails?.author);
|
|
||||||
songInfo.views = data?.videoDetails?.viewCount;
|
|
||||||
songInfo.imageSrc = data?.videoDetails?.thumbnail?.thumbnails?.pop()?.url;
|
|
||||||
songInfo.songDuration = data?.videoDetails?.lengthSeconds;
|
|
||||||
songInfo.image = await getImage(songInfo.imageSrc);
|
|
||||||
songInfo.uploadDate = data?.microformat?.microformatDataRenderer?.uploadDate;
|
|
||||||
songInfo.url = data?.microformat?.microformatDataRenderer?.urlCanonical;
|
|
||||||
|
|
||||||
win.webContents.send("update-song-info", JSON.stringify(songInfo));
|
const microformat = data.microformat?.microformatDataRenderer;
|
||||||
|
if (microformat) {
|
||||||
|
songInfo.uploadDate = microformat.uploadDate;
|
||||||
|
songInfo.url = microformat.urlCanonical?.split("&")[0];
|
||||||
|
songInfo.playlistId = new URL(microformat.urlCanonical).searchParams.get("list");
|
||||||
|
// used for options.resumeOnStart
|
||||||
|
config.set("url", microformat.urlCanonical);
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoDetails = data.videoDetails;
|
||||||
|
if (videoDetails) {
|
||||||
|
songInfo.title = cleanupName(videoDetails.title);
|
||||||
|
songInfo.artist = cleanupName(videoDetails.author);
|
||||||
|
songInfo.views = videoDetails.viewCount;
|
||||||
|
songInfo.songDuration = videoDetails.lengthSeconds;
|
||||||
|
songInfo.elapsedSeconds = videoDetails.elapsedSeconds;
|
||||||
|
songInfo.isPaused = videoDetails.isPaused;
|
||||||
|
songInfo.videoId = videoDetails.videoId;
|
||||||
|
songInfo.album = data?.videoDetails?.album; // Will be undefined if video exist
|
||||||
|
|
||||||
|
const oldUrl = songInfo.imageSrc;
|
||||||
|
songInfo.imageSrc = videoDetails.thumbnail?.thumbnails?.pop()?.url.split("?")[0];
|
||||||
|
if (oldUrl !== songInfo.imageSrc) {
|
||||||
|
songInfo.image = await getImage(songInfo.imageSrc);
|
||||||
|
}
|
||||||
|
|
||||||
|
win.webContents.send("update-song-info", JSON.stringify(songInfo));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// This variable will be filled with the callbacks once they register
|
// This variable will be filled with the callbacks once they register
|
||||||
const callbacks = [];
|
const callbacks = [];
|
||||||
|
|
||||||
// This function will allow plugins to register callback that will be triggered when data changes
|
// This function will allow plugins to register callback that will be triggered when data changes
|
||||||
|
/**
|
||||||
|
* @callback songInfoCallback
|
||||||
|
* @param {songInfo} songInfo
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @param {songInfoCallback} callback
|
||||||
|
*/
|
||||||
const registerCallback = (callback) => {
|
const registerCallback = (callback) => {
|
||||||
callbacks.push(callback);
|
callbacks.push(callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
const registerProvider = (win) => {
|
const registerProvider = (win) => {
|
||||||
win.on("page-title-updated", async () => {
|
|
||||||
// Get and set the new data
|
|
||||||
songInfo.isPaused = await getPausedStatus(win);
|
|
||||||
|
|
||||||
const elapsedSeconds = await getProgress(win);
|
|
||||||
songInfo.elapsedSeconds = elapsedSeconds;
|
|
||||||
|
|
||||||
// Trigger the callbacks
|
|
||||||
callbacks.forEach((c) => {
|
|
||||||
c(songInfo);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// This will be called when the song-info-front finds a new request with song data
|
// This will be called when the song-info-front finds a new request with song data
|
||||||
ipcMain.on("song-info-request", async (_, responseText) => {
|
ipcMain.on("video-src-changed", async (_, responseText) => {
|
||||||
await handleData(responseText, win);
|
await handleData(responseText, win);
|
||||||
callbacks.forEach((c) => {
|
callbacks.forEach((c) => {
|
||||||
c(songInfo);
|
c(songInfo);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
ipcMain.on("playPaused", (_, { isPaused, elapsedSeconds }) => {
|
||||||
|
songInfo.isPaused = isPaused;
|
||||||
|
songInfo.elapsedSeconds = elapsedSeconds;
|
||||||
|
callbacks.forEach((c) => {
|
||||||
|
c(songInfo);
|
||||||
|
});
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
const suffixesToRemove = [' - Topic', 'VEVO'];
|
const suffixesToRemove = [
|
||||||
function cleanupArtistName(artist) {
|
" - topic",
|
||||||
if (!artist) {
|
"vevo",
|
||||||
return artist;
|
" (performance video)",
|
||||||
}
|
" (official music video)",
|
||||||
|
" (official video)",
|
||||||
|
" (clip officiel)",
|
||||||
|
];
|
||||||
|
|
||||||
|
function cleanupName(name) {
|
||||||
|
if (!name) return name;
|
||||||
|
const lowCaseName = name.toLowerCase();
|
||||||
for (const suffix of suffixesToRemove) {
|
for (const suffix of suffixesToRemove) {
|
||||||
if (artist.endsWith(suffix)) {
|
if (lowCaseName.endsWith(suffix)) {
|
||||||
return artist.slice(0, -suffix.length);
|
return name.slice(0, -suffix.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return artist;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = registerCallback;
|
module.exports = registerCallback;
|
||||||
module.exports.setupSongInfo = registerProvider;
|
module.exports.setupSongInfo = registerProvider;
|
||||||
module.exports.getImage = getImage;
|
module.exports.getImage = getImage;
|
||||||
module.exports.cleanupArtistName = cleanupArtistName;
|
module.exports.cleanupName = cleanupName;
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
30
readme.md
30
readme.md
@ -34,16 +34,32 @@ 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)
|
- **Audio compressor**: apply compression to audio (lowers the volume of the loudest parts of the signal and raises the volume of the softest parts)
|
||||||
- **No Google Login**: remove Google login buttons and links from the interface
|
- **Blur navigation bar**: makes navigation bar transparent and blurry
|
||||||
- **Shortcuts**: use your usual shortcuts (media keys, Ctrl/CMD + F…) to control YouTube Music
|
- **Disable autoplay**: makes every song start in "paused" mode
|
||||||
|
- [**Discord**](https://discord.com/): show your friends what you listen to with [Rich Presence](https://user-images.githubusercontent.com/28219076/104362104-a7a0b980-5513-11eb-9744-bb89eabe0016.png)
|
||||||
|
- **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)
|
||||||
|
- **In-app menu**: [gives bars a fancy, dark look](https://user-images.githubusercontent.com/78568641/112215894-923dbf00-8c29-11eb-95c3-3ce15db27eca.png)
|
||||||
|
> (see [this post](https://github.com/th-ch/youtube-music/issues/410#issuecomment-952060709) if you have problem accessing the menu after enabling this plugin and hide-menu option)
|
||||||
|
- [**Last.fm**](https://www.last.fm/): scrobbles support
|
||||||
- **Navigation**: next/back navigation arrows directly integrated in the interface, like in your favorite browser
|
- **Navigation**: next/back navigation arrows directly integrated in the interface, like in your favorite browser
|
||||||
- **Auto confirm when paused**: when the "Continue Watching?" modal appears, automatically click "Yes"
|
- **No Google Login**: remove Google login buttons and links from the interface
|
||||||
- **Hide video player**: no video in the interface when playing music
|
- **Notifications**: display a notification when a song starts playing ([interactive notifications](https://user-images.githubusercontent.com/78568641/114102651-63ce0e00-98d0-11eb-9dfe-c5a02bb54f9c.png) are available on windows)
|
||||||
- **Notifications**: display a notification 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
|
||||||
|
- **Quality changer**: Allows changing the video quality with a [button](https://user-images.githubusercontent.com/78568641/138574366-70324a5e-2d64-4f6a-acdd-dc2a2b9cecc5.png) on the video overlay
|
||||||
|
- **Shortcuts**: Allows setting global hotkeys for playback (play/pause/next/previous) + disable [media osd](https://user-images.githubusercontent.com/84923831/128601225-afa38c1f-dea8-4209-9f72-0f84c1dd8b54.png) by overriding media keys + enable Ctrl/CMD + F to search + enable linux mpris support for mediakeys + [custom hotkeys](https://github.com/Araxeus/youtube-music/blob/1e591d6a3df98449bcda6e63baab249b28026148/providers/song-controls.js#L13-L50) for [advanced users](https://github.com/th-ch/youtube-music/issues/106#issuecomment-952156902)
|
||||||
|
- [**SponsorBlock**](https://github.com/ajayyy/SponsorBlock): skips non-music parts
|
||||||
|
- **Taskbar media control**: control playback 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
|
||||||
|
- **Video Toggle**: Adds a button to switch between Video/Song mode. can also optionally remove the whole video tab
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- **Auto confirm when paused** (Always Enabled): disable the ["Continue Watching?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png) popup that pause music after a certain time
|
||||||
|
|
||||||
|
> If using `Hide Menu` option - you can show the menu with the `alt` key (or `escape` if using the in-app-menu plugin)
|
||||||
|
|
||||||
## Dev
|
## Dev
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const getPort = require("get-port");
|
|
||||||
const NodeEnvironment = require("jest-environment-node");
|
const NodeEnvironment = require("jest-environment-node");
|
||||||
const electronPath = require("electron");
|
const { _electron: electron } = require("playwright");
|
||||||
const { Application } = require("spectron");
|
|
||||||
|
|
||||||
class TestEnvironment extends NodeEnvironment {
|
class TestEnvironment extends NodeEnvironment {
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
@ -14,21 +12,12 @@ class TestEnvironment extends NodeEnvironment {
|
|||||||
await super.setup();
|
await super.setup();
|
||||||
|
|
||||||
const appPath = path.resolve(__dirname, "..");
|
const appPath = path.resolve(__dirname, "..");
|
||||||
const port = await getPort();
|
this.global.__APP__ = await electron.launch({ args: [appPath] });
|
||||||
|
|
||||||
this.global.__APP__ = new Application({
|
|
||||||
path: electronPath,
|
|
||||||
args: [appPath],
|
|
||||||
port,
|
|
||||||
});
|
|
||||||
await this.global.__APP__.start();
|
|
||||||
const { client } = this.global.__APP__;
|
|
||||||
await client.waitUntilWindowLoaded();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async teardown() {
|
async teardown() {
|
||||||
if (this.global.__APP__.isRunning()) {
|
if (this.global.__APP__) {
|
||||||
await this.global.__APP__.stop();
|
await this.global.__APP__.close();
|
||||||
}
|
}
|
||||||
await super.teardown();
|
await super.teardown();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,22 +6,11 @@ describe("YouTube Music App", () => {
|
|||||||
const app = global.__APP__;
|
const app = global.__APP__;
|
||||||
|
|
||||||
test("With default settings, app is launched and visible", async () => {
|
test("With default settings, app is launched and visible", async () => {
|
||||||
expect(app.isRunning()).toBe(true);
|
const window = await app.firstWindow();
|
||||||
|
const title = await window.title();
|
||||||
const win = app.browserWindow;
|
|
||||||
|
|
||||||
const isMenuVisible = await win.isMenuBarVisible();
|
|
||||||
expect(isMenuVisible).toBe(true);
|
|
||||||
|
|
||||||
const isVisible = await win.isVisible();
|
|
||||||
expect(isVisible).toBe(true);
|
|
||||||
|
|
||||||
const { width, height } = await win.getBounds();
|
|
||||||
expect(width).toBeGreaterThan(0);
|
|
||||||
expect(height).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
const { client } = app;
|
|
||||||
const title = await client.getTitle();
|
|
||||||
expect(title).toEqual("YouTube Music");
|
expect(title).toEqual("YouTube Music");
|
||||||
|
|
||||||
|
const url = window.url();
|
||||||
|
expect(url.startsWith("https://music.youtube.com")).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -28,3 +28,14 @@ ytmusic-search-box.ytmusic-nav-bar {
|
|||||||
ytmusic-mealbar-promo-renderer {
|
ytmusic-mealbar-promo-renderer {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Disable Image Selection */
|
||||||
|
img {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide cast button which doesn't work */
|
||||||
|
ytmusic-cast-button {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user