Google Scholar to free PDFs

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

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

// Define base URLs for different sources
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/';

// Function to add a loading indicator to the button container
function addLoadingIndicator(buttonContainer) {
    const span = document.createElement('div');
    span.textContent = 'Loading...';
    span.style.marginBottom = '4px';
    span.style.color = 'gray';
    span.style.fontSize = '15px';
    buttonContainer.appendChild(span);
    return span;
}

// Function to update the loading indicator with a clickable link
function updateLink(span, textContent, href, isNo = false) {
    const link = document.createElement('a');
    link.textContent = textContent;
    link.href = href;
    link.target = '_blank';
    link.style.fontSize = '15px';
    link.innerHTML = textContent.replace('[PDF]', '<b>[PDF]</b>')
    if (isNo) {
        link.style.color = 'gray';
    }
    span.replaceWith(link);
}

// Function to extract the year from a given element
function extractYear(element) {
    const yearMatch = element.textContent.match(/\d{4}/);
    return yearMatch ? parseInt(yearMatch[0]) : 0;
}

// Function to check if a PDF is available on LibGen
function checkLibGenPDF(title, span) {
    GM_xmlhttpRequest({
        method: 'GET',
        url: LIBGEN_URL + encodeURIComponent(title),
        onload: function(response) {
            const parser = new DOMParser();
            const doc = parser.parseFromString(response.responseText, 'text/html');
            const hasTable = doc.querySelector('.table.table-striped') !== null;
            updateLink(span, hasTable ? '[PDF] LibGen' : '[No] LibGen', LIBGEN_URL + encodeURIComponent(title), !hasTable);
        },
        onerror: function() {
            updateLink(span, '[No] LibGen', LIBGEN_URL + encodeURIComponent(title), true);
        }
    });
}

// Function to check if a PDF is available on Sci-Hub
function checkSciHubPDF(url, span) {
    GM_xmlhttpRequest({
        method: 'GET',
        url: SCIHUB_URL + url,
        onload: function(response) {
            const hasPDF = /iframe|pdf|embed/.test(response.responseText);
            updateLink(span, hasPDF ? '[PDF] Sci-Hub' : '[No] Sci-Hub', SCIHUB_URL + url, !hasPDF);
        },
        onerror: function() {
            updateLink(span, '[No] Sci-Hub', SCIHUB_URL + url, true);
        }
    });
}

// Function to check if a PDF is available on Anna's Archive
function checkAnnaPDF(doi, span) {
    const checkUrl = ANNA_CHECK_URL + encodeURIComponent(doi);
    GM_xmlhttpRequest({
        method: 'GET',
        url: checkUrl,
        onload: function(response) {
            const parser = new DOMParser();
            const doc = parser.parseFromString(response.responseText, 'text/html');
            const hasPDF = doc.querySelector('.mt-4.uppercase.text-xs.text-gray-500') !== null;
            updateLink(span, hasPDF ? '[PDF] Anna' : '[No] Anna', ANNA_URL + doi, !hasPDF);
        },
        onerror: function() {
            updateLink(span, '[No] Anna', ANNA_URL + doi, true);
        }
    });
}

// Function to check if a PDF is available on LibSTC
function checkLibSTCPDF(doi, span) {
    const pdfURL = LIBSTC_BASE_URL + doi + '.pdf';
    GM_xmlhttpRequest({
        method: 'HEAD',
        url: pdfURL,
        onload: function(response) {
            const isPDF = response.status === 200 && response.responseHeaders.toLowerCase().includes('application/pdf');
            updateLink(span, isPDF ? '[PDF] LibSTC' : '[No] LibSTC', pdfURL, !isPDF);
        },
        onerror: function() {
            updateLink(span, '[No] LibSTC', pdfURL, true);
        }
    });
}

// Function to check if a PDF is available on Sci-net
function checkSciNetPDF(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 to fetch DOI from a given article link
function fetchDOI(titleLink, callback) {
    const title = encodeURIComponent(titleLink.textContent.trim());
    const apiUrl = `https://api.crossref.org/works?query=${title}&rows=1`;

    GM_xmlhttpRequest({
        method: 'GET',
        url: apiUrl,
        headers: {
            'Accept': 'application/json'
        },
        onload: function(response) {
            try {
                const data = JSON.parse(response.responseText);
                const items = data.message.items;
                const doi = items && items.length ? items[0].DOI : null;
                callback(doi);
            } catch (e) {
                console.error('Error parsing CrossRef response:', e);
                callback(null);
            }
        },
        onerror: function() {
            callback(null);
        }
    });
}

// Function to add buttons to each Google Scholar result
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');

            // Add loading indicators for all services immediately
            const scihubSpan = addLoadingIndicator(buttonContainer);
            const libgenSpan = addLoadingIndicator(buttonContainer);
            const annaSpan = addLoadingIndicator(buttonContainer);
            const scinetSpan = addLoadingIndicator(buttonContainer);
            const libstcSpan = extractYear(yearElement) > 2020 ? addLoadingIndicator(buttonContainer) : null;

            // Start checking services that don't need DOI
            checkSciHubPDF(titleLink.href, scihubSpan);
            checkLibGenPDF(titleLink.textContent, libgenSpan);

            // Fetch DOI and then check services that need it
            fetchDOI(titleLink, (doi) => {
                if (doi) {
                    checkAnnaPDF(doi, annaSpan);
                    checkSciNetPDF(doi, scinetSpan);
                    if (libstcSpan) {
                        checkLibSTCPDF(doi, libstcSpan);
                    }
                } else {
                    updateLink(annaSpan, '[No] Anna', '#', true);
                    updateLink(scinetSpan, '[No] Sci-net', '#', true);
                    if (libstcSpan) updateLink(libstcSpan, '[No] LibSTC', '#', true);
                }
            });
        }
    });
}

// Initial call to add buttons to existing results
addButtons();

// Observe changes in the page to dynamically add buttons when new results appear
new MutationObserver((mutations) => {
    mutations.forEach((mutation) => mutation.addedNodes.length && addButtons());
}).observe(document.body, {childList: true, subtree: true});