mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-10 18:21:47 +00:00
Merge branch 'master' into use-ToastXML
This commit is contained in:
@ -96,9 +96,79 @@ const defaultConfig = {
|
||||
"saveSize": false,
|
||||
"hotkey": "P"
|
||||
},
|
||||
"captions-selector": {
|
||||
enabled: false,
|
||||
disableCaptions: false
|
||||
},
|
||||
"skip-silences": {
|
||||
onlySkipBeginning: false,
|
||||
},
|
||||
visualizer: {
|
||||
enabled: false,
|
||||
type: "butterchurn",
|
||||
// Config per visualizer
|
||||
butterchurn: {
|
||||
preset: "martin [shadow harlequins shape code] - fata morgana",
|
||||
renderingFrequencyInMs: 500,
|
||||
blendTimeInSeconds: 2.7,
|
||||
},
|
||||
vudio: {
|
||||
effect: "lighting",
|
||||
accuracy: 128,
|
||||
lighting: {
|
||||
maxHeight: 160,
|
||||
maxSize: 12,
|
||||
lineWidth: 1,
|
||||
color: "#49f3f7",
|
||||
shadowBlur: 2,
|
||||
shadowColor: "rgba(244,244,244,.5)",
|
||||
fadeSide: true,
|
||||
prettify: false,
|
||||
horizontalAlign: "center",
|
||||
verticalAlign: "middle",
|
||||
dottify: true,
|
||||
},
|
||||
},
|
||||
wave: {
|
||||
animations: [
|
||||
{
|
||||
type: "Cubes",
|
||||
config: {
|
||||
bottom: true,
|
||||
count: 30,
|
||||
cubeHeight: 5,
|
||||
fillColor: { gradient: ["#FAD961", "#F76B1C"] },
|
||||
lineColor: "rgba(0,0,0,0)",
|
||||
radius: 20,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "Cubes",
|
||||
config: {
|
||||
top: true,
|
||||
count: 12,
|
||||
cubeHeight: 5,
|
||||
fillColor: { gradient: ["#FAD961", "#F76B1C"] },
|
||||
lineColor: "rgba(0,0,0,0)",
|
||||
radius: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "Circles",
|
||||
config: {
|
||||
lineColor: {
|
||||
gradient: ["#FAD961", "#FAD961", "#F76B1C"],
|
||||
rotate: 90,
|
||||
},
|
||||
lineWidth: 4,
|
||||
diameter: 20,
|
||||
count: 10,
|
||||
frequencyBand: "base",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -9,14 +9,15 @@ const setDefaultPluginOptions = (store, plugin) => {
|
||||
}
|
||||
|
||||
const migrations = {
|
||||
">1.19.0": (store) => {
|
||||
">=1.20.0": (store) => {
|
||||
setDefaultPluginOptions(store, "visualizer");
|
||||
|
||||
if (store.get("plugins.notifications.toastStyle") === undefined) {
|
||||
const pluginOptions = store.get("plugins.notifications") || {};
|
||||
store.set("plugins.notifications", {
|
||||
...defaults.plugins.notifications,
|
||||
...pluginOptions,
|
||||
});
|
||||
}
|
||||
},
|
||||
">=1.17.0": (store) => {
|
||||
setDefaultPluginOptions(store, "picture-in-picture");
|
||||
|
||||
@ -96,12 +96,15 @@
|
||||
"@cliqz/adblocker-electron": "^1.25.1",
|
||||
"@ffmpeg/core": "^0.11.0",
|
||||
"@ffmpeg/ffmpeg": "^0.11.6",
|
||||
"@foobar404/wave": "^2.0.4",
|
||||
"Simple-YouTube-Age-Restriction-Bypass": "https://gitpkg.now.sh/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/dist?v2.5.4",
|
||||
"async-mutex": "^0.4.0",
|
||||
"browser-id3-writer": "^4.4.0",
|
||||
"butterchurn": "^2.6.7",
|
||||
"butterchurn-presets": "^2.4.7",
|
||||
"chokidar": "^3.5.3",
|
||||
"custom-electron-prompt": "^1.5.0",
|
||||
"custom-electron-titlebar": "^4.1.2",
|
||||
"custom-electron-titlebar": "^4.1.5",
|
||||
"discord-rpc": "^4.0.1",
|
||||
"electron-better-web-request": "^1.0.1",
|
||||
"electron-debug": "^3.2.0",
|
||||
@ -111,11 +114,11 @@
|
||||
"electron-unhandled": "^4.0.1",
|
||||
"electron-updater": "^4.6.3",
|
||||
"filenamify": "^4.3.0",
|
||||
"hark": "^1.2.3",
|
||||
"html-to-text": "^8.2.1",
|
||||
"md5": "^2.3.0",
|
||||
"mpris-service": "^2.1.2",
|
||||
"node-fetch": "^2.6.7",
|
||||
"vudio": "^2.1.1",
|
||||
"ytdl-core": "^4.11.1",
|
||||
"ytpl": "^2.3.0"
|
||||
},
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
const applyCompressor = () => {
|
||||
const audioContext = new AudioContext();
|
||||
const applyCompressor = (e) => {
|
||||
const audioContext = e.detail.audioContext;
|
||||
|
||||
const compressor = audioContext.createDynamicsCompressor();
|
||||
compressor.threshold.value = -50;
|
||||
@ -8,10 +8,11 @@ const applyCompressor = () => {
|
||||
compressor.attack.value = 0;
|
||||
compressor.release.value = 0.25;
|
||||
|
||||
const source = audioContext.createMediaElementSource(document.querySelector("video"));
|
||||
|
||||
source.connect(compressor);
|
||||
e.detail.audioSource.connect(compressor);
|
||||
compressor.connect(audioContext.destination);
|
||||
};
|
||||
|
||||
module.exports = () => document.addEventListener('apiLoaded', applyCompressor, { once: true, passive: true });
|
||||
module.exports = () =>
|
||||
document.addEventListener("audioCanPlay", applyCompressor, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
15
plugins/captions-selector/back.js
Normal file
15
plugins/captions-selector/back.js
Normal file
@ -0,0 +1,15 @@
|
||||
const { ipcMain, dialog } = require("electron");
|
||||
|
||||
module.exports = () => {
|
||||
ipcMain.handle('captionsSelector', async (_, captionLabels, currentIndex) => {
|
||||
return await dialog.showMessageBox({
|
||||
type: "question",
|
||||
buttons: captionLabels,
|
||||
defaultId: currentIndex,
|
||||
title: "Choose Caption",
|
||||
message: "Choose Caption:",
|
||||
detail: `Current Caption: ${captionLabels[currentIndex]}`,
|
||||
cancelId: -1
|
||||
})
|
||||
})
|
||||
};
|
||||
60
plugins/captions-selector/front.js
Normal file
60
plugins/captions-selector/front.js
Normal file
@ -0,0 +1,60 @@
|
||||
const { ElementFromFile, templatePath } = require("../utils");
|
||||
const { ipcRenderer } = require("electron");
|
||||
|
||||
function $(selector) { return document.querySelector(selector); }
|
||||
|
||||
const captionsSettingsButton = ElementFromFile(
|
||||
templatePath(__dirname, "captionsSettingsTemplate.html")
|
||||
);
|
||||
|
||||
module.exports = (options) => {
|
||||
document.addEventListener('apiLoaded', (event) => setup(event, options), { once: true, passive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* If captions are disabled by default,
|
||||
* unload "captions" module when video changes.
|
||||
*/
|
||||
const videoChanged = (api, options) => {
|
||||
if (options.disableCaptions) {
|
||||
setTimeout(() => api.unloadModule("captions"), 100);
|
||||
}
|
||||
}
|
||||
|
||||
function setup(event, options) {
|
||||
const api = event.detail;
|
||||
|
||||
$("video").addEventListener("srcChanged", () => videoChanged(api, options));
|
||||
|
||||
$(".right-controls-buttons").append(captionsSettingsButton);
|
||||
|
||||
captionsSettingsButton.onclick = function chooseQuality() {
|
||||
api.loadModule("captions");
|
||||
|
||||
const captionTrackList = api.getOption("captions", "tracklist");
|
||||
|
||||
if (captionTrackList?.length) {
|
||||
const currentCaptionTrack = api.getOption("captions", "track");
|
||||
const currentIndex = captionTrackList.indexOf(captionTrackList.find(track => track.languageCode === currentCaptionTrack.languageCode));
|
||||
|
||||
const captionLabels = [
|
||||
...captionTrackList.map(track => track.displayName),
|
||||
'None'
|
||||
];
|
||||
|
||||
ipcRenderer.invoke('captionsSelector', captionLabels, currentIndex).then(promise => {
|
||||
if (promise.response === -1) return;
|
||||
|
||||
const newCaptions = captionTrackList[promise.response];
|
||||
if (newCaptions) {
|
||||
api.loadModule("captions");
|
||||
api.setOption("captions", "track", { languageCode: newCaptions.languageCode });
|
||||
} else {
|
||||
api.unloadModule("captions");
|
||||
}
|
||||
|
||||
setTimeout(() => api.playVideo());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
12
plugins/captions-selector/menu.js
Normal file
12
plugins/captions-selector/menu.js
Normal file
@ -0,0 +1,12 @@
|
||||
const { setOptions } = require('../../config/plugins');
|
||||
|
||||
module.exports = (_win, options) => [
|
||||
{
|
||||
label: "No captions by default",
|
||||
type: "checkbox",
|
||||
checked: options.disabledCaptions,
|
||||
click: (item) => {
|
||||
setOptions("captions-selector", { disableCaptions: item.checked });
|
||||
},
|
||||
}
|
||||
];
|
||||
@ -0,0 +1,13 @@
|
||||
<tp-yt-paper-icon-button class="player-captions-button style-scope ytmusic-player" icon="yt-icons:subtitles"
|
||||
title="Open captions selector" aria-label="Open captions selector" 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="M20 4H4c-1.103 0-2 .897-2 2v12c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2zm-9 6H8v4h3v2H8c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2zm7 0h-3v4h3v2h-3c-1.103 0-2-.897-2-2v-4c0-1.103.897-2 2-2h3v2z"
|
||||
class="style-scope tp-yt-iron-icon"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</tp-yt-iron-icon>
|
||||
</tp-yt-paper-icon-button>
|
||||
@ -47,6 +47,7 @@ const togglePiP = async () => {
|
||||
|
||||
win.webContents.on("before-input-event", blockShortcutsInPiP);
|
||||
|
||||
win.setMaximizable(false);
|
||||
win.setFullScreenable(false);
|
||||
|
||||
runAdaptors();
|
||||
@ -62,6 +63,7 @@ const togglePiP = async () => {
|
||||
}
|
||||
} else {
|
||||
win.webContents.removeListener("before-input-event", blockShortcutsInPiP);
|
||||
win.setMaximizable(true);
|
||||
win.setFullScreenable(true);
|
||||
|
||||
runAdaptors();
|
||||
|
||||
@ -10,6 +10,21 @@ const pipButton = ElementFromFile(
|
||||
templatePath(__dirname, "picture-in-picture.html")
|
||||
);
|
||||
|
||||
// will also clone
|
||||
function replaceButton(query, button) {
|
||||
const svg = button.querySelector("#icon svg").cloneNode(true);
|
||||
button.replaceWith(button.cloneNode(true));
|
||||
button.remove();
|
||||
const newButton = $(query);
|
||||
newButton.querySelector("#icon").appendChild(svg);
|
||||
return newButton;
|
||||
}
|
||||
|
||||
function cloneButton(query) {
|
||||
replaceButton(query, $(query));
|
||||
return $(query);
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
if (!menu) {
|
||||
menu = getSongMenu();
|
||||
@ -30,9 +45,6 @@ global.togglePictureInPicture = () => {
|
||||
|
||||
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');
|
||||
@ -44,7 +56,7 @@ const listenForToggle = () => {
|
||||
|
||||
ipcRenderer.on('pip-toggle', (_, isPip) => {
|
||||
if (isPip) {
|
||||
$(".exit-fullscreen-button").replaceWith(clonedExitButton);
|
||||
replaceButton(".exit-fullscreen-button", originalExitButton).onclick = () => togglePictureInPicture();
|
||||
player.onDoubleClick_ = () => {};
|
||||
expandMenu.onmouseleave = () => middleControls.click();
|
||||
if (!playerPage.playerPageOpen_) {
|
||||
@ -67,10 +79,8 @@ function observeMenu(options) {
|
||||
"apiLoaded",
|
||||
() => {
|
||||
listenForToggle();
|
||||
const minButton = $(".player-minimize-button");
|
||||
// remove native listeners
|
||||
minButton.replaceWith(minButton.cloneNode(true));
|
||||
$(".player-minimize-button").onclick = () => {
|
||||
cloneButton(".player-minimize-button").onclick = () => {
|
||||
global.togglePictureInPicture();
|
||||
setTimeout(() => $('#player').click());
|
||||
};
|
||||
|
||||
@ -1,31 +1,55 @@
|
||||
const hark = require("hark/hark.bundle.js");
|
||||
|
||||
module.exports = (options) => {
|
||||
let isSilent = false;
|
||||
let hasAudioStarted = false;
|
||||
|
||||
document.addEventListener("apiLoaded", () => {
|
||||
const smoothing = 0.1;
|
||||
const threshold = -100; // dB (-100 = absolute silence, 0 = loudest)
|
||||
const interval = 2; // ms
|
||||
const history = 10;
|
||||
const speakingHistory = Array(history).fill(0);
|
||||
|
||||
document.addEventListener(
|
||||
"audioCanPlay",
|
||||
(e) => {
|
||||
const video = document.querySelector("video");
|
||||
const speechEvents = hark(video, {
|
||||
threshold: -100, // dB (-100 = absolute silence, 0 = loudest)
|
||||
interval: 2, // ms
|
||||
});
|
||||
const skipSilence = () => {
|
||||
if (options.onlySkipBeginning && hasAudioStarted) {
|
||||
return;
|
||||
}
|
||||
const audioContext = e.detail.audioContext;
|
||||
const sourceNode = e.detail.audioSource;
|
||||
|
||||
if (isSilent && !video.paused) {
|
||||
video.currentTime += 0.2; // in s
|
||||
}
|
||||
};
|
||||
// Use an audio analyser similar to Hark
|
||||
// https://github.com/otalk/hark/blob/master/hark.bundle.js
|
||||
const analyser = audioContext.createAnalyser();
|
||||
analyser.fftSize = 512;
|
||||
analyser.smoothingTimeConstant = smoothing;
|
||||
const fftBins = new Float32Array(analyser.frequencyBinCount);
|
||||
|
||||
speechEvents.on("speaking", function () {
|
||||
sourceNode.connect(analyser);
|
||||
analyser.connect(audioContext.destination);
|
||||
|
||||
const looper = () => {
|
||||
setTimeout(() => {
|
||||
const currentVolume = getMaxVolume(analyser, fftBins);
|
||||
|
||||
let history = 0;
|
||||
if (currentVolume > threshold && isSilent) {
|
||||
// trigger quickly, short history
|
||||
for (
|
||||
let i = speakingHistory.length - 3;
|
||||
i < speakingHistory.length;
|
||||
i++
|
||||
) {
|
||||
history += speakingHistory[i];
|
||||
}
|
||||
if (history >= 2) {
|
||||
// Not silent
|
||||
isSilent = false;
|
||||
hasAudioStarted = true;
|
||||
});
|
||||
|
||||
speechEvents.on("stopped_speaking", function () {
|
||||
}
|
||||
} else if (currentVolume < threshold && !isSilent) {
|
||||
for (let i = 0; i < speakingHistory.length; i++) {
|
||||
history += speakingHistory[i];
|
||||
}
|
||||
if (history == 0) {
|
||||
// Silent
|
||||
if (
|
||||
!(
|
||||
video.paused ||
|
||||
@ -38,7 +62,25 @@ module.exports = (options) => {
|
||||
isSilent = true;
|
||||
skipSilence();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
speakingHistory.shift();
|
||||
speakingHistory.push(0 + (currentVolume > threshold));
|
||||
|
||||
looper();
|
||||
}, interval);
|
||||
};
|
||||
looper();
|
||||
|
||||
const skipSilence = () => {
|
||||
if (options.onlySkipBeginning && hasAudioStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSilent && !video.paused) {
|
||||
video.currentTime += 0.2; // in s
|
||||
}
|
||||
};
|
||||
|
||||
video.addEventListener("play", function () {
|
||||
hasAudioStarted = false;
|
||||
@ -49,5 +91,22 @@ module.exports = (options) => {
|
||||
hasAudioStarted = false;
|
||||
skipSilence();
|
||||
});
|
||||
});
|
||||
},
|
||||
{
|
||||
passive: true,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function getMaxVolume(analyser, fftBins) {
|
||||
var maxVolume = -Infinity;
|
||||
analyser.getFloatFrequencyData(fftBins);
|
||||
|
||||
for (var i = 4, ii = fftBins.length; i < ii; i++) {
|
||||
if (fftBins[i] > maxVolume && fftBins[i] < 0) {
|
||||
maxVolume = fftBins[i];
|
||||
}
|
||||
}
|
||||
|
||||
return maxVolume;
|
||||
}
|
||||
|
||||
@ -63,7 +63,7 @@ module.exports = (win) => {
|
||||
|
||||
// If the page is ready, register the callback
|
||||
win.once("ready-to-show", () => {
|
||||
controls = [previous, playPause, next, like, dislike];
|
||||
controls = [previous, playPause, next, dislike, like];
|
||||
|
||||
// Register the callback
|
||||
registerCallback((songInfo) => {
|
||||
|
||||
@ -22,7 +22,6 @@
|
||||
color: #fff;
|
||||
padding-right: 120px;
|
||||
position: absolute;
|
||||
left: var(--align);
|
||||
}
|
||||
|
||||
.video-switch-button:before {
|
||||
|
||||
@ -31,21 +31,6 @@ module.exports = (_options) => {
|
||||
document.addEventListener("apiLoaded", setup, { once: true, passive: true });
|
||||
}
|
||||
}
|
||||
const mainpanel = document.querySelector("#main-panel");
|
||||
switch (_options.align) {
|
||||
case "right": {
|
||||
mainpanel.style.setProperty("--align", "calc(100% - 240px)");
|
||||
return;
|
||||
}
|
||||
case "middle": {
|
||||
mainpanel.style.setProperty("--align", "calc(50% - 120px)");
|
||||
return;
|
||||
}
|
||||
default:
|
||||
case "left": {
|
||||
mainpanel.style.setProperty("--align", "0px");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function setup(e) {
|
||||
@ -73,6 +58,21 @@ function setup(e) {
|
||||
video.addEventListener('srcChanged', videoStarted);
|
||||
|
||||
observeThumbnail();
|
||||
|
||||
switch (options.align) {
|
||||
case "right": {
|
||||
switchButtonDiv.style.left = "calc(100% - 240px)";
|
||||
return;
|
||||
}
|
||||
case "middle": {
|
||||
switchButtonDiv.style.left = "calc(50% - 120px)";
|
||||
return;
|
||||
}
|
||||
default:
|
||||
case "left": {
|
||||
switchButtonDiv.style.left = "0px";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function changeDisplay(showVideo) {
|
||||
|
||||
6
plugins/visualizer/back.js
Normal file
6
plugins/visualizer/back.js
Normal file
@ -0,0 +1,6 @@
|
||||
const { injectCSS } = require("../utils");
|
||||
const path = require("path");
|
||||
|
||||
module.exports = (win, options) => {
|
||||
injectCSS(win.webContents, path.join(__dirname, "empty-player.css"));
|
||||
};
|
||||
9
plugins/visualizer/empty-player.css
Normal file
9
plugins/visualizer/empty-player.css
Normal file
@ -0,0 +1,9 @@
|
||||
#player {
|
||||
margin: 0 !important;
|
||||
background: black;
|
||||
}
|
||||
|
||||
#song-image,
|
||||
#song-video {
|
||||
display: none !important;
|
||||
}
|
||||
61
plugins/visualizer/front.js
Normal file
61
plugins/visualizer/front.js
Normal file
@ -0,0 +1,61 @@
|
||||
const defaultConfig = require("../../config/defaults");
|
||||
|
||||
module.exports = (options) => {
|
||||
const optionsWithDefaults = {
|
||||
...defaultConfig.plugins.visualizer,
|
||||
...options,
|
||||
};
|
||||
const VisualizerType = require(`./visualizers/${optionsWithDefaults.type}`);
|
||||
|
||||
document.addEventListener(
|
||||
"audioCanPlay",
|
||||
(e) => {
|
||||
const video = document.querySelector("video");
|
||||
const visualizerContainer = document.querySelector("#player");
|
||||
|
||||
let canvas = document.getElementById("visualizer");
|
||||
if (!canvas) {
|
||||
canvas = document.createElement("canvas");
|
||||
canvas.id = "visualizer";
|
||||
canvas.style.position = "absolute";
|
||||
canvas.style.background = "black";
|
||||
visualizerContainer.append(canvas);
|
||||
}
|
||||
|
||||
const resizeCanvas = () => {
|
||||
canvas.width = visualizerContainer.clientWidth;
|
||||
canvas.height = visualizerContainer.clientHeight;
|
||||
};
|
||||
resizeCanvas();
|
||||
|
||||
const gainNode = e.detail.audioContext.createGain();
|
||||
gainNode.gain.value = 1.25;
|
||||
e.detail.audioSource.connect(gainNode);
|
||||
|
||||
const visualizer = new VisualizerType(
|
||||
e.detail.audioContext,
|
||||
e.detail.audioSource,
|
||||
visualizerContainer,
|
||||
canvas,
|
||||
gainNode,
|
||||
video.captureStream(),
|
||||
optionsWithDefaults[optionsWithDefaults.type]
|
||||
);
|
||||
|
||||
const resizeVisualizer = (width, height) => {
|
||||
resizeCanvas();
|
||||
visualizer.resize(width, height);
|
||||
};
|
||||
resizeVisualizer(canvas.width, canvas.height);
|
||||
const visualizerContainerObserver = new ResizeObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
resizeVisualizer(entry.contentRect.width, entry.contentRect.height);
|
||||
});
|
||||
});
|
||||
visualizerContainerObserver.observe(visualizerContainer);
|
||||
|
||||
visualizer.render();
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
};
|
||||
23
plugins/visualizer/menu.js
Normal file
23
plugins/visualizer/menu.js
Normal file
@ -0,0 +1,23 @@
|
||||
const { readdirSync } = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const { setMenuOptions } = require("../../config/plugins");
|
||||
|
||||
const visualizerTypes = readdirSync(path.join(__dirname, "visualizers")).map(
|
||||
(filename) => path.parse(filename).name
|
||||
);
|
||||
|
||||
module.exports = (win, options) => [
|
||||
{
|
||||
label: "Type",
|
||||
submenu: visualizerTypes.map((visualizerType) => ({
|
||||
label: visualizerType,
|
||||
type: "radio",
|
||||
checked: options.type === visualizerType,
|
||||
click: () => {
|
||||
options.type = visualizerType;
|
||||
setMenuOptions("visualizer", options);
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
46
plugins/visualizer/visualizers/butterchurn.js
Normal file
46
plugins/visualizer/visualizers/butterchurn.js
Normal file
@ -0,0 +1,46 @@
|
||||
const butterchurn = require("butterchurn");
|
||||
const butterchurnPresets = require("butterchurn-presets");
|
||||
|
||||
const presets = butterchurnPresets.getPresets();
|
||||
|
||||
class ButterchurnVisualizer {
|
||||
constructor(
|
||||
audioContext,
|
||||
audioSource,
|
||||
visualizerContainer,
|
||||
canvas,
|
||||
audioNode,
|
||||
stream,
|
||||
options
|
||||
) {
|
||||
this.visualizer = butterchurn.default.createVisualizer(
|
||||
audioContext,
|
||||
canvas,
|
||||
{
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
}
|
||||
);
|
||||
|
||||
const preset = presets[options.preset];
|
||||
this.visualizer.loadPreset(preset, options.blendTimeInSeconds);
|
||||
|
||||
this.visualizer.connectAudio(audioNode);
|
||||
|
||||
this.renderingFrequencyInMs = options.renderingFrequencyInMs;
|
||||
}
|
||||
|
||||
resize(width, height) {
|
||||
this.visualizer.setRendererSize(width, height);
|
||||
}
|
||||
|
||||
render() {
|
||||
const renderVisualizer = () => {
|
||||
requestAnimationFrame(() => renderVisualizer());
|
||||
this.visualizer.render();
|
||||
};
|
||||
setTimeout(renderVisualizer(), this.renderingFrequencyInMs);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ButterchurnVisualizer;
|
||||
33
plugins/visualizer/visualizers/vudio.js
Normal file
33
plugins/visualizer/visualizers/vudio.js
Normal file
@ -0,0 +1,33 @@
|
||||
const Vudio = require("vudio/umd/vudio");
|
||||
|
||||
class VudioVisualizer {
|
||||
constructor(
|
||||
audioContext,
|
||||
audioSource,
|
||||
visualizerContainer,
|
||||
canvas,
|
||||
audioNode,
|
||||
stream,
|
||||
options
|
||||
) {
|
||||
this.visualizer = new Vudio(stream, canvas, {
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
// Visualizer config
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
resize(width, height) {
|
||||
this.visualizer.setOption({
|
||||
width: width,
|
||||
height: height,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
this.visualizer.dance();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = VudioVisualizer;
|
||||
31
plugins/visualizer/visualizers/wave.js
Normal file
31
plugins/visualizer/visualizers/wave.js
Normal file
@ -0,0 +1,31 @@
|
||||
const { Wave } = require("@foobar404/wave");
|
||||
|
||||
class WaveVisualizer {
|
||||
constructor(
|
||||
audioContext,
|
||||
audioSource,
|
||||
visualizerContainer,
|
||||
canvas,
|
||||
audioNode,
|
||||
stream,
|
||||
options
|
||||
) {
|
||||
this.visualizer = new Wave(
|
||||
{ context: audioContext, source: audioSource },
|
||||
canvas
|
||||
);
|
||||
options.animations.forEach((animation) => {
|
||||
this.visualizer.addAnimation(
|
||||
eval(`new this.visualizer.animations.${animation.type}(
|
||||
${JSON.stringify(animation.config)}
|
||||
)`)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
resize(width, height) {}
|
||||
|
||||
render() {}
|
||||
}
|
||||
|
||||
module.exports = WaveVisualizer;
|
||||
26
preload.js
26
preload.js
@ -97,6 +97,32 @@ function listenForApiLoad() {
|
||||
}
|
||||
|
||||
function onApiLoaded() {
|
||||
const video = document.querySelector("video");
|
||||
const audioContext = new AudioContext();
|
||||
const audioSource = audioContext.createMediaElementSource(video);
|
||||
|
||||
video.addEventListener(
|
||||
"loadstart",
|
||||
() => {
|
||||
// Emit "audioCanPlay" for each video
|
||||
video.addEventListener(
|
||||
"canplaythrough",
|
||||
() => {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("audioCanPlay", {
|
||||
detail: {
|
||||
audioContext: audioContext,
|
||||
audioSource: audioSource,
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
|
||||
document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api }));
|
||||
ipcRenderer.send('apiLoaded');
|
||||
|
||||
|
||||
@ -108,6 +108,8 @@ winget install th-ch.YouTubeMusic
|
||||
|
||||
- **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
|
||||
|
||||
- **Visualizer**: Different music visualizers
|
||||
|
||||
---
|
||||
|
||||
- **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
|
||||
|
||||
90
yarn.lock
90
yarn.lock
@ -202,6 +202,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.15.tgz#8e66775fb523599acb6a289e12929fa5ab0954d8"
|
||||
integrity sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==
|
||||
|
||||
"@babel/runtime@^7.0.0":
|
||||
version "7.20.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd"
|
||||
integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
"@babel/runtime@^7.7.2":
|
||||
version "7.12.13"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.13.tgz#0a21452352b02542db0ffb928ac2d3ca7cb6d66d"
|
||||
@ -374,6 +381,11 @@
|
||||
regenerator-runtime "^0.13.7"
|
||||
resolve-url "^0.2.1"
|
||||
|
||||
"@foobar404/wave@^2.0.4":
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@foobar404/wave/-/wave-2.0.4.tgz#c9bc54c41b18642c6a4587851e28b8f858af98b0"
|
||||
integrity sha512-FEyg37hDvQtrQVlFxbit7ov5e487BjsR32bZfJ4oAb5i+NnlbGaNyy6iYBZ8ocVHo8fgug+SL+mFdDTzqjvPww==
|
||||
|
||||
"@humanwhocodes/config-array@^0.5.0":
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
|
||||
@ -1364,6 +1376,14 @@ aws4@^1.8.0:
|
||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
|
||||
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
|
||||
|
||||
babel-runtime@^6.26.0:
|
||||
version "6.26.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
|
||||
integrity sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==
|
||||
dependencies:
|
||||
core-js "^2.4.0"
|
||||
regenerator-runtime "^0.11.0"
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
@ -1557,6 +1577,23 @@ builtin-modules@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887"
|
||||
integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==
|
||||
|
||||
butterchurn-presets@^2.4.7:
|
||||
version "2.4.7"
|
||||
resolved "https://registry.yarnpkg.com/butterchurn-presets/-/butterchurn-presets-2.4.7.tgz#41e4e37cd3af2aec124fa8062352816100956c29"
|
||||
integrity sha512-4MdM8ripz/VfH1BCldrIKdAc/1ryJFBDvqlyow6Ivo1frwj0H3duzvSMFC7/wIjAjxb1QpwVHVqGqS9uAFKhpg==
|
||||
dependencies:
|
||||
babel-runtime "^6.26.0"
|
||||
ecma-proposal-math-extensions "0.0.2"
|
||||
lodash "^4.17.4"
|
||||
|
||||
butterchurn@^2.6.7:
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/butterchurn/-/butterchurn-2.6.7.tgz#1ff0c1365731a4fb7ada7bb16ae1c6f09a110c12"
|
||||
integrity sha512-BJiRA8L0L2+84uoG2SSfkp0kclBuN+vQKf217pK7pMlwEO2ZEg3MtO2/o+l8Qpr8Nbejg8tmL1ZHD1jmhiaaqg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.0.0"
|
||||
ecma-proposal-math-extensions "0.0.2"
|
||||
|
||||
cacheable-request@^6.0.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912"
|
||||
@ -1892,6 +1929,11 @@ convert-source-map@^1.7.0:
|
||||
dependencies:
|
||||
safe-buffer "~5.1.1"
|
||||
|
||||
core-js@^2.4.0:
|
||||
version "2.6.12"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
|
||||
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
|
||||
|
||||
core-util-is@1.0.2, core-util-is@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||
@ -1939,10 +1981,10 @@ custom-electron-prompt@^1.5.0:
|
||||
resolved "https://registry.yarnpkg.com/custom-electron-prompt/-/custom-electron-prompt-1.5.0.tgz#b514267f28e9f0d61011e03f76b1e59473af33d4"
|
||||
integrity sha512-DO+CIfO8c5lG+yzAkXD8PbFunPQ+WCJ4QeGN8bCvos7Fxt3xFDW0Qdnm1v9DKkAMj7iG0SujhdfNzsrtA4fl5g==
|
||||
|
||||
custom-electron-titlebar@^4.1.2:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/custom-electron-titlebar/-/custom-electron-titlebar-4.1.3.tgz#6b5d0527858bae89314f3d0b4e6a0b9be4efad81"
|
||||
integrity sha512-9tqiRxp7KG3qgS5Qh0ejSTwzqJ/pkB8RXQrvZHmilIzaWFmvjHiaSnHgCj+1iJcnhvzACYXFiFo2fxD64kFi4A==
|
||||
custom-electron-titlebar@^4.1.5:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/custom-electron-titlebar/-/custom-electron-titlebar-4.1.5.tgz#5032759ee5594fd618a62cff0de46150fd8e2b24"
|
||||
integrity sha512-KpVmO+Fz34zNw/jYbOD6YwxPr6h2hgZmNbjy0AvhlXiH4dxeZJXDlMcBX08NqsESymMQvIWwsGaA//YGW1bXjA==
|
||||
|
||||
dashdash@^1.12.0:
|
||||
version "1.14.1"
|
||||
@ -2266,6 +2308,11 @@ ecc-jsbn@~0.1.1:
|
||||
jsbn "~0.1.0"
|
||||
safer-buffer "^2.1.0"
|
||||
|
||||
ecma-proposal-math-extensions@0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/ecma-proposal-math-extensions/-/ecma-proposal-math-extensions-0.0.2.tgz#a6a3d64819db70cee0d2e1976b6315d00e4714a0"
|
||||
integrity sha512-80BnDp2Fn7RxXlEr5HHZblniY4aQ97MOAicdWWpSo0vkQiISSE9wLR4SqxKsu4gCtXFBIPPzy8JMhay4NWRg/Q==
|
||||
|
||||
ejs@^3.1.6:
|
||||
version "3.1.7"
|
||||
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.7.tgz#c544d9c7f715783dd92f0bddcf73a59e6962d006"
|
||||
@ -3522,13 +3569,6 @@ hard-rejection@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
|
||||
integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==
|
||||
|
||||
hark@^1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/hark/-/hark-1.2.3.tgz#959981400f561be5580ecd4321a9f55b16bacbd0"
|
||||
integrity sha512-u68vz9SCa38ESiFJSDjqK8XbXqWzyot7Cj6Y2b6jk2NJ+II3MY2dIrLMg/kjtIAun4Y1DHF/20hfx4rq1G5GMg==
|
||||
dependencies:
|
||||
wildemitter "^1.2.0"
|
||||
|
||||
has-ansi@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
|
||||
@ -4289,9 +4329,9 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
|
||||
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
|
||||
|
||||
json5@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
|
||||
integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
|
||||
integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
|
||||
dependencies:
|
||||
minimist "^1.2.0"
|
||||
|
||||
@ -4527,7 +4567,7 @@ lodash.truncate@^4.4.2:
|
||||
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
|
||||
integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
|
||||
|
||||
lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.3.0:
|
||||
lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.3.0:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
@ -5595,6 +5635,16 @@ redent@^4.0.0:
|
||||
indent-string "^5.0.0"
|
||||
strip-indent "^4.0.0"
|
||||
|
||||
regenerator-runtime@^0.11.0:
|
||||
version "0.11.1"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
|
||||
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
|
||||
|
||||
regenerator-runtime@^0.13.11:
|
||||
version "0.13.11"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
|
||||
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
|
||||
|
||||
regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7:
|
||||
version "0.13.7"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
|
||||
@ -6562,6 +6612,11 @@ verror@1.10.0, verror@^1.10.0:
|
||||
core-util-is "1.0.2"
|
||||
extsprintf "^1.2.0"
|
||||
|
||||
vudio@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/vudio/-/vudio-2.1.1.tgz#af256c4e1c8ae8bdbbba0e718f8106a30a44e96e"
|
||||
integrity sha512-VkFQcFt/b/kpF5Eg5Sq+oXUo1Zp5aRFF4BSmIrOzau5o+5WMWwX9ae/EGJZstCyZFiCTU5iw1Y+u2BCGW6Y6Jw==
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
@ -6612,11 +6667,6 @@ widest-line@^3.1.0:
|
||||
dependencies:
|
||||
string-width "^4.0.0"
|
||||
|
||||
wildemitter@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/wildemitter/-/wildemitter-1.2.1.tgz#9da3b5ca498e4378628d1783145493c70a10b774"
|
||||
integrity sha512-UMmSUoIQSir+XbBpTxOTS53uJ8s/lVhADCkEbhfRjUGFDPme/XGOb0sBWLx5sTz7Wx/2+TlAw1eK9O5lw5PiEw==
|
||||
|
||||
word-wrap@^1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
||||
|
||||
Reference in New Issue
Block a user