// ==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 });
})();