1
0
Sv443 1 жил өмнө
parent
commit
36a21529c7
1 өөрчлөгдсөн 269 нэмэгдсэн , 278 устгасан
  1. 269 278
      dist/BetterYTM.user.js

+ 269 - 278
dist/BetterYTM.user.js

@@ -17,7 +17,7 @@
 // @license           AGPL-3.0-only
 // @author            Sv443
 // @copyright         Sv443 (https://github.com/Sv443)
-// @icon              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/logo/logo_48.png?b=e4b6562
+// @icon              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/logo/logo_48.png?b=75a7936
 // @match             https://music.youtube.com/*
 // @match             https://www.youtube.com/*
 // @run-at            document-start
@@ -35,37 +35,37 @@
 // @grant             GM.openInTab
 // @grant             unsafeWindow
 // @noframes
-// @resource          css-anchor_improvements https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/anchorImprovements.css?b=e4b6562
-// @resource          css-fix_spacing         https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/fixSpacing.css?b=e4b6562
-// @resource          doc-changelog           https://raw.githubusercontent.com/Sv443/BetterYTM/develop/changelog.md?b=e4b6562
-// @resource          icon-advanced_mode      https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/plus_circle_small.svg?b=e4b6562
-// @resource          icon-arrow_down         https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/arrow_down.svg?b=e4b6562
-// @resource          icon-delete             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/delete.svg?b=e4b6562
-// @resource          icon-error              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/error.svg?b=e4b6562
-// @resource          icon-experimental       https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/beaker_small.svg?b=e4b6562
-// @resource          icon-globe              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/globe.svg?b=e4b6562
-// @resource          icon-help               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/help.svg?b=e4b6562
-// @resource          icon-image              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/image.svg?b=e4b6562
-// @resource          icon-image_filled       https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/image_filled.svg?b=e4b6562
-// @resource          icon-link               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/link.svg?b=e4b6562
-// @resource          icon-lyrics             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/lyrics.svg?b=e4b6562
-// @resource          icon-skip_to            https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/skip_to.svg?b=e4b6562
-// @resource          icon-spinner            https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/spinner.svg?b=e4b6562
-// @resource          img-logo                https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/logo/logo_48.png?b=e4b6562
-// @resource          img-close               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/close.png?b=e4b6562
-// @resource          img-discord             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/discord.png?b=e4b6562
-// @resource          img-github              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/github.png?b=e4b6562
-// @resource          img-greasyfork          https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/greasyfork.png?b=e4b6562
-// @resource          img-openuserjs          https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/openuserjs.png?b=e4b6562
-// @resource          trans-de_DE             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/de_DE.json?b=e4b6562
-// @resource          trans-en_US             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_US.json?b=e4b6562
-// @resource          trans-en_UK             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_UK.json?b=e4b6562
-// @resource          trans-es_ES             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/es_ES.json?b=e4b6562
-// @resource          trans-fr_FR             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/fr_FR.json?b=e4b6562
-// @resource          trans-hi_IN             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/hi_IN.json?b=e4b6562
-// @resource          trans-ja_JA             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/ja_JA.json?b=e4b6562
-// @resource          trans-pt_BR             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/pt_BR.json?b=e4b6562
-// @resource          trans-zh_CN             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/zh_CN.json?b=e4b6562
+// @resource          css-anchor_improvements https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/anchorImprovements.css?b=75a7936
+// @resource          css-fix_spacing         https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/fixSpacing.css?b=75a7936
+// @resource          doc-changelog           https://raw.githubusercontent.com/Sv443/BetterYTM/develop/changelog.md?b=75a7936
+// @resource          icon-advanced_mode      https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/plus_circle_small.svg?b=75a7936
+// @resource          icon-arrow_down         https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/arrow_down.svg?b=75a7936
+// @resource          icon-delete             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/delete.svg?b=75a7936
+// @resource          icon-error              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/error.svg?b=75a7936
+// @resource          icon-experimental       https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/beaker_small.svg?b=75a7936
+// @resource          icon-globe              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/globe.svg?b=75a7936
+// @resource          icon-help               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/help.svg?b=75a7936
+// @resource          icon-image              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/image.svg?b=75a7936
+// @resource          icon-image_filled       https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/image_filled.svg?b=75a7936
+// @resource          icon-link               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/link.svg?b=75a7936
+// @resource          icon-lyrics             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/lyrics.svg?b=75a7936
+// @resource          icon-skip_to            https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/skip_to.svg?b=75a7936
+// @resource          icon-spinner            https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/spinner.svg?b=75a7936
+// @resource          img-logo                https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/logo/logo_48.png?b=75a7936
+// @resource          img-close               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/close.png?b=75a7936
+// @resource          img-discord             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/discord.png?b=75a7936
+// @resource          img-github              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/github.png?b=75a7936
+// @resource          img-greasyfork          https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/greasyfork.png?b=75a7936
+// @resource          img-openuserjs          https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/openuserjs.png?b=75a7936
+// @resource          trans-de_DE             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/de_DE.json?b=75a7936
+// @resource          trans-en_US             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_US.json?b=75a7936
+// @resource          trans-en_UK             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_UK.json?b=75a7936
+// @resource          trans-es_ES             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/es_ES.json?b=75a7936
+// @resource          trans-fr_FR             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/fr_FR.json?b=75a7936
+// @resource          trans-hi_IN             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/hi_IN.json?b=75a7936
+// @resource          trans-ja_JA             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/ja_JA.json?b=75a7936
+// @resource          trans-pt_BR             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/pt_BR.json?b=75a7936
+// @resource          trans-zh_CN             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/zh_CN.json?b=75a7936
 // @require           https://cdn.jsdelivr.net/npm/@sv443-network/[email protected]/dist/index.global.js
 // @require           https://cdn.jsdelivr.net/npm/[email protected]/dist/fuse.basic.js
 // @require           https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.js
@@ -148,131 +148,7 @@ function __asyncValues(o) {
 typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
     var e = new Error(message);
     return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
-};//#MARKER video time & volume
-const videoSelector = getDomain() === "ytm" ? "ytmusic-player video" : "#content ytd-player video";
-/**
- * Returns the current video time in seconds
- * Dispatches mouse movement events in case the video time can't be read from the video or progress bar elements (needs a prior user interaction to work)
- * @returns Returns null if the video time is unavailable or no user interaction has happened prior to calling in case of the fallback behavior being used
- */
-function getVideoTime() {
-    return new Promise((res) => {
-        const domain = getDomain();
-        try {
-            if (domain === "ytm") {
-                const vidElem = document.querySelector(videoSelector);
-                if (vidElem)
-                    return res(Math.floor(vidElem.currentTime));
-                onSelectorOld("tp-yt-paper-slider#progress-bar tp-yt-paper-progress#sliderBar", {
-                    listener: (pbEl) => res(!isNaN(Number(pbEl.value)) ? Math.floor(Number(pbEl.value)) : null)
-                });
-            }
-            else if (domain === "yt") {
-                const vidElem = document.querySelector(videoSelector);
-                if (vidElem)
-                    return res(Math.floor(vidElem.currentTime));
-                // YT doesn't update the progress bar when it's hidden (contrary to YTM which never hides it)
-                ytForceShowVideoTime();
-                const pbSelector = ".ytp-chrome-bottom div.ytp-progress-bar[role=\"slider\"]";
-                let videoTime = -1;
-                const mut = new MutationObserver(() => {
-                    // .observe() is only called when the element exists - no need to check for null
-                    videoTime = Number(document.querySelector(pbSelector).getAttribute("aria-valuenow"));
-                });
-                const observe = (progElem) => {
-                    mut.observe(progElem, {
-                        attributes: true,
-                        attributeFilter: ["aria-valuenow"],
-                    });
-                    if (videoTime >= 0 && !isNaN(videoTime)) {
-                        res(Math.floor(videoTime));
-                        mut.disconnect();
-                    }
-                    else
-                        setTimeout(() => {
-                            res(videoTime >= 0 && !isNaN(videoTime) ? Math.floor(videoTime) : null);
-                            mut.disconnect();
-                        }, 500);
-                };
-                onSelectorOld(pbSelector, { listener: observe });
-            }
-        }
-        catch (err) {
-            error("Couldn't get video time due to error:", err);
-            res(null);
-        }
-    });
-}
-/**
- * Sends events that force the video controls to become visible for about 3 seconds.
- * This only works once (for some reason), then the page needs to be reloaded!
- */
-function ytForceShowVideoTime() {
-    const player = document.querySelector("#movie_player");
-    if (!player)
-        return false;
-    const defaultProps = {
-        // needed because otherwise YTM errors out - see https://github.com/Sv443/BetterYTM/issues/18#show_issue
-        view: UserUtils.getUnsafeWindow(),
-        bubbles: true,
-        cancelable: false,
-    };
-    player.dispatchEvent(new MouseEvent("mouseenter", defaultProps));
-    const { x, y, width, height } = player.getBoundingClientRect();
-    const screenY = Math.round(y + height / 2);
-    const screenX = x + Math.min(50, Math.round(width / 3));
-    player.dispatchEvent(new MouseEvent("mousemove", Object.assign(Object.assign({}, defaultProps), { screenY,
-        screenX, movementX: 5, movementY: 0 })));
-    return true;
-}
-/** Waits for the video element to be in its readyState 4 / canplay state and returns it */
-function waitVideoElementReady() {
-    return new Promise((res) => {
-        onSelectorOld(videoSelector, {
-            listener: (vidElem) => __awaiter(this, void 0, void 0, function* () {
-                if (vidElem) {
-                    // this is just after YT has finished doing their own shenanigans with the video time and volume
-                    if (vidElem.readyState === 4)
-                        res(vidElem);
-                    else
-                        vidElem.addEventListener("canplay", () => res(vidElem), { once: true });
-                }
-            }),
-        });
-    });
-}
-//#MARKER other
-/** Whether the DOM has finished loading and elements can be added or modified */
-let domLoaded = false;
-document.addEventListener("DOMContentLoaded", () => domLoaded = true);
-/** Removes all child nodes of an element without invoking the slow-ish HTML parser */
-function clearInner(element) {
-    while (element.hasChildNodes())
-        clearNode(element.firstChild);
-}
-function clearNode(element) {
-    while (element.hasChildNodes())
-        clearNode(element.firstChild);
-    element.parentNode.removeChild(element);
-}
-/**
- * Adds generic, accessible interaction listeners to the passed element.
- * All listeners have the default behavior prevented and stop immediate propagation.
- * @param listenerOptions Provide a {@linkcode listenerOptions} object to configure the listeners
- */
-function onInteraction(elem, listener, listenerOptions) {
-    const proxListener = (e) => {
-        if (e instanceof KeyboardEvent && !(["Enter", " ", "Space", "Spacebar"].includes(e.key)))
-            return;
-        e.preventDefault();
-        e.stopImmediatePropagation();
-        (listenerOptions === null || listenerOptions === void 0 ? void 0 : listenerOptions.once) && e.type === "keydown" && elem.removeEventListener("click", proxListener, listenerOptions);
-        (listenerOptions === null || listenerOptions === void 0 ? void 0 : listenerOptions.once) && e.type === "click" && elem.removeEventListener("keydown", proxListener, listenerOptions);
-        listener(e);
-    };
-    elem.addEventListener("click", proxListener, listenerOptions);
-    elem.addEventListener("keydown", proxListener, listenerOptions);
-}// I know TS enums are impure but it doesn't really matter here, plus they look cooler
+};// I know TS enums are impure but it doesn't really matter here, plus they look cooler
 var LogLevel;
 (function (LogLevel) {
     LogLevel[LogLevel["Debug"] = 0] = "Debug";
@@ -298,7 +174,7 @@ var PluginIntent;
 })(PluginIntent || (PluginIntent = {}));const modeRaw = "development";
 const branchRaw = "develop";
 const hostRaw = "github";
-const buildNumberRaw = "e4b6562";
+const buildNumberRaw = "75a7936";
 /** The mode in which the script was built (production or development) */
 const mode = (modeRaw.match(/^#{{.+}}$/) ? "production" : modeRaw);
 /** The branch to use in various URLs that point to the GitHub repo */
@@ -333,41 +209,7 @@ const scriptInfo = {
     name: GM.info.script.name,
     version: GM.info.script.version,
     namespace: GM.info.script.namespace,
-};/** Options that are applied to every SelectorObserver instance */
-const defaultObserverOptions = {
-    defaultDebounce: 100,
-};
-/** Global SelectorObserver instances usable throughout the script for improved performance */
-const globservers = {};
-/** Call after DOM load to initialize all SelectorObserver instances */
-function initObservers() {
-    try {
-        // #SECTION body = the entire <body> element - use sparingly due to performance impacts!
-        globservers.body = new UserUtils.SelectorObserver(document.body, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: false }));
-        globservers.body.enable();
-        // #SECTION playerBar = media controls bar at the bottom of the page
-        const playerBarSelector = "ytmusic-app-layout ytmusic-player-bar.ytmusic-app";
-        globservers.playerBar = new UserUtils.SelectorObserver(playerBarSelector, Object.assign(Object.assign({}, defaultObserverOptions), { defaultDebounce: 200 }));
-        globservers.body.addListener(playerBarSelector, {
-            listener: () => globservers.playerBar.enable(),
-        });
-        // #SECTION playerBarInfo = song title, artist, album, etc. inside the player bar
-        const playerBarInfoSelector = `${playerBarSelector} .middle-controls .content-info-wrapper`;
-        globservers.playerBarInfo = new UserUtils.SelectorObserver(playerBarInfoSelector, Object.assign(Object.assign({}, defaultObserverOptions), { attributes: true, attributeFilter: ["title"] }));
-        globservers.playerBarInfo.addListener(playerBarInfoSelector, {
-            listener: () => globservers.playerBarInfo.enable(),
-        });
-        //#SECTION finalize
-        emitInterface("bytm:observersReady");
-    }
-    catch (err) {
-        error("Failed to initialize observers:", err);
-    }
-}
-/** Interface function for adding listeners to the already present observers */
-function addSelectorListener(observerName, selector, options) {
-    globservers[observerName].addListener(selector, options);
-}var de_DE = {
+};var de_DE = {
 	name: "Deutsch (Deutschland)",
 	nameEnglish: "German",
 	emoji: "🇩🇪",
@@ -994,7 +836,7 @@ function initSiteEvents() {
                 }
             });
             // only observe added or removed elements
-            onSelectorOld("#side-panel #contents.ytmusic-player-queue", {
+            addSelectorListener("sidePanel", "#contents.ytmusic-player-queue", {
                 listener: (el) => {
                     queueObs.observe(el, {
                         childList: true,
@@ -1007,7 +849,7 @@ function initSiteEvents() {
                     emitSiteEvent("autoplayQueueChanged", target);
                 }
             });
-            onSelectorOld("#side-panel ytmusic-player-queue #automix-contents", {
+            addSelectorListener("sidePanel", "ytmusic-player-queue #automix-contents", {
                 listener: (el) => {
                     autoplayObs.observe(el, {
                         childList: true,
@@ -1550,7 +1392,7 @@ function compareVersions(a, b) {
 function initVolumeFeatures() {
     return __awaiter(this, void 0, void 0, function* () {
         // not technically an input element but behaves pretty much the same
-        onSelectorOld("tp-yt-paper-slider#volume-slider", {
+        addSelectorListener("playerBarRightControls", "tp-yt-paper-slider#volume-slider", {
             listener: (sliderElem) => __awaiter(this, void 0, void 0, function* () {
                 const volSliderCont = document.createElement("div");
                 volSliderCont.id = "bytm-vol-slider-cont";
@@ -1618,6 +1460,7 @@ function addVolumeSliderLabel(sliderElem, sliderContainer) {
         const labelElem = document.createElement("div");
         labelElem.classList.add("label");
         labelElem.textContent = getLabel(sliderElem.value);
+        labelContElem.appendChild(labelElem);
         // prevent video from minimizing
         labelContElem.addEventListener("click", (e) => e.stopPropagation());
         labelContElem.addEventListener("keydown", (e) => ["Enter", "Space", " "].includes(e.key) && e.stopPropagation());
@@ -1639,11 +1482,8 @@ function addVolumeSliderLabel(sliderElem, sliderContainer) {
         siteEvents.on("configChanged", () => {
             updateLabel();
         });
-        onSelectorOld("#bytm-vol-slider-cont", {
-            listener: (volumeCont) => {
-                labelContElem.appendChild(labelElem);
-                volumeCont.appendChild(labelContElem);
-            },
+        addSelectorListener("playerBarRightControls", "#bytm-vol-slider-cont", {
+            listener: (volumeCont) => volumeCont.appendChild(labelContElem),
         });
         let lastSliderVal = Number(sliderElem.value);
         // show label if hovering over slider or slider is focused
@@ -2926,7 +2766,7 @@ function addWatermark() {
                 exchangeLogo();
         };
         onInteraction(watermark, watermarkOpenMenu);
-        onSelectorOld("ytmusic-nav-bar #left-content", {
+        addSelectorListener("navBar", "ytmusic-nav-bar #left-content", {
             listener: (logoElem) => UserUtils.insertAfter(logoElem, watermark),
         });
         log("Added watermark element");
@@ -2941,7 +2781,7 @@ function improveLogo() {
             improveLogoCalled = true;
             const res = yield UserUtils.fetchAdvanced("https://music.youtube.com/img/on_platform_logo_dark.svg");
             const svg = yield res.text();
-            onSelectorOld("ytmusic-logo a", {
+            addSelectorListener("navBar", "ytmusic-logo a", {
                 listener: (logoElem) => {
                     var _a;
                     logoElem.classList.add("bytm-mod-logo", "bytm-no-select");
@@ -2961,7 +2801,7 @@ function improveLogo() {
 }
 /** Exchanges the default YTM logo into BetterYTM's logo with a sick ass animation */
 function exchangeLogo() {
-    onSelectorOld(".bytm-mod-logo", {
+    addSelectorListener("navBar", ".bytm-mod-logo", {
         listener: (logoElem) => __awaiter(this, void 0, void 0, function* () {
             if (logoElem.classList.contains("bytm-logo-exchanged"))
                 return;
@@ -3018,13 +2858,13 @@ function addConfigMenuOption(container) {
 /** Removes the "Upgrade" / YT Music Premium tab from the sidebar */
 function removeUpgradeTab() {
     return __awaiter(this, void 0, void 0, function* () {
-        onSelectorOld("ytmusic-app-layout tp-yt-app-drawer #contentContainer #guide-content #items ytmusic-guide-entry-renderer:nth-of-type(4)", {
+        addSelectorListener("sideBar", "#contentContainer #guide-content #items ytmusic-guide-entry-renderer:nth-of-type(4)", {
             listener: (tabElemLarge) => {
                 tabElemLarge.remove();
                 log("Removed large upgrade tab");
             },
         });
-        onSelectorOld("ytmusic-app-layout #mini-guide ytmusic-guide-renderer #sections ytmusic-guide-section-renderer[is-primary] #items ytmusic-guide-entry-renderer:nth-of-type(4)", {
+        addSelectorListener("sideBarMini", "ytmusic-guide-renderer #sections ytmusic-guide-section-renderer[is-primary] #items ytmusic-guide-entry-renderer:nth-of-type(4)", {
             listener: (tabElemSmall) => {
                 tabElemSmall.remove();
                 log("Removed small upgrade tab");
@@ -3067,26 +2907,27 @@ function addAnchorImprovements() {
                     UserUtils.addParent(thumbnailElem, anchorElem);
                 }
             };
+            // TODO: needs to be optimized
             // home page
-            onSelectorOld("#contents.ytmusic-section-list-renderer ytmusic-carousel-shelf-renderer ytmusic-responsive-list-item-renderer", {
+            addSelectorListener("body", "#contents.ytmusic-section-list-renderer ytmusic-carousel-shelf-renderer ytmusic-responsive-list-item-renderer", {
                 continuous: true,
                 all: true,
                 listener: addListItemAnchors,
             });
             // related tab in /watch
-            onSelectorOld("ytmusic-tab-renderer[page-type=\"MUSIC_PAGE_TYPE_TRACK_RELATED\"] ytmusic-responsive-list-item-renderer", {
+            addSelectorListener("body", "ytmusic-tab-renderer[page-type=\"MUSIC_PAGE_TYPE_TRACK_RELATED\"] ytmusic-responsive-list-item-renderer", {
                 continuous: true,
                 all: true,
                 listener: addListItemAnchors,
             });
             // playlists
-            onSelectorOld("#contents.ytmusic-section-list-renderer ytmusic-playlist-shelf-renderer ytmusic-responsive-list-item-renderer", {
+            addSelectorListener("body", "#contents.ytmusic-section-list-renderer ytmusic-playlist-shelf-renderer ytmusic-responsive-list-item-renderer", {
                 continuous: true,
                 all: true,
                 listener: addListItemAnchors,
             });
             // generic shelves
-            onSelectorOld("#contents.ytmusic-section-list-renderer ytmusic-shelf-renderer ytmusic-responsive-list-item-renderer", {
+            addSelectorListener("body", "#contents.ytmusic-section-list-renderer ytmusic-shelf-renderer ytmusic-responsive-list-item-renderer", {
                 continuous: true,
                 all: true,
                 listener: addListItemAnchors,
@@ -3102,13 +2943,13 @@ function addAnchorImprovements() {
                 improveSidebarAnchors(items);
                 return items.length;
             };
-            onSelectorOld("ytmusic-app-layout tp-yt-app-drawer #contentContainer #guide-content #items ytmusic-guide-entry-renderer", {
+            addSelectorListener("sideBar", "#contentContainer #guide-content #items ytmusic-guide-entry-renderer", {
                 listener: (sidebarCont) => {
                     const itemsAmt = addSidebarAnchors(sidebarCont);
                     log(`Added anchors around ${itemsAmt} sidebar ${UserUtils.autoPlural("item", itemsAmt)}`);
                 },
             });
-            onSelectorOld("ytmusic-app-layout #mini-guide ytmusic-guide-renderer ytmusic-guide-section-renderer #items ytmusic-guide-entry-renderer", {
+            addSelectorListener("sideBarMini", "ytmusic-guide-renderer ytmusic-guide-section-renderer #items ytmusic-guide-entry-renderer", {
                 listener: (miniSidebarCont) => {
                     const itemsAmt = addSidebarAnchors(miniSidebarCont);
                     log(`Added anchors around ${itemsAmt} mini sidebar ${UserUtils.autoPlural("item", itemsAmt)}`);
@@ -3200,7 +3041,7 @@ function fixSpacing() {
 /** Adds a button to the queue to scroll to the active song */
 function addScrollToActiveBtn() {
     return __awaiter(this, void 0, void 0, function* () {
-        onSelectorOld("#side-panel #tabsContent tp-yt-paper-tab:nth-of-type(1)", {
+        addSelectorListener("sidePanel", "#tabsContent tp-yt-paper-tab:nth-of-type(1)", {
             listener: (tabElem) => __awaiter(this, void 0, void 0, function* () {
                 const containerElem = document.createElement("div");
                 containerElem.id = "bytm-scroll-to-active-btn-cont";
@@ -3339,13 +3180,13 @@ function initThumbnailOverlay() {
                 const imgElem = document.createElement("img");
                 imgElem.classList.add("bytm-generic-btn-img");
                 toggleBtnElem.appendChild(imgElem);
-                onSelectorOld(".middle-controls-buttons ytmusic-like-button-renderer#like-button-renderer", {
+                addSelectorListener("playerBarMiddleButtons", "ytmusic-like-button-renderer#like-button-renderer", {
                     listener: (likeContainer) => UserUtils.insertAfter(likeContainer, toggleBtnElem),
                 });
             }
             log("Added thumbnail overlay");
         });
-        addSelectorListener("body", playerSelector, {
+        addSelectorListener("mainPanel", playerSelector, {
             listener(playerEl) {
                 if (playerEl.getAttribute("player-ui-state") === "INACTIVE") {
                     const obs = new MutationObserver(() => {
@@ -3403,7 +3244,7 @@ function initAutoCloseToasts() {
         try {
             const animTimeout = 300;
             const closeTimeout = Math.max(getFeatures().closeToastsTimeout * 1000 + animTimeout, animTimeout);
-            onSelectorOld("tp-yt-paper-toast#toast", {
+            addSelectorListener("popupContainer", "tp-yt-paper-toast#toast", {
                 all: true,
                 continuous: true,
                 listener: (toastElems) => __awaiter(this, void 0, void 0, function* () {
@@ -3656,7 +3497,7 @@ let currentSongTitle = "";
 /** Adds a lyrics button to the media controls bar */
 function addMediaCtrlLyricsBtn() {
     return __awaiter(this, void 0, void 0, function* () {
-        onSelectorOld(".middle-controls-buttons ytmusic-like-button-renderer#like-button-renderer", { listener: addActualMediaCtrlLyricsBtn });
+        addSelectorListener("playerBarMiddleButtons", "ytmusic-like-button-renderer#like-button-renderer", { listener: addActualMediaCtrlLyricsBtn });
     });
 }
 /** Actually adds the lyrics button after the like button renderer has been verified to exist */
@@ -4018,7 +3859,7 @@ function initQueueButtons() {
         ];
         if (getFeatures().listButtonsPlacement === "everywhere") {
             for (const selector of listSelectors) {
-                onSelectorOld(selector, {
+                addSelectorListener("body", selector, {
                     all: true,
                     continuous: true,
                     listener: (songLists) => {
@@ -4028,7 +3869,6 @@ function initQueueButtons() {
                 });
             }
         }
-        // TODO: support grids?
     });
 }
 /**
@@ -4931,6 +4771,216 @@ function getFeaturesInterface() {
             features[ftKey] = undefined;
     }
     return features;
+}/** Options that are applied to every SelectorObserver instance */
+const defaultObserverOptions = {
+    defaultDebounce: 100,
+};
+/** Global SelectorObserver instances usable throughout the script for improved performance */
+const globservers = {};
+/** Call after DOM load to initialize all SelectorObserver instances */
+function initObservers() {
+    try {
+        // #SECTION body = the entire <body> element - use sparingly due to performance impacts!
+        globservers.body = new UserUtils.SelectorObserver(document.body, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: false }));
+        globservers.body.enable();
+        if (getDomain() !== "ytm")
+            return;
+        //#SECTION navBar = the navigation / title bar at the top of the page
+        const navBarSelector = "ytmusic-nav-bar";
+        globservers.navBar = new UserUtils.SelectorObserver(navBarSelector, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: false }));
+        globservers.body.addListener(navBarSelector, {
+            listener: () => globservers.navBar.enable(),
+        });
+        // #SECTION mainPanel = the main content panel - includes things like the video element
+        const mainPanelSelector = "ytmusic-player-page #main-panel";
+        globservers.mainPanel = new UserUtils.SelectorObserver(mainPanelSelector, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: true }));
+        globservers.body.addListener(mainPanelSelector, {
+            listener: () => globservers.mainPanel.enable(),
+        });
+        // #SECTION sideBar = the sidebar on the left side of the page
+        const sidebarSelector = "ytmusic-app-layout tp-yt-app-drawer";
+        globservers.sideBar = new UserUtils.SelectorObserver(sidebarSelector, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: true }));
+        globservers.body.addListener(sidebarSelector, {
+            listener: () => globservers.sideBar.enable(),
+        });
+        // #SECTION sideBarMini = the minimized sidebar on the left side of the page
+        const sideBarMiniSelector = "ytmusic-app-layout #mini-guide";
+        globservers.sideBarMini = new UserUtils.SelectorObserver(sideBarMiniSelector, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: true }));
+        globservers.body.addListener(sideBarMiniSelector, {
+            listener: () => globservers.sideBarMini.enable(),
+        });
+        // #SECTION sidePanel = the side panel on the right side of the /watch page
+        const sidePanelSelector = "#side-panel";
+        globservers.sidePanel = new UserUtils.SelectorObserver(sidePanelSelector, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: true }));
+        globservers.body.addListener(sidePanelSelector, {
+            listener: () => globservers.sidePanel.enable(),
+        });
+        // #SECTION playerBar = media controls bar at the bottom of the page
+        const playerBarSelector = "ytmusic-app-layout ytmusic-player-bar.ytmusic-app";
+        globservers.playerBar = new UserUtils.SelectorObserver(playerBarSelector, Object.assign(Object.assign({}, defaultObserverOptions), { defaultDebounce: 200 }));
+        globservers.body.addListener(playerBarSelector, {
+            listener: () => {
+                globservers.playerBar.enable();
+            },
+        });
+        // #SECTION playerBarInfo = song title, artist, album, etc. inside the player bar
+        const playerBarInfoSelector = `${playerBarSelector} .middle-controls .content-info-wrapper`;
+        globservers.playerBarInfo = new UserUtils.SelectorObserver(playerBarInfoSelector, Object.assign(Object.assign({}, defaultObserverOptions), { attributes: true, attributeFilter: ["title"] }));
+        globservers.playerBarInfo.addListener(playerBarInfoSelector, {
+            listener: () => globservers.playerBarInfo.enable(),
+        });
+        // #SECTION playerBarMiddleButtons = the buttons inside the player bar (like, dislike, lyrics, etc.)
+        const playerBarMiddleButtonsSelector = ".middle-controls .middle-controls-buttons";
+        globservers.playerBarMiddleButtons = new UserUtils.SelectorObserver(playerBarMiddleButtonsSelector, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: true }));
+        globservers.playerBar.addListener(playerBarMiddleButtonsSelector, {
+            listener: () => globservers.playerBarMiddleButtons.enable(),
+        });
+        // #SECTION playerBarRightControls = the controls on the right side of the player bar (volume, repeat, shuffle, etc.)
+        const playerBarRightControls = ".right-controls .middle-controls-buttons";
+        globservers.playerBarRightControls = new UserUtils.SelectorObserver(playerBarRightControls, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: true }));
+        globservers.playerBar.addListener(playerBarRightControls, {
+            listener: () => globservers.playerBarRightControls.enable(),
+        });
+        // #SECTION popupContainer = the container for popups (e.g. the queue popup)
+        const popupContainerSelector = "ytmusic-app ytmusic-popup-container";
+        globservers.popupContainer = new UserUtils.SelectorObserver(popupContainerSelector, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: true }));
+        globservers.body.addListener(popupContainerSelector, {
+            listener: () => globservers.popupContainer.enable(),
+        });
+        //#SECTION finalize
+        emitInterface("bytm:observersReady");
+    }
+    catch (err) {
+        error("Failed to initialize observers:", err);
+    }
+}
+/** Interface function for adding listeners to the {@linkcode globservers} */
+function addSelectorListener(observerName, selector, options) {
+    globservers[observerName].addListener(selector, options);
+}//#MARKER video time & volume
+const videoSelector = getDomain() === "ytm" ? "ytmusic-player video" : "#content ytd-player video";
+/**
+ * Returns the current video time in seconds
+ * Dispatches mouse movement events in case the video time can't be read from the video or progress bar elements (needs a prior user interaction to work)
+ * @returns Returns null if the video time is unavailable or no user interaction has happened prior to calling in case of the fallback behavior being used
+ */
+function getVideoTime() {
+    return new Promise((res) => {
+        const domain = getDomain();
+        try {
+            if (domain === "ytm") {
+                const vidElem = document.querySelector(videoSelector);
+                if (vidElem)
+                    return res(Math.floor(vidElem.currentTime));
+                addSelectorListener("playerBar", "tp-yt-paper-slider#progress-bar tp-yt-paper-progress#sliderBar", {
+                    listener: (pbEl) => res(!isNaN(Number(pbEl.value)) ? Math.floor(Number(pbEl.value)) : null)
+                });
+            }
+            else if (domain === "yt") {
+                const vidElem = document.querySelector(videoSelector);
+                if (vidElem)
+                    return res(Math.floor(vidElem.currentTime));
+                // YT doesn't update the progress bar when it's hidden (contrary to YTM which never hides it)
+                ytForceShowVideoTime();
+                const pbSelector = ".ytp-chrome-bottom div.ytp-progress-bar[role=\"slider\"]";
+                let videoTime = -1;
+                const mut = new MutationObserver(() => {
+                    // .observe() is only called when the element exists - no need to check for null
+                    videoTime = Number(document.querySelector(pbSelector).getAttribute("aria-valuenow"));
+                });
+                const observe = (progElem) => {
+                    mut.observe(progElem, {
+                        attributes: true,
+                        attributeFilter: ["aria-valuenow"],
+                    });
+                    if (videoTime >= 0 && !isNaN(videoTime)) {
+                        res(Math.floor(videoTime));
+                        mut.disconnect();
+                    }
+                    else
+                        setTimeout(() => {
+                            res(videoTime >= 0 && !isNaN(videoTime) ? Math.floor(videoTime) : null);
+                            mut.disconnect();
+                        }, 500);
+                };
+                addSelectorListener("body", pbSelector, { listener: observe });
+            }
+        }
+        catch (err) {
+            error("Couldn't get video time due to error:", err);
+            res(null);
+        }
+    });
+}
+/**
+ * Sends events that force the video controls to become visible for about 3 seconds.
+ * This only works once (for some reason), then the page needs to be reloaded!
+ */
+function ytForceShowVideoTime() {
+    const player = document.querySelector("#movie_player");
+    if (!player)
+        return false;
+    const defaultProps = {
+        // needed because otherwise YTM errors out - see https://github.com/Sv443/BetterYTM/issues/18#show_issue
+        view: UserUtils.getUnsafeWindow(),
+        bubbles: true,
+        cancelable: false,
+    };
+    player.dispatchEvent(new MouseEvent("mouseenter", defaultProps));
+    const { x, y, width, height } = player.getBoundingClientRect();
+    const screenY = Math.round(y + height / 2);
+    const screenX = x + Math.min(50, Math.round(width / 3));
+    player.dispatchEvent(new MouseEvent("mousemove", Object.assign(Object.assign({}, defaultProps), { screenY,
+        screenX, movementX: 5, movementY: 0 })));
+    return true;
+}
+/** Waits for the video element to be in its readyState 4 / canplay state and returns it */
+function waitVideoElementReady() {
+    return new Promise((res) => {
+        addSelectorListener("body", videoSelector, {
+            listener: (vidElem) => __awaiter(this, void 0, void 0, function* () {
+                if (vidElem) {
+                    // this is just after YT has finished doing their own shenanigans with the video time and volume
+                    if (vidElem.readyState === 4)
+                        res(vidElem);
+                    else
+                        vidElem.addEventListener("canplay", () => res(vidElem), { once: true });
+                }
+            }),
+        });
+    });
+}
+//#MARKER other
+/** Whether the DOM has finished loading and elements can be added or modified */
+let domLoaded = false;
+document.addEventListener("DOMContentLoaded", () => domLoaded = true);
+/** Removes all child nodes of an element without invoking the slow-ish HTML parser */
+function clearInner(element) {
+    while (element.hasChildNodes())
+        clearNode(element.firstChild);
+}
+function clearNode(element) {
+    while (element.hasChildNodes())
+        clearNode(element.firstChild);
+    element.parentNode.removeChild(element);
+}
+/**
+ * Adds generic, accessible interaction listeners to the passed element.
+ * All listeners have the default behavior prevented and stop immediate propagation.
+ * @param listenerOptions Provide a {@linkcode listenerOptions} object to configure the listeners
+ */
+function onInteraction(elem, listener, listenerOptions) {
+    const proxListener = (e) => {
+        if (e instanceof KeyboardEvent && !(["Enter", " ", "Space", "Spacebar"].includes(e.key)))
+            return;
+        e.preventDefault();
+        e.stopImmediatePropagation();
+        (listenerOptions === null || listenerOptions === void 0 ? void 0 : listenerOptions.once) && e.type === "keydown" && elem.removeEventListener("click", proxListener, listenerOptions);
+        (listenerOptions === null || listenerOptions === void 0 ? void 0 : listenerOptions.once) && e.type === "click" && elem.removeEventListener("keydown", proxListener, listenerOptions);
+        listener(e);
+    };
+    elem.addEventListener("click", proxListener, listenerOptions);
+    elem.addEventListener("keydown", proxListener, listenerOptions);
 }let curLogLevel = LogLevel.Info;
 /** Common prefix to be able to tell logged messages apart and filter them in devtools */
 const consPrefix = `[${scriptInfo.name}]`;
@@ -5156,64 +5206,6 @@ function getChangelogHtmlWithDetails() {
             return `Error while preparing changelog: ${err}`;
         }
     });
-}const selectorMap = new Map();
-/**
- * Calls the {@linkcode listener} as soon as the {@linkcode selector} exists in the DOM.
- * Listeners are deleted when they are called once, unless `options.continuous` is set.
- * Multiple listeners with the same selector may be registered.
- * @param selector The selector to listen for
- * @param options Used for switching to `querySelectorAll()` and for calling the listener continuously
- * @template TElem The type of element that the listener will return as its argument (defaults to the generic type HTMLElement)
- * @deprecated To be replaced with UserUtils' SelectorObserver class
- */
-function onSelectorOld(selector, options) {
-    let selectorMapItems = [];
-    if (selectorMap.has(selector))
-        selectorMapItems = selectorMap.get(selector);
-    // I don't feel like dealing with intersecting types, this should work just fine at runtime
-    // @ts-ignore
-    selectorMapItems.push(options);
-    selectorMap.set(selector, selectorMapItems);
-    checkSelectorExists(selector, selectorMapItems);
-}
-function checkSelectorExists(selector, options) {
-    const deleteIndices = [];
-    options.forEach((option, i) => {
-        try {
-            const elements = option.all ? document.querySelectorAll(selector) : document.querySelector(selector);
-            if ((elements !== null && elements instanceof NodeList && elements.length > 0) || elements !== null) {
-                // I don't feel like dealing with intersecting types, this should work just fine at runtime
-                // @ts-ignore
-                option.listener(elements);
-                if (!option.continuous)
-                    deleteIndices.push(i);
-            }
-        }
-        catch (err) {
-            console.error(`Couldn't call listener for selector '${selector}'`, err);
-        }
-    });
-    if (deleteIndices.length > 0) {
-        const newOptsArray = options.filter((_, i) => !deleteIndices.includes(i));
-        if (newOptsArray.length === 0)
-            selectorMap.delete(selector);
-        else {
-            // once again laziness strikes
-            // @ts-ignore
-            selectorMap.set(selector, newOptsArray);
-        }
-    }
-}
-/**
- * Initializes a MutationObserver that checks for all registered selectors whenever an element is added to or removed from the `<body>`
- * @param options For fine-tuning what triggers the MutationObserver's checking function - `subtree` and `childList` are set to true by default
- */
-function initOnSelector(options = {}) {
-    const observer = new MutationObserver(() => {
-        for (const [selector, options] of selectorMap.entries())
-            checkSelectorExists(selector, options);
-    });
-    observer.observe(document.body, Object.assign({ subtree: true, childList: true }, options));
 }/**
  * Constructs a URL from a base URL and a record of query parameters.
  * If a value is null, the parameter will be valueless.
@@ -5520,7 +5512,6 @@ function onDomLoad() {
         try {
             insertGlobalStyle();
             initObservers();
-            initOnSelector();
             yield initVersionCheck();
         }
         catch (err) {