Paper links to free PDFs

Checks for Google Scholar, Sci-Hub, LibGen, Anna's Archive, Sci-net links only when you hover over a link.

当前为 2025-07-19 提交的版本,查看 最新版本

// ==UserScript==
// @name         Paper links to free PDFs
// @namespace    gf.qytechs.cn
// @version      1.2
// @description  Checks for Google Scholar, Sci-Hub, LibGen, Anna's Archive, Sci-net 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.li/';
    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 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] GoogleScholar', gsLink.href);
            } else {
                updateLink(cell, '[No] GoogleScholar', url, true);
            }
        } catch {
            updateLink(cell, '[No] GoogleScholar', 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);
        }
    }


    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;
        let doi = null;

        if (url.includes('doi.org/')) {
            const match = url.match(DOI_REGEX);
            if (match) doi = match[0];
        }

        if (!doi) {
            try {
                const res = await httpRequest({ method: 'GET', url });
                const match = res.responseText.match(DOI_REGEX);
                if (match) doi = match[0].replace(/\/(full\/html|full|pdf|epdf|abs|abstract)$/i, '');
            } catch {}
        }

        if (doi) linkElement.dataset.doi = doi;
        else linkElement.dataset.doiFailed = 'true';

        return doi;
    }

    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.colSpan = 2;
        cellGS.textContent = '...';
        cellGS.className = 'status-checking';

        const row2 = table.insertRow();
        const cellSciHub = row2.insertCell();
        cellSciHub.textContent = '...';
        cellSciHub.className = 'status-checking';
        const cellLibGen = row2.insertCell();
        cellLibGen.textContent = '...';
        cellLibGen.className = 'status-checking';

        const row3 = table.insertRow();
        const cellAnna = row3.insertCell();
        cellAnna.textContent = '...';
        cellAnna.className = 'status-checking';
        const cellSciNet = row3.insertCell();
        cellSciNet.textContent = '...';
        cellSciNet.className = 'status-checking';

        popup.appendChild(table);

        checkGoogleScholar(doi, cellGS);
        checkSciHub(doi, cellSciHub);
        checkLibgen(doi, cellLibGen);
        checkAnna(doi, cellAnna);
        checkSciNet(doi, cellSciNet);

        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');
        }
    });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址