Greasy Fork

Greasy Fork is available in English.

NGA Noimg Fix

尝试将泥潭无法加载的图片修复

目前为 2024-07-12 提交的版本,查看 最新版本

// ==UserScript==
// @name              NGA Noimg Fix
// @name:zh-CN        NGA Noimg 修复
// @namespace         http://greasyfork.icu/users/263018
// @version           1.1.1
// @author            snyssss
// @description       尝试将泥潭无法加载的图片修复
// @description:zh-cn 尝试将泥潭无法加载的图片修复
// @license           MIT

// @match             *://bbs.nga.cn/*
// @match             *://ngabbs.com/*
// @match             *://nga.178.com/*

// @require           https://update.greasyfork.icu/scripts/486070/1405682/NGA%20Library.js

// @grant             GM_setValue
// @grant             GM_getValue
// @grant             GM_registerMenuCommand
// @grant             unsafeWindow

// @run-at            document-start
// @noframes
// ==/UserScript==

(() => {
  // 声明泥潭主模块、回复模块
  let commonui, replyModule;

  // 急速模式
  const FAST_MODE_KEY = "FAST_MODE";
  const FAST_MODE = GM_getValue(FAST_MODE_KEY, true);

  // 缓存,避免重复请求
  const cache = {};

  // 监听元素变化并重新修复
  const observer = new MutationObserver((mutationsList) => {
    const list = [];

    mutationsList.forEach(({ target }) => {
      const content = target.classList.contains("ubbcode")
        ? target
        : target.closest(".ubbcode");

      const item = Object.values(replyModule.data).find(
        (item) => item.contentC === content
      );

      if (item && list.includes(item) === false) {
        list.push(item);
      }
    });

    list.forEach((item) => {
      fixReply(item);
    });
  });

  /**
   * 修复无法加载的图片
   * @param {*} tid      帖子 ID
   * @param {*} pid      回复 ID
   * @param {*} content  回复容器
   * @param {*} postTime 回复时间
   */
  const fixNoimg = async (tid, pid, content, postTime) => {
    // 用正则匹配所有 [noimg] 标记
    const matches = content.innerHTML.match(/\[noimg\]\.(.+?)\[\/noimg\]/g);

    // 没有匹配结果,跳过
    if (matches === null) {
      return;
    }

    // 转换时间戳至时间
    const time = new Date(postTime * 1000);

    // 尝试从缓存里直接读取
    const list = matches.filter((item) => {
      // 缓存模式
      if (cache[item]) {
        content.innerHTML = content.innerHTML.replace(
          item,
          `<img src="${cache[item]}"/>`
        );

        return false;
      }

      // 极速模式
      if (FAST_MODE) {
        // 取得 Noimg 里的图片地址
        const src = item.replace(/\[noimg\]\.(.+?)\[\/noimg\]/, "$1");

        // 加入时间前缀
        const realSrc =
          `./mon_` +
          `${time.getFullYear()}` +
          `${String(time.getMonth() + 1).padStart(2, "0")}/` +
          `${String(time.getDate()).padStart(2, "0")}` +
          `${src}`;

        // 计算完整的图片地址
        const fullSrc = commonui.correctAttachUrl(realSrc);

        // 写入缓存
        cache[item] = fullSrc;

        // 替换图片
        content.innerHTML = content.innerHTML.replace(
          item,
          `<img src="${cache[item]}"/>`
        );

        return false;
      }

      return true;
    });

    // 无需再次修复
    if (list.length === 0) {
      return;
    }

    // 尝试请求带有正确图片地址的回复原文
    const url = `/post.php?action=quote&tid=${tid}&pid=${pid}&lite=js`;

    const response = await fetch(url);

    const result = await Tools.readForumData(response, false);

    // 用正则匹配所有 [img] 标记
    const imgs = result.match(/\[img\](.+?)\[\/img\]/g) || [];

    // 声明前缀
    let prefix = "";

    // 对比图片结果,修复无法加载的图片
    for (let i = 0; i < list.length; i += 1) {
      const label = list[i];

      // 取得 Noimg 里的图片地址
      const src = label.replace(/\[noimg\]\.(.+?)\[\/noimg\]/, "$1");

      // 取得原文里的图片地址
      const realSrc = (() => {
        const img = imgs.find((item) => item.indexOf(src) > 0);

        // 引用会超字数限制,我们姑且认为所有图片都是在同一时间内发出的
        // 如果有图片,更新前缀,反之直接使用前一个前缀
        if (img) {
          prefix = img.replace(/\[img\](.+?)\[\/img\]/, "$1").replace(src, "");
        }

        // 返回结果
        if (prefix) {
          return `${prefix}${src}`;
        }
      })();

      // 如果有图片地址,修复
      if (realSrc) {
        const fullSrc = commonui.correctAttachUrl(realSrc);

        // 写入缓存
        cache[label] = fullSrc;

        // 替换图片
        content.innerHTML = content.innerHTML.replace(
          label,
          `<img src="${fullSrc}"/>`
        );
      }
    }
  };

  /**
   * 修复回复
   * @param {*} item 回复内容,见 commonui.postArg.data
   */
  const fixReply = async (item) => {
    // 跳过泥潭增加的额外内容
    if (Tools.getType(item) !== "object") {
      return;
    }

    // 获取帖子 ID、回复 ID、内容、回复时间
    const { tid, pid, contentC, postTime } = item;

    // 处理引用
    await fixQuote(item);

    // 修复图片
    await fixNoimg(tid, pid, contentC, postTime);

    // 修复引用
    // 兼容屏蔽脚本
    observer.observe(contentC, { childList: true, subtree: true });
  };

  /**
   * 修复引用
   * @param {*} item 回复内容,见 commonui.postArg.data
   */
  const fixQuote = async (item) => {
    // 跳过泥潭增加的额外内容
    if (Tools.getType(item) !== "object") {
      return;
    }

    // 获取内容
    const content = item.contentC;

    // 找到所有引用
    const quotes = content.querySelectorAll(".quote");

    // 处理引用
    await Promise.all(
      [...quotes].map(async (quote) => {
        const { tid, pid } = (() => {
          const ele = quote.querySelector("[title='快速浏览这个帖子']");

          if (ele) {
            const res = ele
              .getAttribute("onclick")
              .match(/fastViewPost(.+,(\S+),(\S+|undefined),.+)/);

            if (res) {
              return {
                tid: parseInt(res[2], 10),
                pid: parseInt(res[3], 10) || 0,
              };
            }
          }

          return {};
        })();

        const timeElement = quote.querySelector(".xtxt");
        const time = timeElement
          ? timeElement.innerHTML.replace(/\((.+)\)/, "$1")
          : null;

        if (time) {
          // 转换为泥潭的时间戳
          const postTime = new Date(time).getTime() / 1000;

          // 修复图片
          await fixNoimg(tid, pid, content, postTime);
        }
      })
    );
  };

  /**
   * 处理 postArg 模块
   * @param {*} value commonui.postArg
   */
  const handleReplyModule = async (value) => {
    // 绑定回复模块
    replyModule = value;

    if (value === undefined) {
      return;
    }

    // 修复
    const afterGet = (_, args) => {
      // 楼层号
      const index = args[0];

      // 找到对应数据
      const data = replyModule.data[index];

      // 开始修复
      if (data) {
        fixReply(data);
      }
    };

    // 如果已经有数据,则直接修复
    Object.values(replyModule.data).forEach(fixReply);

    // 拦截 proc 函数,这是泥潭的回复添加事件
    Tools.interceptProperty(replyModule, "proc", {
      afterGet,
    });
  };

  /**
   * 处理 commonui 模块
   * @param {*} value commonui
   */
  const handleCommonui = (value) => {
    // 绑定主模块
    commonui = value;

    // 拦截 postArg 模块,这是泥潭的回复入口
    Tools.interceptProperty(commonui, "postArg", {
      afterSet: (value) => {
        handleReplyModule(value);
      },
    });
  };

  /**
   * 注册脚本菜单
   */
  const registerMenu = () => {
    GM_registerMenuCommand(`极速模式:${FAST_MODE ? "是" : "否"}`, () => {
      if (
        FAST_MODE === false &&
        confirm(
          `是否开启极速模式?\n极速模式即为不请求原文,而是根据发帖时间推测图片地址。\n对于复制他人图片链接至帖子里的解析可能会失败。`
        ) === false
      ) {
        return;
      }

      GM_setValue("FAST_MODE", !FAST_MODE);

      location.reload();
    });
  };

  // 主函数
  (async () => {
    // 注册脚本菜单
    registerMenu();

    // 处理 commonui 模块
    if (unsafeWindow.commonui) {
      handleCommonui(unsafeWindow.commonui);
      return;
    }

    Tools.interceptProperty(unsafeWindow, "commonui", {
      afterSet: (value) => {
        handleCommonui(value);
      },
    });
  })();
})();