From 7abc67b456c4def29789b131696d0fe8757b92de Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sun, 12 Mar 2023 21:35:03 +0200 Subject: [PATCH 1/4] Create providers/decorators.js --- providers/decorators.js | 113 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 providers/decorators.js diff --git a/providers/decorators.js b/providers/decorators.js new file mode 100644 index 00000000..63ba22cb --- /dev/null +++ b/providers/decorators.js @@ -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; + } + } + }; +} From f1073e37b5fc24ca59a349731d1e5e9bd2cdeb0f Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Sun, 12 Mar 2023 21:41:15 +0200 Subject: [PATCH 2/4] use pseudo decorators --- plugins/discord/menu.js | 11 ++--- plugins/notifications/utils.js | 30 +++++++------ plugins/playback-speed/front.js | 27 ++++++------ plugins/precise-volume/front.js | 75 ++++++++++++--------------------- providers/song-info-front.js | 12 +----- providers/song-info.js | 32 ++++++++------ 6 files changed, 82 insertions(+), 105 deletions(-) diff --git a/plugins/discord/menu.js b/plugins/discord/menu.js index 2853a502..78d45e0e 100644 --- a/plugins/discord/menu.js +++ b/plugins/discord/menu.js @@ -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 [ { diff --git a/plugins/notifications/utils.js b/plugins/notifications/utils.js index 8717b3a1..cb472969 100644 --- a/plugins/notifications/utils.js +++ b/plugins/notifications/utils.js @@ -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)) { diff --git a/plugins/playback-speed/front.js b/plugins/playback-speed/front.js index da01182c..e776923c 100644 --- a/plugins/playback-speed/front.js +++ b/plugins/playback-speed/front.js @@ -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 diff --git a/plugins/precise-volume/front.js b/plugins/precise-volume/front.js index 2840e480..13bc4746 100644 --- a/plugins/precise-volume/front.js +++ b/plugins/precise-volume/front.js @@ -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) diff --git a/providers/song-info-front.js b/providers/song-info-front.js index 829b31cf..000928c6 100644 --- a/providers/song-info-front.js +++ b/providers/song-info-front.js @@ -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(() => { diff --git a/providers/song-info.js b/providers/song-info.js index 1e26b99c..85fbf4b8 100644 --- a/providers/song-info.js +++ b/providers/song-info.js @@ -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} + */ + 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)); } }; From 8124c2b21855ab321277264e4b7e9e74b60a2661 Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Mon, 27 Mar 2023 22:25:38 +0300 Subject: [PATCH 3/4] lint --- providers/song-info.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/song-info.js b/providers/song-info.js index 85fbf4b8..d63bddf0 100644 --- a/providers/song-info.js +++ b/providers/song-info.js @@ -27,7 +27,7 @@ const songInfo = { }; // Grab the native image using the src -const getImage = cache ( +const getImage = cache( /** * @returns {Promise} */ From 4e10dab5a877d966501c94d24aca38404878178c Mon Sep 17 00:00:00 2001 From: Araxeus <78568641+Araxeus@users.noreply.github.com> Date: Mon, 27 Mar 2023 22:25:57 +0300 Subject: [PATCH 4/4] cache downloader `getCoverBuffer()` --- plugins/downloader/back.js | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index 6263930b..5d1f03b2 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -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 {