yt blocker

none

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         yt blocker
// @version      61
// @description  none
// @run-at       document-start
// @author       rssaromeo
// @license      GPLv3
// @match        *://youtube.com/*
// @match        *://*.youtube.com/*
// @icon         data:image/png;base64,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
// @grant        unsafeWindow
// @require      https://update.greasyfork.org/scripts/491829/1671236/tampermonkey%20storage%20proxy.js
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addValueChangeListener
// @namespace    https://greasyfork.org/users/1184528
// ==/UserScript==

const HIDE_WATCHED_VID_PROG = 1
const RCLICK_BLOCKS_URL = true
const RCLICK_BLOCKS_TITLE = false
const UPDATE_INTERVAL_MS = 1e3
const creatorNameCache = {}
let videoCreatorIndex = null
let lastYtDataRef = undefined
let firstNav = true
window.addEventListener("yt-navigate-finish", () => {
  if (firstNav) firstNav = false
  else if (!location.href.includes("@")) location.reload()
})
;(async () => {
  var globalname = null
  unsafeWindow.newJsData ??= [unsafeWindow.ytInitialData]
  // // Save the original fetch
  // const originalFetch = unsafeWindow.fetch;

  // unsafeWindow.fetch = async function(input, init) {
  //     // Check if the request URL contains /youtubei/v1/next
  //     const url = (typeof input === 'string') ? input : input.url;
  //     // if (url.includes('/youtubei/v1/next')) {
  //         console.log('YT next request detected:', url);

  //         // Call the original fetch
  //         const response = await originalFetch(input, init);

  //         // Clone the response so we can read it without affecting YouTube
  //         const cloned = response.clone();
  //           try{
  //         cloned.json().then(data => {

  //             console.log('ytInitialData (soft nav) updated:', data);
  //             // You can now store this data in a global variable
  //           log(findAllKeys(
  //             unsafeWindow.ytInitialData,
  //             "lockupMetadataViewModel"
  //           ), findAllKeys(
  //             data,
  //             "lockupMetadataViewModel"
  //           ))
  //           // unsafeWindow.newJsData.push(data)
  //             deepAssignMerge(unsafeWindow.ytInitialData, data)
  //         });
  //           }
  //           catch(e){}

  //         return response; // return original response
  //     // }

  //     // Otherwise, just call fetch normally
  //     return originalFetch(input, init);
  // };
  //   function deepAssignMerge(...objects) {
  //   const isObject = v => v && typeof v === "object" && !Array.isArray(v);

  //   return objects.reduce((acc, obj) => {
  //     for (const key in obj) {
  //       const prev = acc[key];
  //       const next = obj[key];

  //       if (Array.isArray(prev) && Array.isArray(next)) {
  //         acc[key] = [...new Set([...prev, ...next])];
  //       }
  //       else if (isObject(prev) && isObject(next)) {
  //         acc[key] = deepAssignMerge(prev, next);
  //       }
  //       else {
  //         acc[key] = next;
  //       }
  //     }
  //     return acc;
  //   }, {});
  // }

  function buildVideoCreatorIndex() {
    if (
      unsafeWindow.ytInitialData === lastYtDataRef &&
      videoCreatorIndex
    )
      return
    lastYtDataRef = unsafeWindow.ytInitialData
    videoCreatorIndex = new Map()
    function indexEntry(entry) {
      if (!entry) return
      const videoId =
        entry.videoId ??
        findAllKeys(entry, "watchEndpoint")
          .map((e) => e?.videoId)
          .find(Boolean)
      if (!videoId) return
      const ids = [
        ...new Set(
          findAllKeys(entry, "browseEndpoint")
            .map((e) => e?.browseId)
            .filter((id) => !!id),
        ),
      ]
      if (ids.length)
        videoCreatorIndex.set(
          videoId,
          ids.length === 1 ? ids[0] : ids,
        )
    }
    for (const e of findAllKeys(
      unsafeWindow.ytInitialData,
      "lockupMetadataViewModel",
    ))
      indexEntry(e)
    for (const e of findAllKeys(
      unsafeWindow.ytInitialData,
      "videoRenderer",
    ))
      indexEntry(e)
  }
  const a = loadlib("allfuncs")
  const sp = new storageproxy("globaloptions")
  let ls = sp.get()
  let vidlock = true
  const LOC = {}
  updateLoc()
  function setVidSpeed() {
    unsafeWindow.novidspeedcontroller = true
    for (const vid of a.qsa("video")) {
      vid.playbackRate =
        vidlock || !unsafeWindow.ytInitialData ?
          0
        : Number(!globalname ? 0 : (localStorage.playRate ?? 0))
    }
  }
  ls.blockedCreators ??= []
  ls.blockedTitles ??= []
  ls.blockedTitlesReg ??= []
  ls.blockedCreatorsReg ??= []
  ls.blockedUrls ??= []
  if (LOC.watch) {
    const fastint = setInterval(setVidSpeed, 0)
    setVidSpeed()
    update()
    a.waituntil(() => globalname).then(() => clearInterval(fastint))
  }
  unsafeWindow.findCreatorNameById = findCreatorNameById
  function findCreatorNameById(creatorId, nocache = false) {
    function cacheAndFind(pairs) {
      let found
      for (const { id, name } of pairs) {
        if (!id || !name) continue
        if (id === creatorId) found = name
        creatorNameCache[id] ??= name
      }
      return found
    }
    function lookup() {
      const watchItem = findAllKeys(
        unsafeWindow.ytInitialData,
        "listItemViewModel",
      )
        .flat()
        .find((e) => findValue(e, creatorId))
      const watchName = watchItem?.title
      if (watchName?.content) return watchName.content
      const homeContents =
        unsafeWindow.ytInitialData?.contents
          ?.twoColumnBrowseResultsRenderer?.tabs
      const homeResult = cacheAndFind(
        (
          homeContents?.[0]?.tabRenderer?.content?.richGridRenderer
            ?.contents ?? []
        )
          .map((e) => {
            const meta =
              e?.richItemRenderer?.content?.lockupViewModel?.metadata
                ?.lockupMetadataViewModel
            return {
              name: meta?.metadata?.contentMetadataViewModel
                ?.metadataRows?.[0]?.metadataParts?.[0]?.text
                ?.content,
              id: meta?.image?.decoratedAvatarViewModel
                ?.rendererContext?.commandContext?.onTap
                ?.innertubeCommand?.browseEndpoint?.browseId,
            }
          })
          .filter((e) => e.name && e.id),
      )
      if (homeResult) return homeResult
      const watchContents =
        unsafeWindow.ytInitialData?.contents
          ?.twoColumnWatchNextResults?.results?.results?.contents
      const ownerRuns =
        watchContents?.[1]?.videoSecondaryInfoRenderer?.owner
          ?.videoOwnerRenderer?.title?.runs
      const ownerResult = cacheAndFind(
        (Array.isArray(ownerRuns) ? ownerRuns : [])
          .map((e) => ({
            name: e?.text,
            id: e?.navigationEndpoint?.browseEndpoint?.browseId,
          }))
          .filter((e) => e.name && e.id),
      )
      if (ownerResult) return ownerResult
      const secResults =
        unsafeWindow.ytInitialData?.contents
          ?.twoColumnWatchNextResults?.secondaryResults
          ?.secondaryResults?.results
      const secArr = Array.isArray(secResults) ? secResults : []
      const secFlatResult = cacheAndFind(
        secArr
          .map((e) => {
            const meta =
              e?.lockupViewModel?.metadata?.lockupMetadataViewModel
            return {
              name: meta?.metadata?.contentMetadataViewModel
                ?.metadataRows?.[0]?.metadataParts?.[0]?.text
                ?.content,
              id: meta?.image?.decoratedAvatarViewModel
                ?.rendererContext?.commandContext?.onTap
                ?.innertubeCommand?.browseEndpoint?.browseId,
            }
          })
          .filter((e) => e.name && e.id),
      )
      if (secFlatResult) return secFlatResult
      return cacheAndFind(
        secArr
          .flatMap((e) =>
            (e?.itemSectionRenderer?.contents ?? []).map((item) => {
              const base =
                item?.lockupViewModel?.metadata
                  ?.lockupMetadataViewModel
              return {
                name: base?.metadata?.contentMetadataViewModel
                  ?.metadataRows?.[0]?.metadataParts?.[0]?.text
                  ?.content,
                id: base?.image?.decoratedAvatarViewModel
                  ?.rendererContext?.commandContext?.onTap
                  ?.innertubeCommand?.browseEndpoint?.browseId,
              }
            }),
          )
          .filter((e) => e.name && e.id),
      )
    }
    if (nocache) return lookup()
    return (creatorNameCache[creatorId] ??= lookup())
  }
  const updateIntervalId = setInterval(update, UPDATE_INTERVAL_MS)
  async function customRadioSelection(options) {
    return new Promise((resolve) => {
      const overlay = document.createElement("div")
      Object.assign(overlay.style, {
        position: "fixed",
        top: "0",
        left: "0",
        width: "100%",
        height: "100%",
        backgroundColor: "rgba(0,0,0,0.5)",
        zIndex: "9999",
        scale: "1.5",
      })
      const dialog = document.createElement("div")
      Object.assign(dialog.style, {
        position: "absolute",
        top: "50%",
        left: "50%",
        transform: "translate(-50%,-50%)",
        padding: "20px",
        backgroundColor: "white",
        borderRadius: "5px",
        boxShadow: "0 2px 10px rgba(0,0,0,0.1)",
        zIndex: "10000",
      })
      for (const option of options) {
        const label = document.createElement("label")
        label.style.display = "block"
        const radio = document.createElement("input")
        radio.type = "radio"
        radio.name = "customRadio"
        const optionText =
          a.gettype(option, "string") ? option : (
            JSON.stringify(option)
          )
        radio.value = optionText
        radio.onclick = () => {
          document.body.removeChild(overlay)
          resolve(option)
        }
        label.appendChild(radio)
        label.appendChild(document.createTextNode(optionText))
        dialog.appendChild(label)
      }
      overlay.appendChild(dialog)
      document.body.appendChild(overlay)
    })
  }
  function newBlockBtn(title, creator) {
    const VIEW_SUFFIX_RE = / • \d+\w? views$/
    const elem = a.newelem("button", {
      innerHTML:
        isBlocked(creator, title ?? "", null) ? "unblock" : "block",
      creator,
      title: title ?? "",
      id: "blockbtn",
      async onclick(e) {
        e.stopImmediatePropagation()
        e.stopPropagation()
        e.preventDefault()
        log(this.creator)
        if (Array.isArray(this.creator)) {
          const creatorArr = this.creator
          const blockedList = [
            ...creatorArr.map((id) => ({
              id,
              name: findCreatorNameById(id),
              isBlocked: ls.blockedCreators.includes(id),
            })),
            "unblock all",
            "block all",
            "abort",
          ]
          const choice = await customRadioSelection(blockedList)
          if (choice === "abort") return
          if (choice === "unblock all") {
            for (const c of creatorArr) {
              const idx = ls.blockedCreators.indexOf(c)
              if (idx !== -1) ls.blockedCreators.splice(idx, 1)
            }
          } else if (choice === "block all") {
            for (const c of creatorArr) {
              if (!ls.blockedCreators.includes(c))
                ls.blockedCreators.push(c)
            }
          } else {
            if (choice.isBlocked) {
              ls.blockedCreators.splice(
                ls.blockedCreators.indexOf(choice.id),
                1,
              )
            } else {
              ls.blockedCreators.push(choice.id)
            }
          }
        } else {
          if (isCreatorBlocked(this.creator)) {
            ls.blockedCreators.splice(
              ls.blockedCreators.indexOf(this.creator),
              1,
            )
          } else {
            ls.blockedCreators.push(this.creator)
          }
        }
        update()
        log(ls.blockedCreators, this.creator)
      },
      oncontextmenu(e) {
        e.stopImmediatePropagation()
        e.stopPropagation()
        e.preventDefault()
        if (RCLICK_BLOCKS_URL) {
          const idx = ls.blockedUrls.indexOf(this.url)
          if (idx !== -1) ls.blockedUrls.splice(idx, 1)
          else ls.blockedUrls.push(this.url)
          update()
          log(ls.blockedUrls, this.url)
        }
        if (RCLICK_BLOCKS_TITLE) {
          const idx = ls.blockedTitles.indexOf(this.title)
          if (idx !== -1) ls.blockedTitles.splice(idx, 1)
          else ls.blockedTitles.push(this.title)
          update()
          log(ls.blockedTitles, this.title)
        }
      },
    })
    let _creator = creator
    Object.defineProperty(elem, "creator", {
      get: () => _creator,
      set(newVal) {
        _creator =
          Array.isArray(newVal) ?
            newVal.map((e) => e.replace(VIEW_SUFFIX_RE, ""))
          : newVal.replace(VIEW_SUFFIX_RE, "")
      },
      enumerable: true,
      configurable: true,
    })
    return elem
  }
  update()
  function update() {
    try {
      let failCount = 0
      function getCreatorId(viddiv, urlSel) {
        const id = a
          .qs(urlSel, viddiv)
          ?.href?.split("?v=")
          .at(-1)
          ?.split("&")[0]
        if (!id) {
          failCount++
          return ""
        }
        const cached = videoCreatorIndex?.get(id)
        if (cached) {
          if (failCount > 0) failCount--
          return cached
        }
        const video =
          findAllKeys(
            unsafeWindow.ytInitialData,
            "lockupMetadataViewModel",
          ).find((e) => findValue(e, id)) ||
          findAllKeys(
            unsafeWindow.ytInitialData,
            "videoRenderer",
          ).find((e) => findValue(e, id))
        const res = [
          ...new Set(
            findAllKeys(video, "browseEndpoint")
              .map((e) => e?.browseId)
              .filter((x) => !!x),
          ),
        ]
        const out = res.length === 1 ? res[0] : res
        if (res.length) {
          videoCreatorIndex?.set(id, out)
          if (failCount > 0) failCount--
          return out
        }
        failCount++
        if ((LOC.watch || LOC.root || LOC.search) && failCount > 5) {
          location.reload()
          clearInterval(updateIntervalId)
        }
        error(video, viddiv)
        return ""
      }
      buildVideoCreatorIndex()
      updateLoc()
      unsafeWindow.ls = ls = sp.update()
      // log(LOC)
      if (LOC.search) {
        addVid(
          "div#dismissible.style-scope.ytd-video-renderer:has(a#video-title)",
          "a#video-title",
          "#video-title",
          getCreatorId,
          "#channel-info",
          "#text > a",
        )
      } else if (LOC.root) {
        globalname = null
        addVid(
          "ytd-rich-item-renderer",
          "a:has(> .cbCustomTitle:first-child)",
          ".ytAttributedStringHost:not(.cbCustomTitle)",
          getCreatorId,
          "yt-lockup-metadata-view-model",
          "a.yt-core-attributed-string__link",
        )
        // OLD
        addVid(
          "ytd-rich-item-renderer:has(#content > yt-lockup-view-model > div > div > yt-lockup-metadata-view-model > div.yt-lockup-metadata-view-model-wiz__text-container > div > yt-content-metadata-view-model > div:nth-child(1) > span > span > a)",
          "#content > yt-lockup-view-model > div > a",
          "#content > yt-lockup-view-model > div > div > yt-lockup-metadata-view-model > div.yt-lockup-metadata-view-model-wiz__text-container > h3 > a > span.yt-core-attributed-string.yt-core-attributed-string--white-space-pre-wrap:not(.cbCustomTitle)",
          getCreatorId,
          "#content > yt-lockup-view-model > div > div > yt-lockup-metadata-view-model",
          "#content > yt-lockup-view-model > div > div > yt-lockup-metadata-view-model > div.yt-lockup-metadata-view-model-wiz__text-container > div > yt-content-metadata-view-model > div:nth-child(1) > span > span > a",
        )
        addVid(
          "ytd-rich-item-renderer",
          "#content > yt-lockup-view-model > div > a",
          ".ytLockupMetadataViewModelTitle",
          getCreatorId,
          "yt-lockup-metadata-view-model",
          "a.yt-core-attributed-string__link",
        )
        // END OLD
        addVid(
          "ytd-rich-item-renderer",
          "#content > yt-lockup-view-model > div > a",
          ".ytLockupMetadataViewModelTitle",
          getCreatorId,
          "yt-lockup-metadata-view-model",
          ":is(.ytAttributedStringHost,.ytContentMetadataViewModelMetadataText,.ytAttributedStringWhiteSpacePreWrap,.ytAttributedStringLinkInheritColor,.ytAttributedStringLink)",
        )
      } else if (LOC.feed) {
        addVid(
          "ytd-rich-item-renderer:has(a>.cbCustomTitle):has(a.yt-core-attributed-string__link):has(yt-lockup-metadata-view-model)",
          "a:has(> .cbCustomTitle:first-child)",
          "a:has(> .cbCustomTitle:first-child)>*:not(.cbCustomTitle)",
          getCreatorId,
          "yt-lockup-metadata-view-model",
          "a.yt-core-attributed-string__link",
        )
        addVid(
          "#dismissible:has(#video-title-link)",
          "#video-title-link",
          "#video-title-link>yt-formatted-string:not(.cbCustomTitle)",
          getCreatorId,
          "#byline-container",
          "#container>#text-container>yt-formatted-string#text>a",
        )
      } else if (LOC.userhome || LOC.uservids) {
        const CREATOR = getCreatorNameFromUrl(location.href)
        const h1Sel =
          "#page-header > yt-page-header-renderer > yt-page-header-view-model > div > div.page-header-view-model-wiz__page-header-headline > div > yt-dynamic-text-view-model > h1"
        const h1 = a.qs(h1Sel)
        const btn = a.qs(`${h1Sel} > #blockbtn`)
        if (h1 && !btn) {
          h1.appendChild(newBlockBtn(null, CREATOR ?? ""))
        } else if (btn) {
          const blockInfo = isBlocked(btn.creator, btn.title, btn.url)
          btn.textContent =
            blockInfo ?
              `unblock - ${JSON.stringify(blockInfo)}`
            : "block"
        }
        if (LOC.userhome) {
          addVid(
            "#dismissible:has(#video-title-link)",
            "#video-title-link",
            "#video-title-link",
            () => CREATOR ?? "",
            "#byline-container",
          )
          addVid(
            "#dismissible:has(#video-title-link)",
            "#video-title-link",
            "#video-title-link",
            (viddiv, urlSel) => {
              const id = a
                .qs(urlSel, viddiv)
                ?.href?.split("?v=")
                .at(-1)
                ?.split("&")[0]
              if (!id) return ""
              const video = findAllKeys(
                unsafeWindow.ytInitialData,
                "lockupMetadataViewModel",
              ).find((e) => findValue(e, id))
              return (
                findKey(video, "browseEndpoint")?.browseId ??
                (error(video, viddiv), "")
              )
            },
            "#byline-container",
            "#container>#text-container>yt-formatted-string#text",
          )
        }
        if (LOC.uservids) {
          addVid(
            "ytd-rich-item-renderer",
            ".ytLockupMetadataViewModelTitle",
            ".ytLockupMetadataViewModelTitle>.ytAttributedStringHost.ytAttributedStringWhiteSpacePreWrap",
            () => CREATOR ?? "",
            "yt-lockup-metadata-view-model h3.ytLockupMetadataViewModelHeadingReset",
          )
        }
      } else if (LOC.watch) {
        if (!a.qs("#playrate") && a.qs(".ytp-right-controls")) {
          const rates = [
            [0.1],
            [0.25],
            [1, true],
            [1.4],
            [1.8],
            [2, true],
            [2.5],
          ]
          a.qs(".ytp-right-controls").appendChild(
            a.newelem(
              "div",
              {
                display: "inline flex",
                flexDirection: "row",
                id: "playrate",
                class:
                  "ytp-button ytp-settings-button ytp-hd-quality-badge",
                width: "fit-content",
              },
              rates.map(([r, highlight]) =>
                a.newelem("button", {
                  innerHTML: String(r),
                  ...(highlight ? { backgroundColor: "#449" } : {}),
                  onclick() {
                    localStorage.setItem("playRate", String(r))
                    update()
                  },
                }),
              ),
            ),
          )
        }
        const watchContents =
          unsafeWindow.ytInitialData?.contents
            ?.twoColumnWatchNextResults?.results?.results?.contents
        globalname =
          watchContents?.[1]?.videoSecondaryInfoRenderer?.owner?.videoOwnerRenderer?.attributedTitle?.commandRuns?.[0]?.onTap?.innertubeCommand?.showDialogCommand?.panelLoadingStrategy?.inlineContent?.dialogViewModel?.customContent?.listViewModel?.listItems?.map(
            (e) =>
              e?.listItemViewModel?.title?.commandRuns?.[0]?.onTap
                ?.innertubeCommand?.browseEndpoint?.browseId,
          ) ||
          watchContents?.[1]?.videoSecondaryInfoRenderer?.owner
            ?.videoOwnerRenderer?.title?.runs?.[0]?.navigationEndpoint
            ?.browseEndpoint?.browseId ||
          watchContents?.[1]?.videoSecondaryInfoRenderer
            ?.subscribeButton?.subscribeButtonRenderer?.channelId ||
          unsafeWindow.ytInitialData?.header?.pageHeaderRenderer
            ?.content?.pageHeaderViewModel?.actions
            ?.flexibleActionsViewModel?.actionsRows?.[0]?.actions?.[0]
            ?.subscribeButtonViewModel?.subscribeButtonContent
            ?.onTapCommand?.innertubeCommand?.subscribeEndpoint
            ?.channelIds ||
          watchContents?.[2]?.videoSecondaryInfoRenderer
            ?.subscribeButton?.subscribeButtonRenderer
            ?.onUnsubscribeEndpoints?.[0]?.signalServiceEndpoint
            ?.actions?.[0]?.openPopupAction?.popup
            ?.confirmDialogRenderer?.confirmButton?.buttonRenderer
            ?.serviceEndpoint?.unsubscribeEndpoint?.channelIds?.[0] ||
          watchContents?.[2]?.videoSecondaryInfoRenderer?.subscribeButton?.subscribeButtonRenderer?.onSubscribeEndpoints?.[0]?.showDialogCommand?.panelLoadingStrategy?.inlineContent?.dialogViewModel?.customContent?.listViewModel?.listItems
            ?.map(
              (e) =>
                e?.listItemViewModel?.title?.commandRuns?.[0]?.onTap
                  ?.innertubeCommand?.browseEndpoint?.browseId,
            )
            ?.filter(Boolean)
        if (a.qs("ytd-video-owner-renderer") && globalname) {
          addVid(
            "ytd-watch-metadata",
            () => location.href.split("?v=").at(-1).split("&")[0],
            "#title > h1 > yt-formatted-string",
            () => globalname,
            "ytd-video-owner-renderer",
            null,
          )
          a.qs("ytd-watch-metadata").style.display = ""
          const btn = a.qs("ytd-video-owner-renderer>#blockbtn")
          if (btn) {
            vidlock = isBlocked(btn.creator, btn.title, btn.url)
            const blockInfo = vidlock
            btn.textContent =
              blockInfo ?
                `unblock - ${JSON.stringify(blockInfo)}`
              : "block"
          }
        }
        addVid(
          "yt-lockup-view-model",
          ".ytLockupMetadataViewModelTitle",
          ".ytLockupMetadataViewModelTitle",
          getCreatorId,
          ".ytLockupMetadataViewModelMetadata",
          null,
        )
        setVidSpeed()
      }
    } catch (e) {
      trace("update", e)
    }
  }
  function findAllKeys(obj, keyToFind) {
    const results = []
    function recurse(current) {
      if (!current || typeof current !== "object") return
      for (const key of Object.keys(current)) {
        if (key.includes(keyToFind)) results.push(current[key])
        recurse(current[key])
      }
    }
    recurse(obj)
    return results
  }
  function findAllValues(obj, valueToFind) {
    const results = []
    function recurse(current) {
      if (!current || typeof current !== "object") return
      for (const key of Object.keys(current)) {
        const val = current[key]
        if (typeof val === "string" && val.includes(valueToFind))
          results.push(val)
        recurse(val)
      }
    }
    recurse(obj)
    return results
  }
  function findKey(obj, keyToFind) {
    let result
    function recurse(current) {
      if (
        result !== undefined ||
        !current ||
        typeof current !== "object"
      )
        return
      for (const key of Object.keys(current)) {
        if (key === keyToFind) {
          result = current[key]
          return
        }
        recurse(current[key])
      }
    }
    recurse(obj)
    return result
  }
  function findValue(obj, valueToFind) {
    let result
    function recurse(current) {
      if (
        result !== undefined ||
        !current ||
        typeof current !== "object"
      )
        return
      for (const key of Object.keys(current)) {
        const val = current[key]
        if (val === valueToFind) {
          result = val
          return
        }
        recurse(val)
      }
    }
    recurse(obj)
    return result
  }
  function addVid(
    mainDivSel,
    urlSel,
    titleSel,
    creatorIdResolver,
    blockBtnParentSel,
    creatorNameSel,
  ) {
    let viddiv
    try {
      function tryCall(thing) {
        if (typeof thing === "function") {
          return (
            thing(viddiv, urlSel, titleSel, creatorIdResolver) ?? ""
          )
        }
        return undefined
      }
      for (viddiv of a.qsa(mainDivSel)) {
        const titleEl =
          typeof titleSel === "string" ?
            a.qs(titleSel, viddiv)
          : viddiv
        const creatorEl =
          creatorNameSel ? a.qs(creatorNameSel, viddiv) : viddiv
        const parentEl = a.qs(blockBtnParentSel, viddiv)
        if (!titleEl || !creatorEl || !parentEl) continue
        if (
          !viddiv.getBoundingClientRect().height &&
          viddiv.style.display !== "none"
        )
          continue
        if (viddiv.textContent?.includes("Free with ads")) {
          viddiv.style.display = "none"
          continue
        }
        let btn = a.qs(`${blockBtnParentSel}>#blockbtn`, viddiv)
        const alreadyProcessed = btn && btn.creator !== ""
        if (!btn) btn = parentEl.appendChild(newBlockBtn("", ""))
        if (!alreadyProcessed) {
          btn.url =
            tryCall(urlSel) ??
            a
              .qs(urlSel, viddiv)
              .href.split("?v=")
              .at(-1)
              .split("&")[0]
          btn.title =
            tryCall(titleSel) ??
            a.qs(titleSel, viddiv).textContent.toLowerCase()
          btn.creator =
            creatorIdResolver.length === 0 ?
              creatorIdResolver()
            : creatorIdResolver(viddiv, urlSel)
        }
        const creatorLabel =
          Array.isArray(btn.creator) ?
            btn.creator
              .map((id) => findCreatorNameById(id) ?? id)
              .join(", ")
          : (findCreatorNameById(btn.creator) ??
            (log("failed to find creator name", btn.creator),
            "NO CREATOR NAME FOUND"))
        btn.textContent =
          isCreatorBlocked(btn.creator) ?
            `unblock ${creatorLabel}`
          : `block ${creatorLabel}`
        const prog =
          a.qs(
            ".ytd-thumbnail-overlay-resume-playback-renderer.style-scope",
            viddiv,
          ) ||
          a.qs(
            ".ytThumbnailOverlayProgressBarHostWatchedProgressBarSegment",
            viddiv,
          )
        const watchedPct = prog ? parseFloat(prog.style.width) : 0
        viddiv.style.display =
          (
            isBlocked(btn.creator, btn.title, btn.url) ||
            (HIDE_WATCHED_VID_PROG > 0 &&
              watchedPct >= HIDE_WATCHED_VID_PROG)
          ) ?
            "none"
          : ""
      }
    } catch (e) {
      trace("addVid", e, titleSel, creatorIdResolver, viddiv)
    }
  }
  unsafeWindow.isBlocked = isBlocked
  unsafeWindow.addVid = addVid
  const regexCache = new Map()
  function getCachedRegex(pattern) {
    let re = regexCache.get(pattern)
    if (!re) {
      re = new RegExp(pattern, "i")
      regexCache.set(pattern, re)
    }
    return re
  }
  function isBlocked(creator, title, url) {
    try {
      if (creator === undefined)
        return {
          type: "invalid creator",
          val: { creator, title, url },
        }
      if (url && ls.blockedUrls.includes(url))
        return { type: "blockedUrls", val: url }
      if (ls.blockedTitles.includes(title))
        return { type: "blockedTitles", val: title }
      const creatorInfo = isCreatorBlocked(creator)
      if (creatorInfo) return creatorInfo
      for (const pattern of ls.blockedTitlesReg) {
        if (getCachedRegex(pattern).test(title))
          return { type: "blockedTitlesReg", val: pattern }
      }
      return false
    } catch (e) {
      trace("isBlocked", e)
    }
  }
  function isCreatorBlocked(creator) {
    if (Array.isArray(creator)) {
      const blocked = creator.filter((c) =>
        ls.blockedCreators.includes(c),
      )
      if (blocked.length) {
        return {
          type: "blockedCreators",
          val: blocked.map((e) => findCreatorNameById(e) ?? e),
        }
      }
      for (const pattern of ls.blockedCreatorsReg) {
        if (creator.some((c) => getCachedRegex(pattern).test(c)))
          return { type: "blockedCreatorsReg", val: pattern }
      }
      return undefined
    }
    if (ls.blockedCreators.includes(creator))
      return {
        type: "blockedCreators",
        val: findCreatorNameById(creator) ?? creator,
      }
    for (const pattern of ls.blockedCreatorsReg) {
      if (getCachedRegex(pattern).test(creator))
        return { type: "blockedCreatorsReg", val: pattern }
    }
  }
  function updateLoc() {
    Object.assign(LOC, {
      root: /^https?:\/\/(?:www\.)?youtube\.com\/?(?:\?|#|$)/.test(
        location.href,
      ),
      watch:
        /^https?:\/\/(?:www\.)?youtube\.com\/watch\/?(?:\?|#|$)/.test(
          location.href,
        ),
      search:
        /^https?:\/\/(?:www\.)?youtube\.com\/results\?search_query=.*(?:#|$)/.test(
          location.href,
        ),
      feed: /^https?:\/\/(?:www\.)?youtube\.com\/feed\/subscriptions/.test(
        location.href,
      ),
      userhome: /^https?:\/\/(?:www\.)?youtube\.com\/@[^/]+\/?$/.test(
        location.href,
      ),
      uservids:
        /^https?:\/\/(?:www\.)?youtube\.com\/@[^/]+\/(videos|streams)\/?$/.test(
          location.href,
        ) ||
        /^https?:\/\/(?:www\.)?youtube\.com\/(?:channel|user|c)\/[^/]+\/(videos|streams)\/?$/.test(
          location.href,
        ),
    })
  }
  unsafeWindow.getCreatorNameFromUrl = getCreatorNameFromUrl
  function getCreatorNameFromUrl(url) {
    return url.match(/(?:\/@|\/(?:channel|user|c)\/)([^/]*)/i)?.[1]
  }
})()