ソースを参照

ref: console watermark

Sv443 1 年間 前
コミット
70bbfce254
2 ファイル変更164 行追加50 行削除
  1. 162 49
      dist/BetterYTM.user.js
  2. 2 1
      src/index.ts

+ 162 - 49
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/logo/logo_48.png
+// @icon              http://localhost:8710/assets/logo/logo_48.png?t=dc821ae4-0c37-4e41-90be-5bd832283b17
 // @match             https://music.youtube.com/*
 // @match             https://www.youtube.com/*
 // @run-at            document-start
@@ -34,33 +34,34 @@
 // @grant             GM.xmlHttpRequest
 // @grant             unsafeWindow
 // @noframes
-// @resource          img-arrow_down          https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/arrow_down.svg
-// @resource          img-delete              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/delete.svg
-// @resource          img-error               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/error.svg
-// @resource          img-globe               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/globe.svg
-// @resource          img-help                https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/help.svg
-// @resource          img-lyrics              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/lyrics.svg
-// @resource          img-skip_to             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/skip_to.svg
-// @resource          img-spinner             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/spinner.svg
-// @resource          img-logo                https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/logo/logo_48.png
-// @resource          img-close               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/close.png
-// @resource          img-discord             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/external/discord.png
-// @resource          img-github              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/external/github.png
-// @resource          img-greasyfork          https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/external/greasyfork.png
-// @resource          img-openuserjs          https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/external/openuserjs.png
-// @resource          css-fix_spacing         https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/fixSpacing.css
-// @resource          css-anchor_improvements https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/anchorImprovements.css
-// @resource          doc-changelog           https://raw.githubusercontent.com/Sv443/BetterYTM/develop/changelog.md
-// @resource          trans-de_DE             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/de_DE.json
-// @resource          trans-en_US             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_US.json
-// @resource          trans-en_UK             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_UK.json
-// @resource          trans-es_ES             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/es_ES.json
-// @resource          trans-fr_FR             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/fr_FR.json
-// @resource          trans-hi_IN             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/hi_IN.json
-// @resource          trans-ja_JA             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/ja_JA.json
-// @resource          trans-pt_BR             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/pt_BR.json
-// @resource          trans-zh_CN             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/zh_CN.json
+// @resource          img-arrow_down          http://localhost:8710/assets/icons/arrow_down.svg?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          img-delete              http://localhost:8710/assets/icons/delete.svg?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          img-error               http://localhost:8710/assets/icons/error.svg?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          img-globe               http://localhost:8710/assets/icons/globe.svg?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          img-help                http://localhost:8710/assets/icons/help.svg?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          img-lyrics              http://localhost:8710/assets/icons/lyrics.svg?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          img-skip_to             http://localhost:8710/assets/icons/skip_to.svg?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          img-spinner             http://localhost:8710/assets/icons/spinner.svg?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          img-logo                http://localhost:8710/assets/logo/logo_48.png?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          img-close               http://localhost:8710/assets/icons/close.png?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          img-discord             http://localhost:8710/assets/external/discord.png?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          img-github              http://localhost:8710/assets/external/github.png?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          img-greasyfork          http://localhost:8710/assets/external/greasyfork.png?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          img-openuserjs          http://localhost:8710/assets/external/openuserjs.png?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          css-fix_spacing         http://localhost:8710/assets/style/fixSpacing.css?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          css-anchor_improvements http://localhost:8710/assets/style/anchorImprovements.css?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          doc-changelog           http://localhost:8710/changelog.md?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          trans-de_DE             http://localhost:8710/assets/translations/de_DE.json?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          trans-en_US             http://localhost:8710/assets/translations/en_US.json?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          trans-en_UK             http://localhost:8710/assets/translations/en_UK.json?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          trans-es_ES             http://localhost:8710/assets/translations/es_ES.json?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          trans-fr_FR             http://localhost:8710/assets/translations/fr_FR.json?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          trans-hi_IN             http://localhost:8710/assets/translations/hi_IN.json?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          trans-ja_JA             http://localhost:8710/assets/translations/ja_JA.json?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          trans-pt_BR             http://localhost:8710/assets/translations/pt_BR.json?t=dc821ae4-0c37-4e41-90be-5bd832283b17
+// @resource          trans-zh_CN             http://localhost:8710/assets/translations/zh_CN.json?t=dc821ae4-0c37-4e41-90be-5bd832283b17
 // @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
 // @grant             GM.registerMenuCommand
 // @grant             GM.listValues
@@ -79,7 +80,7 @@ I welcome every contribution on GitHub!
 /* Disclaimer: I am not affiliated with or endorsed by YouTube, Google, Alphabet, Genius or anyone else */
 /* C&D this 🖕 */
 
-(function(UserUtils,marked){'use strict';function _interopNamespaceDefault(e){var n=Object.create(null);if(e){Object.keys(e).forEach(function(k){if(k!=='default'){var d=Object.getOwnPropertyDescriptor(e,k);Object.defineProperty(n,k,d.get?d:{enumerable:true,get:function(){return e[k]}});}})}n.default=e;return Object.freeze(n)}var UserUtils__namespace=/*#__PURE__*/_interopNamespaceDefault(UserUtils);/******************************************************************************
+(function(UserUtils,marked,Fuse){'use strict';function _interopNamespaceDefault(e){var n=Object.create(null);if(e){Object.keys(e).forEach(function(k){if(k!=='default'){var d=Object.getOwnPropertyDescriptor(e,k);Object.defineProperty(n,k,d.get?d:{enumerable:true,get:function(){return e[k]}});}})}n.default=e;return Object.freeze(n)}var UserUtils__namespace=/*#__PURE__*/_interopNamespaceDefault(UserUtils);/******************************************************************************
 Copyright (c) Microsoft Corporation.
 
 Permission to use, copy, modify, and/or distribute this software for any
@@ -96,6 +97,18 @@ PERFORMANCE OF THIS SOFTWARE.
 /* global Reflect, Promise, SuppressedError, Symbol */
 
 
+function __rest(s, e) {
+    var t = {};
+    for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
+        t[p] = s[p];
+    if (s != null && typeof Object.getOwnPropertySymbols === "function")
+        for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
+            if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
+                t[p[i]] = s[p[i]];
+        }
+    return t;
+}
+
 function __awaiter(thisArg, _arguments, P, generator) {
     function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
     return new (P || (P = Promise))(function (resolve, reject) {
@@ -243,7 +256,7 @@ const scriptInfo = {
     name: GM.info.script.name,
     version: GM.info.script.version,
     namespace: GM.info.script.namespace,
-    buildNumber: "7536b9e", // asserted as generic string instead of literal
+    buildNumber: "c62fd41", // asserted as generic string instead of literal
 };/** Options that are applied to every SelectorObserver instance */
 const defaultObserverOptions = {
     defaultDebounce: 100,
@@ -1196,6 +1209,7 @@ var updates = {
 };
 var dependencies = {
 	"@sv443-network/userutils": "^5.0.0",
+	"fuse.js": "^7.0.0",
 	marked: "^12.0.0",
 	nanoevents: "^9.0.0"
 };
@@ -2860,7 +2874,7 @@ function initNumKeysSkip() {
 }/** Base URL of geniURL */
 const geniUrlBase = "https://api.sv443.net/geniurl";
 /** GeniURL endpoint that gives song metadata when provided with a `?q` or `?artist` and `?song` parameter - [more info](https://api.sv443.net/geniurl) */
-const geniURLSearchTopUrl = `${geniUrlBase}/search/top`;
+const geniURLSearchUrl = `${geniUrlBase}/search`;
 /** Ratelimit budget timeframe in seconds - should reflect what's in geniURL's docs */
 const geniUrlRatelimitTimeframe = 30;
 //#MARKER new cache
@@ -2922,12 +2936,22 @@ function deleteLyricsCacheEntry(artist, song) {
 }
 /**
  * Adds the provided entry into the lyrics URL cache, synchronously to RAM and asynchronously to GM storage
- * {@linkcode artist} and {@linkcode song} need to be sanitized first!
+ * Also adds a penalty to the viewed timestamp and added timestamp to decrease entry's lifespan in cache
+ *
+ * ⚠️ {@linkcode artist} and {@linkcode song} need to be sanitized first!
+ * @param penaltyFr Fraction to remove from the timestamp values - has to be between 0 and 1 - default is 0 (no penalty) - (0.25 = only penalized by a quarter of the predefined max penalty)
  */
-function addLyricsCacheEntry(artist, song, url) {
+function addLyricsCacheEntryPenalized(artist, song, url, penaltyFr = 0) {
     const { cache } = lyricsCache.getData();
+    penaltyFr = UserUtils.clamp(penaltyFr, 0, 1);
+    const viewedPenalty = 1000 * 60 * 60 * 24 * 5 * penaltyFr; // 5 days
+    const addedPenalty = 1000 * 60 * 60 * 24 * 15 * penaltyFr; // 15 days
     cache.push({
-        artist, song, url, viewed: Date.now(), added: Date.now(),
+        artist,
+        song,
+        url,
+        viewed: Date.now() - viewedPenalty,
+        added: Date.now() - addedPenalty,
     });
     cache.sort((a, b) => b.viewed - a.viewed);
     if (cache.length > maxLyricsCacheSize)
@@ -3023,6 +3047,8 @@ function addActualMediaCtrlLyricsBtn(likeContainer) {
 //#MARKER utils
 /** Removes everything in parentheses from the passed song name */
 function sanitizeSong(songName) {
+    if (typeof songName !== "string")
+        return songName;
     const parensRegex = /\(.+\)/gmi;
     const squareParensRegex = /\[.+\]/gmi;
     // trim right after the song name:
@@ -3064,7 +3090,7 @@ function getCurrentLyricsUrl() {
             }
             if (!artistName)
                 return undefined;
-            const url = yield fetchLyricsUrl(sanitizeArtists(artistName), sanitizeSong(songName));
+            const url = yield fetchLyricsUrlTop(sanitizeArtists(artistName), sanitizeSong(songName));
             if (url) {
                 emitInterface("bytm:lyricsLoaded", {
                     type: "current",
@@ -3081,25 +3107,41 @@ function getCurrentLyricsUrl() {
         }
     });
 }
-/** Fetches the actual lyrics URL from geniURL - **the passed parameters need to be sanitized first!** */
-function fetchLyricsUrl(artist, song) {
+/** Fetches the top lyrics URL result from geniURL - **the passed parameters need to be sanitized first!** */
+function fetchLyricsUrlTop(artist, song) {
+    var _a, _b;
+    return __awaiter(this, void 0, void 0, function* () {
+        try {
+            return (_b = (_a = (yield fetchLyricsUrls(artist, song))) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.url;
+        }
+        catch (err) {
+            error("Couldn't get lyrics URL due to error:", err);
+            return undefined;
+        }
+    });
+}
+/**
+ * Fetches the 5 best matching lyrics URLs from geniURL using a combo exact-ish and fuzzy search
+ * **the passed parameters need to be sanitized first!**
+ */
+function fetchLyricsUrls(artist, song) {
     var _a, _b, _c;
     return __awaiter(this, void 0, void 0, function* () {
         try {
             const cacheEntry = getLyricsCacheEntry(artist, song);
             if (cacheEntry) {
                 info(`Found lyrics URL in cache: ${cacheEntry.url}`);
-                return cacheEntry.url;
+                return [cacheEntry];
             }
             const startTs = Date.now();
-            const fetchUrl = constructUrlString(geniURLSearchTopUrl, {
+            const fetchUrl = constructUrlString(geniURLSearchUrl, {
                 disableFuzzy: null,
                 utm_source: "BetterYTM",
                 utm_content: `v${scriptInfo.version}`,
                 artist,
                 song,
             });
-            log(`Requesting URL from geniURL at '${fetchUrl}'`);
+            log(`Requesting URLs from geniURL at '${fetchUrl}'`);
             const fetchRes = yield UserUtils.fetchAdvanced(fetchUrl);
             if (fetchRes.status === 429) {
                 const waitSeconds = Number((_a = fetchRes.headers.get("retry-after")) !== null && _a !== void 0 ? _a : geniUrlRatelimitTimeframe);
@@ -3107,18 +3149,90 @@ function fetchLyricsUrl(artist, song) {
                 return undefined;
             }
             else if (fetchRes.status < 200 || fetchRes.status >= 300) {
-                error(`Couldn't fetch lyrics URL from geniURL - status: ${fetchRes.status} - response: ${(_c = (_b = (yield fetchRes.json()).message) !== null && _b !== void 0 ? _b : yield fetchRes.text()) !== null && _c !== void 0 ? _c : "(none)"}`);
+                error(`Couldn't fetch lyrics URLs from geniURL - status: ${fetchRes.status} - response: ${(_c = (_b = (yield fetchRes.json()).message) !== null && _b !== void 0 ? _b : yield fetchRes.text()) !== null && _c !== void 0 ? _c : "(none)"}`);
                 return undefined;
             }
             const result = yield fetchRes.json();
-            if (typeof result === "object" && result.error) {
+            if (typeof result === "object" && result.error || !result || !result.all) {
                 error("Couldn't fetch lyrics URL:", result.message);
                 return undefined;
             }
-            const url = result.url;
-            info(`Found lyrics URL (after ${Date.now() - startTs}ms): ${url}`);
-            addLyricsCacheEntry(artist, song, url);
-            return url;
+            const allResults = result.all;
+            if (allResults.length === 0) {
+                warn("No lyrics URL found for the provided song");
+                return undefined;
+            }
+            const exactish = (input) => {
+                return input.toLowerCase()
+                    .replace(/[\s\-_&,.()[\]]+/gm, "");
+            };
+            const allResultsSan = allResults
+                .filter(({ meta, url }) => (meta.title || meta.fullTitle) && meta.artists && url)
+                .map(({ meta, url }) => {
+                var _a;
+                return ({
+                    meta: Object.assign(Object.assign({}, meta), { title: sanitizeSong(String((_a = meta.title) !== null && _a !== void 0 ? _a : meta.fullTitle)), artists: sanitizeArtists(String(meta.artists)) }),
+                    url,
+                });
+            });
+            // exact-ish matches, best matching one first
+            const exactishResults = [...allResultsSan].sort((a, b) => {
+                const aTitleScore = exactish(a.meta.title).localeCompare(exactish(song));
+                const bTitleScore = exactish(b.meta.title).localeCompare(exactish(song));
+                const aArtistScore = exactish(a.meta.primaryArtist.name).localeCompare(exactish(artist));
+                const bArtistScore = exactish(b.meta.primaryArtist.name).localeCompare(exactish(artist));
+                return aTitleScore + aArtistScore - bTitleScore - bArtistScore;
+            });
+            // use fuse.js for fuzzy match
+            // search song title and artist separately, then combine the scores
+            const titleFuse = new Fuse([...allResultsSan], {
+                keys: ["title"],
+                includeScore: true,
+                threshold: 0.4,
+            });
+            const artistFuse = new Fuse([...allResultsSan], {
+                keys: ["primaryArtist.name"],
+                includeScore: true,
+                threshold: 0.4,
+            });
+            let fuzzyResults = allResultsSan.map(r => {
+                var _a, _b, _c, _d;
+                const titleRes = titleFuse.search(r.meta.title);
+                const artistRes = artistFuse.search(r.meta.primaryArtist.name);
+                const titleScore = (_b = (_a = titleRes[0]) === null || _a === void 0 ? void 0 : _a.score) !== null && _b !== void 0 ? _b : 0;
+                const artistScore = (_d = (_c = artistRes[0]) === null || _c === void 0 ? void 0 : _c.score) !== null && _d !== void 0 ? _d : 0;
+                return Object.assign(Object.assign({}, r), { score: titleScore + artistScore });
+            });
+            // I love TS
+            fuzzyResults = fuzzyResults
+                .map((_a) => {
+                var { score } = _a, rest = __rest(_a, ["score"]);
+                return rest;
+            });
+            const hasExactMatch = exactishResults.slice(0, 3).includes(fuzzyResults[0]);
+            const finalResults = [
+                ...(hasExactMatch
+                    ? [fuzzyResults[0]]
+                    : []),
+                ...fuzzyResults.slice(1),
+            ].slice(0, 5);
+            // add results to the cache with a penalty to their time to live
+            // so every entry is deleted faster if it's not considered as relevant
+            finalResults.forEach(({ meta: { artists, title }, url }, i) => {
+                const penaltyFraction = hasExactMatch
+                    // if there's an exact match, give it 0 penalty and penalize all other results with the full value
+                    ? i === 0 ? 0 : 1
+                    // if there's no exact match, penalize all results with a fraction of the full penalty since they're more likely to be unrelated
+                    : 0.6;
+                addLyricsCacheEntryPenalized(sanitizeArtists(artists), sanitizeSong(title), url, penaltyFraction);
+            });
+            finalResults.length > 0 && log("Found", finalResults.length, "lyrics", UserUtils.autoPlural("URL", finalResults), "in", Date.now() - startTs, "ms:", finalResults);
+            // returns search results sorted by relevance
+            return finalResults.map(r => ({
+                artist: r.meta.primaryArtist.name,
+                song: r.meta.title,
+                url: r.url,
+            }));
         }
         catch (err) {
             error("Couldn't get lyrics URL due to error:", err);
@@ -3177,7 +3291,6 @@ function initQueueButtons() {
             log(`Added buttons to ${queueItems.length} existing "current song queue" ${UserUtils.autoPlural("item", queueItems)}`);
         }
         // generic lists
-        // TODO:FIXME: dragging the items around removes the queue buttons
         const addGenericListQueueBtns = (listElem) => {
             if (listElem.classList.contains("bytm-list-has-queue-btns"))
                 return;
@@ -3278,7 +3391,7 @@ function addQueueButtons(queueItem, containerParentSelector = ".song-info", list
                         imgEl.src = yield getResourceUrl("img-spinner");
                         imgEl.classList.add("bytm-spinner");
                     }
-                    lyricsUrl = (_b = cachedLyricsEntry === null || cachedLyricsEntry === void 0 ? void 0 : cachedLyricsEntry.url) !== null && _b !== void 0 ? _b : yield fetchLyricsUrl(artistsSan, songSan);
+                    lyricsUrl = (_b = cachedLyricsEntry === null || cachedLyricsEntry === void 0 ? void 0 : cachedLyricsEntry.url) !== null && _b !== void 0 ? _b : yield fetchLyricsUrlTop(artistsSan, songSan);
                     if (lyricsUrl) {
                         emitInterface("bytm:lyricsLoaded", {
                             type: "queue",
@@ -3872,7 +3985,7 @@ const globalFuncs = {
     tp,
     getFeatures,
     saveFeatures,
-    fetchLyricsUrl,
+    fetchLyricsUrlTop,
     getLyricsCacheEntry,
     sanitizeArtists,
     sanitizeSong,
@@ -5849,4 +5962,4 @@ function registerMenuCommands() {
         }), "s");
     }
 }
-preInit();})(UserUtils,marked);//# sourceMappingURL=http://localhost:8710/BetterYTM.user.js.map
+preInit();})(UserUtils,marked,Fuse);//# sourceMappingURL=http://localhost:8710/BetterYTM.user.js.map

+ 2 - 1
src/index.ts

@@ -56,8 +56,9 @@ import {
     "─ Lots of ambition and dedication",
     `─ My song metadata API: ${geniUrlBase}`,
     "─ My userscript utility library: https://github.com/Sv443-Network/UserUtils",
-    "─ This tiny event listener library: https://github.com/ai/nanoevents",
+    "─ The fuse.js library: https://github.com/krisk/Fuse",
     "─ This markdown parser library: https://github.com/markedjs/marked",
+    "─ This tiny event listener library: https://github.com/ai/nanoevents",
   ].join("\n"));
   console.log();
 }