Merge pull request #1065 from Araxeus/add-crossfade-menu-options

[crossfade] add menu options
This commit is contained in:
th-ch
2023-04-04 22:03:49 +02:00
committed by GitHub
9 changed files with 194 additions and 33 deletions

View File

@ -105,6 +105,13 @@ const defaultConfig = {
"skip-silences": {
onlySkipBeginning: false,
},
"crossfade": {
enabled: false,
fadeInDuration: 1500, // ms
fadeOutDuration: 5000, // ms
secondsBeforeEnd: 10, // s
fadeScaling: "linear", // 'linear', 'logarithmic' or a positive number in dB
},
visualizer: {
enabled: false,
type: "butterchurn",

View File

@ -2,6 +2,7 @@ const { ipcRenderer, ipcMain } = require("electron");
const defaultConfig = require("./defaults");
const { getOptions, setOptions, setMenuOptions } = require("./plugins");
const { sendToFront } = require("../providers/app-controls");
const activePlugins = {};
/**
@ -58,6 +59,9 @@ module.exports.PluginConfig = class PluginConfig {
#defaultConfig;
#enableFront;
#subscribers = {};
#allSubscribers = [];
constructor(name, { enableFront = false, initialOptions = undefined } = {}) {
const pluginDefaultConfig = defaultConfig.plugins[name] || {};
const pluginConfig = initialOptions || getOptions(name) || {};
@ -80,11 +84,13 @@ module.exports.PluginConfig = class PluginConfig {
set = (option, value) => {
this.#config[option] = value;
this.#onChange(option);
this.#save();
};
toggle = (option) => {
this.#config[option] = !this.#config[option];
this.#onChange(option);
this.#save();
};
@ -93,7 +99,18 @@ module.exports.PluginConfig = class PluginConfig {
};
setAll = (options) => {
this.#config = { ...this.#config, ...options };
if (!options || typeof options !== "object")
throw new Error("Options must be an object.");
let changed = false;
for (const [key, val] of Object.entries(options)) {
if (this.#config[key] !== val) {
this.#config[key] = val;
this.#onChange(key, false);
changed = true;
}
}
if (changed) this.#allSubscribers.forEach((fn) => fn(this.#config));
this.#save();
};
@ -109,6 +126,15 @@ module.exports.PluginConfig = class PluginConfig {
setAndMaybeRestart = (option, value) => {
this.#config[option] = value;
setMenuOptions(this.#name, this.#config);
this.#onChange(option);
};
subscribe = (valueName, fn) => {
this.#subscribers[valueName] = fn;
};
subscribeAll = (fn) => {
this.#allSubscribers.push(fn);
};
/** Called only from back */
@ -116,24 +142,64 @@ module.exports.PluginConfig = class PluginConfig {
setOptions(this.#name, this.#config);
}
#onChange(valueName, single = true) {
this.#subscribers[valueName]?.(this.#config[valueName]);
if (single) this.#allSubscribers.forEach((fn) => fn(this.#config));
}
#setupFront() {
const ignoredMethods = ["subscribe", "subscribeAll"];
if (process.type === "renderer") {
for (const [fnName, fn] of Object.entries(this)) {
if (typeof fn !== "function") return;
if (typeof fn !== "function" || fn.name in ignoredMethods) return;
this[fnName] = async (...args) => {
return await ipcRenderer.invoke(
`${this.name}-config-${fnName}`,
...args,
);
};
this.subscribe = (valueName, fn) => {
if (valueName in this.#subscribers) {
console.error(`Already subscribed to ${valueName}`);
}
this.#subscribers[valueName] = fn;
ipcRenderer.on(
`${this.name}-config-changed-${valueName}`,
(_, value) => {
fn(value);
},
);
ipcRenderer.send(`${this.name}-config-subscribe`, valueName);
};
this.subscribeAll = (fn) => {
ipcRenderer.on(`${this.name}-config-changed`, (_, value) => {
fn(value);
});
ipcRenderer.send(`${this.name}-config-subscribe-all`);
};
}
} else if (process.type === "browser") {
for (const [fnName, fn] of Object.entries(this)) {
if (typeof fn !== "function") return;
if (typeof fn !== "function" || fn.name in ignoredMethods) return;
ipcMain.handle(`${this.name}-config-${fnName}`, (_, ...args) => {
return fn(...args);
});
}
ipcMain.on(`${this.name}-config-subscribe`, (_, valueName) => {
this.subscribe(valueName, (value) => {
sendToFront(`${this.name}-config-changed-${valueName}`, value);
});
});
ipcMain.on(`${this.name}-config-subscribe-all`, () => {
this.subscribeAll((value) => {
sendToFront(`${this.name}-config-changed`, value);
});
});
}
}
};

View File

@ -115,7 +115,7 @@
"browser-id3-writer": "^4.4.0",
"butterchurn": "^2.6.7",
"butterchurn-presets": "^2.4.7",
"custom-electron-prompt": "^1.5.4",
"custom-electron-prompt": "^1.5.7",
"custom-electron-titlebar": "^4.1.6",
"electron-better-web-request": "^1.0.1",
"electron-debug": "^3.2.0",

View File

@ -1,7 +1,9 @@
const { ipcMain } = require("electron");
const { Innertube } = require("youtubei.js");
module.exports = async (win, options) => {
require("./config");
module.exports = async () => {
const yt = await Innertube.create();
ipcMain.handle("audio-url", async (_, videoID) => {

View File

@ -0,0 +1,3 @@
const { PluginConfig } = require("../../config/dynamic");
const config = new PluginConfig("crossfade", { enableFront: true });
module.exports = { ...config };

View File

@ -8,13 +8,12 @@ let transitionAudio; // Howler audio used to fade out the current music
let firstVideo = true;
let waitForTransition;
// Crossfade options that can be overridden in plugin options
let crossfadeOptions = {
fadeInDuration: 1500, // ms
fadeOutDuration: 5000, // ms
exitMusicBeforeEnd: 10, // s
fadeScaling: "linear",
};
const defaultConfig = require("../../config/defaults").plugins.crossfade;
const configProvider = require("./config");
let config;
const configGetNum = (key) => Number(config[key]) || defaultConfig[key];
const getStreamURL = async (videoID) => {
const url = await ipcRenderer.invoke("audio-url", videoID);
@ -32,7 +31,7 @@ const isReadyToCrossfade = () => {
const watchVideoIDChanges = (cb) => {
navigation.addEventListener("navigate", (event) => {
const currentVideoID = getVideoIDFromURL(
event.currentTarget.currentEntry.url
event.currentTarget.currentEntry.url,
);
const nextVideoID = getVideoIDFromURL(event.destination.url);
@ -67,9 +66,10 @@ const createAudioForCrossfade = async (url) => {
const syncVideoWithTransitionAudio = async () => {
const video = document.querySelector("video");
const videoFader = new VolumeFader(video, {
fadeScaling: crossfadeOptions.fadeScaling,
fadeDuration: crossfadeOptions.fadeInDuration,
fadeScaling: configGetNum("fadeScaling"),
fadeDuration: configGetNum("fadeInDuration"),
});
await transitionAudio.play();
@ -94,8 +94,7 @@ const syncVideoWithTransitionAudio = async () => {
// Exit just before the end for the transition
const transitionBeforeEnd = () => {
if (
video.currentTime >=
video.duration - crossfadeOptions.exitMusicBeforeEnd &&
video.currentTime >= video.duration - configGetNum("secondsBeforeEnd") &&
isReadyToCrossfade()
) {
video.removeEventListener("timeupdate", transitionBeforeEnd);
@ -115,7 +114,7 @@ const onApiLoaded = () => {
});
};
const crossfade = (cb) => {
const crossfade = async (cb) => {
if (!isReadyToCrossfade()) {
cb();
return;
@ -130,8 +129,8 @@ const crossfade = (cb) => {
const fader = new VolumeFader(transitionAudio._sounds[0]._node, {
initialVolume: video.volume,
fadeScaling: crossfadeOptions.fadeScaling,
fadeDuration: crossfadeOptions.fadeOutDuration,
fadeScaling: configGetNum("fadeScaling"),
fadeDuration: configGetNum("fadeOutDuration"),
});
// Fade out the music
@ -142,11 +141,12 @@ const crossfade = (cb) => {
});
};
module.exports = (options) => {
crossfadeOptions = {
...crossfadeOptions,
options,
};
module.exports = async () => {
config = await configProvider.getAll();
configProvider.subscribeAll((newConfig) => {
config = newConfig;
});
document.addEventListener("apiLoaded", onApiLoaded, {
once: true,

72
plugins/crossfade/menu.js Normal file
View File

@ -0,0 +1,72 @@
const config = require("./config");
const defaultOptions = require("../../config/defaults").plugins.crossfade;
const prompt = require("custom-electron-prompt");
const promptOptions = require("../../providers/prompt-options");
module.exports = (win) => [
{
label: "Advanced",
click: async () => {
const newOptions = await promptCrossfadeValues(win, config.getAll());
if (newOptions) config.setAll(newOptions);
},
},
];
async function promptCrossfadeValues(win, options) {
const res = await prompt(
{
title: "Crossfade Options",
type: "multiInput",
multiInputOptions: [
{
label: "Fade in duration (ms)",
value: options.fadeInDuration || defaultOptions.fadeInDuration,
inputAttrs: {
type: "number",
required: true,
min: 0,
step: 100,
},
},
{
label: "Fade out duration (ms)",
value: options.fadeOutDuration || defaultOptions.fadeOutDuration,
inputAttrs: {
type: "number",
required: true,
min: 0,
step: 100,
},
},
{
label: "Crossfade x seconds before end",
value:
options.secondsBeforeEnd || defaultOptions.secondsBeforeEnd,
inputAttrs: {
type: "number",
required: true,
min: 0,
},
},
{
label: "Fade scaling",
selectOptions: { linear: "Linear", logarithmic: "Logarithmic" },
value: options.fadeScaling || defaultOptions.fadeScaling,
},
],
resizable: true,
height: 360,
...promptOptions(),
},
win,
).catch(console.error);
if (!res) return undefined;
return {
fadeInDuration: Number(res[0]),
fadeOutDuration: Number(res[1]),
secondsBeforeEnd: Number(res[2]),
fadeScaling: res[3],
};
}

View File

@ -1,12 +1,10 @@
const path = require("path");
const is = require("electron-is");
const { app, BrowserWindow, ipcMain, ipcRenderer } = require("electron");
const config = require("../config");
module.exports.restart = () => {
is.main() ? restart() : ipcRenderer.send('restart');
process.type === 'browser' ? restart() : ipcRenderer.send('restart');
};
module.exports.setupAppControls = () => {
@ -21,3 +19,16 @@ function restart() {
// execPath will be undefined if not running portable app, resulting in default behavior
app.quit();
}
function sendToFront(channel, ...args) {
BrowserWindow.getAllWindows().forEach(win => {
win.webContents.send(channel, ...args);
});
}
module.exports.sendToFront =
process.type === 'browser'
? sendToFront
: () => {
console.error('sendToFront called from renderer');
};

View File

@ -2382,12 +2382,12 @@ __metadata:
languageName: node
linkType: hard
"custom-electron-prompt@npm:^1.5.4":
version: 1.5.4
resolution: "custom-electron-prompt@npm:1.5.4"
"custom-electron-prompt@npm:^1.5.7":
version: 1.5.7
resolution: "custom-electron-prompt@npm:1.5.7"
peerDependencies:
electron: ">=10.0.0"
checksum: 93995b5f0e9d14401a8c4fdd358af32d8b7585b59b111667cfa55f9505109c08914f3140953125b854e5d09e811de8c76c7fec718934c13e8a1ad09fe1b85270
checksum: 7dd7b2fb6e0acdee35474893d0e98b5e701c411c76be716cc02c5c9ac42db4fdaa7d526e22fd8c7047c2f143559d185bed8731bd455a1d11982404512d5f5021
languageName: node
linkType: hard
@ -8883,7 +8883,7 @@ __metadata:
browser-id3-writer: ^4.4.0
butterchurn: ^2.6.7
butterchurn-presets: ^2.4.7
custom-electron-prompt: ^1.5.4
custom-electron-prompt: ^1.5.7
custom-electron-titlebar: ^4.1.6
del-cli: ^5.0.0
electron: ^22.0.2