Paper links to free PDFs

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

2025-07-19 기준 버전입니다. 최신 버전을 확인하세요.

// ==UserScript==
// @name         Paper links to free PDFs
// @namespace    greatest.deepsurf.us
// @version      1.0
// @description  Checks for Sci-Hub, LibGen, Anna's Archive, and Sci-net links only when you hover over a link.
// @author       Bui Quoc Dung
// @match        *://*/*
// @license      AGPL-3.0-or-later
// @grant        GM.xmlHttpRequest
// @connect      *
// ==/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 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: 5px;
            padding: 5px;
            box-shadow: 0 4px 8px rgba(0,0,0,0.1);
            font-family: sans-serif;
            font-size: 13px;
            display: none; /* Ẩn theo mặc định */
        }
        .doi-enhancer-popup table {
            border-collapse: collapse;
        }
        .doi-enhancer-popup td {
            padding: 5px 8px;
            text-align: center;
            white-space: nowrap;
            border-right: 1px solid #eee;
        }
        .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: #888;
            padding: 5px 10px;
        }
    `;
    const styleSheet = document.createElement("style");
    styleSheet.innerText = styles;
    document.head.appendChild(styleSheet);


    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 checkSciHub(doi, cell) {
        const url = SCIHUB_URL + doi;
        try {
            const res = await httpRequest({ method: 'GET', url: url });
            if (/iframe|embed/.test(res.responseText)) {
                updateLink(cell, '[PDF] Sci-Hub', url);
            } else {
                updateLink(cell, '[No] Sci-Hub', url, true);
            }
        } catch {
            updateLink(cell, '[No] Sci-Hub', url, true);
        }
    }

    async function checkLibgen(doi, cell) {
        const query = encodeURIComponent(doi);
        const searchUrl = LIBGEN_SEARCH_URL + query;
        try {
            const res = await httpRequest({ method: 'GET', url: searchUrl });
            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');
                if (hasPDF) {
                    updateLink(cell, '[PDF] LibGen', searchUrl);
                } else {
                    updateLink(cell, '[No] LibGen', searchUrl, true);
                }
            } else {
                updateLink(cell, '[No] LibGen', searchUrl, true);
            }
        } catch (e) {
            console.error('LibGen check failed for DOI:', doi, e);
            updateLink(cell, '[No] LibGen', searchUrl, 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 bodyText = res.responseText;

            if (bodyText.includes("Rate limited") && retry < 10) {
                setTimeout(() => checkAnna(doi, cell, retry + 1), 5000);
                return;
            }

            const doc = new DOMParser().parseFromString(bodyText, '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: url });
            if (/iframe|pdf|embed/.test(res.responseText)) {
                updateLink(cell, '[PDF] Sci-net', url);
            } else {
                updateLink(cell, '[No] Sci-net', url, true);
            }
        } catch {
            updateLink(cell, '[No] Sci-net', url, true);
        }
    }



    function processDoiLink(linkElement) {
        if (linkElement.dataset.doiProcessed) {
            return;
        }
        linkElement.dataset.doiProcessed = 'true';

        const doiMatch = linkElement.href.match(DOI_REGEX);
        if (!doiMatch) return;

        let popup = null;
        let hideTimeout;

        const hidePopup = () => {
            hideTimeout = setTimeout(() => {
                if (popup) popup.style.display = 'none';
            }, 300);
        };

        linkElement.addEventListener('mouseenter', () => {
            clearTimeout(hideTimeout);

            if (!linkElement.dataset.doiChecksInitiated) {
                linkElement.dataset.doiChecksInitiated = 'true';

                const doi = doiMatch[0];

                popup = document.createElement('div');
                popup.className = 'doi-enhancer-popup';
                document.body.appendChild(popup);
                linkElement.popupElement = popup;

                const table = document.createElement('table');
                const tbody = document.createElement('tbody');
                const singleRow = tbody.insertRow();
                table.appendChild(tbody);
                popup.appendChild(table);

                const services = ['Sci-Hub', 'LibGen', 'Anna', 'Sci-net'];
                const cells = {};

                services.forEach(name => {
                    const cell = singleRow.insertCell();
                    cell.textContent = '...';
                    cell.className = 'status-checking';
                    cells[name] = cell;
                });


                checkSciHub(doi, cells['Sci-Hub']);
                checkLibgen(doi, cells['LibGen']);
                checkAnna(doi, cells['Anna']);
                checkSciNet(doi, cells['Sci-net']);


                popup.addEventListener('mouseenter', () => clearTimeout(hideTimeout));
                popup.addEventListener('mouseleave', hidePopup);
            } else {

                popup = linkElement.popupElement;
            }


            const rect = linkElement.getBoundingClientRect();
            popup.style.display = 'block';
            popup.style.top = `${window.scrollY + rect.bottom}px`;
            popup.style.left = `${window.scrollX + rect.left}px`;
        });

        linkElement.addEventListener('mouseleave', hidePopup);
    }

    function scanPageForDoiLinks() {
        const links = document.querySelectorAll('a[href*="doi.org/"]');
        links.forEach(processDoiLink);
    }

    scanPageForDoiLinks();

    const observer = new MutationObserver(scanPageForDoiLinks);
    observer.observe(document.body, { childList: true, subtree: true });

})();