Greasy Fork

Greasy Fork is available in English.

自用bilibili脚本

吧啦吧啦

当前为 2023-10-08 提交的版本,查看 最新版本

// ==UserScript==
// @name         自用bilibili脚本
// @namespace    mimiko/bilibili
// @version      0.0.23
// @description  吧啦吧啦
// @author       Mimiko
// @license      MIT
// @match        *://*.bilibili.com/*
// @grant        GM.addStyle
// grant        GM.registerMenuCommand
// grant        GM.xmlHttpRequest
// @run-at       document-end
// ==/UserScript==
// http://greasyfork.icu/zh-CN/scripts/436748-%E8%87%AA%E7%94%A8bilibili%E8%84%9A%E6%9C%AC
"use strict";
(() => {
  if (window.top !== window.self) return;
  // variable
  /** 工具类 */ const Z = {
    /** `watchPath`方法的数据 */ dataWatch: {
      /** 回调函数列表 */ list: [],
      /** 路由 */ path: "",
      /** 定时器 */ timer: 0,
    },
    /** 防抖函数 */ debounce: (fn, delay) => {
      let timer = 0;
      return (...args) => {
        if (timer) window.clearTimeout(timer);
        timer = window.setTimeout(() => fn(...args), delay);
      };
    },
    /** 获取元素;异步函数,会重复执行直至目标元素出现 */ getElements: (
      selector,
    ) =>
      new Promise((resolve) => {
        const fn = () => {
          if (document.hidden) return;
          const $el = document.querySelectorAll(selector);
          if (!$el.length) return;
          window.clearInterval(timer);
          resolve([...$el]);
        };
        const timer = window.setInterval(fn, 50);
        fn();
      }),
    /** 隐藏元素;通过在目标元素上写入内联样式实现 */ hideElements: (
      ...listSelector
    ) => {
      GM.addStyle(
        listSelector
          .map((selector) => `${selector} { display: none !important; }`)
          .join("\n"),
      );
    },
    /** 从本地存储中读取数据 */ load: (name) => {
      try {
        const data = localStorage.getItem(`mimiko-gms/${name}`);
        if (!data) return null;
        return JSON.parse(data);
      } catch (e) {
        alert(`读取缓存失败:${e.message}`);
        return null;
      }
    },
    /** 移除元素 */ removeElements: (selector) =>
      document.querySelectorAll(selector).forEach(($it) => $it.remove()),
    /** 保存数据到本地存储 */ save: (name, data) =>
      localStorage.setItem(`mimiko-gms/${name}`, JSON.stringify(data)),
    /** 等待一段时间 */ sleep: (ts) =>
      new Promise((resolve) => setTimeout(resolve, ts)),
    /** 监听路由变化 */ watchPath: (callback) => {
      Z.dataWatch.list.push(callback);
      window.clearInterval(Z.dataWatch.timer);
      Z.dataWatch.timer = window.setInterval(() => {
        const { pathname } = window.location;
        if (pathname === Z.dataWatch.path) return;
        Z.dataWatch.path = pathname;
        Z.dataWatch.list.forEach((it) => it());
      }, 200);
    },
  };
  /** 本地存储;目前用于记录哪些视频已经在首页出现过 */ class Cache {
    /** 记录视频上限 */ limit = 5e4;
    /** 视频列表 */ list;
    /** 构造函数;会从本地存储中读取记录 */ constructor() {
      this.list = Z.load("cache-recommend") ?? [];
    }
    /** 添加视频 */ add(id) {
      const it = this.get(id);
      const it2 = [id, it[1] + 1];
      const list2 = this.list.filter((it) => it[0] !== id);
      list2.push(it2);
      this.list = list2.slice(-this.limit);
    }
    /** 清空视频 */ clear() {
      this.list = [];
      this.save();
    }
    /** 获取视频 */ get(id) {
      return this.list.find((it) => it[0] === id) ?? [id, 0];
    }
    /** 保存记录 */ save() {
      Z.save("cache-recommend", this.list);
    }
  }
  const cache = new Cache();
  // function
  /** 首页 */ const asIndex = async () => {
    // style
    GM.addStyle(`
    body { min-width: auto; }
    .container:first-child { display: none; }

    .feed-card { display: block !important; margin-top: 0 !important; }
    .feed-card.is-hidden { position: relative; }

    .feed-card.is-hidden .bili-video-card {
      transition: opacity 0.3s;
      opacity: 0.1;
    }
    .feed-card.is-hidden .bili-video-card:hover { opacity: 1; }

    .feed-card.is-hidden .reason {
      position: absolute;
      width: 160px;
      height: 32px;
      left: 50%;
      top: 32%;
      text-align: center;
      line-height: 32px;
      background-color: rgba(0, 0, 0, 0.8);
      color: #fff;
      font-size: 12px;
      border-radius: 4px;
      pointer-events: none;
      transform: translate(-50%, -50%);
      transition: opacity 0.3s;
      z-index: 1;
    }
    .feed-card.is-hidden:hover .reason { opacity: 0; }
    `);
    // container
    const [container] = await Z.getElements(".container");
    const ctn = document.createElement("div");
    container.classList.forEach((it) => ctn.classList.add(it));
    container.parentNode?.append(ctn);
    // add cache clear button
    const [groupBtn] = await Z.getElements(".feed-roll-btn");
    const btnClear = document.createElement("button");
    btnClear.classList.add("primary-btn", "roll-btn");
    btnClear.innerHTML = "<span>✖</span>";
    btnClear.setAttribute("title", "清空缓存");
    btnClear.addEventListener("click", () => {
      cache.clear();
      alert("缓存已清空");
    });
    btnClear.style.marginTop = "10px";
    groupBtn.append(btnClear);
    // check items
    const hide = () => {
      observer.disconnect();
      const listItem = [...container.children].filter((it) =>
        it.classList.contains("feed-card"),
      );
      listItem.forEach((it) => {
        const check = () => {
          // play count too low
          const stats = it.querySelector(".bili-video-card__stats");
          if (!stats) return "播放量不存在";
          const text = stats.textContent;
          if (!text) return "播放量为空";
          if (!text.includes("万")) return "播放量不足1万";
          const amount = parseFloat(text.split("万")[0]);
          if (amount < 3) return "播放量不足3万";
          // has been viewed
          const link = it.querySelector("a");
          if (!link) return "链接不存在";
          // info
          const info = it.querySelector(".bili-video-card__info--icon-text");
          const id = link.href.replace(/.*\/BV/, "").replace(/\/\?.*/, "");
          if (cache.get(id)[1] > (info?.textContent === "已关注" ? 2 : 0))
            return `已出现${cache.get(id)[1]}次`;
          cache.add(id);
          return "";
        };
        const reason = check();
        if (!reason) {
          ctn.prepend(it);
          return;
        }
        if (reason === "播放量不存在") return;
        it.classList.add("is-hidden");
        const tip = document.createElement("div");
        tip.classList.add("reason");
        tip.textContent = reason;
        it.append(tip);
        ctn.append(it);
      });
      cache.save();
      observer.observe(container, {
        childList: true,
      });
    };
    const observer = new MutationObserver(hide);
    hide();
  };
  /** 视频页 */ const asVideo = () => {
    // hide share
    GM.addStyle(`
    .video-share-popover { display: none; }
    `);
    // fullscreen
    const execFS = async () => {
      const [btn] = await Z.getElements(".bpx-player-ctrl-web");
      btn.click();
    };
    const execWS = async () => {
      const [btn] = await Z.getElements(".bpx-player-ctrl-wide");
      btn.click();
    };
    // auto fullscreen
    const autoFS = Z.debounce(async () => {
      if (window.innerWidth > 1080) return;
      const [player] = await Z.getElements("#bilibili-player");
      if (player.classList.contains("mode-webscreen")) return;
      await execFS();
    }, 1e3);
    Z.watchPath(autoFS);
    window.addEventListener("resize", autoFS);
    // reset hotkey
    document.addEventListener("keydown", (e) => {
      if (
        e.target instanceof HTMLInputElement ||
        e.target instanceof HTMLTextAreaElement
      )
        return;
      if (["q", "w", "e", "d", "f", "m"].includes(e.key)) {
        e.preventDefault();
        e.stopPropagation();
      }
      if (e.key === "f") {
        execFS();
        return;
      }
      if (e.key === "w") execWS();
    });
  };
  /** 主函数 */ const main = async () => {
    const { hostname, pathname } = window.location;
    if (hostname !== "www.bilibili.com") return;
    if (["/", "/index.html"].includes(pathname)) await asIndex();
    if (pathname.startsWith("/video/")) asVideo();
  };
  // execute
  main();
})();