Ver código fonte

add autoclick best genius result feature

Sv443 3 anos atrás
pai
commit
7586a6ffe7
1 arquivos alterados com 171 adições e 49 exclusões
  1. 171 49
      BetterYTM.user.js

+ 171 - 49
BetterYTM.user.js

@@ -10,6 +10,7 @@
 // @description:de  Verbesserungen für YouTube Music
 // @match           https://music.youtube.com/*
 // @match           https://www.youtube.com/*
+// @match           https://genius.com/search*
 // @icon            https://www.google.com/s2/favicons?domain=music.youtube.com
 // @run-at          document-start
 // @connect         self
@@ -38,22 +39,26 @@
  * If this userscript ever becomes something I might add like a menu to toggle these
  */
 const features = Object.freeze({
-    // --- Quality of Life ---
+
+// --- Quality of Life ---
     /** Whether arrow keys should skip forwards and backwards by 10 seconds */
     arrowKeySupport: true,
     /** Whether to remove the "Upgrade" / YT Music Premium tab */
     removeUpgradeTab: true,
 
-    // --- Extra Features ---
+// --- Extra Features ---
     /** Whether to add a button or key combination (TODO) to switch between the YT and YTM sites on a video */
     switchBetweenSites: true,
-    /** Adds a button to the media controls bar to open the current song's genius.com lyrics in a new tab */
+    /** Adds a button to the media controls bar to search for the current song's lyrics on genius.com in a new tab */
     geniusLyrics: true,
+    /** This option makes the genius.com lyrics search button from above automatically open the best matching result */
+    geniusAutoclickBestResult: true,
 
-    // --- Other ---
+// --- Other ---
     /** Set to true to remove the watermark under the YTM logo */
     removeWatermark: false,
 
+
     // /** The theme color - accepts any CSS color value - default is "#ff0000" */
     // themeColor: "#0f0",
 });
@@ -74,11 +79,12 @@ const dbg = true;
 //#MARKER types
 
 
-/** @typedef {"yt"|"ytm"} Domain Constant string representation of which domain this script is currently running on */
+/** @typedef {"yt"|"ytm"|"genius"} Domain Constant string representation of which domain this script is currently running on */
 
 
 //#MARKER init
 
+
 /** Specifies the hard limit for repetitive tasks */
 const triesLimit = 20;
 
@@ -117,7 +123,6 @@ function onDomLoad()
 
     try
     {
-        // YTM-specific
         if(domain === "ytm")
         {
             if(features.arrowKeySupport)
@@ -136,9 +141,17 @@ function onDomLoad()
                 addGeniusButton();
         }
 
-        // Both YTM and YT
-        if(features.switchBetweenSites)
-            initSiteSwitch(domain);
+        if(["ytm", "yt"].includes(domain))
+        {
+            if(features.switchBetweenSites)
+                initSiteSwitch(domain);
+        }
+
+        if(domain === "genius")
+        {
+            if(features.geniusAutoclickBestResult)
+                autoclickGeniusResult();
+        }
     }
     catch(err)
     {
@@ -454,18 +467,166 @@ function addGeniusButton()
     obs.observe(songTitleElem, { attributes: true, attributeFilter: [ "title" ] });
 }
 
+/**
+ * Returns the genius.com search URL for the current song
+ * @returns {string}
+ */
+function getGeniusUrl()
+{
+    try
+    {
+        const sanitizeSongName = (songName) => {
+            let sanitized;
+
+            if(songName.match(/\(|feat|ft/gmi))
+            {
+                // should hopefully trim right after the song name
+                sanitized = songName.substring(0, songName.indexOf("("));
+            }
+
+            return (sanitized || songName).trim();
+        };
+
+        const songNameRaw = document.querySelector(".content-info-wrapper > yt-formatted-string").title;
+        const songName = sanitizeSongName(songNameRaw);
+
+        const songMeta = document.querySelector("span.subtitle > yt-formatted-string:first-child").title;
+        const artistName = songMeta.split(/\s*\u2022\s*/gmiu)[0]; // split at • (•) character
+        // TODO: artist might need further splitting before comma or ampersand
+
+        const sn = encodeURIComponent(songName);
+        const an = encodeURIComponent(artistName);
+
+        const acParams = features.geniusAutoclickBestResult ? `&bytm-ac-sn=${sn}&bytm-ac-an=${an}` : "";
+
+        const url = `https://genius.com/search?q=${sn}%20${an}${acParams}`;
+
+        dbg && console.info(`BetterYTM: Resolved genius.com URL for song '${songName}' by '${artistName}': ${url}`);
+
+        return url;
+    }
+    catch(err)
+    {
+        console.error(`BetterYTM: Couldn't resolve genius.com URL:`, err);
+    }
+}
+
+//#SECTION autoclick best genius.com result
+
+/**
+ * Automatically clicks the best matching result in a genius.com search
+ */
+function autoclickGeniusResult()
+{
+    if(!location.pathname.includes("/search"))
+        return;
+
+    const miniCards = document.querySelectorAll(".mini_card-title_and_subtitle");
+
+    if(!miniCards || miniCards.length == 0)
+    {
+        if(geniusAutoclickTries < Math.round(triesLimit * 2.5)) // tries limit higher due to lower timeout
+        {
+            geniusAutoclickTries++;
+            return setTimeout(autoclickGeniusResult, 100); // TODO: improve this
+        }
+        else
+            return console.error(`BetterYTM: Couldn't find result minicards after ${geniusAutoclickTries} tries`);
+    }
+
+    const params = getGeniusAcParams();
+
+    if(!params)
+        return console.info("BetterYTM: No query params present, not autoclicking");
+
+    const { songName, artistName } = params;
+
+    const resultNode = findMatchingGeniusResult(songName, artistName);
+
+    if(!resultNode)
+        return console.error("BetterYTM: Couldn't find matching result node");
+
+    dbg && console.info(`BetterYTM: Found matching result node after ${geniusAutoclickTries} tries:`, resultNode);
+
+    resultNode.click();
+}
+
+let geniusAutoclickTries = 0;
+
+/**
+ * Finds a result minicard node that matches the provided song and artist names (case insensitive)
+ * @param {string} song
+ * @param {string} artist
+ * @returns {Node|null}
+ */
+function findMatchingGeniusResult(song, artist)
+{
+    const miniCards = document.querySelectorAll(".mini_card-title_and_subtitle");
+
+    dbg && console.info(`BetterYTM: Found ${miniCards.length} minicards in results, searching for match...`);
+
+    for(const card of miniCards)
+    {
+        if(card.childNodes && card.childNodes.length > 0)
+        {
+            const title = Array.from(card.childNodes).find(cn => cn.classList && cn.classList.contains("mini_card-title"));
+            const subTitle = Array.from(card.childNodes).find(cn => cn.classList && cn.classList.contains("mini_card-subtitle"));
+
+            if(!title || !subTitle || !title.innerText || !subTitle.innerText)
+                continue;
+
+            const songName = title.innerText.toLowerCase();
+            const artistName = subTitle.innerText.toLowerCase();
+
+            // TODO: there can be multiple artists and since their order and spelling on YTM and genius can differ, I need to split them and compare one by one
+            if(songName.includes(song.toLowerCase()) && artistName.includes(artist.toLowerCase()))
+                return card;
+        }
+    }
+
+    return null;
+}
+
+/**
+ * Returns autoclick query params if they exist, else returns null
+ * @returns {({ songName: string, artistName: string })|null}
+ */
+function getGeniusAcParams()
+{
+    const params = location.search.substring(1).split(/&/g);
+
+    if(params.find(p => p.includes("bytm-ac-sn=")) && params.find(p => p.includes("bytm-ac-an=")))
+    {
+        const songName = decodeURIComponent(params.find(p => p.includes("bytm-ac-sn=")).split(/=/)[1]);
+        const artistName = decodeURIComponent(params.find(p => p.includes("bytm-ac-an=")).split(/=/)[1]);
+
+        return { songName, artistName };
+    }
+
+    return null;
+}
+
 
 //#MARKER other
 
 
 /**
  * Returns the current domain as a constant string representation
+ * @throws {Error} If script runs on an unexpected website
  * @returns {Domain}
  */
 function getDomain()
 {
     const { hostname } = new URL(location.href);
-    return hostname.toLowerCase().includes("music") ? "ytm" : "yt"; // other cases are caught by the `@match`es at the top
+
+    if(hostname.includes("music.youtube"))
+        return "ytm";
+    else if(hostname.includes("youtube"))
+        return "yt";
+    else if(hostname.includes("genius"))
+        return "genius";
+    else
+        throw new Error("BetterYTM is running on an unexpected website");
 }
 
 /**
@@ -495,45 +656,6 @@ function getVideoTime()
     }
 }
 
-/**
- * Returns the genius.com search URL for the current song
- * @returns {string}
- */
-function getGeniusUrl()
-{
-    try
-    {
-        const sanitizeSongName = (songName) => {
-            let sanitized;
-
-            if(songName.match(/\(|feat|ft/gmi))
-            {
-                // should hopefully trim right after the song name
-                sanitized = songName.substring(0, songName.indexOf("("));
-            }
-
-            return (sanitized || songName).trim();
-        };
-
-        const songNameRaw = document.querySelector(".content-info-wrapper > yt-formatted-string").title;
-        const songName = sanitizeSongName(songNameRaw);
-
-        const songMeta = document.querySelector("span.subtitle > yt-formatted-string:first-child").title;
-        const artist = songMeta.split(/\s*\u2022\s*/gmiu)[0]; // split at &bull; (•) character
-        // TODO: artist might need further splitting before comma or ampersand
-
-        const url = `https://genius.com/search?q=${encodeURIComponent(songName)}%20${encodeURIComponent(artist)}`;
-
-        dbg && console.info(`BetterYTM: Resolved genius.com URL for song '${songName}' by '${artist}': ${url}`);
-
-        return url;
-    }
-    catch(err)
-    {
-        console.error(`BetterYTM: Couldn't resolve genius.com URL:`, err);
-    }
-}
-
 /**
  * Inserts `afterNode` as a sibling just after the provided `beforeNode`
  * @param {HTMLElement} beforeNode