NGA Noimg Fix

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

  1. // ==UserScript==
  2. // @name NGA Noimg Fix
  3. // @name:zh-CN NGA Noimg 修复
  4. // @namespace https://greasyfork.org/users/263018
  5. // @version 1.2.0
  6. // @author snyssss
  7. // @description 尝试将泥潭无法加载的图片修复
  8. // @description:zh-cn 尝试将泥潭无法加载的图片修复
  9. // @license MIT
  10.  
  11. // @match *://bbs.nga.cn/*
  12. // @match *://ngabbs.com/*
  13. // @match *://nga.178.com/*
  14.  
  15. // @require https://update.greasyfork.org/scripts/486070/1405682/NGA%20Library.js
  16.  
  17. // @grant GM_setValue
  18. // @grant GM_getValue
  19. // @grant GM_registerMenuCommand
  20. // @grant unsafeWindow
  21.  
  22. // @run-at document-start
  23. // @noframes
  24. // ==/UserScript==
  25.  
  26. (() => {
  27. // 声明泥潭主模块、回复模块
  28. let commonui, replyModule;
  29.  
  30. // 急速模式
  31. const FAST_MODE_KEY = "FAST_MODE";
  32. const FAST_MODE = GM_getValue(FAST_MODE_KEY, true);
  33.  
  34. // 图片属性
  35. const IMG_ATTRS_KEY = "IMG_ATTRS";
  36. const IMG_ATTRS = GM_getValue(IMG_ATTRS_KEY, { style: "max-width: 100%" });
  37.  
  38. // 缓存,避免重复请求
  39. const cache = {};
  40.  
  41. // 监听元素变化并重新修复
  42. const observer = new MutationObserver((mutationsList) => {
  43. const list = [];
  44.  
  45. mutationsList.forEach(({ target }) => {
  46. const content = target.classList.contains("ubbcode")
  47. ? target
  48. : target.closest(".ubbcode");
  49.  
  50. const item = Object.values(replyModule.data).find(
  51. (item) => item.contentC === content
  52. );
  53.  
  54. if (item && list.includes(item) === false) {
  55. list.push(item);
  56. }
  57. });
  58.  
  59. list.forEach(fixReply);
  60. });
  61.  
  62. /**
  63. * 修复无法加载的图片
  64. * @param {*} tid 帖子 ID
  65. * @param {*} pid 回复 ID
  66. * @param {*} content 回复容器
  67. * @param {*} postTime 回复时间
  68. */
  69. const fixNoimg = async (tid, pid, content, postTime) => {
  70. // 用正则匹配所有 [noimg] 标记
  71. const matches = content.innerHTML.match(/\[noimg\]\.(.+?)\[\/noimg\]/g);
  72.  
  73. // 没有匹配结果,跳过
  74. if (matches === null) {
  75. return;
  76. }
  77.  
  78. // 替换图片方法
  79. const replace = (key, value) => {
  80. // 写入缓存
  81. cache[key] = value;
  82.  
  83. // 生成图片
  84. const img = document.createElement("img");
  85.  
  86. // 设置图片属性
  87. Object.entries({
  88. ...IMG_ATTRS,
  89. src: value,
  90. }).forEach(([key, value]) => {
  91. img.setAttribute(key, value);
  92. });
  93.  
  94. // 替换图片
  95. content.innerHTML = content.innerHTML.replace(key, img.outerHTML);
  96. };
  97.  
  98. // 转换时间戳至时间
  99. const time = new Date(postTime * 1000);
  100.  
  101. // 尝试从缓存里直接读取
  102. const list = matches.filter((item) => {
  103. // 缓存模式
  104. if (cache[item]) {
  105. replace(item, cache[item]);
  106.  
  107. return false;
  108. }
  109.  
  110. // 极速模式
  111. if (FAST_MODE) {
  112. // 取得 Noimg 里的图片地址
  113. const src = item.replace(/\[noimg\]\.(.+?)\[\/noimg\]/, "$1");
  114.  
  115. // 加入时间前缀
  116. const realSrc =
  117. `./mon_` +
  118. `${time.getFullYear()}` +
  119. `${String(time.getMonth() + 1).padStart(2, "0")}/` +
  120. `${String(time.getDate()).padStart(2, "0")}` +
  121. `${src}`;
  122.  
  123. // 计算完整的图片地址
  124. const fullSrc = commonui.correctAttachUrl(realSrc);
  125.  
  126. // 替换图片
  127. replace(item, fullSrc);
  128.  
  129. return false;
  130. }
  131.  
  132. return true;
  133. });
  134.  
  135. // 无需再次修复
  136. if (list.length === 0) {
  137. return;
  138. }
  139.  
  140. // 尝试请求带有正确图片地址的回复原文
  141. const url = `/post.php?action=quote&tid=${tid}&pid=${pid}&lite=js`;
  142.  
  143. const response = await fetch(url);
  144.  
  145. const result = await Tools.readForumData(response, false);
  146.  
  147. // 用正则匹配所有 [img] 标记
  148. const imgs = result.match(/\[img\](.+?)\[\/img\]/g) || [];
  149.  
  150. // 声明前缀
  151. let prefix = "";
  152.  
  153. // 对比图片结果,修复无法加载的图片
  154. for (let i = 0; i < list.length; i += 1) {
  155. const item = list[i];
  156.  
  157. // 取得 Noimg 里的图片地址
  158. const src = item.replace(/\[noimg\]\.(.+?)\[\/noimg\]/, "$1");
  159.  
  160. // 取得原文里的图片地址
  161. const realSrc = (() => {
  162. const img = imgs.find((item) => item.indexOf(src) > 0);
  163.  
  164. // 引用会超字数限制,我们姑且认为所有图片都是在同一时间内发出的
  165. // 如果有图片,更新前缀,反之直接使用前一个前缀
  166. if (img) {
  167. prefix = img.replace(/\[img\](.+?)\[\/img\]/, "$1").replace(src, "");
  168. }
  169.  
  170. // 返回结果
  171. if (prefix) {
  172. return `${prefix}${src}`;
  173. }
  174. })();
  175.  
  176. // 如果有图片地址,修复
  177. if (realSrc) {
  178. // 计算完整的图片地址
  179. const fullSrc = commonui.correctAttachUrl(realSrc);
  180.  
  181. // 替换图片
  182. replace(item, fullSrc);
  183. }
  184. }
  185. };
  186.  
  187. /**
  188. * 修复回复
  189. * @param {*} item 回复内容,见 commonui.postArg.data
  190. */
  191. const fixReply = async (item) => {
  192. // 跳过泥潭增加的额外内容
  193. if (Tools.getType(item) !== "object") {
  194. return;
  195. }
  196.  
  197. // 获取帖子 ID、回复 ID、内容、回复时间
  198. const { tid, pid, contentC, postTime } = item;
  199.  
  200. // 处理引用
  201. await fixQuote(item);
  202.  
  203. // 修复图片
  204. await fixNoimg(tid, pid, contentC, postTime);
  205.  
  206. // 监听元素变化并重新修复
  207. // 兼容屏蔽脚本
  208. observer.observe(contentC, { childList: true, subtree: true });
  209. };
  210.  
  211. /**
  212. * 修复引用
  213. * @param {*} item 回复内容,见 commonui.postArg.data
  214. */
  215. const fixQuote = async (item) => {
  216. // 跳过泥潭增加的额外内容
  217. if (Tools.getType(item) !== "object") {
  218. return;
  219. }
  220.  
  221. // 获取内容
  222. const content = item.contentC;
  223.  
  224. // 找到所有引用
  225. const quotes = content.querySelectorAll(".quote");
  226.  
  227. // 处理引用
  228. await Promise.all(
  229. [...quotes].map(async (quote) => {
  230. const { tid, pid } = (() => {
  231. const ele = quote.querySelector("[title='快速浏览这个帖子']");
  232.  
  233. if (ele) {
  234. const res = ele
  235. .getAttribute("onclick")
  236. .match(/fastViewPost(.+,(\S+),(\S+|undefined),.+)/);
  237.  
  238. if (res) {
  239. return {
  240. tid: parseInt(res[2], 10),
  241. pid: parseInt(res[3], 10) || 0,
  242. };
  243. }
  244. }
  245.  
  246. return {};
  247. })();
  248.  
  249. const timeElement = quote.querySelector(".xtxt");
  250. const time = timeElement
  251. ? timeElement.innerHTML.replace(/\((.+)\)/, "$1")
  252. : null;
  253.  
  254. if (time) {
  255. // 转换为泥潭的时间戳
  256. const postTime = new Date(time).getTime() / 1000;
  257.  
  258. // 修复图片
  259. await fixNoimg(tid, pid, quote, postTime);
  260. }
  261. })
  262. );
  263. };
  264.  
  265. /**
  266. * 处理 postArg 模块
  267. * @param {*} value commonui.postArg
  268. */
  269. const handleReplyModule = async (value) => {
  270. // 绑定回复模块
  271. replyModule = value;
  272.  
  273. if (value === undefined) {
  274. return;
  275. }
  276.  
  277. // 修复
  278. const afterGet = (_, args) => {
  279. // 楼层号
  280. const index = args[0];
  281.  
  282. // 找到对应数据
  283. const data = replyModule.data[index];
  284.  
  285. // 开始修复
  286. if (data) {
  287. fixReply(data);
  288. }
  289. };
  290.  
  291. // 如果已经有数据,则直接修复
  292. Object.values(replyModule.data).forEach(fixReply);
  293.  
  294. // 拦截 proc 函数,这是泥潭的回复添加事件
  295. Tools.interceptProperty(replyModule, "proc", {
  296. afterGet,
  297. });
  298. };
  299.  
  300. /**
  301. * 处理 commonui 模块
  302. * @param {*} value commonui
  303. */
  304. const handleCommonui = (value) => {
  305. // 绑定主模块
  306. commonui = value;
  307.  
  308. // 拦截 postArg 模块,这是泥潭的回复入口
  309. Tools.interceptProperty(commonui, "postArg", {
  310. afterSet: (value) => {
  311. handleReplyModule(value);
  312. },
  313. });
  314. };
  315.  
  316. /**
  317. * 注册脚本菜单
  318. */
  319. const registerMenu = () => {
  320. // 极速模式
  321. {
  322. const func = () => {
  323. if (
  324. FAST_MODE === false &&
  325. confirm(
  326. `是否开启极速模式?\n极速模式即为不请求原文,而是根据发帖时间推测图片地址。\n对于复制他人图片链接至帖子里的解析可能会失败。`
  327. ) === false
  328. ) {
  329. return;
  330. }
  331.  
  332. GM_setValue(FAST_MODE_KEY, !FAST_MODE);
  333.  
  334. location.reload();
  335. };
  336.  
  337. GM_registerMenuCommand(`极速模式:${FAST_MODE ? "是" : "否"}`, func);
  338. }
  339.  
  340. // 图片属性
  341. {
  342. const func = () => {
  343. const attr = prompt(
  344. `给图片添加额外的属性或样式`,
  345. JSON.stringify(IMG_ATTRS)
  346. );
  347.  
  348. if ((attr || "").length > 0) {
  349. try {
  350. const newValue = JSON.parse(attr);
  351.  
  352. if (Tools.getType(newValue) !== "object") {
  353. throw new Error();
  354. }
  355.  
  356. GM_setValue(IMG_ATTRS_KEY, newValue);
  357.  
  358. location.reload();
  359. } catch {
  360. func();
  361. }
  362. }
  363. };
  364.  
  365. GM_registerMenuCommand(`图片属性`, func);
  366. }
  367. };
  368.  
  369. // 主函数
  370. (async () => {
  371. // 注册脚本菜单
  372. registerMenu();
  373.  
  374. // 处理 commonui 模块
  375. if (unsafeWindow.commonui) {
  376. handleCommonui(unsafeWindow.commonui);
  377. return;
  378. }
  379.  
  380. Tools.interceptProperty(unsafeWindow, "commonui", {
  381. afterSet: (value) => {
  382. handleCommonui(value);
  383. },
  384. });
  385. })();
  386. })();