您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Checks for Google Scholar, Sci-Hub, LibGen, Anna's Archive, Sci-net, Semantic Scholar, Unpaywall, CORE, and EuropePMC links only when you hover over a link.
// ==UserScript== // @name Paper links to free PDFs // @namespace greasyfork.org // @version 1.6 // @description Checks for Google Scholar, Sci-Hub, LibGen, Anna's Archive, Sci-net, Semantic Scholar, Unpaywall, CORE, and EuropePMC links only when you hover over a link. // @author Bui Quoc Dung // @match *://*/* // @grant GM.xmlHttpRequest // @connect * // @license AGPL-3.0-or-later // ==/UserScript== (function () { 'use strict'; const SCIHUB_URL = 'https://tesble.com/'; const LIBGEN_URL = 'https://libgen.bz/'; const LIBGEN_SEARCH_URL = LIBGEN_URL + 'index.php?req='; const ANNA_URL = 'https://annas-archive.org'; const ANNA_SCIDB_URL = ANNA_URL + '/scidb/'; const ANNA_CHECK_URL = ANNA_URL + '/search?index=journals&q='; const SCINET_URL = 'https://sci-net.xyz/'; const GOOGLE_SCHOLAR_URL = 'https://scholar.google.com/scholar?hl=en&as_sdt=0%2C5&q='; const UNPAYWALL_EMAIL = '[email protected]'; const DOI_REGEX = /\b(10\.\d{4,}(?:\.\d+)*\/(?:(?!["&'<>])\S)+)\b/i; const styles = ` .doi-enhancer-popup { position: absolute; z-index: 9999; background-color: white; border: 1px solid #ccc; border-radius: 6px; padding: 6px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); font-family: sans-serif; font-size: 13px; max-width: 600px; } .doi-enhancer-popup .doi-header { margin-bottom: 6px; color: #333; word-break: break-word; padding-left: 7px; } .doi-enhancer-popup table { border-collapse: collapse; width: 100%; } .doi-enhancer-popup td { padding: 4px 6px; text-align: center; border-right: 1px solid #eee; white-space: nowrap; } .doi-enhancer-popup td:last-child { border-right: none; } .doi-enhancer-popup a { color: #007bff; text-decoration: none; } .doi-enhancer-popup a:hover { text-decoration: underline; } .doi-enhancer-popup .status-no a { color: #888; } .doi-enhancer-popup .status-checking { color: #999; } `; const styleEl = document.createElement('style'); styleEl.textContent = styles; document.head.appendChild(styleEl); let currentPopup = null; let hideTimeout = null; function httpRequest(details) { return new Promise((resolve, reject) => { GM.xmlHttpRequest({ ...details, timeout: 15000, onload: resolve, onerror: reject, ontimeout: reject, }); }); } function updateLink(cell, text, href, isNo = false) { cell.innerHTML = ''; const link = document.createElement('a'); link.href = href; link.target = '_blank'; link.rel = 'noopener noreferrer'; link.innerHTML = text.replace('[PDF]', '<b>[PDF]</b>').replace('[Maybe]', '<b>[Maybe]</b>'); cell.className = isNo ? 'status-no' : 'status-yes'; cell.appendChild(link); } async function checkGoogleScholar(doi, cell) { const url = GOOGLE_SCHOLAR_URL + encodeURIComponent(doi); try { const res = await httpRequest({ method: 'GET', url }); const doc = new DOMParser().parseFromString(res.responseText, 'text/html'); const gsLink = doc.querySelector('.gs_or_ggsm a'); if (gsLink) updateLink(cell, '[PDF] Google', gsLink.href); else updateLink(cell, '[No] Google', url, true); } catch { updateLink(cell, '[No] Google', url, true); } } async function checkSciHub(doi, cell) { const url = SCIHUB_URL + doi; try { const res = await httpRequest({ method: 'GET', url }); const hasPDF = /iframe|embed/.test(res.responseText); updateLink(cell, hasPDF ? '[PDF] Sci-Hub' : '[No] Sci-Hub', url, !hasPDF); } catch { updateLink(cell, '[No] Sci-Hub', url, true); } } async function checkLibgen(doi, cell) { const url = LIBGEN_SEARCH_URL + encodeURIComponent(doi); try { const res = await httpRequest({ method: 'GET', url }); const doc = new DOMParser().parseFromString(res.responseText, 'text/html'); const linkEl = doc.querySelector('.table.table-striped a[href^="edition.php?id="]'); if (linkEl) { const detailUrl = LIBGEN_URL + linkEl.getAttribute('href'); const detailRes = await httpRequest({ method: 'GET', url: detailUrl }); const detailDoc = new DOMParser().parseFromString(detailRes.responseText, 'text/html'); const hasPDF = !!detailDoc.querySelector('table'); updateLink(cell, hasPDF ? '[PDF] LibGen' : '[No] LibGen', url, !hasPDF); } else { updateLink(cell, '[No] LibGen', url, true); } } catch { updateLink(cell, '[No] LibGen', url, true); } } async function checkAnna(doi, cell, retry = 0) { const checkUrl = ANNA_CHECK_URL + encodeURIComponent(doi); const directUrl = ANNA_SCIDB_URL + doi; try { const res = await httpRequest({ method: 'GET', url: checkUrl }); const text = res.responseText; if (text.includes("Rate limited") && retry < 10) { setTimeout(() => checkAnna(doi, cell, retry + 1), 5000); return; } const doc = new DOMParser().parseFromString(text, 'text/html'); const found = doc.querySelector('.mt-4.uppercase.text-xs.text-gray-500') || [...doc.querySelectorAll('div.text-gray-500')].some(div => div.textContent.includes(doi)); if (found) { const res2 = await httpRequest({ method: 'GET', url: directUrl }); const doc2 = new DOMParser().parseFromString(res2.responseText, 'text/html'); const hasPDF = doc2.querySelector('.pdfViewer, #viewerContainer, iframe[src*="viewer.html?file="]'); updateLink(cell, hasPDF ? '[PDF] Anna' : '[Maybe] Anna', directUrl); } else { updateLink(cell, '[No] Anna', checkUrl, true); } } catch { updateLink(cell, '[No] Anna', checkUrl, true); } } async function checkSciNet(doi, cell) { const url = SCINET_URL + doi; try { const res = await httpRequest({ method: 'GET', url }); const hasPDF = /iframe|pdf|embed/.test(res.responseText); updateLink(cell, hasPDF ? '[PDF] Sci-net' : '[No] Sci-net', url, !hasPDF); } catch { updateLink(cell, '[No] Sci-net', url, true); } } async function checkSemanticScholar(doi, cell) { const apiUrl = `https://api.semanticscholar.org/graph/v1/paper/${encodeURIComponent(doi)}?fields=title,openAccessPdf`; try { const res = await httpRequest({ method: 'GET', url: apiUrl }); const data = JSON.parse(res.responseText); const pdfUrl = data?.openAccessPdf?.url; if (pdfUrl) { updateLink(cell, '[PDF] Semantic', pdfUrl); } else { updateLink(cell, '[No] Semantic', `https://www.semanticscholar.org/search?q=${encodeURIComponent(doi)}`, true); } } catch { updateLink(cell, '[No] Semantic', `https://www.semanticscholar.org/search?q=${encodeURIComponent(doi)}`, true); } } async function checkUnpaywall(doi, cell) { const url = `https://api.unpaywall.org/v2/${encodeURIComponent(doi)}?email=${encodeURIComponent(UNPAYWALL_EMAIL)}`; try { const res = await httpRequest({ method: 'GET', url }); const data = JSON.parse(res.responseText); if (data?.is_oa && data.best_oa_location?.url) { updateLink(cell, '[PDF] Unpaywall', data.best_oa_location.url); } else { updateLink(cell, '[No] Unpaywall', `https://unpaywall.org/`, true); } } catch { updateLink(cell, '[No] Unpaywall', `https://unpaywall.org/`, true); } } async function checkCore(doi, cell) { const url = `https://api.core.ac.uk/v3/search/works/?q=doi:${encodeURIComponent(doi)}`; try { const res = await httpRequest({ method: 'GET', url, headers: { accept: 'application/json' } }); const data = JSON.parse(res.responseText); const result = data?.results?.find(r => r.doi?.toLowerCase() === doi.toLowerCase()); const pdfUrl = result?.downloadUrl; if (pdfUrl) { updateLink(cell, '[PDF] Core', pdfUrl); } else { updateLink(cell, '[No] Core', `https://core.ac.uk/search/?q=${encodeURIComponent(doi)}`, true); } } catch { updateLink(cell, '[No] Core', `https://core.ac.uk/search/?q=${encodeURIComponent(doi)}`, true); } } async function checkEuropePMC(doi, cell) { const url = `https://www.ebi.ac.uk/europepmc/webservices/rest/search?query=DOI:${encodeURIComponent(doi)}&format=json`; try { const res = await httpRequest({ method: 'GET', url }); const data = JSON.parse(res.responseText); const result = data?.resultList?.result?.find(r => r.doi?.toLowerCase() === doi.toLowerCase()); if (result && result.isOpenAccess === 'Y' && result.fullTextIdList?.fullTextId?.length) { const accid = result.fullTextIdList.fullTextId[0]; const pdfUrl = `https://europepmc.org/backend/ptpmcrender.fcgi?accid=${accid}&blobtype=pdf`; updateLink(cell, '[PDF] EuropePMC', pdfUrl); return; } updateLink(cell, '[No] EuropePMC', `https://europepmc.org/search?query=${encodeURIComponent(doi)}`, true); } catch { updateLink(cell, '[No] EuropePMC', `https://europepmc.org/search?query=${encodeURIComponent(doi)}`, true); } } function removeCurrentPopup() { if (currentPopup) { currentPopup.remove(); currentPopup = null; } } async function getDoiFromLink(linkElement) { if (linkElement.dataset.doi) return linkElement.dataset.doi; if (linkElement.dataset.doiFailed) return null; const url = linkElement.href; const keywords = ['doi', 'article', 'journal', 'mdpi', 'abs', 'content', 'abstract', 'pubmed', 'document', 'fulltext', 'research']; if (!keywords.some(k => url.toLowerCase().includes(k))) { linkElement.dataset.doiFailed = 'true'; return null; } const cleanDOI = doi => { const match = doi.match(/^10\.\d{4,}(?:\.\d+)*\/[^\s\/?#]+/); return match ? match[0] : doi.trim(); }; let doi = url.match(/\/doi\/(?:abs|full|pdf)?\/(10\.\d{4,}(?:\.\d+)*\/[^?#]+)/i)?.[1] || url.match(/10\.\d{4,}(?:\.\d+)*\/[^\s"'<>{}[\]|\\^`]+/)?.[0]; if (!doi) { try { const res = await httpRequest({ method: 'GET', url }); const html = res.responseText; const doiLinkMatch = html.match(/<a[^>]+href=["'][^"'>]*doi[^"'>]*["'][^>]*>/gi); if (doiLinkMatch) { for (const tag of doiLinkMatch) { const hrefMatch = tag.match(/href=["']([^"']+)["']/i); if (hrefMatch) { const linkDoi = hrefMatch[1].match(/\b10\.\d{4,}(?:\.\d+)*\/[^\s"'<>{}[\]()]+/i)?.[0]; if (linkDoi) { doi = linkDoi; break; } } } } if (!doi) { doi = html.match(/10\.\d{4,}(?:\.\d+)*\/[^\s"'<>{}[\]()]+/)?.[0]; } } catch (e) { console.warn('Failed to fetch or parse:', url, e); } } if (doi) { linkElement.dataset.doi = cleanDOI(doi); return linkElement.dataset.doi; } else { linkElement.dataset.doiFailed = 'true'; return null; } } function showPopup(linkElement, doi, mouseX, mouseY) { clearTimeout(hideTimeout); removeCurrentPopup(); const popup = document.createElement('div'); popup.className = 'doi-enhancer-popup'; currentPopup = popup; const doiLine = document.createElement('div'); doiLine.className = 'doi-header'; doiLine.textContent = `DOI: ${doi}`; popup.appendChild(doiLine); const table = document.createElement('table'); const row1 = table.insertRow(); const cellGS = row1.insertCell(); cellGS.textContent = '...'; cellGS.className = 'status-checking'; const cellSemantic = row1.insertCell(); cellSemantic.textContent = '...'; cellSemantic.className = 'status-checking'; const cellUnpay = row1.insertCell(); cellUnpay.textContent = '...'; cellUnpay.className = 'status-checking'; const row2 = table.insertRow(); const cellSH = row2.insertCell(); cellSH.textContent = '...'; cellSH.className = 'status-checking'; const cellLG = row2.insertCell(); cellLG.textContent = '...'; cellLG.className = 'status-checking'; const cellAnna = row2.insertCell(); cellAnna.textContent = '...'; cellAnna.className = 'status-checking'; const row3 = table.insertRow(); const cellSci = row3.insertCell(); cellSci.textContent = '...'; cellSci.className = 'status-checking'; const cellCore = row3.insertCell(); cellCore.textContent = '...'; cellCore.className = 'status-checking'; const cellPMC = row3.insertCell(); cellPMC.textContent = '...'; cellPMC.className = 'status-checking'; popup.appendChild(table); checkGoogleScholar(doi, cellGS); checkSemanticScholar(doi, cellSemantic); checkUnpaywall(doi, cellUnpay); checkSciHub(doi, cellSH); checkAnna(doi, cellAnna); checkLibgen(doi, cellLG); checkSciNet(doi, cellSci); checkCore(doi, cellCore); checkEuropePMC(doi, cellPMC); popup.addEventListener('mouseenter', () => clearTimeout(hideTimeout)); popup.addEventListener('mouseleave', () => removeCurrentPopup()); document.body.appendChild(popup); popup.style.top = `${mouseY + 10}px`; popup.style.left = `${mouseX + 10}px`; } document.addEventListener('mouseover', async (event) => { const link = event.target.closest('a'); if (!link || !link.href || link.dataset.doiCheckInProgress) return; if (link.closest('.doi-enhancer-popup')) return; link.dataset.doiCheckInProgress = 'true'; try { const doi = await getDoiFromLink(link); if (doi) { link.addEventListener('mouseenter', (e) => { if (!currentPopup) showPopup(link, doi, e.pageX, e.pageY); }); link.addEventListener('mouseleave', () => { hideTimeout = setTimeout(removeCurrentPopup, 150); }); if (!currentPopup) showPopup(link, doi, event.pageX, event.pageY); } } finally { link.removeAttribute('data-doiCheckInProgress'); } }); })();