ソースを参照

chore: build dev

Sv443 1 ヶ月 前
コミット
0439dce107
1 ファイル変更108 行追加85 行削除
  1. 108 85
      dist/BetterYTM.user.js

+ 108 - 85
dist/BetterYTM.user.js

@@ -8,7 +8,7 @@
 // @license           AGPL-3.0-only
 // @author            Sv443
 // @copyright         Sv443 (https://github.com/Sv443)
-// @icon              https://cdn.jsdelivr.net/gh/Sv443/BetterYTM@3a9ecb3b/assets/images/logo/logo_dev_48.png
+// @icon              https://cdn.jsdelivr.net/gh/Sv443/BetterYTM@b3b490ad/assets/images/logo/logo_dev_48.png
 // @match             https://music.youtube.com/*
 // @match             https://www.youtube.com/*
 // @run-at            document-start
@@ -313,7 +313,7 @@ const rawConsts = {
     mode: "development",
     branch: "develop",
     host: "github",
-    buildNumber: "3a9ecb3b",
+    buildNumber: "b3b490ad",
     assetSource: "jsdelivr",
     devServerPort: "8710",
 };
@@ -979,7 +979,7 @@ const siteEvents = new UserUtils.NanoEmitter({
     publicEmit: true,
 });
 let observers = [];
-let lastWatchId = null;
+let lastVidId = null;
 let lastPathname = null;
 let lastFullscreen;
 /** Creates MutationObservers that check if parts of the site have changed, then emit an event on the `siteEvents` instance. */
@@ -1068,8 +1068,8 @@ async function initSiteEvents() {
                             var _a;
                             if (!target || !((_a = target === null || target === void 0 ? void 0 : target.href) === null || _a === void 0 ? void 0 : _a.includes("/watch")))
                                 return;
-                            const watchId = new URL(target.href).searchParams.get("v");
-                            checkWatchIdChange(watchId);
+                            const videoID = new URL(target.href).searchParams.get("v");
+                            checkVideoIdChange(videoID);
                         });
                         urlRefObs.observe(el, {
                             attributeFilter: ["href"],
@@ -1078,8 +1078,8 @@ async function initSiteEvents() {
                 });
             }
             if (getDomain() === "ytm") {
-                setInterval(checkWatchIdChange, 250);
-                checkWatchIdChange();
+                setInterval(checkVideoIdChange, 250);
+                checkVideoIdChange();
             }
         }, {
             once: true,
@@ -1110,18 +1110,18 @@ function emitSiteEvent(key, ...args) {
 }
 //#region other
 /** Checks if the watch ID has changed and emits a `watchIdChanged` siteEvent if it has */
-function checkWatchIdChange(newId) {
-    const newWatchId = newId !== null && newId !== void 0 ? newId : new URL(location.href).searchParams.get("v");
-    if (newWatchId && newWatchId !== lastWatchId) {
-        info(`Detected watch ID change - old ID: "${lastWatchId}" - new ID: "${newWatchId}"`);
-        emitSiteEvent("watchIdChanged", newWatchId, lastWatchId);
-        lastWatchId = newWatchId;
+function checkVideoIdChange(newID) {
+    const newVidID = newID !== null && newID !== void 0 ? newID : new URL(location.href).searchParams.get("v");
+    if (newVidID && newVidID !== lastVidId) {
+        info(`Detected watch ID change - old ID: "${lastVidId}" - new ID: "${newVidID}"`);
+        emitSiteEvent("watchIdChanged", newVidID, lastVidId);
+        lastVidId = newVidID;
     }
 }
 /** Periodically called to check for changes in the URL and emit associated siteEvents */
 function runIntervalChecks() {
-    if (!lastWatchId)
-        checkWatchIdChange();
+    if (!lastVidId)
+        checkVideoIdChange();
     if (location.pathname !== lastPathname) {
         emitSiteEvent("pathChanged", String(location.pathname), lastPathname);
         lastPathname = String(location.pathname);
@@ -1632,7 +1632,6 @@ async function initAutoScrollToActiveSong() {
         scrollToCurrentSongInQueue();
     }
 }
-let remVidsCache = [];
 /**
  * Remembers the time of the last played video and resumes playback from that time.
  * **Needs to be called *before* DOM is ready!**
@@ -1643,15 +1642,16 @@ async function initRememberSongTime() {
     const storedDataRaw = await GM.getValue("bytm-rem-songs");
     if (!storedDataRaw)
         await GM.setValue("bytm-rem-songs", "[]");
+    let remVids;
     try {
-        remVidsCache = JSON.parse(String(storedDataRaw !== null && storedDataRaw !== void 0 ? storedDataRaw : "[]"));
+        remVids = JSON.parse(String(storedDataRaw !== null && storedDataRaw !== void 0 ? storedDataRaw : "[]"));
     }
     catch (err) {
         error("Error parsing stored video time data, defaulting to empty cache:", err);
         await GM.setValue("bytm-rem-songs", "[]");
-        remVidsCache = [];
+        remVids = [];
     }
-    log(`Initialized video time restoring with ${remVidsCache.length} initial entr${remVidsCache.length === 1 ? "y" : "ies"}`);
+    log(`Initialized video time restoring with ${remVids.length} initial ${UserUtils.autoPlural("entry", remVids)}:`, remVids);
     await remTimeRestoreTime();
     try {
         if (!UserUtils.isDomLoaded())
@@ -1665,29 +1665,30 @@ async function initRememberSongTime() {
 }
 /** Tries to restore the time of the currently playing video */
 async function remTimeRestoreTime() {
+    const remVids = JSON.parse(await GM.getValue("bytm-rem-songs", "[]"));
     if (location.pathname.startsWith("/watch")) {
-        const watchID = new URL(location.href).searchParams.get("v");
-        if (!watchID)
+        const videoID = new URL(location.href).searchParams.get("v");
+        if (!videoID)
             return;
         if (initialParams.has("t"))
             return info("Not restoring song time because the URL has the '&t' parameter", LogLevel.Info);
-        const entry = remVidsCache.find(entry => entry.watchID === watchID);
+        const entry = remVids.find(entry => entry.id === videoID);
         if (entry) {
-            if (Date.now() - entry.updateTimestamp > getFeature("rememberSongTimeDuration") * 1000) {
-                await remTimeDeleteEntry(entry.watchID);
+            if (Date.now() - entry.updated > getFeature("rememberSongTimeDuration") * 1000) {
+                await remTimeDeleteEntry(entry.id);
                 return;
             }
-            else if (isNaN(Number(entry.songTime)))
-                return;
+            else if (isNaN(Number(entry.time)) || entry.time < 0)
+                return warn("Invalid time in remembered song time entry:", entry);
             else {
                 let vidElem;
                 const doRestoreTime = async () => {
                     var _a;
                     if (!vidElem)
                         vidElem = await waitVideoElementReady();
-                    const vidRestoreTime = entry.songTime - ((_a = getFeature("rememberSongTimeReduction")) !== null && _a !== void 0 ? _a : 0);
+                    const vidRestoreTime = entry.time - ((_a = getFeature("rememberSongTimeReduction")) !== null && _a !== void 0 ? _a : 0);
                     vidElem.currentTime = UserUtils.clamp(Math.max(vidRestoreTime, 0), 0, vidElem.duration);
-                    await remTimeDeleteEntry(entry.watchID);
+                    await remTimeDeleteEntry(entry.id);
                     info(`Restored ${getDomain() === "ytm" ? getCurrentMediaType() : "video"} time to ${Math.floor(vidRestoreTime / 60)}m, ${(vidRestoreTime % 60).toFixed(1)}s`, LogLevel.Info);
                 };
                 if (!UserUtils.isDomLoaded())
@@ -1703,33 +1704,34 @@ let remVidCheckTimeout;
 /** Only call once as this calls itself after a timeout! - Updates the currently playing video's entry in GM storage */
 async function remTimeStartUpdateLoop() {
     var _a, _b, _c;
+    const remVids = JSON.parse(await GM.getValue("bytm-rem-songs", "[]"));
     if (location.pathname.startsWith("/watch")) {
-        const watchID = getWatchId();
+        const id = getWatchId();
         const songTime = (_a = await getVideoTime()) !== null && _a !== void 0 ? _a : 0;
-        if (watchID && songTime !== lastSongTime) {
+        if (id && songTime !== lastSongTime) {
             lastSongTime = songTime;
             const paused = (_c = (_b = getVideoElement()) === null || _b === void 0 ? void 0 : _b.paused) !== null && _c !== void 0 ? _c : false;
             // don't immediately update to reduce race conditions and only update if the video is playing
             // also it just sounds better if the song starts at the beginning if only a couple seconds have passed
             if (songTime > getFeature("rememberSongTimeMinPlayTime") && !paused) {
                 const entry = {
-                    watchID,
-                    songTime,
-                    updateTimestamp: Date.now(),
+                    id,
+                    time: songTime,
+                    updated: Date.now(),
                 };
                 await remTimeUpsertEntry(entry);
             }
             // if the song is rewound to the beginning, update the entry accordingly
             else if (!paused) {
-                const entry = remVidsCache.find(entry => entry.watchID === watchID);
-                if (entry && songTime <= entry.songTime)
-                    await remTimeUpsertEntry(Object.assign(Object.assign({}, entry), { songTime, updateTimestamp: Date.now() }));
+                const entry = remVids.find(entry => entry.id === id);
+                if (entry && songTime <= entry.time)
+                    await remTimeUpsertEntry(Object.assign(Object.assign({}, entry), { time: songTime, updated: Date.now() }));
             }
         }
     }
-    const expiredEntries = remVidsCache.filter(entry => Date.now() - entry.updateTimestamp > getFeature("rememberSongTimeDuration") * 1000);
+    const expiredEntries = remVids.filter(entry => Date.now() - entry.updated > getFeature("rememberSongTimeDuration") * 1000);
     for (const entry of expiredEntries)
-        await remTimeDeleteEntry(entry.watchID);
+        await remTimeDeleteEntry(entry.id);
     // for no overlapping calls and better error handling:
     if (remVidCheckTimeout)
         clearTimeout(remVidCheckTimeout);
@@ -1737,17 +1739,19 @@ async function remTimeStartUpdateLoop() {
 }
 /** Updates an existing or inserts a new entry to be remembered */
 async function remTimeUpsertEntry(data) {
-    const foundIdx = remVidsCache.findIndex(entry => entry.watchID === data.watchID);
+    const remVids = JSON.parse(await GM.getValue("bytm-rem-songs", "[]"));
+    const foundIdx = remVids.findIndex(entry => entry.id === data.id);
     if (foundIdx >= 0)
-        remVidsCache[foundIdx] = data;
+        remVids[foundIdx] = data;
     else
-        remVidsCache.push(data);
-    await GM.setValue("bytm-rem-songs", JSON.stringify(remVidsCache));
+        remVids.push(data);
+    await GM.setValue("bytm-rem-songs", JSON.stringify(remVids));
 }
 /** Deletes an entry in the "remember cache" */
-async function remTimeDeleteEntry(watchID) {
-    remVidsCache = [...remVidsCache.filter(entry => entry.watchID !== watchID)];
-    await GM.setValue("bytm-rem-songs", JSON.stringify(remVidsCache));
+async function remTimeDeleteEntry(videoID) {
+    const remVids = JSON.parse(await GM.getValue("bytm-rem-songs", "[]"))
+        .filter(entry => entry.id !== videoID);
+    await GM.setValue("bytm-rem-songs", JSON.stringify(remVids));
 }const interactionKeys = ["Enter", " ", "Space"];
 /**
  * Adds generic, accessible interaction listeners to the passed element.
@@ -2157,7 +2161,7 @@ async function renderBody$4() {
         const removeBtn = await createCircularBtn({
             resourceName: "icon-delete",
             title: t("remove_entry"),
-            async onClick() {
+            onClick() {
                 autoLikeStore.setData({
                     channels: autoLikeStore.getData().channels.filter((ch) => ch.id !== chanId),
                 });
@@ -2829,21 +2833,21 @@ function isValidChannelId(channelId) {
     return channelId.match(/^(UC|@)[a-zA-Z0-9_-]+$/) !== null;
 }
 /** Returns the thumbnail URL for a video with either a given quality identifier or index */
-function getThumbnailUrl(watchId, qualityOrIndex = "maxresdefault") {
-    return `https://img.youtube.com/vi/${watchId}/${qualityOrIndex}.jpg`;
+function getThumbnailUrl(videoID, qualityOrIndex = "maxresdefault") {
+    return `https://img.youtube.com/vi/${videoID}/${qualityOrIndex}.jpg`;
 }
-/** Returns the best available thumbnail URL for a video with the given watch ID */
-async function getBestThumbnailUrl(watchId) {
+/** Returns the best available thumbnail URL for a video with the given video ID */
+async function getBestThumbnailUrl(videoID) {
     try {
         const priorityList = ["maxresdefault", "sddefault", "hqdefault", 0];
         for (const quality of priorityList) {
             let response;
-            const url = getThumbnailUrl(watchId, quality);
+            const url = getThumbnailUrl(videoID, quality);
             try {
                 response = await sendRequest({ url, method: "HEAD", timeout: 6000 });
             }
             catch (err) {
-                error(`Error while sending HEAD request to thumbnail URL for video '${watchId}' with quality '${quality}':`, err);
+                error(`Error while sending HEAD request to thumbnail URL for video ID '${videoID}' with quality '${quality}':`, err);
                 void err;
             }
             if (response && response.status < 300 && response.status >= 200)
@@ -2851,7 +2855,7 @@ async function getBestThumbnailUrl(watchId) {
         }
     }
     catch (err) {
-        throw new Error(`Couldn't get thumbnail URL for video '${watchId}': ${err}`);
+        throw new Error(`Couldn't get thumbnail URL for video ID '${videoID}': ${err}`);
     }
 }
 /** Opens the given URL in a new tab, using GM.openInTab if available */
@@ -4816,12 +4820,12 @@ async function initShowVotes() {
     addSelectorListener("playerBar", ".middle-controls-buttons ytmusic-like-button-renderer", {
         async listener(voteCont) {
             try {
-                const watchId = getWatchId();
-                if (!watchId) {
+                const videoID = getWatchId();
+                if (!videoID) {
                     await siteEvents.once("watchIdChanged");
                     return initShowVotes();
                 }
-                const voteObj = await fetchVideoVotes(watchId);
+                const voteObj = await fetchVideoVotes(videoID);
                 if (!voteObj || !("likes" in voteObj) || !("dislikes" in voteObj) || !("rating" in voteObj))
                     return error("Couldn't fetch votes from the Return YouTube Dislike API");
                 if (getFeature("showVotes")) {
@@ -5298,6 +5302,8 @@ async function addQueueButtons(queueItem, containerParentSelector = ".song-info"
     queueBtnsCont.classList.add(...["bytm-queue-btn-container", ...classes]);
     const lyricsIconUrl = await getResourceUrl("icon-lyrics");
     const deleteIconUrl = await getResourceUrl("icon-delete");
+    const spinnerIconUrl = await getResourceUrl("icon-spinner");
+    await UserUtils.preloadImages([lyricsIconUrl, deleteIconUrl, spinnerIconUrl]);
     //#region lyrics btn
     let lyricsBtnElem;
     if (getFeature("lyricsQueueButton")) {
@@ -5310,14 +5316,14 @@ async function addQueueButtons(queueItem, containerParentSelector = ".song-info"
         lyricsBtnElem.role = "link";
         lyricsBtnElem.tabIndex = 0;
         onInteraction(lyricsBtnElem, async (e) => {
-            var _a;
+            var _a, _b;
             e.preventDefault();
             e.stopImmediatePropagation();
             let song, artist;
             if (listType === "currentQueue") {
                 const songInfo = queueItem.querySelector(".song-info");
                 if (!songInfo)
-                    return;
+                    return error("Couldn't find song info element in queue item", queueItem);
                 const [songEl, artistEl] = songInfo.querySelectorAll("yt-formatted-string");
                 song = songEl === null || songEl === void 0 ? void 0 : songEl.textContent;
                 artist = artistEl === null || artistEl === void 0 ? void 0 : artistEl.textContent;
@@ -5338,7 +5344,7 @@ async function addQueueButtons(queueItem, containerParentSelector = ".song-info"
                 }
             }
             else
-                return;
+                return error("Invalid list type:", listType);
             if (!song || !artist)
                 return error("Couldn't get song or artist name from queue item - song:", song, "- artist:", artist);
             let lyricsUrl;
@@ -5351,15 +5357,20 @@ async function addQueueButtons(queueItem, containerParentSelector = ".song-info"
             if (cachedLyricsEntry)
                 lyricsUrl = cachedLyricsEntry.url;
             else if (!queueItem.hasAttribute("data-bytm-loading")) {
-                const imgEl = lyricsBtnElem === null || lyricsBtnElem === void 0 ? void 0 : lyricsBtnElem.querySelector("img");
-                if (!imgEl)
-                    return;
+                const imgEl = lyricsBtnElem === null || lyricsBtnElem === void 0 ? void 0 : lyricsBtnElem.querySelector("img, svg");
                 if (!cachedLyricsEntry) {
                     queueItem.setAttribute("data-bytm-loading", "");
-                    imgEl.src = await getResourceUrl("icon-spinner");
-                    imgEl.classList.add("bytm-spinner");
+                    if (imgEl) {
+                        if (imgEl.tagName === "IMG")
+                            imgEl.src = await getResourceUrl("icon-spinner");
+                        else if (lyricsBtnElem) {
+                            setInnerHtml(lyricsBtnElem, await resourceAsString("icon-spinner"));
+                            (_a = lyricsBtnElem.querySelector("svg")) === null || _a === void 0 ? void 0 : _a.classList.add("bytm-generic-btn-img");
+                        }
+                    }
+                    imgEl === null || imgEl === void 0 ? void 0 : imgEl.classList.add("bytm-spinner");
                 }
-                lyricsUrl = (_a = cachedLyricsEntry === null || cachedLyricsEntry === void 0 ? void 0 : cachedLyricsEntry.url) !== null && _a !== void 0 ? _a : await fetchLyricsUrlTop(artistsSan, songSan);
+                lyricsUrl = (_b = cachedLyricsEntry === null || cachedLyricsEntry === void 0 ? void 0 : cachedLyricsEntry.url) !== null && _b !== void 0 ? _b : await fetchLyricsUrlTop(artistsSan, songSan);
                 if (lyricsUrl) {
                     emitInterface("bytm:lyricsLoaded", {
                         type: "queue",
@@ -5368,9 +5379,17 @@ async function addQueueButtons(queueItem, containerParentSelector = ".song-info"
                         url: lyricsUrl,
                     });
                 }
-                const resetImgElem = () => {
-                    imgEl.src = lyricsIconUrl;
-                    imgEl.classList.remove("bytm-spinner");
+                const resetImgElem = async () => {
+                    var _a;
+                    if (imgEl) {
+                        if (imgEl.tagName === "IMG")
+                            imgEl.src = lyricsIconUrl;
+                        else if (lyricsBtnElem) {
+                            setInnerHtml(lyricsBtnElem, await resourceAsString("icon-lyrics"));
+                            (_a = lyricsBtnElem.querySelector("svg")) === null || _a === void 0 ? void 0 : _a.classList.add("bytm-generic-btn-img");
+                        }
+                    }
+                    imgEl === null || imgEl === void 0 ? void 0 : imgEl.classList.remove("bytm-spinner");
                 };
                 if (!cachedLyricsEntry) {
                     queueItem.removeAttribute("data-bytm-loading");
@@ -5402,6 +5421,8 @@ async function addQueueButtons(queueItem, containerParentSelector = ".song-info"
         onInteraction(deleteBtnElem, async (e) => {
             e.preventDefault();
             e.stopImmediatePropagation();
+            imgElem.src = spinnerIconUrl;
+            imgElem.classList.add("bytm-spinner");
             // container of the queue item popup menu - element gets reused for every queue item
             let queuePopupCont = document.querySelector("ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown");
             try {
@@ -5648,8 +5669,8 @@ async function setInitialTabVolume(sliderElem) {
     sliderElem.dispatchEvent(new Event("change", { bubbles: true }));
     log(`Set initial tab volume to ${initialVol}%${reloadTabVol > 0 ? " (from GM storage)" : " (from configuration)"}`);
 }//#region misc
-function noop() {
-}
+/** No-operation function used when `reloadRequired` is set to `false` to explicitly indicate that no `enable` function is needed */
+const noop = () => void 0;
 /** Creates an HTML string for the given adornment properties */
 const getAdornHtml = async (className, title, resource, extraAttributes) => {
     var _a;
@@ -5715,15 +5736,15 @@ const options = {
         { value: "lighter", label: t("color_lightness_lighter") },
     ],
 };
-//#region rendering
+//#region renderers
 /** Renders a long number with a thousands separator */
-function renderLongNumberValue(val, maximumFractionDigits = 0) {
+function renderNumberVal(val, maximumFractionDigits = 0) {
     return Number(val).toLocaleString(getLocale().replace(/_/g, "-"), {
         style: "decimal",
         maximumFractionDigits,
     });
 }
-//#region features
+//#region # features
 /**
  * Contains all possible features with their default values and other configuration.
  *
@@ -6214,12 +6235,12 @@ const featInfo = {
     lyricsCacheMaxSize: {
         type: "slider",
         category: "lyrics",
-        default: 2000,
+        default: 5000,
         min: 100,
-        max: 10000,
+        max: 50000,
         step: 100,
         unit: (val) => ` ${tp("unit_entries", val)}`,
-        renderValue: renderLongNumberValue,
+        renderValue: renderNumberVal,
         advanced: true,
         textAdornment: adornments.advanced,
         reloadRequired: false,
@@ -6520,8 +6541,10 @@ const migrations = {
         ]);
     },
     // 9 -> 10 (v2.3.0)
-    10: (oldData) => useDefaultConfig(oldData, [
+    10: (oldData) => useNewDefaultIfUnchanged(useDefaultConfig(oldData, [
         "aboveQueueBtnsSticky", "autoScrollToActiveSongMode",
+    ]), [
+        { key: "lyricsCacheMaxSize", oldDefault: 2000 },
     ]),
 };
 /** Uses the default config as the base, then overwrites all values with the passed {@linkcode baseData}, then sets all passed {@linkcode resetKeys} to their default values */
@@ -7392,22 +7415,22 @@ const voteCache = new Map();
 const voteCacheTTL = 1000 * 60 * 10;
 /**
  * Fetches the votes object for a YouTube video from the [Return YouTube Dislike API.](https://returnyoutubedislike.com/docs)
- * @param watchId The watch ID of the video
+ * @param videoID The video ID of the video
  */
-async function fetchVideoVotes(watchId) {
+async function fetchVideoVotes(videoID) {
     try {
-        if (voteCache.has(watchId)) {
-            const cached = voteCache.get(watchId);
+        if (voteCache.has(videoID)) {
+            const cached = voteCache.get(videoID);
             if (Date.now() - cached.timestamp < voteCacheTTL) {
-                info(`Returning cached video votes for watch ID '${watchId}':`, cached);
+                info(`Returning cached video votes for video ID '${videoID}':`, cached);
                 return cached;
             }
             else
-                voteCache.delete(watchId);
+                voteCache.delete(videoID);
         }
         const votesRaw = JSON.parse((await sendRequest({
             method: "GET",
-            url: `https://returnyoutubedislikeapi.com/votes?videoId=${watchId}`,
+            url: `https://returnyoutubedislikeapi.com/votes?videoId=${videoID}`,
         })).response);
         if (!("id" in votesRaw) || !("likes" in votesRaw) || !("dislikes" in votesRaw) || !("rating" in votesRaw)) {
             error("Couldn't parse video votes due to an error:", votesRaw);
@@ -7421,7 +7444,7 @@ async function fetchVideoVotes(watchId) {
             timestamp: Date.now(),
         };
         voteCache.set(votesObj.id, votesObj);
-        info(`Fetched video votes for watch ID '${watchId}':`, votesObj);
+        info(`Fetched video votes for watch ID '${videoID}':`, votesObj);
         return votesObj;
     }
     catch (err) {