Google Scholar to free PDFs

Adds Sci-Hub, LibGen, Anna's Archive, Sci-net, LibSTC buttons Spacefrontiers to Google Scholar results

// ==UserScript==
// @name         Google Scholar to free PDFs
// @namespace    ScholarToSciHub
// @version      1.15
// @description  Adds Sci-Hub, LibGen, Anna's Archive, Sci-net, LibSTC buttons Spacefrontiers to Google Scholar results
// @author       Bui Quoc Dung
// @match        https://scholar.google.*/*
// @license      AGPL-3.0-or-later
// @grant        GM_xmlhttpRequest
// ==/UserScript==

const SCIHUB_URL = 'https://www.tesble.com/';
const LIBGEN_URL = 'https://libgen.li/index.php?req=';
const ANNA_URL = 'https://annas-archive.org/scidb/';
const ANNA_CHECK_URL = 'https://annas-archive.org/search?index=journals&q=';
const LIBSTC_BASE_URL = 'https://hub.libstc.cc/';
const SCINET_URL = 'https://sci-net.xyz/';
const SPACEFRONTIERS_BASE_URL = 'https://spacefrontiers.org/';
const CROSSREF_URL = 'https://api.crossref.org/works?query.title=';
const DOI_REGEX = /\b(10\.\d{4,}(?:\.\d+)*\/(?:(?!["&'<>])\S)+)\b/gi;

function updateLink(span, textContent, href, isNo = false) {
    const link = document.createElement('a');
    link.textContent = textContent;
    link.href = href;
    link.target = '_blank';
    link.rel = 'noreferrer noopener';
    link.style.fontSize = '15px';
    if (isNo) link.style.color = 'gray';
    link.innerHTML = textContent.replace('[PDF]', '<b>[PDF]</b>').replace('[Chat]', '<b>[Chat]</b>');
    span.replaceWith(link);
}

function fetchDOI(titleLink, callback) {
    GM_xmlhttpRequest({
        method: 'GET',
        url: titleLink.href,
        onload: res => {
            const matches = res.responseText.match(DOI_REGEX);
            if (matches && matches.length) {
                callback(matches[0]);
            } else {
                const title = encodeURIComponent(titleLink.textContent.trim());
                const crossrefUrl = `${CROSSREF_URL}${title}&rows=1`;
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: crossrefUrl,
                    onload: crRes => {
                        try {
                            const data = JSON.parse(crRes.responseText);
                            const items = data.message.items;
                            if (items && items.length && items[0].DOI) {
                                callback(items[0].DOI);
                            } else {
                                callback(null);
                            }
                        } catch (e) {
                            callback(null);
                        }
                    },
                    onerror: () => callback(null)
                });
            }
        },
        onerror: () => callback(null)
    });
}

function addLoadingIndicator(container) {
    const span = document.createElement('div');
    span.textContent = 'Loading...';
    span.style.marginBottom = '4px';
    span.style.color = 'gray';
    span.style.fontSize = '15px';
    container.appendChild(span);
    return span;
}

function checkLibGen(title, doi, span) {
    const encodedTitle = encodeURIComponent(title);
    const encodedDOI = doi ? encodeURIComponent(doi) : null;
    const trySearch = (query, fallback) => {
        GM_xmlhttpRequest({
            method: 'GET',
            url: LIBGEN_URL + query,
            onload: res => {
                const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
                const table = doc.querySelector('.table.table-striped');
                const hasPDF = table && table.querySelector('tbody') && table.querySelector('tbody').children.length > 0;
                if (hasPDF) {
                    updateLink(span, '[PDF] LibGen', LIBGEN_URL + query, false);
                } else {
                    fallback();
                }
            },
            onerror: fallback
        });
    };

    trySearch(encodedTitle, () => {
        if (encodedDOI) {
            trySearch(encodedDOI, () => {
                updateLink(span, '[No] LibGen', LIBGEN_URL + encodedDOI, true);
            });
        } else {
            updateLink(span, '[No] LibGen', LIBGEN_URL + encodedTitle, true);
        }
    });
}


function checkSciHub(href, doi, span) {
    const tryURL = (url, fallback) => {
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            onload: res => {
                const hasPDF = /iframe|pdf|embed/i.test(res.responseText);
                if (hasPDF) {
                    updateLink(span, '[PDF] Sci-Hub', url, false);
                } else {
                    fallback();
                }
            },
            onerror: fallback
        });
    };

    tryURL(SCIHUB_URL + href, () => {
        if (doi) {
            tryURL(SCIHUB_URL + doi, () => {
                tryURL(SCIHUB_URL + doi, () => {
                    updateLink(span, '[No] Sci-Hub', SCIHUB_URL + doi, true);
                });
            });
        } else {
            updateLink(span, '[No] Sci-Hub', SCIHUB_URL + href, true);
        }
    });
}

function checkAnna(doi, span) {
    const checkUrl = ANNA_CHECK_URL + encodeURIComponent(doi);
    GM_xmlhttpRequest({
        method: 'GET',
        url: checkUrl,
        onload: res => {
            const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
            const hasPDF = !!doc.querySelector('.mt-4.uppercase.text-xs.text-gray-500');
            updateLink(span, hasPDF ? '[PDF] Anna' : '[No] Anna', ANNA_URL + doi, !hasPDF);
        },
        onerror: () => updateLink(span, '[No] Anna', ANNA_URL + doi, true)
    });
}

function checkSpaceFrontiers(doi, span) {
    if (!doi) {
        updateLink(span, '[No] Spacefrontiers', '#', true);
        return;
    }

    const checkUrl = SPACEFRONTIERS_BASE_URL + 'r/' + doi;
    const context = encodeURIComponent(JSON.stringify({ uris: [`doi://${doi}`] }));
    const chatLink = SPACEFRONTIERS_BASE_URL + 'c?context=' + context + '&no-auto-search=1';

    GM_xmlhttpRequest({
        method: 'GET',
        url: checkUrl,
        onload: res => {
            const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
            const chatElement = doc.querySelector('span.relative.flex.items-center.gap-2');
            const hasPDF = chatElement && chatElement.textContent.includes('Chat with the Research');

            if (hasPDF) {
                updateLink(span, '[Chat] Spacefrontiers', chatLink, false);
            } else {
                updateLink(span, '[No] Spacefrontiers', checkUrl, true);
            }
        },
        onerror: () => updateLink(span, '[No] Spacefrontiers', checkUrl, true)
    });
}

function checkLibSTC(doi, span) {
    const url = LIBSTC_BASE_URL + doi + '.pdf';
    GM_xmlhttpRequest({
        method: 'HEAD',
        url: url,
        onload: res => {
            const isPDF = res.status === 200 && res.responseHeaders.toLowerCase().includes('application/pdf');
            updateLink(span, isPDF ? '[PDF] LibSTC' : '[No] LibSTC', url, !isPDF);
        },
        onerror: () => updateLink(span, '[No] LibSTC', url, true)
    });
}

function checkSciNet(doi, span) {
    const sciNetUrl = SCINET_URL + doi;
    GM_xmlhttpRequest({
        method: 'GET',
        url: sciNetUrl,
        onload: function(response) {
            const hasPDF = /iframe|pdf|embed/.test(response.responseText);
            updateLink(span, hasPDF ? '[PDF] Sci-net' : '[No] Sci-net', sciNetUrl, !hasPDF);
        },
        onerror: function() {
            updateLink(span, '[No] Sci-net', sciNetUrl, true);
        }
    });
}

function addButtons() {
    document.querySelectorAll('#gs_res_ccl_mid .gs_r.gs_or.gs_scl').forEach(result => {
        const titleLink = result.querySelector('.gs_rt a');
        const yearElement = result.querySelector('.gs_a');
        if (!titleLink || !yearElement) return;

        let buttonContainer = result.querySelector('.gs_or_ggsm');
        if (!buttonContainer) {
            const div = document.createElement('div');
            div.className = 'gs_ggs gs_fl';
            div.innerHTML = '<div class="gs_ggsd"><div class="gs_or_ggsm"></div></div>';
            result.insertBefore(div, result.firstChild);
            buttonContainer = div.querySelector('.gs_or_ggsm');
        }

        if (buttonContainer.classList.contains('scihub-processed')) return;
        buttonContainer.classList.add('scihub-processed');

        const row1 = document.createElement('span');
        row1.style.display = 'inline-flex';
        row1.style.gap = '6px';
        const scihubSpan = addLoadingIndicator(row1);
        const libgenSpan = addLoadingIndicator(row1);
        buttonContainer.appendChild(row1);

        const row2 = document.createElement('span');
        row2.style.display = 'inline-flex';
        row2.style.gap = '6px';
        const annaSpan = addLoadingIndicator(row2);
        const scinetSpan = addLoadingIndicator(row2);
        buttonContainer.appendChild(row2);

        const row3 = document.createElement('span');
        row3.style.display = 'flex';
        row3.style.gap = '6px';
        const libstcSpan = addLoadingIndicator(row3);
        buttonContainer.appendChild(row3);

        const row4 = document.createElement('span');
        row4.style.display = 'flex';
        row4.style.gap = '6px';
        const spacefrontiersSpan = addLoadingIndicator(row4);
        buttonContainer.appendChild(row4);

        fetchDOI(titleLink, (doi) => {
            checkLibGen(titleLink.textContent, doi, libgenSpan);
            checkSciHub(titleLink.href, doi, scihubSpan);

            if (doi) {
                checkAnna(doi, annaSpan);
                checkSciNet(doi, scinetSpan);
                checkLibSTC(doi, libstcSpan);
                checkSpaceFrontiers(doi, spacefrontiersSpan);
            } else {
                updateLink(annaSpan, '[No] Anna', '#', true);
                updateLink(scinetSpan, '[No] Sci-net', '#', true);
                updateLink(libstcSpan, '[No] LibSTC', '#', true);
                updateLink(spacefrontiersSpan, '[No] Spacefrontiers', '#', true);
            }
        });
    });
}

addButtons();

new MutationObserver((mutations) => {
    mutations.forEach((mutation) => mutation.addedNodes.length && addButtons());
}).observe(document.body, {childList: true, subtree: true});