Greasy Fork is available in English.
bilibili 枝网(asoulcnki.asia)查重 API 版
// ==UserScript== // @name bilibili 枝网查重 API 版 // @namespace https://github.com/sparanoid/userscript // @supportURL https://github.com/sparanoid/userscript/issues // @version 0.1.14 // @description bilibili 枝网(asoulcnki.asia)查重 API 版 // @author Sparanoid // @license AGPL // @compatible chrome 80 or later // @compatible edge 80 or later // @compatible firefox 74 or later // @compatible safari 13.1 or later // @match https://*.bilibili.com/* // @icon https://experiments.sparanoid.net/favicons/v2/www.bilibili.com.ico // @grant none // @run-at document-start // ==/UserScript== window.addEventListener('load', () => { const DEBUG = true; const NAMESPACE = 'bilibili-asoulcnki'; const apiBase = 'https://asoulcnki.asia'; const refTag = '?utm_source=bilibili-asoulcnki-plugin&utm_campaign=tampermonkey' const feedbackUrl = 'https://t.bilibili.com/545085157213602473'; console.log(`${NAMESPACE} loaded`); async function fetchResult(url = '', data = {}) { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); return response.json(); } function debug(description = '', msg = '', force = false) { if (DEBUG || force) { console.log(`${NAMESPACE}: ${description}`, msg) } } function formatDate(timestamp) { let date = timestamp.toString().length === 10 ? new Date(+timestamp * 1000) : new Date(+timestamp); return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; } function rateColor(percent) { return `hsl(${100 - percent}, 70%, 45%)`; } function percentDisplay(num) { return num.toFixed(2).replace('.00', ''); } function sanitize(string) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', "/": '/', }; const reg = /[&<>"'/]/ig; return string.replace(reg, match => map[match]); } function attachEl(item) { let injectWrap = item.querySelector('.con .info'); // .text - comment content // .text-con - reply content let content = item.querySelector('.con .text') || item.querySelector('.reply-con .text-con'); let id = item.dataset.id; // Simple way to attach element on replies initially loaded with comment // which wouldn't trigger mutation inside observeComments let replies = item.querySelectorAll('.con .reply-box .reply-item'); if (replies.length > 0) { [...replies].map(reply => { attachEl(reply); }); } if (injectWrap.querySelector('.asoulcnki')) { debug('already loaded for this comment'); } else { // Insert asoulcnki check button let asoulcnkiEl = document.createElement('span'); asoulcnkiEl.classList.add('asoulcnki', 'btn-hover', 'btn-highlight'); asoulcnkiEl.innerHTML = '狠狠地查'; asoulcnkiEl.addEventListener('click', e => { let contentPrepared = ''; // Copy meme icons alt text for (let node of content.childNodes.values()) { if (node.nodeType === 3) { contentPrepared += node.textContent; } else if (node.nodeName === 'IMG' && node.nodeType === 1) { contentPrepared += node.alt; } else if (node.nodeName === 'BR' && node.nodeType === 1) { contentPrepared += '\n'; } else if (node.nodeName === 'A' && node.nodeType === 1 && node.classList.contains('comment-jump-url')) { contentPrepared += node.href.replace(/https?:\/\/www\.bilibili\.com\/video\//, ''); } else { contentPrepared += node.innerText; } } // Need regex to stripe `回复 @username :` let contentProcessed = contentPrepared.replace(/回复 @.*:/, ''); debug('content processed', contentProcessed); // ask to confirm if words count not enough if (contentProcessed.length < 10 && !confirm('内容过短(少于 10 字),可能无法得到正确结果,是否继续查询?')) return; fetchResult(`${apiBase}/v1/api/check`, { text: contentProcessed }) .then(data => { debug('data returned', data); let resultContent = ''; if (data.code !== 0) { resultContent = `返回结果错误,可能是文本内容过短,或请访问 <a href="${apiBase}/${refTag}" target="_blank">枝网</a> 查看服务是否正常\n枝网返回结果参考:${data?.code || ''} ${data?.message || ''}`; } else { let result = data.data; let startTime = result.start_time; let endTime = result.end_time; let rate = result.rate * 100; let relatedItems = result.related; resultContent = `<a href="${apiBase}/${refTag}" target="_blank">枝网</a>文本复制检测报告(油猴一键版 ${feedbackUrl}) 查重时间:${formatDate(Date.now())} 总文字复制比:<b style="color: ${rateColor(rate)}">${percentDisplay(rate)}%</b>\n`; if (relatedItems.length === 0) { resultContent += `一眼原创,再偷必究(查重结果仅作娱乐参考)`; } else { let selfOriginal = +relatedItems[0].reply.rpid === +id ? `(<span style="color: blue;">本文原创/原偷,已收录</span>)` : ''; let relatedCountAlert = relatedItems.length === 5 ? `(最多只显示最近 5 次)` : ''; resultContent += `重复次数:${relatedItems.length}${selfOriginal}${relatedCountAlert}\n`; relatedItems.map((item, idx) => { let rate = item.rate * 100; resultContent += `#${idx + 1} <span style="color: ${rateColor(rate)}">${percentDisplay(rate)}%</span> <a href="${item.reply_url.trim()}" title="${sanitize(item.reply.content)}" target="_blank">${item.reply_url.trim()}</a> 发布于:${formatDate(item.reply.ctime)} 作者:${item.reply.m_name} (UID <a href="https://space.bilibili.com/${item.reply.mid}" target="_blank">${item.reply.mid}</a>)\n\n`; }); resultContent += `查重结果仅作娱乐参考,请注意辨别是否为原创`; } } // Insert result let resultWrap = document.createElement('div'); resultWrap.style.position = 'relative'; resultWrap.style.padding = '.5rem'; resultWrap.style.margin = '.5rem 0'; resultWrap.style.background = 'hsla(0, 0%, 50%, .1)'; resultWrap.style.borderRadius = '4px'; resultWrap.style.whiteSpace = 'pre'; resultWrap.style.flexBasis = '100%'; resultWrap.classList.add('asoulcnki-result'); resultWrap.innerHTML = resultContent; // Create close button let asoulcnkiCloseBtn = document.createElement('span'); asoulcnkiCloseBtn.classList.add('asoulcnki-close'); asoulcnkiCloseBtn.innerHTML = '+'; asoulcnkiCloseBtn.style.position = 'absolute'; asoulcnkiCloseBtn.style.top = '.5rem'; asoulcnkiCloseBtn.style.right = '.5rem'; asoulcnkiCloseBtn.style.width = '16px'; asoulcnkiCloseBtn.style.height = '16px'; asoulcnkiCloseBtn.style.fontSize = '16px'; asoulcnkiCloseBtn.style.lineHeight = '1'; asoulcnkiCloseBtn.style.textAlign = 'center'; asoulcnkiCloseBtn.style.transform = 'rotate(45deg)'; asoulcnkiCloseBtn.style.cursor = 'pointer'; asoulcnkiCloseBtn.addEventListener('click', e => { injectWrap.querySelector('.asoulcnki-result').remove(); }); resultWrap.append(asoulcnkiCloseBtn); // Remove previous result if exists if (injectWrap.querySelector('.asoulcnki-result')) { injectWrap.querySelector('.asoulcnki-result').remove(); } injectWrap.append(resultWrap); }) .catch(error => { alert(`枝网后端出错,请检查网络,报错信息:${error}`); debug('fetch error', error); }); }, false); injectWrap.querySelector('.operation').before(asoulcnkiEl); // Insert comment ID link let idLink = document.createElement('a'); idLink.innerHTML = '#'; idLink.setAttribute('title', '当前评论 ID: ' + id); idLink.setAttribute('href', '#reply' + id); idLink.style.marginRight = '.25em'; injectWrap.prepend(idLink); } } function observeComments(wrapper) { // .comment-list - general list for video, zhuanlan, and dongtai // .reply-box - replies attached to specific comment let commentLists = wrapper ? wrapper.querySelectorAll('.comment-list, .reply-box') : document.querySelectorAll('.comment-list, .reply-box'); if (commentLists) { [...commentLists].map(commentList => { // Directly attach elements for pure static server side rendered comments // and replies list. Used by zhuanlan posts with reply hash in URL. // TODO: need a better solution [...commentList.querySelectorAll('.list-item, .reply-item')].map(item => { attachEl(item); }); const observer = new MutationObserver((mutationsList, observer) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { debug('observed mutations', [...mutation.addedNodes].length); [...mutation.addedNodes].map(item => { attachEl(item); // Check if the comment has replies // I check replies here to make sure I can disable subtree option for // MutationObserver to get better performance. let replies = item.querySelectorAll('.con .reply-box .reply-item'); if (replies.length > 0) { observeComments(item) debug(item.dataset.id + ' has rendered reply(ies)', replies.length); } }) } } }); observer.observe(commentList, { attributes: false, childList: true, subtree: false }); }); } } // .bb-comment loads directly for zhuanlan post. So load it directly observeComments(); // .bb-comment loads dynamcially for dontai and videos. So observe it first const wrapperObserver = new MutationObserver((mutationsList, observer) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { [...mutation.addedNodes].map(item => { debug('mutation wrapper added', item); if (item.classList?.contains('bb-comment')) { debug('mutation wrapper added (found target)', item); observeComments(item); // Stop observing // TODO: when observer stops it won't work for dynamic homepage ie. https://space.bilibili.com/703007996/dynamic // so disable it here. This may have some performance impact on low-end machines. // wrapperObserver.disconnect(); } }) } } }); wrapperObserver.observe(document.body, { attributes: false, childList: true, subtree: true }); }, false);