mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-13 19:31:46 +00:00
Merge branch 'master' into master
This commit is contained in:
@ -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,
|
||||
});
|
||||
|
||||
@ -4,8 +4,8 @@ const { dialog, app } = require("electron");
|
||||
|
||||
const registerCallback = require("../../providers/song-info");
|
||||
|
||||
// Application ID registered by @xn-oah
|
||||
const clientId = "942539762227630162";
|
||||
// Application ID registered by @Zo-Bro-23
|
||||
const clientId = "1043858434585526382";
|
||||
|
||||
/**
|
||||
* @typedef {Object} Info
|
||||
|
||||
@ -51,7 +51,7 @@ function sendToaster(songInfo) {
|
||||
//download image and get path
|
||||
let imgSrc = notificationImage(songInfo, true);
|
||||
toDelete = {
|
||||
appID: is.dev() ? undefined : "com.github.th-ch.youtube-music",
|
||||
appID: "com.github.th-ch.youtube-music",
|
||||
title: songInfo.title || "Playing",
|
||||
message: songInfo.artist,
|
||||
id: parseInt(Math.random() * 1000000, 10),
|
||||
|
||||
@ -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,37 +1,112 @@
|
||||
const hark = require("hark/hark.bundle.js");
|
||||
|
||||
module.exports = () => {
|
||||
module.exports = (options) => {
|
||||
let isSilent = false;
|
||||
let hasAudioStarted = false;
|
||||
|
||||
document.addEventListener("apiLoaded", () => {
|
||||
const video = document.querySelector("video");
|
||||
const speechEvents = hark(video, {
|
||||
threshold: -100, // dB (-100 = absolute silence, 0 = loudest)
|
||||
interval: 2, // ms
|
||||
});
|
||||
const skipSilence = () => {
|
||||
if (isSilent && !video.paused) {
|
||||
video.currentTime += 0.2; // in s
|
||||
}
|
||||
};
|
||||
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);
|
||||
|
||||
speechEvents.on("speaking", function () {
|
||||
isSilent = false;
|
||||
});
|
||||
document.addEventListener(
|
||||
"audioCanPlay",
|
||||
(e) => {
|
||||
const video = document.querySelector("video");
|
||||
const audioContext = e.detail.audioContext;
|
||||
const sourceNode = e.detail.audioSource;
|
||||
|
||||
speechEvents.on("stopped_speaking", function () {
|
||||
if (!(video.paused || video.seeking || video.ended)) {
|
||||
isSilent = true;
|
||||
// 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);
|
||||
|
||||
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;
|
||||
}
|
||||
} else if (currentVolume < threshold && !isSilent) {
|
||||
for (let i = 0; i < speakingHistory.length; i++) {
|
||||
history += speakingHistory[i];
|
||||
}
|
||||
if (history == 0) {
|
||||
// Silent
|
||||
if (
|
||||
!(
|
||||
video.paused ||
|
||||
video.seeking ||
|
||||
video.ended ||
|
||||
video.muted ||
|
||||
video.volume === 0
|
||||
)
|
||||
) {
|
||||
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;
|
||||
skipSilence();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
video.addEventListener("play", function () {
|
||||
skipSilence();
|
||||
});
|
||||
|
||||
video.addEventListener("seeked", function () {
|
||||
skipSilence();
|
||||
});
|
||||
});
|
||||
video.addEventListener("seeked", function () {
|
||||
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) => {
|
||||
|
||||
@ -2,6 +2,10 @@
|
||||
align-items: unset !important;
|
||||
}
|
||||
|
||||
#main-panel {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.video-switch-button {
|
||||
z-index: 999;
|
||||
box-sizing: border-box;
|
||||
|
||||
@ -38,7 +38,7 @@ function setup(e) {
|
||||
player = $('ytmusic-player');
|
||||
video = $('video');
|
||||
|
||||
$('ytmusic-player-page').prepend(switchButtonDiv);
|
||||
$('#main-panel').append(switchButtonDiv);
|
||||
|
||||
if (options.hideVideo) {
|
||||
$('.video-switch-button-checkbox').checked = false;
|
||||
@ -58,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) {
|
||||
|
||||
@ -33,6 +33,38 @@ module.exports = (win, options) => [
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Alignment",
|
||||
submenu: [
|
||||
{
|
||||
label: "Left",
|
||||
type: "radio",
|
||||
checked: options.align === 'left',
|
||||
click: () => {
|
||||
options.align = 'left';
|
||||
setMenuOptions("video-toggle", options);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Middle",
|
||||
type: "radio",
|
||||
checked: options.align === 'middle',
|
||||
click: () => {
|
||||
options.align = 'middle';
|
||||
setMenuOptions("video-toggle", options);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Right",
|
||||
type: "radio",
|
||||
checked: options.align === 'right',
|
||||
click: () => {
|
||||
options.align = 'right';
|
||||
setMenuOptions("video-toggle", options);
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Force Remove Video Tab",
|
||||
type: "checkbox",
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user