Merge pull request #1068 from Araxeus/add-pseudo-decorators

Create providers/decorators.js
This commit is contained in:
th-ch
2023-04-02 21:23:37 +02:00
committed by GitHub
8 changed files with 199 additions and 124 deletions

View File

@ -4,13 +4,14 @@ const { setMenuOptions } = require("../../config/plugins");
const promptOptions = require("../../providers/prompt-options");
const { clear, connect, registerRefresh, isConnected } = require("./back");
let hasRegisterred = false;
const { singleton } = require("../../providers/decorators")
const registerRefreshOnce = singleton((refreshMenu) => {
registerRefresh(refreshMenu);
});
module.exports = (win, options, refreshMenu) => {
if (!hasRegisterred) {
registerRefresh(refreshMenu);
hasRegisterred = true;
}
registerRefreshOnce(refreshMenu);
return [
{

View File

@ -10,6 +10,7 @@ const { fetchFromGenius } = require('../lyrics-genius/back');
const { isEnabled } = require('../../config/plugins');
const { getImage } = require('../../providers/song-info');
const { injectCSS } = require('../utils');
const { cache } = require("../../providers/decorators")
const {
presets,
cropMaxWidth,
@ -34,13 +35,6 @@ const ffmpeg = require('@ffmpeg/ffmpeg').createFFmpeg({
});
const ffmpegMutex = new Mutex();
const cache = {
getCoverBuffer: {
buffer: null,
url: null,
},
};
const config = require('./config');
/** @type {Innertube} */
@ -295,19 +289,10 @@ async function iterableStreamToMP3(
}
}
async function getCoverBuffer(url) {
const store = cache.getCoverBuffer;
if (store.url === url) {
return store.buffer;
}
store.url = url;
const getCoverBuffer = cache(async (url) => {
const nativeImage = cropMaxWidth(await getImage(url));
store.buffer =
nativeImage && !nativeImage.isEmpty() ? nativeImage.toPNG() : null;
return store.buffer;
}
return nativeImage && !nativeImage.isEmpty() ? nativeImage.toPNG() : null;
});
async function writeID3(buffer, metadata, sendFeedback) {
try {

View File

@ -8,6 +8,8 @@ const userData = app.getPath("userData");
const tempIcon = path.join(userData, "tempIcon.png");
const tempBanner = path.join(userData, "tempBanner.png");
const { cache } = require("../../providers/decorators")
module.exports.ToastStyles = {
logo: 1,
banner_centered_top: 2,
@ -31,6 +33,18 @@ module.exports.urgencyLevels = [
{ name: "High", value: "critical" },
];
const nativeImageToLogo = cache((nativeImage) => {
const tempImage = nativeImage.resize({ height: 256 });
const margin = Math.max(tempImage.getSize().width - 256, 0);
return tempImage.crop({
x: Math.round(margin / 2),
y: 0,
width: 256,
height: 256,
});
});
module.exports.notificationImage = (songInfo) => {
if (!songInfo.image) return icon;
if (!config.get("interactive")) return nativeImageToLogo(songInfo.image);
@ -44,7 +58,7 @@ module.exports.notificationImage = (songInfo) => {
};
};
module.exports.saveImage = (img, save_path) => {
module.exports.saveImage = cache((img, save_path) => {
try {
fs.writeFileSync(save_path, img.toPNG());
} catch (err) {
@ -52,19 +66,7 @@ module.exports.saveImage = (img, save_path) => {
return icon;
}
return save_path;
}
function nativeImageToLogo(nativeImage) {
const tempImage = nativeImage.resize({ height: 256 });
const margin = Math.max((tempImage.getSize().width - 256), 0);
return tempImage.crop({
x: Math.round(margin / 2),
y: 0,
width: 256, height: 256
})
}
});
module.exports.save_temp_icons = () => {
for (const kind of Object.keys(module.exports.icons)) {

View File

@ -1,5 +1,6 @@
const { getSongMenu } = require("../../providers/dom-elements");
const { ElementFromFile, templatePath } = require("../utils");
const { singleton } = require("../../providers/decorators")
function $(selector) { return document.querySelector(selector); }
@ -22,7 +23,16 @@ const updatePlayBackSpeed = () => {
};
let menu;
let observingSlider = false;
const setupSliderListener = singleton(() => {
$('#playback-speed-slider').addEventListener('immediate-value-changed', e => {
playbackSpeed = e.detail.value || MIN_PLAYBACK_SPEED;
if (isNaN(playbackSpeed)) {
playbackSpeed = 1;
}
updatePlayBackSpeed();
})
});
const observePopupContainer = () => {
const observer = new MutationObserver(() => {
@ -32,10 +42,7 @@ const observePopupContainer = () => {
if (menu && menu.parentElement.eventSink_?.matches('ytmusic-menu-renderer.ytmusic-player-bar') && !menu.contains(slider)) {
menu.prepend(slider);
if (!observingSlider) {
setupSliderListener();
observingSlider = true;
}
setupSliderListener();
}
});
@ -68,16 +75,6 @@ const setupWheelListener = () => {
})
}
function setupSliderListener() {
$('#playback-speed-slider').addEventListener('immediate-value-changed', e => {
playbackSpeed = e.detail.value || MIN_PLAYBACK_SPEED;
if (isNaN(playbackSpeed)) {
playbackSpeed = 1;
}
updatePlayBackSpeed();
})
}
function forcePlaybackRate(e) {
if (e.target.playbackRate !== playbackSpeed) {
e.target.playbackRate = playbackSpeed

View File

@ -4,6 +4,8 @@ const { setOptions, setMenuOptions, isEnabled } = require("../../config/plugins"
function $(selector) { return document.querySelector(selector); }
const { debounce } = require("../../providers/decorators");
let api, options;
module.exports = (_options) => {
@ -16,7 +18,27 @@ module.exports = (_options) => {
}, { once: true, passive: true })
};
module.exports.moveVolumeHud = moveVolumeHud;
//without this function it would rewrite config 20 time when volume change by 20
const writeOptions = debounce(() => {
setOptions("precise-volume", options);
}, 1000);
module.exports.moveVolumeHud = debounce((showVideo) => {
const volumeHud = $("#volumeHud");
if (!volumeHud) return;
volumeHud.style.top = showVideo
? `${($("ytmusic-player").clientHeight - $("video").clientHeight) / 2}px`
: 0;
}, 250);
const hideVolumeHud = debounce((volumeHud) => {
volumeHud.style.opacity = 0;
}, 2000);
const hideVolumeSlider = debounce((slider) => {
slider.classList.remove("on-hover");
}, 2500);
/** Restore saved volume and setup tooltip */
function firstRun() {
@ -67,33 +89,14 @@ function injectVolumeHud(noVid) {
}
}
let hudMoveTimeout;
function moveVolumeHud(showVideo) {
clearTimeout(hudMoveTimeout);
const volumeHud = $('#volumeHud');
if (!volumeHud) return;
hudMoveTimeout = setTimeout(() => {
volumeHud.style.top = showVideo ? `${($('ytmusic-player').clientHeight - $('video').clientHeight) / 2}px` : 0;
}, 250)
}
let hudFadeTimeout;
function showVolumeHud(volume) {
let volumeHud = $("#volumeHud");
const volumeHud = $("#volumeHud");
if (!volumeHud) return;
volumeHud.textContent = volume + '%';
volumeHud.textContent = `${volume}%`;
volumeHud.style.opacity = 1;
if (hudFadeTimeout) {
clearTimeout(hudFadeTimeout);
}
hudFadeTimeout = setTimeout(() => {
volumeHud.style.opacity = 0;
hudFadeTimeout = null;
}, 2000);
hideVolumeHud(volumeHud);
}
/** Add onwheel event to video player */
@ -110,17 +113,6 @@ function saveVolume(volume) {
writeOptions();
}
//without this function it would rewrite config 20 time when volume change by 20
let writeTimeout;
function writeOptions() {
if (writeTimeout) clearTimeout(writeTimeout);
writeTimeout = setTimeout(() => {
setOptions("precise-volume", options);
writeTimeout = null;
}, 1000)
}
/** Add onwheel event to play bar and also track if play bar is hovered*/
function setupPlaybar() {
const playerbar = $("ytmusic-player-bar");
@ -199,23 +191,12 @@ function updateVolumeSlider() {
}
}
let volumeHoverTimeoutID;
function showVolumeSlider() {
const slider = $("#volume-slider");
// This class display the volume slider if not in minimized mode
slider.classList.add("on-hover");
// Reset timeout if previous one hasn't completed
if (volumeHoverTimeoutID) {
clearTimeout(volumeHoverTimeoutID);
}
// Timeout to remove volume preview after 3 seconds if playbar isn't hovered
volumeHoverTimeoutID = setTimeout(() => {
volumeHoverTimeoutID = null;
if (!$("ytmusic-player-bar").classList.contains("on-hover")) {
slider.classList.remove("on-hover");
}
}, 3000);
hideVolumeSlider(slider);
}
// Set new volume as tooltip for volume slider and icon + expanding slider (appears when window size is small)

113
providers/decorators.js Normal file
View File

@ -0,0 +1,113 @@
module.exports = {
singleton,
debounce,
cache,
throttle,
memoize,
retry,
};
/**
* @template T
* @param {T} fn
* @returns {T}
*/
function singleton(fn) {
let called = false;
return (...args) => {
if (called) return;
called = true;
return fn(...args);
};
}
/**
* @template T
* @param {T} fn
* @param {number} delay
* @returns {T}
*/
function debounce(fn, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
}
/**
* @template T
* @param {T} fn
* @returns {T}
*/
function cache(fn) {
let lastArgs;
let lastResult;
return (...args) => {
if (
args.length !== lastArgs?.length ||
args.some((arg, i) => arg !== lastArgs[i])
) {
lastArgs = args;
lastResult = fn(...args);
}
return lastResult;
};
}
/*
the following are currently unused, but potentially useful in the future
*/
/**
* @template T
* @param {T} fn
* @param {number} delay
* @returns {T}
*/
function throttle(fn, delay) {
let timeout;
return (...args) => {
if (timeout) return;
timeout = setTimeout(() => {
timeout = undefined;
fn(...args);
}, delay);
};
}
/**
* @template T
* @param {T} fn
* @returns {T}
*/
function memoize(fn) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (!cache.has(key)) {
cache.set(key, fn(...args));
}
return cache.get(key);
};
}
/**
* @template T
* @param {T} fn
* @returns {T}
*/
function retry(fn, { retries = 3, delay = 1000 } = {}) {
return (...args) => {
try {
return fn(...args);
} catch (e) {
if (retries > 0) {
retries--;
setTimeout(() => retry(fn, { retries, delay })(...args), delay);
} else {
throw e;
}
}
};
}

View File

@ -1,5 +1,6 @@
const { ipcRenderer } = require("electron");
const { getImage } = require("./song-info");
const { singleton } = require("../providers/decorators");
global.songInfo = {};
@ -14,17 +15,8 @@ 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)
const srcChangedEvent = new CustomEvent('srcChanged');
const singleton = (fn) => {
let called = false;
return (...args) => {
if (called) return;
called = true;
return fn(...args);
}
}
module.exports.setupSeekedListener = singleton(() => {
document.querySelector('video')?.addEventListener('seeked', v => ipcRenderer.send('seeked', v.target.currentTime));
$('video')?.addEventListener('seeked', v => ipcRenderer.send('seeked', v.target.currentTime));
});
module.exports.setupTimeChangedListener = singleton(() => {

View File

@ -4,6 +4,8 @@ const fetch = require("node-fetch");
const config = require("../config");
const { cache } = require("../providers/decorators")
// Fill songInfo with empty values
/**
* @typedef {songInfo} SongInfo
@ -25,16 +27,21 @@ const songInfo = {
};
// Grab the native image using the src
const getImage = async (src) => {
const result = await fetch(src);
const buffer = await result.buffer();
const output = nativeImage.createFromBuffer(buffer);
if (output.isEmpty() && !src.endsWith(".jpg") && src.includes(".jpg")) { // fix hidden webp files (https://github.com/th-ch/youtube-music/issues/315)
return getImage(src.slice(0, src.lastIndexOf(".jpg") + 4));
} else {
return output;
const getImage = cache(
/**
* @returns {Promise<Electron.NativeImage>}
*/
async (src) => {
const result = await fetch(src);
const buffer = await result.buffer();
const output = nativeImage.createFromBuffer(buffer);
if (output.isEmpty() && !src.endsWith(".jpg") && src.includes(".jpg")) { // fix hidden webp files (https://github.com/th-ch/youtube-music/issues/315)
return getImage(src.slice(0, src.lastIndexOf(".jpg") + 4));
} else {
return output;
}
}
};
);
const handleData = async (responseText, win) => {
const data = JSON.parse(responseText);
@ -60,13 +67,10 @@ const handleData = async (responseText, win) => {
songInfo.videoId = videoDetails.videoId;
songInfo.album = data?.videoDetails?.album; // Will be undefined if video exist
const oldUrl = songInfo.imageSrc;
const thumbnails = videoDetails.thumbnail?.thumbnails;
songInfo.imageSrc = thumbnails[thumbnails.length - 1]?.url.split("?")[0];
if (oldUrl !== songInfo.imageSrc) {
songInfo.image = await getImage(songInfo.imageSrc);
}
songInfo.image = await getImage(songInfo.imageSrc);
win.webContents.send("update-song-info", JSON.stringify(songInfo));
}
};