apply fix from eslint

This commit is contained in:
JellyBrick
2023-08-29 19:14:51 +09:00
parent c722896a73
commit 897cfd3c7d
33 changed files with 471 additions and 524 deletions

View File

@ -46,6 +46,7 @@ module.exports = {
allowTemplateLiterals: false, allowTemplateLiterals: false,
}], }],
'quote-props': ['error', 'consistent'], 'quote-props': ['error', 'consistent'],
'semi': ['error', 'always'],
}, },
env: { env: {
browser: true, browser: true,

View File

@ -4,10 +4,9 @@ const { restart } = require('../providers/app-controls');
function getEnabled() { function getEnabled() {
const plugins = store.get('plugins'); const plugins = store.get('plugins');
const enabledPlugins = Object.entries(plugins).filter(([plugin, options]) => return Object.entries(plugins).filter(([plugin]) =>
isEnabled(plugin), isEnabled(plugin),
); );
return enabledPlugins;
} }
function isEnabled(plugin) { function isEnabled(plugin) {

View File

@ -1,3 +1,5 @@
/* eslint-disable */
// Constants // Constants
const element = document.documentElement; const element = document.documentElement;
const { body } = document; const { body } = document;
@ -40,7 +42,8 @@ const bubbleCanvas = function (t) {
e.mouseX = 0; e.mouseX = 0;
e.mouseY = 0; e.mouseY = 0;
window.addEventListener('mousemove', (t) => { window.addEventListener('mousemove', (t) => {
(e.mouseX = t.clientX), (e.mouseY = t.clientY); e.mouseX = t.clientX;
e.mouseY = t.clientY;
}); });
e.randomise(); e.randomise();
}; };

View File

@ -36,7 +36,6 @@ if (!gotTheLock) {
} }
app.commandLine.appendSwitch('enable-features', 'SharedArrayBuffer'); // Required for downloader app.commandLine.appendSwitch('enable-features', 'SharedArrayBuffer'); // Required for downloader
app.allowRendererProcessReuse = true; // https://github.com/electron/electron/issues/18397
if (config.get('options.disableHardwareAcceleration')) { if (config.get('options.disableHardwareAcceleration')) {
if (is.dev()) { if (is.dev()) {
console.log('Disabling hardware acceleration'); console.log('Disabling hardware acceleration');
@ -60,9 +59,9 @@ require('electron-debug')({
}); });
let icon = 'assets/youtube-music.png'; let icon = 'assets/youtube-music.png';
if (process.platform == 'win32') { if (process.platform === 'win32') {
icon = 'assets/generated/icon.ico'; icon = 'assets/generated/icon.ico';
} else if (process.platform == 'darwin') { } else if (process.platform === 'darwin') {
icon = 'assets/generated/icon.icns'; icon = 'assets/generated/icon.icns';
} }
@ -393,7 +392,7 @@ app.on('ready', () => {
setupProtocolHandler(mainWindow); setupProtocolHandler(mainWindow);
app.on('second-instance', (_event, commandLine, _workingDirectory) => { app.on('second-instance', (_, commandLine) => {
const uri = `${APP_PROTOCOL}://`; const uri = `${APP_PROTOCOL}://`;
const protocolArgv = commandLine.find((arg) => arg.startsWith(uri)); const protocolArgv = commandLine.find((arg) => arg.startsWith(uri));
if (protocolArgv) { if (protocolArgv) {
@ -541,7 +540,7 @@ function removeContentSecurityPolicy(
// When multiple listeners are defined, apply them all // When multiple listeners are defined, apply them all
session.webRequest.setResolver('onHeadersReceived', (listeners) => { session.webRequest.setResolver('onHeadersReceived', (listeners) => {
const response = listeners.reduce( return listeners.reduce(
async (accumulator, listener) => { async (accumulator, listener) => {
if (accumulator.cancel) { if (accumulator.cancel) {
return accumulator; return accumulator;
@ -552,7 +551,5 @@ function removeContentSecurityPolicy(
}, },
{ cancel: false }, { cancel: false },
); );
return response;
}); });
} }

80
package-lock.json generated
View File

@ -39,7 +39,7 @@
"node-fetch": "2.7.0", "node-fetch": "2.7.0",
"simple-youtube-age-restriction-bypass": "git+https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass.git#v2.5.9", "simple-youtube-age-restriction-bypass": "git+https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass.git#v2.5.9",
"vudio": "2.1.1", "vudio": "2.1.1",
"youtubei.js": "4.3.0", "youtubei.js": "6.1.0",
"ytpl": "2.3.0" "ytpl": "2.3.0"
}, },
"devDependencies": { "devDependencies": {
@ -1919,11 +1919,6 @@
"bluebird": "^3.5.5" "bluebird": "^3.5.5"
} }
}, },
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
},
"node_modules/boolean": { "node_modules/boolean": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
@ -2607,37 +2602,6 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-what": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/cssom": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
"integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="
},
"node_modules/custom-electron-prompt": { "node_modules/custom-electron-prompt": {
"version": "1.5.7", "version": "1.5.7",
"resolved": "https://registry.npmjs.org/custom-electron-prompt/-/custom-electron-prompt-1.5.7.tgz", "resolved": "https://registry.npmjs.org/custom-electron-prompt/-/custom-electron-prompt-1.5.7.tgz",
@ -5018,11 +4982,6 @@
"resolved": "https://registry.npmjs.org/howler/-/howler-2.2.3.tgz", "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.3.tgz",
"integrity": "sha512-QM0FFkw0LRX1PR8pNzJVAY25JhIWvbKMBFM4gqk+QdV+kPXOhleWGCB6AiAF/goGjIHK2e/nIElplvjQwhr0jg==" "integrity": "sha512-QM0FFkw0LRX1PR8pNzJVAY25JhIWvbKMBFM4gqk+QdV+kPXOhleWGCB6AiAF/goGjIHK2e/nIElplvjQwhr0jg=="
}, },
"node_modules/html-escaper": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
"integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="
},
"node_modules/html-to-text": { "node_modules/html-to-text": {
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
@ -5937,18 +5896,6 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true "dev": true
}, },
"node_modules/linkedom": {
"version": "0.14.26",
"resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.14.26.tgz",
"integrity": "sha512-mK6TrydfFA7phrnp+1j57ycBwFI5bGSW6YXlw9acHoqF+mP/y+FooEYYyniOt5Ot57FSKB3iwmnuQ1UUyNLm5A==",
"dependencies": {
"css-select": "^5.1.0",
"cssom": "^0.5.0",
"html-escaper": "^3.0.3",
"htmlparser2": "^8.0.1",
"uhyphen": "^0.2.0"
}
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
@ -6579,17 +6526,6 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0" "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
} }
}, },
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.12.3", "version": "1.12.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
@ -8407,11 +8343,6 @@
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/uhyphen": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",
"integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA=="
},
"node_modules/unbox-primitive": { "node_modules/unbox-primitive": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
@ -8815,15 +8746,14 @@
} }
}, },
"node_modules/youtubei.js": { "node_modules/youtubei.js": {
"version": "4.3.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-4.3.0.tgz", "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-6.1.0.tgz",
"integrity": "sha512-HdU6Awdr1nUWy0Ph7WdmoYPWL0ovx+S4w40eeTzAENr5xiUENsLuXcvULRc2fRCIxi+n7Q6142VVhmM4yK/g5g==", "integrity": "sha512-EJmPuQ1pLimrcp5nPDSeZHVthT7KJLxp9Z5sYmihXKn1ct8e7cntKaFnPvy5QsbjpipICpDpQEs+d+owDhjgIg==",
"funding": [ "funding": [
"https://github.com/sponsors/LuanRT" "https://github.com/sponsors/LuanRT"
], ],
"dependencies": { "dependencies": {
"jintr": "^1.0.0", "jintr": "^1.1.0",
"linkedom": "^0.14.12",
"tslib": "^2.5.0", "tslib": "^2.5.0",
"undici": "^5.19.1" "undici": "^5.19.1"
} }

View File

@ -133,7 +133,7 @@
"node-fetch": "2.7.0", "node-fetch": "2.7.0",
"simple-youtube-age-restriction-bypass": "git+https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass.git#v2.5.9", "simple-youtube-age-restriction-bypass": "git+https://github.com/zerodytrash/Simple-YouTube-Age-Restriction-Bypass.git#v2.5.9",
"vudio": "2.1.1", "vudio": "2.1.1",
"youtubei.js": "4.3.0", "youtubei.js": "6.1.0",
"ytpl": "2.3.0" "ytpl": "2.3.0"
}, },
"overrides": { "overrides": {

View File

@ -2,9 +2,9 @@
// https://robwu.nl/crxviewer/?crx=https%3A%2F%2Faddons.mozilla.org%2Fen-US%2Ffirefox%2Faddon%2Fadblock-for-youtube%2F // https://robwu.nl/crxviewer/?crx=https%3A%2F%2Faddons.mozilla.org%2Fen-US%2Ffirefox%2Faddon%2Fadblock-for-youtube%2F
/* /*
Parts of this code is derived from set-constant.js: Parts of this code is derived from set-constant.js:
https://github.com/gorhill/uBlock/blob/5de0ce975753b7565759ac40983d31978d1f84ca/assets/resources/scriptlets.js#L704 https://github.com/gorhill/uBlock/blob/5de0ce975753b7565759ac40983d31978d1f84ca/assets/resources/scriptlets.js#L704
*/ */
{ {
const pruner = function (o) { const pruner = function (o) {

View File

@ -15,347 +15,342 @@
* v0.2.0, 07/2016 * v0.2.0, 07/2016
*/ */
(function (root) { 'use strict';
'use strict';
// Internal utility: check if value is a valid volume level and throw if not // Internal utility: check if value is a valid volume level and throw if not
const validateVolumeLevel = (value) => { const validateVolumeLevel = (value) => {
// Number between 0 and 1? // Number between 0 and 1?
if (!Number.isNaN(value) && value >= 0 && value <= 1) { if (!Number.isNaN(value) && value >= 0 && value <= 1) {
// Yup, that's fine // Yup, that's fine
} else {
// Abort and throw an exception
throw new TypeError('Number between 0 and 1 expected as volume!');
}
};
// Main class
class VolumeFader {
/**
* VolumeFader Constructor
*
* @param media {HTMLMediaElement} - audio or video element to be controlled
* @param options {Object} - an object with optional settings
* @throws {TypeError} if options.initialVolume or options.fadeDuration are invalid
*
* options:
* .logger: {Function} logging `function(stuff, …)` for execution information (default: no logging)
* .fadeScaling: {Mixed} either 'linear', 'logarithmic' or a positive number in dB (default: logarithmic)
* .initialVolume: {Number} media volume 0…1 to apply during setup (volume not touched by default)
* .fadeDuration: {Number} time in milliseconds to complete a fade (default: 1000 ms)
*/
constructor(media, options) {
// Passed media element of correct type?
if (media instanceof HTMLMediaElement) {
// Save reference to media element
this.media = media;
} else { } else {
// Abort and throw an exception // Abort and throw an exception
throw new TypeError('Number between 0 and 1 expected as volume!'); throw new TypeError('Media element expected!');
}
};
// Main class
class VolumeFader {
/**
* VolumeFader Constructor
*
* @param media {HTMLMediaElement} - audio or video element to be controlled
* @param options {Object} - an object with optional settings
* @throws {TypeError} if options.initialVolume or options.fadeDuration are invalid
*
* options:
* .logger: {Function} logging `function(stuff, …)` for execution information (default: no logging)
* .fadeScaling: {Mixed} either 'linear', 'logarithmic' or a positive number in dB (default: logarithmic)
* .initialVolume: {Number} media volume 0…1 to apply during setup (volume not touched by default)
* .fadeDuration: {Number} time in milliseconds to complete a fade (default: 1000 ms)
*/
constructor(media, options) {
// Passed media element of correct type?
if (media instanceof HTMLMediaElement) {
// Save reference to media element
this.media = media;
} else {
// Abort and throw an exception
throw new TypeError('Media element expected!');
}
// Make sure options is an object
options = options || {};
// Log function passed?
if (typeof options.logger === 'function') {
// Set log function to the one specified
this.logger = options.logger;
} else {
// Set log function explicitly to false
this.logger = false;
}
// Linear volume fading?
if (options.fadeScaling == 'linear') {
// Pass levels unchanged
this.scale = {
internalToVolume: (level) => level,
volumeToInternal: (level) => level,
};
// Log setting
this.logger && this.logger('Using linear fading.');
}
// No linear, but logarithmic fading…
else {
let dynamicRange;
// Default dynamic range?
if (
options.fadeScaling === undefined
|| options.fadeScaling == 'logarithmic'
) {
// Set default of 60 dB
dynamicRange = 3;
}
// Custom dynamic range?
else if (
!Number.isNaN(options.fadeScaling)
&& options.fadeScaling > 0
) {
// Turn amplitude dB into a multiple of 10 power dB
dynamicRange = options.fadeScaling / 2 / 10;
}
// Unsupported value
else {
// Abort and throw exception
throw new TypeError(
"Expected 'linear', 'logarithmic' or a positive number as fade scaling preference!",
);
}
// Use exponential/logarithmic scaler for expansion/compression
this.scale = {
internalToVolume: (level) =>
this.exponentialScaler(level, dynamicRange),
volumeToInternal: (level) =>
this.logarithmicScaler(level, dynamicRange),
};
// Log setting if not default
options.fadeScaling
&& this.logger
&& this.logger(
'Using logarithmic fading with '
+ String(10 * dynamicRange)
+ ' dB dynamic range.',
);
}
// Set initial volume?
if (options.initialVolume !== undefined) {
// Validate volume level and throw if invalid
validateVolumeLevel(options.initialVolume);
// Set initial volume
this.media.volume = options.initialVolume;
// Log setting
this.logger
&& this.logger(
'Set initial volume to ' + String(this.media.volume) + '.',
);
}
// Fade duration given?
if (options.fadeDuration === undefined) {
// Set default fade duration (1000 ms)
this.fadeDuration = 1000;
} else {
// Try to set given fade duration (will log if successful and throw if not)
this.setFadeDuration(options.fadeDuration);
}
// Indicate that fader is not active yet
this.active = false;
// Initialization done
this.logger && this.logger('Initialized for', this.media);
} }
/** // Make sure options is an object
* Re(start) the update cycle. options = options || {};
* (this.active must be truthy for volume updates to take effect)
*
* @return {Object} VolumeFader instance for chaining
*/
start() {
// Set fader to be active
this.active = true;
// Start by running the update method // Log function passed?
this.updateVolume(); if (typeof options.logger === 'function') {
// Set log function to the one specified
// Return instance for chaining this.logger = options.logger;
return this; } else {
// Set log function explicitly to false
this.logger = false;
} }
/** // Linear volume fading?
* Stop the update cycle. if (options.fadeScaling === 'linear') {
* (interrupting any fade) // Pass levels unchanged
* this.scale = {
* @return {Object} VolumeFader instance for chaining internalToVolume: (level) => level,
*/ volumeToInternal: (level) => level,
stop() {
// Set fader to be inactive
this.active = false;
// Return instance for chaining
return this;
}
/**
* Set fade duration.
* (used for future calls to fadeTo)
*
* @param {Number} fadeDuration - fading length in milliseconds
* @throws {TypeError} if fadeDuration is not a number greater than zero
* @return {Object} VolumeFader instance for chaining
*/
setFadeDuration(fadeDuration) {
// If duration is a valid number > 0…
if (!Number.isNaN(fadeDuration) && fadeDuration > 0) {
// Set fade duration
this.fadeDuration = fadeDuration;
// Log setting
this.logger
&& this.logger('Set fade duration to ' + String(fadeDuration) + ' ms.');
} else {
// Abort and throw an exception
throw new TypeError('Positive number expected as fade duration!');
}
// Return instance for chaining
return this;
}
/**
* Define a new fade and start fading.
*
* @param {Number} targetVolume - level to fade to in the range 0…1
* @param {Function} callback - (optional) function to be called when fade is complete
* @throws {TypeError} if targetVolume is not in the range 0…1
* @return {Object} VolumeFader instance for chaining
*/
fadeTo(targetVolume, callback) {
// Validate volume and throw if invalid
validateVolumeLevel(targetVolume);
// Define new fade
this.fade = {
// Volume start and end point on internal fading scale
volume: {
start: this.scale.volumeToInternal(this.media.volume),
end: this.scale.volumeToInternal(targetVolume),
},
// Time start and end point
time: {
start: Date.now(),
end: Date.now() + this.fadeDuration,
},
// Optional callback function
callback,
}; };
// Start fading // Log setting
this.start(); this.logger && this.logger('Using linear fading.');
// Log new fade
this.logger && this.logger('New fade started:', this.fade);
// Return instance for chaining
return this;
} }
// No linear, but logarithmic fading…
else {
let dynamicRange;
// Convenience shorthand methods for common fades // Default dynamic range?
fadeIn(callback) { if (
this.fadeTo(1, callback); options.fadeScaling === undefined
} || options.fadeScaling === 'logarithmic'
) {
fadeOut(callback) { // Set default of 60 dB
this.fadeTo(0, callback); dynamicRange = 3;
}
/**
* Internal: Update media volume.
* (calls itself through requestAnimationFrame)
*
* @param {Number} targetVolume - linear level to fade to (0…1)
* @param {Function} callback - (optional) function to be called when fade is complete
*/
updateVolume() {
// Fader active and fade available to process?
if (this.active && this.fade) {
// Get current time
const now = Date.now();
// Time left for fading?
if (now < this.fade.time.end) {
// Compute current fade progress
const progress
= (now - this.fade.time.start)
/ (this.fade.time.end - this.fade.time.start);
// Compute current level on internal scale
const level
= progress * (this.fade.volume.end - this.fade.volume.start)
+ this.fade.volume.start;
// Map fade level to volume level and apply it to media element
this.media.volume = this.scale.internalToVolume(level);
// Schedule next update
root.requestAnimationFrame(this.updateVolume.bind(this));
} else {
// Log end of fade
this.logger
&& this.logger(
'Fade to ' + String(this.fade.volume.end) + ' complete.',
);
// Time is up, jump to target volume
this.media.volume = this.scale.internalToVolume(this.fade.volume.end);
// Set fader to be inactive
this.active = false;
// Done, call back (if callable)
typeof this.fade.callback === 'function' && this.fade.callback();
// Clear fade
this.fade = undefined;
}
} }
} // Custom dynamic range?
else if (
/** !Number.isNaN(options.fadeScaling)
* Internal: Exponential scaler with dynamic range limit. && options.fadeScaling > 0
* ) {
* @param {Number} input - logarithmic input level to be expanded (float, 0…1) // Turn amplitude dB into a multiple of 10 power dB
* @param {Number} dynamicRange - expanded output range, in multiples of 10 dB (float, 0…∞) dynamicRange = options.fadeScaling / 2 / 10;
* @return {Number} - expanded level (float, 0…1) }
*/ // Unsupported value
exponentialScaler(input, dynamicRange) { else {
// Special case: make zero (or any falsy input) return zero // Abort and throw exception
if (input == 0) { throw new TypeError(
// Since the dynamic range is limited, "Expected 'linear', 'logarithmic' or a positive number as fade scaling preference!",
// allow a zero to produce a plain zero instead of a small faction );
// (audio would not be recognized as silent otherwise)
return 0;
} }
// Scale 0…1 to minus something × 10 dB // Use exponential/logarithmic scaler for expansion/compression
input = (input - 1) * dynamicRange; this.scale = {
internalToVolume: (level) =>
this.exponentialScaler(level, dynamicRange),
volumeToInternal: (level) =>
this.logarithmicScaler(level, dynamicRange),
};
// Compute power of 10 // Log setting if not default
return 10 ** input; options.fadeScaling
&& this.logger
&& this.logger(
'Using logarithmic fading with '
+ String(10 * dynamicRange)
+ ' dB dynamic range.',
);
} }
/** // Set initial volume?
* Internal: Logarithmic scaler with dynamic range limit. if (options.initialVolume !== undefined) {
* // Validate volume level and throw if invalid
* @param {Number} input - exponential input level to be compressed (float, 0…1) validateVolumeLevel(options.initialVolume);
* @param {Number} dynamicRange - coerced input range, in multiples of 10 dB (float, 0…∞)
* @return {Number} - compressed level (float, 0…1) // Set initial volume
*/ this.media.volume = options.initialVolume;
logarithmicScaler(input, dynamicRange) {
// Special case: make zero (or any falsy input) return zero // Log setting
if (input == 0) { this.logger
// Logarithm of zero would be -∞, which would map to zero anyway && this.logger(
return 0; 'Set initial volume to ' + String(this.media.volume) + '.',
);
}
// Fade duration given?
if (options.fadeDuration === undefined) {
// Set default fade duration (1000 ms)
this.fadeDuration = 1000;
} else {
// Try to set given fade duration (will log if successful and throw if not)
this.setFadeDuration(options.fadeDuration);
}
// Indicate that fader is not active yet
this.active = false;
// Initialization done
this.logger && this.logger('Initialized for', this.media);
}
/**
* Re(start) the update cycle.
* (this.active must be truthy for volume updates to take effect)
*
* @return {Object} VolumeFader instance for chaining
*/
start() {
// Set fader to be active
this.active = true;
// Start by running the update method
this.updateVolume();
// Return instance for chaining
return this;
}
/**
* Stop the update cycle.
* (interrupting any fade)
*
* @return {Object} VolumeFader instance for chaining
*/
stop() {
// Set fader to be inactive
this.active = false;
// Return instance for chaining
return this;
}
/**
* Set fade duration.
* (used for future calls to fadeTo)
*
* @param {Number} fadeDuration - fading length in milliseconds
* @throws {TypeError} if fadeDuration is not a number greater than zero
* @return {Object} VolumeFader instance for chaining
*/
setFadeDuration(fadeDuration) {
// If duration is a valid number > 0…
if (!Number.isNaN(fadeDuration) && fadeDuration > 0) {
// Set fade duration
this.fadeDuration = fadeDuration;
// Log setting
this.logger
&& this.logger('Set fade duration to ' + String(fadeDuration) + ' ms.');
} else {
// Abort and throw an exception
throw new TypeError('Positive number expected as fade duration!');
}
// Return instance for chaining
return this;
}
/**
* Define a new fade and start fading.
*
* @param {Number} targetVolume - level to fade to in the range 0…1
* @param {Function} callback - (optional) function to be called when fade is complete
* @throws {TypeError} if targetVolume is not in the range 0…1
* @return {Object} VolumeFader instance for chaining
*/
fadeTo(targetVolume, callback) {
// Validate volume and throw if invalid
validateVolumeLevel(targetVolume);
// Define new fade
this.fade = {
// Volume start and end point on internal fading scale
volume: {
start: this.scale.volumeToInternal(this.media.volume),
end: this.scale.volumeToInternal(targetVolume),
},
// Time start and end point
time: {
start: Date.now(),
end: Date.now() + this.fadeDuration,
},
// Optional callback function
callback,
};
// Start fading
this.start();
// Log new fade
this.logger && this.logger('New fade started:', this.fade);
// Return instance for chaining
return this;
}
// Convenience shorthand methods for common fades
fadeIn(callback) {
this.fadeTo(1, callback);
}
fadeOut(callback) {
this.fadeTo(0, callback);
}
/**
* Internal: Update media volume.
* (calls itself through requestAnimationFrame)
*/
updateVolume() {
// Fader active and fade available to process?
if (this.active && this.fade) {
// Get current time
const now = Date.now();
// Time left for fading?
if (now < this.fade.time.end) {
// Compute current fade progress
const progress
= (now - this.fade.time.start)
/ (this.fade.time.end - this.fade.time.start);
// Compute current level on internal scale
const level
= (progress * (this.fade.volume.end - this.fade.volume.start)) + this.fade.volume.start;
// Map fade level to volume level and apply it to media element
this.media.volume = this.scale.internalToVolume(level);
// Schedule next update
window.requestAnimationFrame(this.updateVolume.bind(this));
} else {
// Log end of fade
this.logger
&& this.logger(
'Fade to ' + String(this.fade.volume.end) + ' complete.',
);
// Time is up, jump to target volume
this.media.volume = this.scale.internalToVolume(this.fade.volume.end);
// Set fader to be inactive
this.active = false;
// Done, call back (if callable)
typeof this.fade.callback === 'function' && this.fade.callback();
// Clear fade
this.fade = undefined;
} }
// Compute base-10 logarithm
input = Math.log10(input);
// Scale minus something × 10 dB to 0…1 (clipping at 0)
return Math.max(1 + input / dynamicRange, 0);
} }
} }
// Export class to root scope /**
root.VolumeFader = VolumeFader; * Internal: Exponential scaler with dynamic range limit.
})(window); *
* @param {Number} input - logarithmic input level to be expanded (float, 0…1)
* @param {Number} dynamicRange - expanded output range, in multiples of 10 dB (float, 0…∞)
* @return {Number} - expanded level (float, 0…1)
*/
exponentialScaler(input, dynamicRange) {
// Special case: make zero (or any falsy input) return zero
if (input === 0) {
// Since the dynamic range is limited,
// allow a zero to produce a plain zero instead of a small faction
// (audio would not be recognized as silent otherwise)
return 0;
}
// Scale 0…1 to minus something × 10 dB
input = (input - 1) * dynamicRange;
// Compute power of 10
return 10 ** input;
}
/**
* Internal: Logarithmic scaler with dynamic range limit.
*
* @param {Number} input - exponential input level to be compressed (float, 0…1)
* @param {Number} dynamicRange - coerced input range, in multiples of 10 dB (float, 0…∞)
* @return {Number} - compressed level (float, 0…1)
*/
logarithmicScaler(input, dynamicRange) {
// Special case: make zero (or any falsy input) return zero
if (input === 0) {
// Logarithm of zero would be -∞, which would map to zero anyway
return 0;
}
// Compute base-10 logarithm
input = Math.log10(input);
// Scale minus something × 10 dB to 0…1 (clipping at 0)
return Math.max(1 + (input / dynamicRange), 0);
}
}
module.exports = {
VolumeFader
};

View File

@ -2,23 +2,25 @@ const { ipcRenderer } = require('electron');
const { Howl } = require('howler'); const { Howl } = require('howler');
// Extracted from https://github.com/bitfasching/VolumeFader // Extracted from https://github.com/bitfasching/VolumeFader
require('./fader'); const { VolumeFader } = require('./fader');
let transitionAudio; // Howler audio used to fade out the current music let transitionAudio; // Howler audio used to fade out the current music
let firstVideo = true; let firstVideo = true;
let waitForTransition; let waitForTransition;
const defaultConfig = require('../../config/defaults').plugins.crossfade; /**
* @type {PluginConfig}
*/
const configProvider = require('./config'); const configProvider = require('./config');
const defaultConfig = require('../../config/defaults').plugins.crossfade;
let config; let config;
const configGetNumber = (key) => Number(config[key]) || defaultConfig[key]; const configGetNumber = (key) => Number(config[key]) || defaultConfig[key];
const getStreamURL = async (videoID) => { const getStreamURL = async (videoID) => {
const url = await ipcRenderer.invoke('audio-url', videoID); return await ipcRenderer.invoke('audio-url', videoID);
return url;
}; };
const getVideoIDFromURL = (url) => new URLSearchParams(url.split('?')?.at(-1)).get('v'); const getVideoIDFromURL = (url) => new URLSearchParams(url.split('?')?.at(-1)).get('v');
@ -26,7 +28,7 @@ const getVideoIDFromURL = (url) => new URLSearchParams(url.split('?')?.at(-1)).g
const isReadyToCrossfade = () => transitionAudio && transitionAudio.state() === 'loaded'; const isReadyToCrossfade = () => transitionAudio && transitionAudio.state() === 'loaded';
const watchVideoIDChanges = (cb) => { const watchVideoIDChanges = (cb) => {
navigation.addEventListener('navigate', (event) => { window.navigation.addEventListener('navigate', (event) => {
const currentVideoID = getVideoIDFromURL( const currentVideoID = getVideoIDFromURL(
event.currentTarget.currentEntry.url, event.currentTarget.currentEntry.url,
); );
@ -126,7 +128,7 @@ const crossfade = async (cb) => {
} }
let resolveTransition; let resolveTransition;
waitForTransition = new Promise((resolve, reject) => { waitForTransition = new Promise((resolve) => {
resolveTransition = resolve; resolveTransition = resolve;
}); });

View File

@ -1,10 +1,9 @@
const config = require('./config');
const defaultOptions = require('../../config/defaults').plugins.crossfade;
const prompt = require('custom-electron-prompt'); const prompt = require('custom-electron-prompt');
const config = require('./config');
const promptOptions = require('../../providers/prompt-options'); const promptOptions = require('../../providers/prompt-options');
const defaultOptions = require('../../config/defaults').plugins.crossfade;
module.exports = (win) => [ module.exports = (win) => [
{ {

View File

@ -174,10 +174,10 @@ module.exports = (win, { autoReconnect, activityTimoutEnabled, activityTimoutTim
} }
} else if (!hideDurationLeft) { } else if (!hideDurationLeft) {
// Add the start and end time of the song // Add the start and end time of the song
const songStartTime = Date.now() - songInfo.elapsedSeconds * 1000; const songStartTime = Date.now() - (songInfo.elapsedSeconds * 1000);
activityInfo.startTimestamp = songStartTime; activityInfo.startTimestamp = songStartTime;
activityInfo.endTimestamp activityInfo.endTimestamp
= songStartTime + songInfo.songDuration * 1000; = songStartTime + (songInfo.songDuration * 1000);
} }
info.rpc.user?.setActivity(activityInfo).catch(console.error); info.rpc.user?.setActivity(activityInfo).catch(console.error);

View File

@ -31,6 +31,8 @@ const {
sendFeedback: sendFeedback_, sendFeedback: sendFeedback_,
} = require('./utils'); } = require('./utils');
const config = require('./config');
const { fetchFromGenius } = require('../lyrics-genius/back'); const { fetchFromGenius } = require('../lyrics-genius/back');
const { isEnabled } = require('../../config/plugins'); const { isEnabled } = require('../../config/plugins');
const { getImage, cleanupName } = require('../../providers/song-info'); const { getImage, cleanupName } = require('../../providers/song-info');
@ -39,8 +41,6 @@ const { cache } = require('../../providers/decorators');
const ffmpegMutex = new Mutex(); const ffmpegMutex = new Mutex();
const config = require('./config');
/** @type {Innertube} */ /** @type {Innertube} */
let yt; let yt;
let win; let win;
@ -187,14 +187,14 @@ async function downloadSongUnsafe(
return; return;
} }
const download_options = { const downloadOptions = {
type: 'audio', // Audio, video or video+audio type: 'audio', // Audio, video or video+audio
quality: 'best', // Best, bestefficiency, 144p, 240p, 480p, 720p and so on. quality: 'best', // Best, bestefficiency, 144p, 240p, 480p, 720p and so on.
format: 'any', // Media container format format: 'any', // Media container format
}; };
const format = info.chooseFormat(download_options); const format = info.chooseFormat(downloadOptions);
const stream = await info.download(download_options); const stream = await info.download(downloadOptions);
console.info( console.info(
`Downloading ${metadata.artist} - ${metadata.title} [${metadata.id}]`, `Downloading ${metadata.artist} - ${metadata.title} [${metadata.id}]`,
@ -244,14 +244,14 @@ async function downloadSongUnsafe(
async function iterableStreamToMP3( async function iterableStreamToMP3(
stream, stream,
metadata, metadata,
content_length, contentLength,
sendFeedback, sendFeedback,
increasePlaylistProgress = () => { increasePlaylistProgress = () => {
}, },
) { ) {
const chunks = []; const chunks = [];
let downloaded = 0; let downloaded = 0;
const total = content_length; const total = contentLength;
for await (const chunk of stream) { for await (const chunk of stream) {
downloaded += chunk.length; downloaded += chunk.length;
chunks.push(chunk); chunks.push(chunk);
@ -281,7 +281,7 @@ async function iterableStreamToMP3(
ffmpeg.setProgress(({ ratio }) => { ffmpeg.setProgress(({ ratio }) => {
sendFeedback(`Converting: ${Math.floor(ratio * 100)}%`, ratio); sendFeedback(`Converting: ${Math.floor(ratio * 100)}%`, ratio);
increasePlaylistProgress(0.15 + ratio * 0.85); increasePlaylistProgress(0.15 + (ratio * 0.85));
}); });
await ffmpeg.run( await ffmpeg.run(
@ -377,7 +377,7 @@ async function downloadPlaylist(givenUrl) {
}); });
} catch (error) { } catch (error) {
sendError( sendError(
`Error getting playlist info: make sure it isn\'t a private or "Mixed for you" playlist\n\n${error}`, `Error getting playlist info: make sure it isn't a private or "Mixed for you" playlist\n\n${error}`,
); );
return; return;
} }
@ -434,7 +434,7 @@ async function downloadPlaylist(givenUrl) {
const increaseProgress = (itemPercentage) => { const increaseProgress = (itemPercentage) => {
const currentProgress = (counter - 1) / playlist.items.length; const currentProgress = (counter - 1) / playlist.items.length;
const newProgress = currentProgress + progressStep * itemPercentage; const newProgress = currentProgress + (progressStep * itemPercentage);
win.setProgressBar(newProgress); win.setProgressBar(newProgress);
}; };

View File

@ -41,7 +41,7 @@ const menuObserver = new MutationObserver(() => {
// TODO: re-enable once contextIsolation is set to true // TODO: re-enable once contextIsolation is set to true
// contextBridge.exposeInMainWorld("downloader", { // contextBridge.exposeInMainWorld("downloader", {
// download: () => { // download: () => {
global.download = () => { global.download = () => {
let videoUrl = getSongMenu() let videoUrl = getSongMenu()
// Selector of first button which is always "Start Radio" // Selector of first button which is always "Start Radio"

View File

@ -8,7 +8,7 @@ function $(selector) {
return document.querySelector(selector); return document.querySelector(selector);
} }
module.exports = (options) => { module.exports = () => {
const visible = () => Boolean($('.cet-menubar').firstChild); const visible = () => Boolean($('.cet-menubar').firstChild);
const bar = new Titlebar({ const bar = new Titlebar({
icon: 'https://cdn-icons-png.flaticon.com/512/5358/5358672.png', icon: 'https://cdn-icons-png.flaticon.com/512/5358/5358672.png',
@ -38,7 +38,7 @@ module.exports = (options) => {
}); });
if (isEnabled('picture-in-picture')) { if (isEnabled('picture-in-picture')) {
ipcRenderer.on('pip-toggle', (_, pipEnabled) => { ipcRenderer.on('pip-toggle', () => {
bar.refreshMenu(); bar.refreshMenu();
}); });
} }
@ -62,7 +62,7 @@ function setupSearchOpenObserver() {
} }
function setupMenuOpenObserver() { function setupMenuOpenObserver() {
const menuOpenObserver = new MutationObserver((mutations) => { const menuOpenObserver = new MutationObserver(() => {
$('#nav-bar-background').style.webkitAppRegion $('#nav-bar-background').style.webkitAppRegion
= [...$('.cet-menubar').childNodes].some((c) => c.classList.contains('open')) = [...$('.cet-menubar').childNodes].some((c) => c.classList.contains('open'))
? 'no-drag' : 'drag'; ? 'no-drag' : 'drag';

View File

@ -16,10 +16,10 @@ const createFormData = (parameters) => {
return formData; return formData;
}; };
const createQueryString = (parameters, api_sig) => { const createQueryString = (parameters, apiSignature) => {
// Creates a querystring // Creates a querystring
const queryData = []; const queryData = [];
parameters.api_sig = api_sig; parameters.api_sig = apiSignature;
for (const key in parameters) { for (const key in parameters) {
queryData.push(`${encodeURIComponent(key)}=${encodeURIComponent(parameters[key])}`); queryData.push(`${encodeURIComponent(key)}=${encodeURIComponent(parameters[key])}`);
} }
@ -49,15 +49,15 @@ const createApiSig = (parameters, secret) => {
return sig; return sig;
}; };
const createToken = async ({ api_key, api_root, secret }) => { const createToken = async ({ apiKey, apiRoot, secret }) => {
// Creates and stores the auth token // Creates and stores the auth token
const data = { const data = {
method: 'auth.gettoken', method: 'auth.gettoken',
api_key, apiKey,
format: 'json', format: 'json',
}; };
const api_sig = createApiSig(data, secret); const apiSigature = createApiSig(data, secret);
let response = await fetch(`${api_root}${createQueryString(data, api_sig)}`); let response = await fetch(`${apiRoot}${createQueryString(data, apiSigature)}`);
response = await response.json(); response = await response.json();
return response?.token; return response?.token;
}; };
@ -78,8 +78,8 @@ const getAndSetSessionKey = async (config) => {
method: 'auth.getsession', method: 'auth.getsession',
token: config.token, token: config.token,
}; };
const api_sig = createApiSig(data, config.secret); const apiSignature = createApiSig(data, config.secret);
let res = await fetch(`${config.api_root}${createQueryString(data, api_sig)}`); let res = await fetch(`${config.api_root}${createQueryString(data, apiSignature)}`);
res = await res.json(); res = await res.json();
if (res.error) { if (res.error) {
await authenticate(config); await authenticate(config);
@ -110,7 +110,7 @@ const postSongDataToAPI = async (songInfo, config, data) => {
postData.api_sig = createApiSig(postData, config.secret); postData.api_sig = createApiSig(postData, config.secret);
fetch('https://ws.audioscrobbler.com/2.0/', { method: 'POST', body: createFormData(postData) }) fetch('https://ws.audioscrobbler.com/2.0/', { method: 'POST', body: createFormData(postData) })
.catch((error) => { .catch((error) => {
if (error.response.data.error == 9) { if (error.response.data.error === 9) {
// Session key is invalid, so remove it from the config and reauthenticate // Session key is invalid, so remove it from the config and reauthenticate
config.session_key = undefined; config.session_key = undefined;
setOptions('last-fm', config); setOptions('last-fm', config);

View File

@ -8,7 +8,7 @@ const fetch = require('node-fetch');
const { cleanupName } = require('../../providers/song-info'); const { cleanupName } = require('../../providers/song-info');
const { injectCSS } = require('../utils'); const { injectCSS } = require('../utils');
const eastAsianChars = /\p{Script=Han}|\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}|\p{Script=Han}/u; const eastAsianChars = /\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}|\p{Script=Han}/u;
let revRomanized = false; let revRomanized = false;
module.exports = async (win, options) => { module.exports = async (win, options) => {
@ -89,7 +89,7 @@ const getLyricsList = async (queryString) => {
* @returns The lyrics of the song URL provided, null if none * @returns The lyrics of the song URL provided, null if none
*/ */
const getLyrics = async (url) => { const getLyrics = async (url) => {
response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
return null; return null;
} }

View File

@ -65,14 +65,14 @@ module.exports = () => {
function setLyrics(lyricsContainer) { function setLyrics(lyricsContainer) {
lyricsContainer.innerHTML = `<div id="contents" class="style-scope ytmusic-section-list-renderer description ytmusic-description-shelf-renderer genius-lyrics"> lyricsContainer.innerHTML = `<div id="contents" class="style-scope ytmusic-section-list-renderer description ytmusic-description-shelf-renderer genius-lyrics">
${ ${
hasLyrics hasLyrics
? lyrics.replaceAll(/\r\n|\r|\n/g, '<br/>') ? lyrics.replaceAll(/\r\n|\r|\n/g, '<br/>')
: 'Could not retrieve lyrics from genius' : 'Could not retrieve lyrics from genius'
} }
</div> </div>
<yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer" style="align-self: baseline"></yt-formatted-string>`; <yt-formatted-string class="footer style-scope ytmusic-description-shelf-renderer" style="align-self: baseline"></yt-formatted-string>`;
if (hasLyrics) { if (hasLyrics) {
lyricsContainer.querySelector('.footer').textContent = 'Source: Genius'; lyricsContainer.querySelector('.footer').textContent = 'Source: Genius';
enableLyricsTab(); enableLyricsTab();

View File

@ -2,7 +2,7 @@ const { toggleRomanized } = require('./back');
const { setOptions } = require('../../config/plugins'); const { setOptions } = require('../../config/plugins');
module.exports = (win, options, refreshMenu) => [ module.exports = (win, options) => [
{ {
label: 'Romanized Lyrics', label: 'Romanized Lyrics',
type: 'checkbox', type: 'checkbox',

View File

@ -15,7 +15,7 @@ function removeLoginElements() {
const libraryIconPath const libraryIconPath
= 'M16,6v2h-2v5c0,1.1-0.9,2-2,2s-2-0.9-2-2s0.9-2,2-2c0.37,0,0.7,0.11,1,0.28V6H16z M18,20H4V6H3v15h15V20z M21,3H6v15h15V3z M7,4h13v13H7V4z'; = 'M16,6v2h-2v5c0,1.1-0.9,2-2,2s-2-0.9-2-2s0.9-2,2-2c0.37,0,0.7,0.11,1,0.28V6H16z M18,20H4V6H3v15h15V20z M21,3H6v15h15V3z M7,4h13v13H7V4z';
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
menuEntries = document.querySelectorAll( const menuEntries = document.querySelectorAll(
'#items ytmusic-guide-entry-renderer', '#items ytmusic-guide-entry-renderer',
); );
for (const item of menuEntries) { for (const item of menuEntries) {

View File

@ -2,8 +2,11 @@ const path = require('node:path');
const { Notification, app, ipcMain } = require('electron'); const { Notification, app, ipcMain } = require('electron');
const { notificationImage, icons, save_temp_icons, secondsToMinutes, ToastStyles } = require('./utils'); const { notificationImage, icons, saveTempIcon, secondsToMinutes, ToastStyles } = require('./utils');
/**
* @type {PluginConfig}
*/
const config = require('./config'); const config = require('./config');
const getSongControls = require('../../providers/song-controls'); const getSongControls = require('../../providers/song-controls');
@ -25,7 +28,7 @@ module.exports = (win) => {
ipcMain.on('timeChanged', (_, t) => currentSeconds = t); ipcMain.on('timeChanged', (_, t) => currentSeconds = t);
if (app.isPackaged) { if (app.isPackaged) {
save_temp_icons(); saveTempIcon();
} }
let savedSongInfo; let savedSongInfo;
@ -108,42 +111,42 @@ function sendNotification(songInfo) {
// https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/toast-schema // https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/toast-schema
// https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=xml // https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=xml
// https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.toasttemplatetype // https://learn.microsoft.com/en-us/uwp/api/windows.ui.notifications.toasttemplatetype
toastXml: get_xml(songInfo, iconSrc), toastXml: getXml(songInfo, iconSrc),
}); });
savedNotification.on('close', (_) => { savedNotification.on('close', () => {
savedNotification = undefined; savedNotification = undefined;
}); });
savedNotification.show(); savedNotification.show();
} }
const get_xml = (songInfo, iconSrc) => { const getXml = (songInfo, iconSrc) => {
switch (config.get('toastStyle')) { switch (config.get('toastStyle')) {
default: default:
case ToastStyles.logo: case ToastStyles.logo:
case ToastStyles.legacy: { case ToastStyles.legacy: {
return xml_logo(songInfo, iconSrc); return xmlLogo(songInfo, iconSrc);
} }
case ToastStyles.banner_top_custom: { case ToastStyles.banner_top_custom: {
return xml_banner_top_custom(songInfo, iconSrc); return xmlBannerTopCustom(songInfo, iconSrc);
} }
case ToastStyles.hero: { case ToastStyles.hero: {
return xml_hero(songInfo, iconSrc); return xmlHero(songInfo, iconSrc);
} }
case ToastStyles.banner_bottom: { case ToastStyles.banner_bottom: {
return xml_banner_bottom(songInfo, iconSrc); return xmlBannerBottom(songInfo, iconSrc);
} }
case ToastStyles.banner_centered_bottom: { case ToastStyles.banner_centered_bottom: {
return xml_banner_centered_bottom(songInfo, iconSrc); return xmlBannerCenteredBottom(songInfo, iconSrc);
} }
case ToastStyles.banner_centered_top: { case ToastStyles.banner_centered_top: {
return xml_banner_centered_top(songInfo, iconSrc); return xmlBannerCenteredTop(songInfo, iconSrc);
} }
} }
}; };
@ -186,19 +189,19 @@ const toast = (content, isPaused) => `\
${getButtons(isPaused)} ${getButtons(isPaused)}
</toast>`; </toast>`;
const xml_image = ({ title, artist, isPaused }, imgSrc, placement) => toast(`\ const xmlImage = ({ title, artist, isPaused }, imgSrc, placement) => toast(`\
<image id="1" src="${imgSrc}" name="Image" ${placement}/> <image id="1" src="${imgSrc}" name="Image" ${placement}/>
<text id="1">${title}</text> <text id="1">${title}</text>
<text id="2">${artist}</text>\ <text id="2">${artist}</text>\
`, isPaused); `, isPaused);
const xml_logo = (songInfo, imgSrc) => xml_image(songInfo, imgSrc, 'placement="appLogoOverride"'); const xmlLogo = (songInfo, imgSrc) => xmlImage(songInfo, imgSrc, 'placement="appLogoOverride"');
const xml_hero = (songInfo, imgSrc) => xml_image(songInfo, imgSrc, 'placement="hero"'); const xmlHero = (songInfo, imgSrc) => xmlImage(songInfo, imgSrc, 'placement="hero"');
const xml_banner_bottom = (songInfo, imgSrc) => xml_image(songInfo, imgSrc, ''); const xmlBannerBottom = (songInfo, imgSrc) => xmlImage(songInfo, imgSrc, '');
const xml_banner_top_custom = (songInfo, imgSrc) => toast(`\ const xmlBannerTopCustom = (songInfo, imgSrc) => toast(`\
<image id="1" src="${imgSrc}" name="Image" /> <image id="1" src="${imgSrc}" name="Image" />
<text></text> <text></text>
<group> <group>
@ -206,11 +209,11 @@ const xml_banner_top_custom = (songInfo, imgSrc) => toast(`\
<text hint-style="body">${songInfo.title}</text> <text hint-style="body">${songInfo.title}</text>
<text hint-style="captionSubtle">${songInfo.artist}</text> <text hint-style="captionSubtle">${songInfo.artist}</text>
</subgroup> </subgroup>
${xml_more_data(songInfo)} ${xmlMoreData(songInfo)}
</group>\ </group>\
`, songInfo.isPaused); `, songInfo.isPaused);
const xml_more_data = ({ album, elapsedSeconds, songDuration }) => `\ const xmlMoreData = ({ album, elapsedSeconds, songDuration }) => `\
<subgroup hint-textStacking="bottom"> <subgroup hint-textStacking="bottom">
${album ${album
? `<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${album}</text>` : ''} ? `<text hint-style="captionSubtle" hint-wrap="true" hint-align="right">${album}</text>` : ''}
@ -218,7 +221,7 @@ const xml_more_data = ({ album, elapsedSeconds, songDuration }) => `\
</subgroup>\ </subgroup>\
`; `;
const xml_banner_centered_bottom = ({ title, artist, isPaused }, imgSrc) => toast(`\ const xmlBannerCenteredBottom = ({ title, artist, isPaused }, imgSrc) => toast(`\
<text></text> <text></text>
<group> <group>
<subgroup hint-weight="1" hint-textStacking="center"> <subgroup hint-weight="1" hint-textStacking="center">
@ -229,7 +232,7 @@ const xml_banner_centered_bottom = ({ title, artist, isPaused }, imgSrc) => toas
<image id="1" src="${imgSrc}" name="Image" hint-removeMargin="true" />\ <image id="1" src="${imgSrc}" name="Image" hint-removeMargin="true" />\
`, isPaused); `, isPaused);
const xml_banner_centered_top = ({ title, artist, isPaused }, imgSrc) => toast(`\ const xmlBannerCenteredTop = ({ title, artist, isPaused }, imgSrc) => toast(`\
<image id="1" src="${imgSrc}" name="Image" /> <image id="1" src="${imgSrc}" name="Image" />
<text></text> <text></text>
<group> <group>

View File

@ -69,18 +69,18 @@ module.exports.notificationImage = (songInfo) => {
} }
}; };
module.exports.saveImage = cache((img, save_path) => { module.exports.saveImage = cache((img, savePath) => {
try { try {
fs.writeFileSync(save_path, img.toPNG()); fs.writeFileSync(savePath, img.toPNG());
} catch (error) { } catch (error) {
console.log(`Error writing song icon to disk:\n${error.toString()}`); console.log(`Error writing song icon to disk:\n${error.toString()}`);
return icon; return icon;
} }
return save_path; return savePath;
}); });
module.exports.save_temp_icons = () => { module.exports.saveTempIcon = () => {
for (const kind of Object.keys(module.exports.icons)) { for (const kind of Object.keys(module.exports.icons)) {
const destinationPath = path.join(userData, 'icons', `${kind}.png`); const destinationPath = path.join(userData, 'icons', `${kind}.png`);
if (fs.existsSync(destinationPath)) { if (fs.existsSync(destinationPath)) {

View File

@ -1,7 +1,6 @@
const path = require('node:path'); const path = require('node:path');
const { app, ipcMain } = require('electron'); const { app, ipcMain } = require('electron');
const electronLocalshortcut = require('electron-localshortcut');
const { setOptions } = require('../../config/plugins'); const { setOptions } = require('../../config/plugins');
const { injectCSS } = require('../utils'); const { injectCSS } = require('../utils');

View File

@ -52,7 +52,7 @@ const observer = new MutationObserver(() => {
menu.prepend(pipButton); menu.prepend(pipButton);
}); });
global.togglePictureInPicture = async () => { const togglePictureInPicture = async () => {
if (useNativePiP) { if (useNativePiP) {
const isInPiP = document.pictureInPictureElement !== null; const isInPiP = document.pictureInPictureElement !== null;
const video = $('video'); const video = $('video');
@ -72,6 +72,7 @@ global.togglePictureInPicture = async () => {
ipcRenderer.send('picture-in-picture'); ipcRenderer.send('picture-in-picture');
return false; return false;
}; };
global.togglePictureInPicture = togglePictureInPicture;
const listenForToggle = () => { const listenForToggle = () => {
const originalExitButton = $('.exit-fullscreen-button'); const originalExitButton = $('.exit-fullscreen-button');
@ -123,7 +124,7 @@ function observeMenu(options) {
listenForToggle(); listenForToggle();
cloneButton('.player-minimize-button').addEventListener('click', async () => { cloneButton('.player-minimize-button').addEventListener('click', async () => {
await global.togglePictureInPicture(); await togglePictureInPicture();
setTimeout(() => $('#player').click()); setTimeout(() => $('#player').click());
}); });

View File

@ -56,8 +56,9 @@ const observePopupContainer = () => {
}; };
const observeVideo = () => { const observeVideo = () => {
$('video').addEventListener('ratechange', forcePlaybackRate); const video = $('video');+
$('video').addEventListener('srcChanged', forcePlaybackRate); video.addEventListener('ratechange', forcePlaybackRate);
video.addEventListener('srcChanged', forcePlaybackRate);
}; };
const setupWheelListener = () => { const setupWheelListener = () => {

View File

@ -1,15 +1,15 @@
const { injectCSS } = require('../utils');
const path = require('node:path'); const path = require('node:path');
const { globalShortcut } = require('electron');
const { injectCSS } = require('../utils');
/* /*
This is used to determine if plugin is actually active This is used to determine if plugin is actually active
(not if its only enabled in options) (not if its only enabled in options)
*/ */
let enabled = false; let enabled = false;
const { globalShortcut } = require('electron');
module.exports = (win, options) => { 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'));

View File

@ -26,7 +26,7 @@ const writeOptions = debounce(() => {
setOptions('precise-volume', options); setOptions('precise-volume', options);
}, 1000); }, 1000);
module.exports.moveVolumeHud = debounce((showVideo) => { const moveVolumeHud = debounce((showVideo) => {
const volumeHud = $('#volumeHud'); const volumeHud = $('#volumeHud');
if (!volumeHud) { if (!volumeHud) {
return; return;
@ -36,6 +36,7 @@ module.exports.moveVolumeHud = debounce((showVideo) => {
? `${($('ytmusic-player').clientHeight - $('video').clientHeight) / 2}px` ? `${($('ytmusic-player').clientHeight - $('video').clientHeight) / 2}px`
: 0; : 0;
}, 250); }, 250);
module.exports.moveVolumeHud = moveVolumeHud;
const hideVolumeHud = debounce((volumeHud) => { const hideVolumeHud = debounce((volumeHud) => {
volumeHud.style.opacity = 0; volumeHud.style.opacity = 0;
@ -215,7 +216,7 @@ const tooltipTargets = [
]; ];
function setTooltip(volume) { function setTooltip(volume) {
for (target of tooltipTargets) { for (const target of tooltipTargets) {
$(target).title = `${volume}%`; $(target).title = `${volume}%`;
} }
} }

View File

@ -6,7 +6,7 @@ const { setMenuOptions } = require('../../config/plugins');
const promptOptions = require('../../providers/prompt-options'); const promptOptions = require('../../providers/prompt-options');
function changeOptions(changedOptions, options, win) { function changeOptions(changedOptions, options, win) {
for (option in changedOptions) { for (const option in changedOptions) {
options[option] = changedOptions[option]; options[option] = changedOptions[option];
} }

View File

@ -15,6 +15,15 @@ module.exports = () => {
}; };
function setup(event) { function setup(event) {
/**
* @type {{
* getAvailableQualityLevels: () => string[],
* getPlaybackQuality: () => string,
* getAvailableQualityLabels: () => string[],
* setPlaybackQualityRange: (quality: string) => void,
* setPlaybackQuality: (quality: string) => void,
* }}
*/
const api = event.detail; const api = event.detail;
$('.top-row-buttons.ytmusic-player').prepend(qualitySettingsButton); $('.top-row-buttons.ytmusic-player').prepend(qualitySettingsButton);

View File

@ -6,7 +6,7 @@ const getSongControls = require('../../providers/song-controls');
const config = require('../../config'); const config = require('../../config');
function setupMPRIS() { function setupMPRIS() {
const player = mpris({ return mpris({
name: 'youtube-music', name: 'youtube-music',
identity: 'YouTube Music', identity: 'YouTube Music',
canRaise: true, canRaise: true,
@ -15,8 +15,6 @@ function setupMPRIS() {
supportedInterfaces: ['player'], supportedInterfaces: ['player'],
desktopEntry: 'youtube-music', desktopEntry: 'youtube-music',
}); });
return player;
} }
/** @param {Electron.BrowserWindow} win */ /** @param {Electron.BrowserWindow} win */
@ -108,7 +106,9 @@ function registerMPRIS(win) {
player.on('position', seekTo); player.on('position', seekTo);
player.on('shuffle', (enableShuffle) => { player.on('shuffle', (enableShuffle) => {
shuffle(); if (enableShuffle) {
shuffle();
}
}); });
let mprisVolNewer = false; let mprisVolNewer = false;

View File

@ -18,7 +18,7 @@ const data = {
const post = async (data) => { const post = async (data) => {
const port = 1608; const port = 1608;
headers = { const headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json', 'Accept': 'application/json',
'Access-Control-Allow-Headers': '*', 'Access-Control-Allow-Headers': '*',
@ -43,15 +43,6 @@ module.exports = async (win) => {
data.progress = secToMilisec(t); data.progress = secToMilisec(t);
post(data); post(data);
}); });
ipcMain.on('playPaused', (_, { isPaused, elapsedSeconds }) => {
if (!data.title) {
return;
}
data.status = isPaused ? 'stopped' : 'playing';
data.progress = secToMilisec(elapsedSeconds);
post(data);
});
registerCallback((songInfo) => { registerCallback((songInfo) => {
if (!songInfo.title && !songInfo.artist) { if (!songInfo.title && !songInfo.artist) {

View File

@ -2,6 +2,6 @@ const path = require('node:path');
const { injectCSS } = require('../utils'); const { injectCSS } = require('../utils');
module.exports = (win, options) => { module.exports = (win) => {
injectCSS(win.webContents, path.join(__dirname, 'empty-player.css')); injectCSS(win.webContents, path.join(__dirname, 'empty-player.css'));
}; };

View File

@ -17,12 +17,13 @@ class WaveVisualizer {
for (const animation of options.animations) { for (const animation of options.animations) {
this.visualizer.addAnimation( this.visualizer.addAnimation(
eval(`new this.visualizer.animations.${animation.type}( eval(`new this.visualizer.animations.${animation.type}(
${JSON.stringify(animation.config)} ${JSON.stringify(animation.config)}
)`), )`),
); );
} }
} }
// eslint-disable-next-line no-unused-vars
resize(width, height) { resize(width, height) {
} }

View File

@ -17,19 +17,21 @@ ipcRenderer.on('update-song-info', async (_, extractedSongInfo) => {
// Used because 'loadeddata' or 'loadedmetadata' weren't firing on song start for some users (https://github.com/th-ch/youtube-music/issues/473) // Used because 'loadeddata' or 'loadedmetadata' weren't firing on song start for some users (https://github.com/th-ch/youtube-music/issues/473)
const srcChangedEvent = new CustomEvent('srcChanged'); const srcChangedEvent = new CustomEvent('srcChanged');
module.exports.setupSeekedListener = singleton(() => { const setupSeekedListener = singleton(() => {
$('video')?.addEventListener('seeked', (v) => ipcRenderer.send('seeked', v.target.currentTime)); $('video')?.addEventListener('seeked', (v) => ipcRenderer.send('seeked', v.target.currentTime));
}); });
module.exports.setupSeekedListener = setupSeekedListener;
module.exports.setupTimeChangedListener = singleton(() => { const setupTimeChangedListener = singleton(() => {
const progressObserver = new MutationObserver((mutations) => { const progressObserver = new MutationObserver((mutations) => {
ipcRenderer.send('timeChanged', mutations[0].target.value); ipcRenderer.send('timeChanged', mutations[0].target.value);
global.songInfo.elapsedSeconds = mutations[0].target.value; global.songInfo.elapsedSeconds = mutations[0].target.value;
}); });
progressObserver.observe($('#progress-bar'), { attributeFilter: ['value'] }); progressObserver.observe($('#progress-bar'), { attributeFilter: ['value'] });
}); });
module.exports.setupTimeChangedListener = setupTimeChangedListener;
module.exports.setupRepeatChangedListener = singleton(() => { const setupRepeatChangedListener = singleton(() => {
const repeatObserver = new MutationObserver((mutations) => { const repeatObserver = new MutationObserver((mutations) => {
ipcRenderer.send('repeatChanged', mutations[0].target.__dataHost.getState().queue.repeatMode); ipcRenderer.send('repeatChanged', mutations[0].target.__dataHost.getState().queue.repeatMode);
}); });
@ -38,53 +40,66 @@ module.exports.setupRepeatChangedListener = singleton(() => {
// Emit the initial value as well; as it's persistent between launches. // Emit the initial value as well; as it's persistent between launches.
ipcRenderer.send('repeatChanged', $('ytmusic-player-bar').getState().queue.repeatMode); ipcRenderer.send('repeatChanged', $('ytmusic-player-bar').getState().queue.repeatMode);
}); });
module.exports.setupRepeatChangedListener = setupRepeatChangedListener;
module.exports.setupVolumeChangedListener = singleton((api) => { const setupVolumeChangedListener = singleton((api) => {
$('video').addEventListener('volumechange', (_) => { $('video').addEventListener('volumechange', () => {
ipcRenderer.send('volumeChanged', api.getVolume()); ipcRenderer.send('volumeChanged', api.getVolume());
}); });
// Emit the initial value as well; as it's persistent between launches. // Emit the initial value as well; as it's persistent between launches.
ipcRenderer.send('volumeChanged', api.getVolume()); ipcRenderer.send('volumeChanged', api.getVolume());
}); });
module.exports.setupVolumeChangedListener = setupVolumeChangedListener;
module.exports = () => { module.exports = () => {
document.addEventListener('apiLoaded', (apiEvent) => { document.addEventListener('apiLoaded', (apiEvent) => {
ipcRenderer.on('setupTimeChangedListener', async () => { ipcRenderer.on('setupTimeChangedListener', async () => {
this.setupTimeChangedListener(); setupTimeChangedListener();
}); });
ipcRenderer.on('setupRepeatChangedListener', async () => { ipcRenderer.on('setupRepeatChangedListener', async () => {
this.setupRepeatChangedListener(); setupRepeatChangedListener();
}); });
ipcRenderer.on('setupVolumeChangedListener', async () => { ipcRenderer.on('setupVolumeChangedListener', async () => {
this.setupVolumeChangedListener(apiEvent.detail); setupVolumeChangedListener(apiEvent.detail);
}); });
ipcRenderer.on('setupSeekedListener', async () => { ipcRenderer.on('setupSeekedListener', async () => {
this.setupSeekedListener(); setupSeekedListener();
}); });
const playPausedHandler = (e, status) => {
if (Math.round(e.target.currentTime) > 0) {
ipcRenderer.send('playPaused', {
isPaused: status === 'pause',
elapsedSeconds: Math.floor(e.target.currentTime),
});
}
};
const playPausedHandlers = {
playing: (e) => playPausedHandler(e, 'playing'),
pause: (e) => playPausedHandler(e, 'pause'),
};
const video = $('video'); const video = $('video');
// Name = "dataloaded" and abit later "dataupdated" // Name = "dataloaded" and abit later "dataupdated"
apiEvent.detail.addEventListener('videodatachange', (name, _dataEvent) => { apiEvent.detail.addEventListener('videodatachange', (name) => {
if (name !== 'dataloaded') { if (name !== 'dataloaded') {
return; return;
} }
video.dispatchEvent(srcChangedEvent); video.dispatchEvent(srcChangedEvent);
for (const status of ['playing', 'pause']) { // for fix issue that pause event not fired
video.addEventListener(status, playPausedHandlers[status]);
}
setTimeout(sendSongInfo, 200); setTimeout(sendSongInfo, 200);
}); });
for (const status of ['playing', 'pause']) { for (const status of ['playing', 'pause']) {
video.addEventListener(status, (e) => { video.addEventListener(status, playPausedHandlers[status]);
if (Math.round(e.target.currentTime) > 0) {
ipcRenderer.send('playPaused', {
isPaused: status === 'pause',
elapsedSeconds: Math.floor(e.target.currentTime),
});
}
});
} }
function sendSongInfo() { function sendSongInfo() {