// ==UserScript==
// @name Google Scholar to free PDFs
// @namespace ScholarToSciHub
// @version 1.13
// @description Adds Sci-Hub, LibGen, Anna's Archive, Sci-net, Spacefrontiers, LibSTC buttons 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>');
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 {
// Nếu không tìm thấy DOI trong trang, gọi Crossref API
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 checkLibGenPDF(title, span) {
GM_xmlhttpRequest({
method: 'GET',
url: LIBGEN_URL + encodeURIComponent(title),
onload: res => {
const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
const hasPDF = !!doc.querySelector('.table.table-striped');
updateLink(span, hasPDF ? '[PDF] LibGen' : '[No] LibGen', LIBGEN_URL + encodeURIComponent(title), !hasPDF);
},
onerror: () => updateLink(span, '[No] LibGen', LIBGEN_URL + encodeURIComponent(title), true)
});
}
function checkSciHubPDF(url, span) {
GM_xmlhttpRequest({
method: 'GET',
url: SCIHUB_URL + url,
onload: res => {
const hasPDF = /iframe|pdf|embed/i.test(res.responseText);
updateLink(span, hasPDF ? '[PDF] Sci-Hub' : '[No] Sci-Hub', SCIHUB_URL + url, !hasPDF);
},
onerror: () => updateLink(span, '[No] Sci-Hub', SCIHUB_URL + url, true)
});
}
function checkAnnaPDF(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, '[PDF] Spacefrontiers', checkUrl, false);
} else {
updateLink(span, '[No] Spacefrontiers', checkUrl, true);
}
},
onerror: () => {
updateLink(span, '[No] Spacefrontiers', checkUrl, true);
}
});
}
function checkLibSTCPDF(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 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 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');
// Hàng 1: Sci-Hub + LibGen
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);
// Hàng 2: Anna + SciNet
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);
// Hàng 3: Spacefrontiers (riêng một hàng)
const row3 = document.createElement('span');
row3.style.display = 'flex'; // block kiểu flex
row3.style.gap = '6px';
const spacefrontiersSpan = addLoadingIndicator(row3);
buttonContainer.appendChild(row3);
// Hàng 4: LibSTC (riêng một hàng)
const row4 = document.createElement('span');
row4.style.display = 'flex';
row4.style.gap = '6px';
const libstcSpan = addLoadingIndicator(row4);
buttonContainer.appendChild(row4);
// Kiểm tra PDF và cập nhật nút
checkSciHubPDF(titleLink.href, scihubSpan);
checkLibGenPDF(titleLink.textContent, libgenSpan);
fetchDOI(titleLink, (doi) => {
if (doi) {
checkAnnaPDF(doi, annaSpan);
checkSciNetPDF(doi, scinetSpan);
checkSpaceFrontiers(doi, spacefrontiersSpan); // <-- gọi Spacefrontiers
checkLibSTCPDF(doi, libstcSpan);
} else {
updateLink(annaSpan, '[No] Anna', '#', true);
updateLink(scinetSpan, '[No] Sci-net', '#', true);
updateLink(spacefrontiersSpan, '[No] Spacefrontiers', '#', true); // cập nhật khi không có DOI
updateLink(libstcSpan, '[No] LibSTC', '#', true);
}
});
});
}
addButtons();
new MutationObserver((mutations) => {
mutations.forEach((mutation) => mutation.addedNodes.length && addButtons());
}).observe(document.body, {childList: true, subtree: true});