Compare commits

...

73 Commits

Author SHA1 Message Date
TC
b843825f72 Bump version to 1.18.0 2022-09-05 08:48:06 +02:00
b66d3bc3d4 Merge pull request #816 from th-ch/fix-downloader
Bump ytdl-core (bug fix)
2022-09-05 08:45:22 +02:00
TC
9adabd41d9 Bump ytdl-core (bug fix) 2022-09-05 00:43:58 +02:00
TC
3f3df09819 Set NODE_ENV programmatically 2022-09-05 00:38:24 +02:00
TC
1f5f597561 Prepare migration for sandboxing (path.join in preload) 2022-09-05 00:31:29 +02:00
TC
91e4433aba Hide login button in plugin 2022-09-04 23:56:41 +02:00
2d3ce4a8b3 Merge pull request #813 from th-ch/fix-tests
Bump electron and fix tests in CI
2022-09-04 23:36:19 +02:00
TC
971b7f05c5 Bump electron to latest version 2022-09-04 22:32:16 +02:00
TC
bb6115fec1 Remove jest 2022-09-04 22:24:09 +02:00
TC
2a6dc30366 Remove test environment 2022-09-04 22:24:09 +02:00
TC
5e2d843742 Migrate to playwright/test 2022-09-04 22:24:09 +02:00
TC
7aaef26cc8 Add electron flags in tests 2022-09-04 22:21:51 +02:00
TC
0a08eaaa3c Skip downloading browsers in playwright 2022-09-04 22:21:43 +02:00
TC
0d22446f20 Bump playwright and electron 2022-08-25 23:03:38 +02:00
a0543d15a6 Merge pull request #800 from th-ch/custom-css-file
Allow user to pass custom CSS file
2022-08-25 22:52:42 +02:00
TC
e62ee35b42 Rename cssFiles option to themes and add menu entry 2022-08-25 22:50:33 +02:00
TC
ef6fb402bf Allow user to pass custom CSS file 2022-08-21 23:36:02 +02:00
a8301f44be Merge pull request #799 from th-ch/snyk-upgrade-260cfeaaaf6aa5359f78e9f589e807a0
[Snyk] Upgrade html-to-text from 8.2.0 to 8.2.1
2022-08-21 22:54:52 +02:00
1ead86a220 Merge pull request #772 from th-ch/snyk-upgrade-3ee19fd614169422c21264d4fe016fa1
[Snyk] Upgrade electron-store from 8.0.1 to 8.0.2
2022-08-21 22:54:14 +02:00
03e716fe17 Merge pull request #756 from th-ch/dependabot/npm_and_yarn/jpeg-js-0.4.4
Bump jpeg-js from 0.4.3 to 0.4.4
2022-08-21 22:53:54 +02:00
f0bb328981 Merge pull request #749 from foonathan/master
Support MPRIS loop and volume change
2022-08-21 22:53:29 +02:00
f40183f0ca fix: upgrade html-to-text from 8.2.0 to 8.2.1
Snyk has created this PR to upgrade html-to-text from 8.2.0 to 8.2.1.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-08-20 22:45:27 +00:00
5b004acdc1 Update readme.md 2022-08-12 20:19:49 +02:00
0f96da9928 Change volume observer 2022-07-14 20:59:35 +02:00
dfba3d9c2d Support precise volume changes in MPRIS when possible 2022-07-11 20:20:13 +02:00
d9c51063f4 Add MPRIS volume control
Fixes #776.
2022-07-11 19:55:16 +02:00
cd9012691a fix: upgrade electron-store from 8.0.1 to 8.0.2
Snyk has created this PR to upgrade electron-store from 8.0.1 to 8.0.2.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-07-01 19:06:53 +00:00
2499f574ef Use triple equals in more places 2022-06-26 19:43:03 +02:00
e7e873866d Use triple equals 2022-06-26 17:37:25 +02:00
4ccbc741b8 Merge pull request #742 from th-ch/snyk-upgrade-45c0c305b24fe7d1bf9286a6c90b0320
[Snyk] Upgrade @cliqz/adblocker-electron from 1.23.7 to 1.23.8
2022-06-25 21:23:52 +02:00
8ec965a1a4 Merge pull request #745 from lukaszg84/master
Use ; instead of space for play/pause.
2022-06-25 21:22:50 +02:00
0936e9a258 Merge pull request #750 from EsmailELBoBDev2/patch-1
Update readme.md
2022-06-25 21:20:10 +02:00
32a5597573 Merge pull request #753 from Araxeus/fix-lyrics-font-size
fix lyrics font size
2022-06-25 21:18:44 +02:00
9932fd7647 Fix mpris playback status on play/pause 2022-06-22 18:30:49 +02:00
68429be1ce Bump jpeg-js from 0.4.3 to 0.4.4
Bumps [jpeg-js](https://github.com/eugeneware/jpeg-js) from 0.4.3 to 0.4.4.
- [Release notes](https://github.com/eugeneware/jpeg-js/releases)
- [Commits](https://github.com/eugeneware/jpeg-js/compare/v0.4.3...v0.4.4)

---
updated-dependencies:
- dependency-name: jpeg-js
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-17 01:59:01 +00:00
d7ac493337 fix lyrics font size 2022-06-16 17:21:13 +03:00
686a0a340e Update readme.md 2022-06-14 20:37:32 +02:00
bba499044b Style changes for review 2022-06-14 19:43:36 +02:00
6e1c50ede1 Support MPRIS loop 2022-06-14 15:45:58 +02:00
6e739e2723 Use ; instead of space for play/pause. 2022-06-10 11:08:36 +02:00
86029a0a73 fix: upgrade @cliqz/adblocker-electron from 1.23.7 to 1.23.8
Snyk has created this PR to upgrade @cliqz/adblocker-electron from 1.23.7 to 1.23.8.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-06-07 19:11:28 +00:00
b458925aa6 Merge pull request #734 from Araxeus/fix-in-app-menu-top-gap
fix top gap between nav-bar and browse-page
2022-06-06 20:51:11 +02:00
86a1c3c850 Merge pull request #605 from Araxeus/migrate-from-remote-to-ipc
migrate from remote to ipc + fix restart in portable app
2022-06-06 20:48:59 +02:00
8666f934cd Merge pull request #717 from th-ch/snyk-upgrade-7b576b920d24e12003dead59064f8564
[Snyk] Upgrade custom-electron-prompt from 1.4.2 to 1.5.0
2022-06-06 20:40:40 +02:00
59a93916a8 Merge pull request #685 from Araxeus/pip-part-2
Picture in Picture v2
2022-06-06 20:36:18 +02:00
f06a3c8c70 fix top gap between nav-bar and browse-page 2022-05-28 19:22:14 +03:00
7fe937b21e lint 2022-05-20 19:26:55 +03:00
a4aa22aae9 update Electron 2022-05-18 20:21:37 +03:00
54d25a26c7 fix: upgrade custom-electron-prompt from 1.4.2 to 1.5.0
Snyk has created this PR to upgrade custom-electron-prompt from 1.4.2 to 1.5.0.

See this package in npm:


See this project in Snyk:
https://app.snyk.io/org/th-ch/project/81809c53-bb7b-46b9-a0d7-806d45d74ac6?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-05-12 19:29:13 +00:00
f5622970c6 Merge branch 'master' into migrate-from-remote-to-ipc 2022-05-01 23:09:13 +03:00
ea09825ece Merge branch 'master' into pip-part-2 2022-05-01 23:06:30 +03:00
7bd69e447a update Electron 2022-04-30 15:24:37 +03:00
f6de5c7c22 PiP options defaults + migrations 2022-04-20 15:21:10 +03:00
77d4e9cb84 add optional PiP hotkey 2022-04-15 15:57:48 +03:00
b420998458 disable the video-toggle button when in PiP mode 2022-04-15 15:27:31 +03:00
feb06b015e dont save maximized state in PiP mode 2022-04-15 15:27:16 +03:00
0f192aab2b fix precise-volume position in PiP 2022-04-13 17:17:25 +03:00
30840804fa fix: sync pip and index.js options 2022-04-11 00:15:19 +03:00
d23bfe9368 v3 2022-04-10 21:16:43 +03:00
768ec7bda7 v2 2022-04-10 19:19:20 +03:00
c25a6f9d2a launch pip from video overlay v1 2022-04-10 01:12:06 +03:00
cb910a6fd7 Merge branch 'master' into migrate-from-remote-to-ipc 2022-04-09 17:04:06 +03:00
28b5645a56 Merge branch 'master' into migrate-from-remote-to-ipc 2022-04-08 17:00:49 +03:00
f4df6fceee Merge branch 'migrate-from-remote-to-ipc' of https://github.com/Araxeus/youtube-music into migrate-from-remote-to-ipc 2022-04-07 22:12:54 +03:00
d69c8a754e Merge branch 'local-upstream/master' into migrate-from-remote-to-ipc 2022-04-07 22:12:25 +03:00
c3d90d8b27 update Electron
+ update electron-unhandled
2022-04-07 22:09:54 +03:00
bed8d0a7f2 update Electron
+ update electron-unhandled
2022-03-30 18:47:51 +03:00
704fba9aba fix portable app restart 2022-02-24 20:52:16 +02:00
407887254f Merge branch 'master' into migrate-from-remote-to-ipc 2022-02-23 17:34:24 +02:00
7088941179 update electron 2022-02-22 18:27:45 +02:00
1eeaf1dd0a remove @electron/remote 2022-02-20 19:38:42 +02:00
9abf7a77d8 Merge branch 'local-upstream/master' into migrate-from-remote-to-ipc 2022-02-20 19:34:18 +02:00
5bd97685b9 migrate from remote to ipc 2022-02-13 23:45:53 +02:00
33 changed files with 779 additions and 2244 deletions

View File

@ -91,6 +91,8 @@ jobs:
- name: Test - name: Test
uses: GabrielBB/xvfb-action@v1 uses: GabrielBB/xvfb-action@v1
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
with: with:
run: yarn test run: yarn test

View File

@ -2,8 +2,32 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC. All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [v1.18.0](https://github.com/th-ch/youtube-music/compare/v1.17.0...v1.18.0)
- Bump ytdl-core (bug fix) [`#816`](https://github.com/th-ch/youtube-music/pull/816)
- Bump electron and fix tests in CI [`#813`](https://github.com/th-ch/youtube-music/pull/813)
- Allow user to pass custom CSS file [`#800`](https://github.com/th-ch/youtube-music/pull/800)
- [Snyk] Upgrade html-to-text from 8.2.0 to 8.2.1 [`#799`](https://github.com/th-ch/youtube-music/pull/799)
- [Snyk] Upgrade electron-store from 8.0.1 to 8.0.2 [`#772`](https://github.com/th-ch/youtube-music/pull/772)
- Bump jpeg-js from 0.4.3 to 0.4.4 [`#756`](https://github.com/th-ch/youtube-music/pull/756)
- Support MPRIS loop and volume change [`#749`](https://github.com/th-ch/youtube-music/pull/749)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.23.7 to 1.23.8 [`#742`](https://github.com/th-ch/youtube-music/pull/742)
- Use ; instead of space for play/pause. [`#745`](https://github.com/th-ch/youtube-music/pull/745)
- Update readme.md [`#750`](https://github.com/th-ch/youtube-music/pull/750)
- fix lyrics font size [`#753`](https://github.com/th-ch/youtube-music/pull/753)
- fix top gap between nav-bar and browse-page [`#734`](https://github.com/th-ch/youtube-music/pull/734)
- migrate from remote to ipc + fix restart in portable app [`#605`](https://github.com/th-ch/youtube-music/pull/605)
- [Snyk] Upgrade custom-electron-prompt from 1.4.2 to 1.5.0 [`#717`](https://github.com/th-ch/youtube-music/pull/717)
- Picture in Picture v2 [`#685`](https://github.com/th-ch/youtube-music/pull/685)
- Add MPRIS volume control [`#776`](https://github.com/th-ch/youtube-music/issues/776)
- Remove jest [`bb6115f`](https://github.com/th-ch/youtube-music/commit/bb6115fec1a18a416edb365a442eb0b0ee330768)
- migrate from remote to ipc [`5bd9768`](https://github.com/th-ch/youtube-music/commit/5bd97685b9e07c656e0b57a9e02819afc70af1b1)
- v3 [`d23bfe9`](https://github.com/th-ch/youtube-music/commit/d23bfe936840b947ca101fd304464f65d36e88cc)
#### [v1.17.0](https://github.com/th-ch/youtube-music/compare/v1.16.0...v1.17.0) #### [v1.17.0](https://github.com/th-ch/youtube-music/compare/v1.16.0...v1.17.0)
> 16 May 2022
- Bump ejs from 3.1.6 to 3.1.7 [`#712`](https://github.com/th-ch/youtube-music/pull/712) - Bump ejs from 3.1.6 to 3.1.7 [`#712`](https://github.com/th-ch/youtube-music/pull/712)
- fix injectCSS `did-finish-load` listener overload [`#693`](https://github.com/th-ch/youtube-music/pull/693) - fix injectCSS `did-finish-load` listener overload [`#693`](https://github.com/th-ch/youtube-music/pull/693)
- [Snyk] Upgrade @cliqz/adblocker-electron from 1.23.6 to 1.23.7 [`#689`](https://github.com/th-ch/youtube-music/pull/689) - [Snyk] Upgrade @cliqz/adblocker-electron from 1.23.6 to 1.23.7 [`#689`](https://github.com/th-ch/youtube-music/pull/689)

View File

@ -83,6 +83,13 @@ const defaultConfig = {
mode: "custom", mode: "custom",
forceHide: false, forceHide: false,
}, },
"picture-in-picture": {
"enabled": false,
"alwaysOnTop": true,
"savePosition": true,
"saveSize": false,
"hotkey": "P"
},
}, },
}; };

View File

@ -2,8 +2,16 @@ const Store = require("electron-store");
const defaults = require("./defaults"); const defaults = require("./defaults");
const setDefaultPluginOptions = (store, plugin) => {
if (!store.get(`plugins.${plugin}`)) {
store.set(`plugins.${plugin}`, defaults.plugins[plugin]);
}
}
const migrations = { const migrations = {
">=1.17.0": (store) => { ">=1.17.0": (store) => {
setDefaultPluginOptions(store, "picture-in-picture");
if (store.get("plugins.video-toggle.mode") === undefined) { if (store.get("plugins.video-toggle.mode") === undefined) {
store.set("plugins.video-toggle.mode", "custom"); store.set("plugins.video-toggle.mode", "custom");
} }

View File

@ -2,8 +2,6 @@
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");
@ -15,6 +13,7 @@ const { fileExists, injectCSS } = require("./plugins/utils");
const { isTesting } = require("./utils/testing"); const { isTesting } = require("./utils/testing");
const { setUpTray } = require("./tray"); const { setUpTray } = require("./tray");
const { setupSongInfo } = require("./providers/song-info"); const { setupSongInfo } = require("./providers/song-info");
const { setupAppControls, restart } = require("./providers/app-controls");
// Catch errors and log them // Catch errors and log them
unhandled({ unhandled({
@ -85,6 +84,22 @@ 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"));
// Load user CSS
const themes = config.get("options.themes");
if (Array.isArray(themes)) {
themes.forEach((cssFile) => {
fileExists(
cssFile,
() => {
injectCSS(win.webContents, cssFile);
},
() => {
console.warn(`CSS file "${cssFile}" does not exist, ignoring`);
}
);
});
}
win.webContents.once("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");
@ -120,14 +135,13 @@ function createMainWindow() {
contextIsolation: false, contextIsolation: false,
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
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()
? { ? {
// Only necessary when testing with Spectron // Sandbox is only enabled in tests for now
contextIsolation: false, // See https://www.electronjs.org/docs/latest/tutorial/sandbox#preload-scripts
nodeIntegration: true, sandbox: false,
} }
: undefined), : undefined),
}, },
frame: !is.macOS() && !useInlineMenu, frame: !is.macOS() && !useInlineMenu,
@ -138,7 +152,6 @@ 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;
@ -175,6 +188,10 @@ function createMainWindow() {
win.webContents.loadURL(urlToLoad); win.webContents.loadURL(urlToLoad);
win.on("closed", onClosed); win.on("closed", onClosed);
const setPiPOptions = config.plugins.isEnabled("picture-in-picture")
? (key, value) => require("./plugins/picture-in-picture/back").setOptions({ [key]: value })
: () => {};
win.on("move", () => { win.on("move", () => {
if (win.isMaximized()) return; if (win.isMaximized()) return;
let position = win.getPosition(); let position = win.getPosition();
@ -183,6 +200,8 @@ function createMainWindow() {
config.plugins.getOptions("picture-in-picture")["isInPiP"]; config.plugins.getOptions("picture-in-picture")["isInPiP"];
if (!isPiPEnabled) { if (!isPiPEnabled) {
lateSave("window-position", { x: position[0], y: position[1] }); lateSave("window-position", { x: position[0], y: position[1] });
} else if(config.plugins.getOptions("picture-in-picture")["savePosition"]) {
lateSave("pip-position", position, setPiPOptions);
} }
}); });
@ -190,31 +209,36 @@ function createMainWindow() {
win.on("resize", () => { win.on("resize", () => {
const windowSize = win.getSize(); const windowSize = win.getSize();
const isMaximized = win.isMaximized(); const isMaximized = win.isMaximized();
if (winWasMaximized !== isMaximized) {
winWasMaximized = isMaximized;
config.set("window-maximized", isMaximized);
}
const isPiPEnabled = const isPiPEnabled =
config.plugins.isEnabled("picture-in-picture") && config.plugins.isEnabled("picture-in-picture") &&
config.plugins.getOptions("picture-in-picture")["isInPiP"]; config.plugins.getOptions("picture-in-picture")["isInPiP"];
if (!isMaximized && !isPiPEnabled) {
if (!isPiPEnabled && winWasMaximized !== isMaximized) {
winWasMaximized = isMaximized;
config.set("window-maximized", isMaximized);
}
if (isMaximized) return;
if (!isPiPEnabled) {
lateSave("window-size", { lateSave("window-size", {
width: windowSize[0], width: windowSize[0],
height: windowSize[1], height: windowSize[1],
}); });
} else if(config.plugins.getOptions("picture-in-picture")["saveSize"]) {
lateSave("pip-size", windowSize, setPiPOptions);
} }
}); });
let savedTimeouts = {}; let savedTimeouts = {};
function lateSave(key, value) { function lateSave(key, value, fn = config.set) {
if (savedTimeouts[key]) clearTimeout(savedTimeouts[key]); if (savedTimeouts[key]) clearTimeout(savedTimeouts[key]);
savedTimeouts[key] = setTimeout(() => { savedTimeouts[key] = setTimeout(() => {
config.set(key, value); fn(key, value);
savedTimeouts[key] = undefined; savedTimeouts[key] = undefined;
}, 1000) }, 600);
} }
win.webContents.on("render-process-gone", (event, webContents, details) => { win.webContents.on("render-process-gone", (event, webContents, details) => {
@ -261,6 +285,7 @@ app.once("browser-window-created", (event, win) => {
setupSongInfo(win); setupSongInfo(win);
loadPlugins(win); loadPlugins(win);
setupAppControls();
win.webContents.on("did-fail-load", ( win.webContents.on("did-fail-load", (
_event, _event,
@ -448,13 +473,8 @@ function showUnresponsiveDialog(win, details) {
cancelId: 0 cancelId: 0
}).then( result => { }).then( result => {
switch (result.response) { switch (result.response) {
case 1: //if relaunch - relaunch+exit case 1: restart(); break;
app.relaunch(); case 2: app.quit(); break;
case 2:
app.quit();
break;
default:
break;
} }
}); });
} }

34
menu.js
View File

@ -3,6 +3,7 @@ const path = require("path");
const { app, Menu, dialog } = require("electron"); const { app, Menu, dialog } = require("electron");
const is = require("electron-is"); const is = require("electron-is");
const { restart } = require("./providers/app-controls");
const { getAllPlugins } = require("./plugins/utils"); const { getAllPlugins } = require("./plugins/utils");
const config = require("./config"); const config = require("./config");
@ -99,6 +100,34 @@ const mainMenuTemplate = (win) => {
config.set("options.ForceShowLikeButtons", item.checked); config.set("options.ForceShowLikeButtons", item.checked);
}, },
}, },
{
label: "Theme",
submenu: [
{
label: "No theme",
type: "radio",
checked: !config.get("options.themes"), // todo rename "themes"
click: () => {
config.set("options.themes", []);
},
},
{ type: "separator" },
{
label: "Import custom CSS file",
type: "radio",
checked: false,
click: async () => {
const { filePaths } = await dialog.showOpenDialog({
filters: [{ name: "CSS Files", extensions: ["css"] }],
properties: ["openFile", "multiSelections"],
});
if (filePaths) {
config.set("options.themes", filePaths);
}
},
},
],
},
], ],
}, },
{ {
@ -302,10 +331,7 @@ const mainMenuTemplate = (win) => {
}, },
{ {
label: "Restart App", label: "Restart App",
click: () => { click: restart
app.relaunch();
app.quit();
},
}, },
{ role: "quit" }, { role: "quit" },
], ],

View File

@ -1,7 +1,7 @@
{ {
"name": "youtube-music", "name": "youtube-music",
"productName": "YouTube Music", "productName": "YouTube Music",
"version": "1.17.0", "version": "1.18.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",
@ -67,7 +67,8 @@
} }
}, },
"scripts": { "scripts": {
"test": "jest", "test": "playwright test",
"test:debug": "DEBUG=pw:browser* playwright test",
"start": "electron .", "start": "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",
@ -92,42 +93,41 @@
"npm": "Please use yarn and not npm" "npm": "Please use yarn and not npm"
}, },
"dependencies": { "dependencies": {
"@cliqz/adblocker-electron": "^1.23.7", "@cliqz/adblocker-electron": "^1.23.8",
"@electron/remote": "^2.0.8",
"@ffmpeg/core": "^0.10.0", "@ffmpeg/core": "^0.10.0",
"@ffmpeg/ffmpeg": "^0.10.1", "@ffmpeg/ffmpeg": "^0.10.1",
"Simple-YouTube-Age-Restriction-Bypass": "https://gitpkg.now.sh/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/dist?v2.4.6", "Simple-YouTube-Age-Restriction-Bypass": "https://gitpkg.now.sh/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/dist?v2.4.6",
"async-mutex": "^0.3.2", "async-mutex": "^0.3.2",
"browser-id3-writer": "^4.4.0", "browser-id3-writer": "^4.4.0",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"custom-electron-prompt": "^1.4.2", "custom-electron-prompt": "^1.5.0",
"custom-electron-titlebar": "^4.1.0", "custom-electron-titlebar": "^4.1.0",
"discord-rpc": "^4.0.1", "discord-rpc": "^4.0.1",
"electron-better-web-request": "^1.0.1", "electron-better-web-request": "^1.0.1",
"electron-debug": "^3.2.0", "electron-debug": "^3.2.0",
"electron-is": "^3.0.0", "electron-is": "^3.0.0",
"electron-localshortcut": "^3.2.1", "electron-localshortcut": "^3.2.1",
"electron-store": "^8.0.1", "electron-store": "^8.0.2",
"electron-unhandled": "^3.0.2", "electron-unhandled": "^4.0.1",
"electron-updater": "^4.6.3", "electron-updater": "^4.6.3",
"filenamify": "^4.3.0", "filenamify": "^4.3.0",
"hark": "^1.2.3", "hark": "^1.2.3",
"html-to-text": "^8.2.0", "html-to-text": "^8.2.1",
"md5": "^2.3.0", "md5": "^2.3.0",
"mpris-service": "^2.1.2", "mpris-service": "^2.1.2",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
"node-notifier": "^10.0.1", "node-notifier": "^10.0.1",
"ytdl-core": "^4.11.0", "ytdl-core": "^4.11.1",
"ytpl": "^2.3.0" "ytpl": "^2.3.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.25.1",
"auto-changelog": "^2.4.0", "auto-changelog": "^2.4.0",
"electron": "^17.0.0", "electron": "^20.1.1",
"electron-builder": "^23.0.3", "electron-builder": "^23.0.3",
"electron-devtools-installer": "^3.1.1", "electron-devtools-installer": "^3.1.1",
"electron-icon-maker": "0.0.5", "electron-icon-maker": "0.0.5",
"jest": "^27.3.1", "playwright": "^1.25.1",
"playwright": "^1.17.1",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"xo": "^0.45.0" "xo": "^0.45.0"
}, },

View File

@ -3,7 +3,6 @@ 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");
@ -137,7 +136,7 @@ const toMP3 = async (
safeVideoName + "." + extension safeVideoName + "." + extension
); );
const folder = options.downloadFolder || remote.app.getPath("downloads"); const folder = options.downloadFolder || await ipcRenderer.invoke('getDownloadsFolder');
const name = metadata.title const name = metadata.title
? `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}` ? `${metadata.artist ? `${metadata.artist} - ` : ""}${metadata.title}`
: videoName; : videoName;

View File

@ -1,6 +1,7 @@
const { ipcRenderer } = require("electron"); const { ipcRenderer } = require("electron");
const config = require("../../config"); const config = require("../../config");
const { Titlebar, Color } = require("custom-electron-titlebar"); const { Titlebar, Color } = require("custom-electron-titlebar");
const { isEnabled } = require("../../config/plugins");
function $(selector) { return document.querySelector(selector); } function $(selector) { return document.querySelector(selector); }
module.exports = (options) => { module.exports = (options) => {
@ -29,6 +30,12 @@ module.exports = (options) => {
} }
}); });
if (isEnabled("picture-in-picture")) {
ipcRenderer.on("pip-toggle", (_, pipEnabled) => {
bar.refreshMenu();
});
}
ipcRenderer.on("hideIcon", (_, hide) => hideIcon(hide)); ipcRenderer.on("hideIcon", (_, hide) => hideIcon(hide));
// Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it) // Increases the right margin of Navbar background when the scrollbar is visible to avoid blocking it (z-index doesn't affect it)

View File

@ -13,6 +13,11 @@
height: 75px !important; height: 75px !important;
} }
/* fixes top gap between nav-bar and browse-page */
#browse-page {
padding-top: 0 !important;
}
/* remove window dragging for nav bar (conflict with titlebar drag) */ /* remove window dragging for nav bar (conflict with titlebar drag) */
ytmusic-nav-bar, ytmusic-nav-bar,
.tab-titleiron-icon, .tab-titleiron-icon,

View File

@ -7,6 +7,6 @@
} }
.description { .description {
font-size: 1.1vw !important; font-size: clamp(1.4rem, 1.1vmax, 3rem) !important;
text-align: center !important; text-align: center !important;
} }

View File

@ -1,3 +1,4 @@
.ytmusic-pivot-bar-renderer[tab-id="FEmusic_liked"] { .ytmusic-pivot-bar-renderer[tab-id="FEmusic_liked"],
.sign-in-link {
display: none !important; display: none !important;
} }

View File

@ -0,0 +1,37 @@
const { Menu, app } = require("electron");
const { setApplicationMenu } = require("../../../menu");
module.exports = (win, options, setOptions, togglePip, isInPip) => {
if (isInPip) {
Menu.setApplicationMenu(Menu.buildFromTemplate([
{
label: "App",
submenu: [
{
label: "Exit Picture in Picture",
click: togglePip,
},
{
label: "Always on top",
type: "checkbox",
checked: options.alwaysOnTop,
click: (item) => {
setOptions({ alwaysOnTop: item.checked });
win.setAlwaysOnTop(item.checked);
},
},
{
label: "Restart",
click: () => {
app.relaunch();
app.quit();
},
},
{ role: "quit" },
],
},
]));
} else {
setApplicationMenu(win);
}
};

View File

@ -1,79 +1,109 @@
const path = require("path"); const path = require("path");
const { app, ipcMain } = require("electron"); const { app, ipcMain } = require("electron");
const electronLocalshortcut = require("electron-localshortcut");
const { setOptions } = require("../../config/plugins"); const { setOptions, isEnabled } = require("../../config/plugins");
const { injectCSS } = require("../utils"); const { injectCSS } = require("../utils");
let isInPiPMode = false; let isInPiP = false;
let originalPosition; let originalPosition;
let originalSize; let originalSize;
let originalFullScreen;
let originalMaximized;
const pipPosition = [10, 10]; let win;
const pipSize = [450, 275]; let options;
const togglePiP = async (win) => { const pipPosition = () => (options.savePosition && options["pip-position"]) || [10, 10];
isInPiPMode = !isInPiPMode; const pipSize = () => (options.saveSize && options["pip-size"]) || [450, 275];
setOptions("picture-in-picture", { isInPiP: isInPiPMode });
if (isInPiPMode) { const setLocalOptions = (_options) => {
options = { ...options, ..._options };
setOptions("picture-in-picture", _options);
}
const adaptors = [];
const runAdaptors = () => adaptors.forEach(a => a());
if (isEnabled("in-app-menu")) {
let adaptor = require("./adaptors/in-app-menu");
adaptors.push(() => adaptor(win, options, setLocalOptions, togglePiP, isInPiP));
}
const togglePiP = async () => {
isInPiP = !isInPiP;
setLocalOptions({ isInPiP });
if (isInPiP) {
originalFullScreen = win.isFullScreen();
if (originalFullScreen) win.setFullScreen(false);
originalMaximized = win.isMaximized();
if (originalMaximized) win.unmaximize();
originalPosition = win.getPosition(); originalPosition = win.getPosition();
originalSize = win.getSize(); originalSize = win.getSize();
win.webContents.on("before-input-event", blockShortcutsInPiP); win.webContents.on("before-input-event", blockShortcutsInPiP);
win.setFullScreenable(false); win.setFullScreenable(false);
await win.webContents.executeJavaScript(
// Go fullscreen runAdaptors();
` win.webContents.send("pip-toggle", true);
if (!document.querySelector("ytmusic-player-page").playerPageOpen_) {
document.querySelector(".toggle-player-page-button").click();
}
document.querySelector(".fullscreen-button").click();
document.querySelector("ytmusic-player-bar").classList.add("pip");
`
);
win.setFullScreenable(true);
app.dock?.hide(); app.dock?.hide();
win.setVisibleOnAllWorkspaces(true, { win.setVisibleOnAllWorkspaces(true, {
visibleOnFullScreen: true, visibleOnFullScreen: true,
}); });
app.dock?.show(); app.dock?.show();
win.setAlwaysOnTop(true, "screen-saver", 1); if (options.alwaysOnTop) {
win.setAlwaysOnTop(true, "screen-saver", 1);
}
} else { } else {
win.webContents.removeListener("before-input-event", blockShortcutsInPiP); win.webContents.removeListener("before-input-event", blockShortcutsInPiP);
win.setFullScreenable(true);
await win.webContents.executeJavaScript( runAdaptors();
// Exit fullscreen win.webContents.send("pip-toggle", false);
`
document.querySelector(".exit-fullscreen-button").click();
document.querySelector("ytmusic-player-bar").classList.remove("pip");
`
);
win.setVisibleOnAllWorkspaces(false); win.setVisibleOnAllWorkspaces(false);
win.setAlwaysOnTop(false); win.setAlwaysOnTop(false);
if (originalFullScreen) win.setFullScreen(true);
if (originalMaximized) win.maximize();
} }
const [x, y] = isInPiPMode ? pipPosition : originalPosition; const [x, y] = isInPiP ? pipPosition() : originalPosition;
const [w, h] = isInPiPMode ? pipSize : originalSize; const [w, h] = isInPiP ? pipSize() : originalSize;
win.setPosition(x, y); win.setPosition(x, y);
win.setSize(w, h); win.setSize(w, h);
win.setWindowButtonVisibility?.(!isInPiPMode); win.setWindowButtonVisibility?.(!isInPiP);
};
module.exports = (win) => {
injectCSS(win.webContents, path.join(__dirname, "style.css"));
ipcMain.on("picture-in-picture", async () => {
await togglePiP(win);
});
}; };
const blockShortcutsInPiP = (event, input) => { const blockShortcutsInPiP = (event, input) => {
const blockedShortcuts = ["f", "escape"]; const key = input.key.toLowerCase();
if (blockedShortcuts.includes(input.key.toLowerCase())) {
if (key === "f") {
event.preventDefault(); event.preventDefault();
} else if (key === 'escape') {
togglePiP();
event.preventDefault();
};
};
module.exports = (_win, _options) => {
options ??= _options;
win ??= _win;
setLocalOptions({ isInPiP });
injectCSS(win.webContents, path.join(__dirname, "style.css"));
ipcMain.on("picture-in-picture", async () => {
await togglePiP();
});
if (options.hotkey) {
electronLocalshortcut.register(win, options.hotkey, togglePiP);
} }
}; };
module.exports.setOptions = setLocalOptions;

View File

@ -3,6 +3,8 @@ const { ipcRenderer } = require("electron");
const { getSongMenu } = require("../../providers/dom-elements"); const { getSongMenu } = require("../../providers/dom-elements");
const { ElementFromFile, templatePath } = require("../utils"); const { ElementFromFile, templatePath } = require("../utils");
function $(selector) { return document.querySelector(selector); }
let menu = null; let menu = null;
const pipButton = ElementFromFile( const pipButton = ElementFromFile(
templatePath(__dirname, "picture-in-picture.html") templatePath(__dirname, "picture-in-picture.html")
@ -14,7 +16,7 @@ const observer = new MutationObserver(() => {
if (!menu) return; if (!menu) return;
} }
if (menu.contains(pipButton)) return; if (menu.contains(pipButton)) return;
const menuUrl = document.querySelector( const menuUrl = $(
'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint' 'tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint'
)?.href; )?.href;
if (menuUrl && !menuUrl.includes("watch?")) return; if (menuUrl && !menuUrl.includes("watch?")) return;
@ -26,11 +28,57 @@ global.togglePictureInPicture = () => {
ipcRenderer.send("picture-in-picture"); ipcRenderer.send("picture-in-picture");
}; };
const listenForToggle = () => {
const originalExitButton = $(".exit-fullscreen-button");
const clonedExitButton = originalExitButton.cloneNode(true);
clonedExitButton.onclick = () => togglePictureInPicture();
const appLayout = $("ytmusic-app-layout");
const expandMenu = $('#expanding-menu');
const middleControls = $('.middle-controls');
const playerPage = $("ytmusic-player-page");
const togglePlayerPageButton = $(".toggle-player-page-button");
const fullScreenButton = $(".fullscreen-button");
const player = $('#player');
const onPlayerDblClick = player.onDoubleClick_;
ipcRenderer.on('pip-toggle', (_, isPip) => {
if (isPip) {
$(".exit-fullscreen-button").replaceWith(clonedExitButton);
player.onDoubleClick_ = () => {};
expandMenu.onmouseleave = () => middleControls.click();
if (!playerPage.playerPageOpen_) {
togglePlayerPageButton.click();
}
fullScreenButton.click();
appLayout.classList.add("pip");
} else {
$(".exit-fullscreen-button").replaceWith(originalExitButton);
player.onDoubleClick_ = onPlayerDblClick;
expandMenu.onmouseleave = undefined;
originalExitButton.click();
appLayout.classList.remove("pip");
}
});
}
function observeMenu(options) { function observeMenu(options) {
document.addEventListener( document.addEventListener(
"apiLoaded", "apiLoaded",
() => { () => {
observer.observe(document.querySelector("ytmusic-popup-container"), { listenForToggle();
const minButton = $(".player-minimize-button");
// remove native listeners
minButton.replaceWith(minButton.cloneNode(true));
$(".player-minimize-button").onclick = () => {
global.togglePictureInPicture();
setTimeout(() => $('#player').click());
};
// allows easily closing the menu by programmatically clicking outside of it
$("#expanding-menu").removeAttribute("no-cancel-on-outside-click");
// TODO: think about wether an additional button in songMenu is needed
observer.observe($("ytmusic-popup-container"), {
childList: true, childList: true,
subtree: true, subtree: true,
}); });

View File

@ -0,0 +1,60 @@
const prompt = require("custom-electron-prompt");
const promptOptions = require("../../providers/prompt-options");
const { setOptions } = require("./back.js");
module.exports = (win, options) => [
{
label: "Always on top",
type: "checkbox",
checked: options.alwaysOnTop,
click: (item) => {
setOptions({ alwaysOnTop: item.checked });
win.setAlwaysOnTop(item.checked);
},
},
{
label: "Save window position",
type: "checkbox",
checked: options.savePosition,
click: (item) => {
setOptions({ savePosition: item.checked });
},
},
{
label: "Save window size",
type: "checkbox",
checked: options.saveSize,
click: (item) => {
setOptions({ saveSize: item.checked });
},
},
{
label: "Hotkey",
type: "checkbox",
checked: options.hotkey,
click: async (item) => {
const output = await prompt({
title: "Picture in Picture Hotkey",
label: "Choose a hotkey for toggling Picture in Picture",
type: "keybind",
keybindOptions: [{
value: "hotkey",
label: "Hotkey",
default: options.hotkey
}],
...promptOptions()
}, win)
if (output) {
const { value, accelerator } = output[0];
setOptions({ [value]: accelerator });
item.checked = !!accelerator;
} else {
// Reset checkbox if prompt was canceled
item.checked = !item.checked;
}
},
}
];

View File

@ -1,11 +1,26 @@
ytmusic-player-bar.pip svg, /* improve visibility of the player bar elements */
ytmusic-player-bar.pip yt-formatted-string { ytmusic-app-layout.pip ytmusic-player-bar svg,
filter: drop-shadow(2px 4px 6px black); ytmusic-app-layout.pip ytmusic-player-bar .time-info,
color: white; ytmusic-app-layout.pip ytmusic-player-bar yt-formatted-string,
ytmusic-app-layout.pip ytmusic-player-bar .yt-formatted-string {
filter: drop-shadow(2px 4px 6px black);
color: white !important;
fill: white !important;
} }
ytmusic-player-bar.pip ytmusic-player-expanding-menu { /* improve the style of the player bar expanding menu */
ytmusic-app-layout.pip ytmusic-player-expanding-menu {
border-radius: 30px; border-radius: 30px;
background-color: rgba(0, 0, 0, 0.3); background-color: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(5px) brightness(20%); backdrop-filter: blur(5px) brightness(20%);
} }
/* fix volumeHud position when both in-app-menu and PiP are active */
.cet-container ytmusic-app-layout.pip #volumeHud {
top: 22px !important;
}
/* disable the video-toggle button when in PiP mode */
ytmusic-app-layout.pip .video-switch-button {
display: none !important;
}

View File

@ -7,9 +7,18 @@ This is used to determine if plugin is actually active
*/ */
let enabled = false; let enabled = false;
module.exports = (win) => { const { globalShortcut } = require('electron');
module.exports = (win, options) => {
enabled = true; enabled = true;
injectCSS(win.webContents, path.join(__dirname, "volume-hud.css")); injectCSS(win.webContents, path.join(__dirname, "volume-hud.css"));
if (options.globalShortcuts?.volumeUp) {
globalShortcut.register((options.globalShortcuts.volumeUp), () => win.webContents.send('changeVolume', true));
}
if (options.globalShortcuts?.volumeDown) {
globalShortcut.register((options.globalShortcuts.volumeDown), () => win.webContents.send('changeVolume', false));
}
} }
module.exports.enabled = () => enabled; module.exports.enabled = () => enabled;

View File

@ -1,22 +1,25 @@
const { ipcRenderer } = require("electron"); const { ipcRenderer } = require("electron");
const { globalShortcut } = require('@electron/remote');
const { setOptions, setMenuOptions, isEnabled } = require("../../config/plugins"); const { setOptions, setMenuOptions, isEnabled } = require("../../config/plugins");
function $(selector) { return document.querySelector(selector); } function $(selector) { return document.querySelector(selector); }
let api;
module.exports = (options) => { let api, options;
module.exports = (_options) => {
options = _options;
document.addEventListener('apiLoaded', e => { document.addEventListener('apiLoaded', e => {
api = e.detail; api = e.detail;
firstRun(options); ipcRenderer.on('changeVolume', (_, toIncrease) => changeVolume(toIncrease));
ipcRenderer.on('setVolume', (_, value) => setVolume(value));
firstRun();
}, { once: true, passive: true }) }, { once: true, passive: true })
}; };
module.exports.moveVolumeHud = moveVolumeHud; module.exports.moveVolumeHud = moveVolumeHud;
/** Restore saved volume and setup tooltip */ /** Restore saved volume and setup tooltip */
function firstRun(options) { function firstRun() {
if (typeof options.savedVolume === "number") { if (typeof options.savedVolume === "number") {
// Set saved volume as tooltip // Set saved volume as tooltip
setTooltip(options.savedVolume); setTooltip(options.savedVolume);
@ -26,16 +29,14 @@ function firstRun(options) {
} }
} }
setupPlaybar(options); setupPlaybar();
setupLocalArrowShortcuts(options); setupLocalArrowShortcuts();
setupGlobalShortcuts(options);
const noVid = $("#main-panel")?.computedStyleMap().get("display").value === "none"; const noVid = $("#main-panel")?.computedStyleMap().get("display").value === "none";
injectVolumeHud(noVid); injectVolumeHud(noVid);
if (!noVid) { if (!noVid) {
setupVideoPlayerOnwheel(options); setupVideoPlayerOnwheel();
if (!isEnabled('video-toggle')) { if (!isEnabled('video-toggle')) {
//video-toggle handles hud positioning on its own //video-toggle handles hud positioning on its own
const videoMode = () => api.getPlayerResponse().videoDetails?.musicVideoType !== 'MUSIC_VIDEO_TYPE_ATV'; const videoMode = () => api.getPlayerResponse().videoDetails?.musicVideoType !== 'MUSIC_VIDEO_TYPE_ATV';
@ -96,22 +97,22 @@ function showVolumeHud(volume) {
} }
/** Add onwheel event to video player */ /** Add onwheel event to video player */
function setupVideoPlayerOnwheel(options) { function setupVideoPlayerOnwheel() {
$("#main-panel").addEventListener("wheel", event => { $("#main-panel").addEventListener("wheel", event => {
event.preventDefault(); event.preventDefault();
// Event.deltaY < 0 means wheel-up // Event.deltaY < 0 means wheel-up
changeVolume(event.deltaY < 0, options); changeVolume(event.deltaY < 0);
}); });
} }
function saveVolume(volume, options) { function saveVolume(volume) {
options.savedVolume = volume; options.savedVolume = volume;
writeOptions(options); writeOptions();
} }
//without this function it would rewrite config 20 time when volume change by 20 //without this function it would rewrite config 20 time when volume change by 20
let writeTimeout; let writeTimeout;
function writeOptions(options) { function writeOptions() {
if (writeTimeout) clearTimeout(writeTimeout); if (writeTimeout) clearTimeout(writeTimeout);
writeTimeout = setTimeout(() => { writeTimeout = setTimeout(() => {
@ -121,13 +122,13 @@ function writeOptions(options) {
} }
/** 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*/
function setupPlaybar(options) { function setupPlaybar() {
const playerbar = $("ytmusic-player-bar"); const playerbar = $("ytmusic-player-bar");
playerbar.addEventListener("wheel", event => { playerbar.addEventListener("wheel", event => {
event.preventDefault(); event.preventDefault();
// Event.deltaY < 0 means wheel-up // Event.deltaY < 0 means wheel-up
changeVolume(event.deltaY < 0, options); changeVolume(event.deltaY < 0);
}); });
// Keep track of mouse position for showVolumeSlider() // Keep track of mouse position for showVolumeSlider()
@ -139,11 +140,11 @@ function setupPlaybar(options) {
playerbar.classList.remove("on-hover"); playerbar.classList.remove("on-hover");
}); });
setupSliderObserver(options); setupSliderObserver();
} }
/** Save volume + Update the volume tooltip when volume-slider is manually changed */ /** Save volume + Update the volume tooltip when volume-slider is manually changed */
function setupSliderObserver(options) { function setupSliderObserver() {
const sliderObserver = new MutationObserver(mutations => { const sliderObserver = new MutationObserver(mutations => {
for (const mutation of mutations) { for (const mutation of mutations) {
// This checks that volume-slider was manually set // This checks that volume-slider was manually set
@ -151,7 +152,7 @@ function setupSliderObserver(options) {
(typeof options.savedVolume !== "number" || Math.abs(options.savedVolume - mutation.target.value) > 4)) { (typeof options.savedVolume !== "number" || Math.abs(options.savedVolume - mutation.target.value) > 4)) {
// Diff>4 means it was manually set // Diff>4 means it was manually set
setTooltip(mutation.target.value); setTooltip(mutation.target.value);
saveVolume(mutation.target.value, options); saveVolume(mutation.target.value);
} }
} }
}); });
@ -163,29 +164,32 @@ function setupSliderObserver(options) {
}); });
} }
/** if (toIncrease = false) then volume decrease */ function setVolume(value) {
function changeVolume(toIncrease, options) { api.setVolume(value);
// Apply volume change if valid
const steps = Number(options.steps || 1);
api.setVolume(toIncrease ?
Math.min(api.getVolume() + steps, 100) :
Math.max(api.getVolume() - steps, 0));
// Save the new volume // Save the new volume
saveVolume(api.getVolume(), options); saveVolume(value);
// change slider position (important) // change slider position (important)
updateVolumeSlider(options); updateVolumeSlider();
// Change tooltips to new value // Change tooltips to new value
setTooltip(options.savedVolume); setTooltip(value);
// Show volume slider // Show volume slider
showVolumeSlider(); showVolumeSlider();
// Show volume HUD // Show volume HUD
showVolumeHud(options.savedVolume); showVolumeHud(value);
} }
function updateVolumeSlider(options) { /** if (toIncrease = false) then volume decrease */
function changeVolume(toIncrease) {
// Apply volume change if valid
const steps = Number(options.steps || 1);
setVolume(toIncrease ?
Math.min(api.getVolume() + steps, 100) :
Math.max(api.getVolume() - steps, 0));
}
function updateVolumeSlider() {
// Slider value automatically rounds to multiples of 5 // Slider value automatically rounds to multiples of 5
for (const slider of ["#volume-slider", "#expand-volume-slider"]) { for (const slider of ["#volume-slider", "#expand-volume-slider"]) {
$(slider).value = $(slider).value =
@ -228,26 +232,17 @@ function setTooltip(volume) {
} }
} }
function setupGlobalShortcuts(options) { function setupLocalArrowShortcuts() {
if (options.globalShortcuts.volumeUp) {
globalShortcut.register((options.globalShortcuts.volumeUp), () => changeVolume(true, options));
}
if (options.globalShortcuts.volumeDown) {
globalShortcut.register((options.globalShortcuts.volumeDown), () => changeVolume(false, options));
}
}
function setupLocalArrowShortcuts(options) {
if (options.arrowsShortcut) { if (options.arrowsShortcut) {
window.addEventListener('keydown', (event) => { window.addEventListener('keydown', (event) => {
switch (event.code) { switch (event.code) {
case "ArrowUp": case "ArrowUp":
event.preventDefault(); event.preventDefault();
changeVolume(true, options); changeVolume(true);
break; break;
case "ArrowDown": case "ArrowDown":
event.preventDefault(); event.preventDefault();
changeVolume(false, options); changeVolume(false);
break; break;
} }
}); });

View File

@ -0,0 +1,15 @@
const { ipcMain, dialog } = require("electron");
module.exports = () => {
ipcMain.handle('qualityChanger', async (_, qualityLabels, currentIndex) => {
return await dialog.showMessageBox({
type: "question",
buttons: qualityLabels,
defaultId: currentIndex,
title: "Choose Video Quality",
message: "Choose Video Quality:",
detail: `Current Quality: ${qualityLabels[currentIndex]}`,
cancelId: -1
})
})
};

View File

@ -1,5 +1,5 @@
const { ElementFromFile, templatePath } = require("../utils"); const { ElementFromFile, templatePath } = require("../utils");
const { dialog } = require('@electron/remote'); const { ipcRenderer } = require("electron");
function $(selector) { return document.querySelector(selector); } function $(selector) { return document.querySelector(selector); }
@ -18,24 +18,17 @@ function setup(event) {
$('.top-row-buttons.ytmusic-player').prepend(qualitySettingsButton); $('.top-row-buttons.ytmusic-player').prepend(qualitySettingsButton);
qualitySettingsButton.onclick = function chooseQuality() { qualitySettingsButton.onclick = function chooseQuality() {
if (api.getPlayerState() === 2) api.playVideo(); setTimeout(() => $('#player').click());
else if (api.getPlayerState() === 1) api.pauseVideo();
const currentIndex = api.getAvailableQualityLevels().indexOf(api.getPlaybackQuality()) const qualityLevels = api.getAvailableQualityLevels();
dialog.showMessageBox({ const currentIndex = qualityLevels.indexOf(api.getPlaybackQuality());
type: "question",
buttons: api.getAvailableQualityLabels(), ipcRenderer.invoke('qualityChanger', api.getAvailableQualityLabels(), currentIndex).then(promise => {
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; if (promise.response === -1) return;
const newQuality = api.getAvailableQualityLevels()[promise.response]; const newQuality = qualityLevels[promise.response];
api.setPlaybackQualityRange(newQuality); api.setPlaybackQualityRange(newQuality);
api.setPlaybackQuality(newQuality) api.setPlaybackQuality(newQuality)
}) });
} }
} }

View File

@ -2,6 +2,7 @@ const mpris = require("mpris-service");
const { ipcMain } = require("electron"); const { ipcMain } = require("electron");
const registerCallback = require("../../providers/song-info"); const registerCallback = require("../../providers/song-info");
const getSongControls = require("../../providers/song-controls"); const getSongControls = require("../../providers/song-controls");
const config = require("../../config");
function setupMPRIS() { function setupMPRIS() {
const player = mpris({ const player = mpris({
@ -19,7 +20,7 @@ function setupMPRIS() {
function registerMPRIS(win) { function registerMPRIS(win) {
const songControls = getSongControls(win); const songControls = getSongControls(win);
const { playPause, next, previous } = songControls; const { playPause, next, previous, volumeMinus10, volumePlus10 } = songControls;
try { try {
const secToMicro = n => Math.round(Number(n) * 1e6); const secToMicro = n => Math.round(Number(n) * 1e6);
const microToSec = n => Math.round(Number(n) / 1e6); const microToSec = n => Math.round(Number(n) / 1e6);
@ -34,6 +35,35 @@ function registerMPRIS(win) {
let currentSeconds = 0; let currentSeconds = 0;
ipcMain.on('timeChanged', (_, t) => currentSeconds = t); ipcMain.on('timeChanged', (_, t) => currentSeconds = t);
let currentLoopStatus = undefined;
let manuallySwitchingStatus = false;
ipcMain.on("repeatChanged", (_, mode) => {
if (manuallySwitchingStatus)
return;
if (mode === "Repeat off")
currentLoopStatus = "None";
else if (mode === "Repeat one")
currentLoopStatus = "Track";
else if (mode === "Repeat all")
currentLoopStatus = "Playlist";
player.loopStatus = currentLoopStatus;
});
player.on("loopStatus", (status) => {
// switchRepeat cycles between states in that order
const switches = ["None", "Playlist", "Track"];
const currentIndex = switches.indexOf(currentLoopStatus);
const targetIndex = switches.indexOf(status);
// Get a delta in the range [0,2]
const delta = (targetIndex - currentIndex + 3) % 3;
manuallySwitchingStatus = true;
songControls.switchRepeat(delta);
manuallySwitchingStatus = false;
})
player.getPosition = () => secToMicro(currentSeconds) player.getPosition = () => secToMicro(currentSeconds)
player.on("raise", () => { player.on("raise", () => {
@ -53,21 +83,45 @@ function registerMPRIS(win) {
playPause() playPause()
} }
}); });
player.on("playpause", () => {
player.playbackStatus = player.playbackStatus === 'Playing' ? "Paused" : "Playing";
playPause();
});
player.on("playpause", playPause);
player.on("next", next); player.on("next", next);
player.on("previous", previous); player.on("previous", previous);
player.on('seek', seekBy); player.on('seek', seekBy);
player.on('position', seekTo); player.on('position', seekTo);
registerCallback(songInfo => { ipcMain.on('volumeChanged', (_, value) => {
if (player) { player.volume = value;
const data = { });
'mpris:length': secToMicro(songInfo.songDuration), player.on('volume', (newVolume) => {
'mpris:artUrl': songInfo.imageSrc, if (config.plugins.isEnabled('precise-volume')) {
'xesam:title': songInfo.title, // With precise volume we can set the volume to the exact value.
'xesam:artist': [songInfo.artist], win.webContents.send('setVolume', newVolume)
} else {
// With keyboard shortcuts we can only change the volume in increments of 10, so round it.
const deltaVolume = Math.round((newVolume - player.volume) / 10);
if (deltaVolume > 0) {
for (let i = 0; i < deltaVolume; i++)
volumePlus10();
} else {
for (let i = 0; i < -deltaVolume; i++)
volumeMinus10();
}
}
});
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': '/' 'mpris:trackid': '/'
}; };
if (songInfo.album) data['xesam:album'] = songInfo.album; if (songInfo.album) data['xesam:album'] = songInfo.album;

View File

@ -1,3 +1,5 @@
const { test, expect } = require("@playwright/test");
const { sortSegments } = require("../segments"); const { sortSegments } = require("../segments");
test("Segment sorting", () => { test("Segment sorting", () => {

View File

@ -32,9 +32,16 @@ module.exports.listenAction = (channel, callback) => {
return ipcMain.on(channel, callback); return ipcMain.on(channel, callback);
}; };
module.exports.fileExists = (path, callbackIfExists) => { module.exports.fileExists = (
path,
callbackIfExists,
callbackIfError = undefined
) => {
fs.access(path, fs.F_OK, (err) => { fs.access(path, fs.F_OK, (err) => {
if (err) { if (err) {
if (callbackIfError) {
callbackIfError();
}
return; return;
} }

View File

@ -1,25 +1,34 @@
const path = require("path"); require("./providers/front-logger")();
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 setupSongInfo = require("./providers/song-info-front"); const setupSongInfo = require("./providers/song-info-front");
const { setupSongControls } = require("./providers/song-controls-front"); const { setupSongControls } = require("./providers/song-controls-front");
const { ipcRenderer } = require("electron");
const plugins = config.plugins.getEnabled(); const plugins = config.plugins.getEnabled();
let api; let api;
plugins.forEach(([plugin, options]) => { plugins.forEach(async ([plugin, options]) => {
const preloadPath = path.join(__dirname, "plugins", plugin, "preload.js"); const preloadPath = await ipcRenderer.invoke(
"getPath",
__dirname,
"plugins",
plugin,
"preload.js"
);
fileExists(preloadPath, () => { fileExists(preloadPath, () => {
const run = require(preloadPath); const run = require(preloadPath);
run(options); run(options);
}); });
const actionPath = path.join(__dirname, "plugins", plugin, "actions.js"); const actionPath = await ipcRenderer.invoke(
"getPath",
__dirname,
"plugins",
plugin,
"actions.js"
);
fileExists(actionPath, () => { fileExists(actionPath, () => {
const actions = require(actionPath).actions || {}; const actions = require(actionPath).actions || {};
@ -32,8 +41,14 @@ plugins.forEach(([plugin, options]) => {
}); });
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
plugins.forEach(([plugin, options]) => { plugins.forEach(async ([plugin, options]) => {
const pluginPath = path.join(__dirname, "plugins", plugin, "front.js"); const pluginPath = await ipcRenderer.invoke(
"getPath",
__dirname,
"plugins",
plugin,
"front.js"
);
fileExists(pluginPath, () => { fileExists(pluginPath, () => {
const run = require(pluginPath); const run = require(pluginPath);
run(options); run(options);
@ -49,12 +64,8 @@ document.addEventListener("DOMContentLoaded", () => {
// inject song-controls // inject song-controls
setupSongControls(); setupSongControls();
// inject front logger
setupFrontLogger();
// Add action for reloading // Add action for reloading
global.reload = () => global.reload = () => ipcRenderer.send('reload');
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 // Blocks the "Are You Still There?" popup by setting the last active time to Date.now every 15min
setInterval(() => window._lact = Date.now(), 900000); setInterval(() => window._lact = Date.now(), 900000);

View File

@ -1,6 +1,23 @@
const app = require("electron").app || require('@electron/remote').app; const path = require("path");
const is = require("electron-is");
const { app, BrowserWindow, ipcMain, ipcRenderer } = require("electron");
const config = require("../config");
module.exports.restart = () => { module.exports.restart = () => {
app.relaunch(); is.main() ? restart() : ipcRenderer.send('restart');
app.exit();
}; };
module.exports.setupAppControls = () => {
ipcMain.on('restart', restart);
ipcMain.handle('getDownloadsFolder', () => app.getPath("downloads"));
ipcMain.on('reload', () => BrowserWindow.getFocusedWindow().webContents.loadURL(config.get("url")));
ipcMain.handle('getPath', (_, ...args) => path.join(...args));
}
function restart() {
app.relaunch({ execPath: process.env.PORTABLE_EXECUTABLE_FILE });
// execPath will be undefined if not running portable app, resulting in default behavior
app.quit();
}

View File

@ -12,7 +12,7 @@ module.exports = (win) => {
// Playback // Playback
previous: () => pressKey(win, "k"), previous: () => pressKey(win, "k"),
next: () => pressKey(win, "j"), next: () => pressKey(win, "j"),
playPause: () => pressKey(win, "space"), playPause: () => pressKey(win, ";"),
like: () => pressKey(win, "+"), like: () => pressKey(win, "+"),
dislike: () => pressKey(win, "_"), dislike: () => pressKey(win, "_"),
go10sBack: () => pressKey(win, "h"), go10sBack: () => pressKey(win, "h"),
@ -20,7 +20,10 @@ module.exports = (win) => {
go1sBack: () => pressKey(win, "h", ["shift"]), go1sBack: () => pressKey(win, "h", ["shift"]),
go1sForward: () => pressKey(win, "l", ["shift"]), go1sForward: () => pressKey(win, "l", ["shift"]),
shuffle: () => pressKey(win, "s"), shuffle: () => pressKey(win, "s"),
switchRepeat: () => pressKey(win, "r"), switchRepeat: (n = 1) => {
for (let i = 0; i < n; i++)
pressKey(win, "r");
},
// General // General
volumeMinus10: () => pressKey(win, "-"), volumeMinus10: () => pressKey(win, "-"),
volumePlus10: () => pressKey(win, "="), volumePlus10: () => pressKey(win, "="),

View File

@ -22,6 +22,8 @@ module.exports = () => {
if (config.plugins.isEnabled('tuna-obs') || if (config.plugins.isEnabled('tuna-obs') ||
(is.linux() && config.plugins.isEnabled('shortcuts'))) { (is.linux() && config.plugins.isEnabled('shortcuts'))) {
setupTimeChangeListener(); setupTimeChangeListener();
setupRepeatChangeListener();
setupVolumeChangeListener(apiEvent.detail);
} }
const video = $('video'); const video = $('video');
// name = "dataloaded" and abit later "dataupdated" // name = "dataloaded" and abit later "dataupdated"
@ -63,3 +65,21 @@ function setupTimeChangeListener() {
}); });
progressObserver.observe($('#progress-bar'), { attributeFilter: ["value"] }) progressObserver.observe($('#progress-bar'), { attributeFilter: ["value"] })
} }
function setupRepeatChangeListener() {
const repeatObserver = new MutationObserver(mutations => {
ipcRenderer.send('repeatChanged', mutations[0].target.title);
});
repeatObserver.observe($('#right-controls .repeat'), { attributeFilter: ["title"] });
// Emit the initial value as well; as it's persistent between launches.
ipcRenderer.send('repeatChanged', $('#right-controls .repeat').title);
}
function setupVolumeChangeListener(api) {
$('video').addEventListener('volumechange', (_) => {
ipcRenderer.send('volumeChanged', api.getVolume());
});
// Emit the initial value as well; as it's persistent between launches.
ipcRenderer.send('volumeChanged', api.getVolume());
}

View File

@ -33,6 +33,10 @@ 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).
### MacOS
If you get an error "is damaged and cant be opened." when launching the app, run `xattr -cr /Applications/YouTube\ Music.app` in Terminal.
## 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
@ -85,7 +89,7 @@ Install the `youtube-music-bin` package from the AUR. For AUR installation instr
- **Tuna-OBS**: Integration with [OBS](https://obsproject.com/)'s plugin [Tuna](https://obsproject.com/forum/resources/tuna.843/) - **Tuna-OBS**: Integration with [OBS](https://obsproject.com/)'s plugin [Tuna](https://obsproject.com/forum/resources/tuna.843/)
- **Video Toggle**: Adds a button to switch between Video/Song mode. can also optionally remove the whole video tab - **Video Toggle**: Adds a [button](https://user-images.githubusercontent.com/28893833/173663950-63e6610e-a532-49b7-9afa-54cb57ddfc15.png) to switch between Video/Song mode. can also optionally remove the whole video tab
--- ---
@ -93,6 +97,12 @@ Install the `youtube-music-bin` package from the AUR. For AUR installation instr
> If using `Hide Menu` option - you can show the menu with the `alt` key (or `escape` if using the in-app-menu plugin) > If using `Hide Menu` option - you can show the menu with the `alt` key (or `escape` if using the in-app-menu plugin)
## Themes
You can load CSS files to change the look of the application (Options > Visual Tweaks > Themes).
Some predefined themes are available in https://github.com/OceanicSquirrel/themes-for-ytmdesktop-player.
## Dev ## Dev
```sh ```sh

View File

@ -1,30 +0,0 @@
const path = require("path");
const NodeEnvironment = require("jest-environment-node");
const { _electron: electron } = require("playwright");
class TestEnvironment extends NodeEnvironment {
constructor(config) {
super(config);
}
async setup() {
await super.setup();
const appPath = path.resolve(__dirname, "..");
this.global.__APP__ = await electron.launch({ args: [appPath] });
}
async teardown() {
if (this.global.__APP__) {
await this.global.__APP__.close();
}
await super.teardown();
}
runScript(script) {
return super.runScript(script);
}
}
module.exports = TestEnvironment;

View File

@ -1,16 +1,29 @@
/** const path = require("path");
* @jest-environment ./tests/environment
*/
describe("YouTube Music App", () => { const { _electron: electron } = require("playwright");
const app = global.__APP__; const { test, expect } = require("@playwright/test");
test("With default settings, app is launched and visible", async () => { process.env.NODE_ENV = "test";
const window = await app.firstWindow();
const title = await window.title();
expect(title).toEqual("YouTube Music");
const url = window.url(); const appPath = path.resolve(__dirname, "..");
expect(url.startsWith("https://music.youtube.com")).toBe(true);
test("YouTube Music App - With default settings, app is launched and visible", async () => {
const app = await electron.launch({
args: [
"--no-sandbox",
"--disable-gpu",
"--whitelisted-ips=",
"--disable-dev-shm-usage",
appPath,
],
}); });
const window = await app.firstWindow();
const title = await window.title();
expect(title).toEqual("YouTube Music");
const url = window.url();
expect(url.startsWith("https://music.youtube.com")).toBe(true);
await app.close();
}); });

View File

@ -2,6 +2,7 @@ const path = require("path");
const { app, Menu, nativeImage, Tray } = require("electron"); const { app, Menu, nativeImage, Tray } = require("electron");
const { restart } = require("./providers/app-controls");
const config = require("./config"); const config = require("./config");
const getSongControls = require("./providers/song-controls"); const getSongControls = require("./providers/song-controls");
@ -65,10 +66,7 @@ module.exports.setUpTray = (app, win) => {
}, },
{ {
label: "Restart App", label: "Restart App",
click: () => { click: restart
app.relaunch();
app.quit();
},
}, },
{ role: "quit" }, { role: "quit" },
]; ];

2161
yarn.lock

File diff suppressed because it is too large Load Diff