// ==UserScript==
// @name Google Scholar to free PDFs
// @namespace ScholarToSciHub
// @version 1.7
// @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 LIBSTC_BASE_URL = 'https://hub.libstc.cc/';
const ANNA_URL = 'https://annas-archive.org/scidb/';
const ANNA_CHECK_URL = 'https://annas-archive.org/search?index=journals&q=';
// Regular expression to extract DOI from text
const DOI_REGEX = /\b(10\.\d{4,}(?:\.\d+)*\/(?:(?!["&'<>])\S)+)\b/gi;
// 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'; // Màu giống với "Loading..."
}
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, buttonContainer) {
const span = addLoadingIndicator(buttonContainer);
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, buttonContainer) {
const span = addLoadingIndicator(buttonContainer);
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, buttonContainer) {
const span = addLoadingIndicator(buttonContainer);
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, buttonContainer) {
const span = addLoadingIndicator(buttonContainer);
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 fetch DOI from a given article link
function fetchDOI(titleLink, callback) {
GM_xmlhttpRequest({
method: 'GET',
url: titleLink.href,
onload: function(response) {
const matches = response.responseText.match(DOI_REGEX);
callback(matches && matches.length ? matches[0] : 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');
checkSciHubPDF(titleLink.href, buttonContainer);
checkLibGenPDF(titleLink.textContent, buttonContainer);
fetchDOI(titleLink, (doi) => {
if (doi) {
checkAnnaPDF(doi, buttonContainer);
if (extractYear(yearElement) > 2020) {
checkLibSTCPDF(doi, buttonContainer);
}
}
});
}
});
}
// 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});