// ==UserScript== // @name Book Links // @namespace http://tampermonkey.net/ // @version 1.0 // @description Show Anna's Archive and Waterstones links when browsing books // @match https://bookshop.org/p/books/* // @match https://www.waterstones.com/book/* // @match https://www.amazon.fr/dp/* // @match https://www.amazon.fr/*/dp/* // @match https://www.amazon.com/dp/* // @match https://www.amazon.com/*/dp/* // @connect annas-archive.gl // @connect www.waterstones.com // @updateURL https://raw.githubusercontent.com/alexture/book-links/main/book-links.user.js // @downloadURL https://raw.githubusercontent.com/alexture/book-links/main/book-links.user.js // @grant GM_xmlhttpRequest // ==/UserScript== (function () { 'use strict'; function isbn10to13(isbn10) { const base = '978' + isbn10.slice(0, 9); let sum = 0; for (let i = 0; i < 12; i++) { sum += parseInt(base[i]) * (i % 2 === 0 ? 1 : 3); } return base + String((10 - (sum % 10)) % 10); } function extractISBN() { const url = window.location.href; const host = window.location.hostname; if (host.includes('waterstones.com')) { const m = url.match(/\/(97[89]\d{10})(?:[/?#]|$)/); return m ? m[1] : null; } if (host.includes('amazon.')) { // Numeric ASIN in URL = ISBN-10 const asinMatch = url.match(/\/dp\/(\d{10})(?:[/?#]|$)/); if (asinMatch) return isbn10to13(asinMatch[1]); const text = document.body?.innerText || ''; // ISBN-13 in product details const m13 = text.match(/ISBN-?13[\s:‎‏ ]+([0-9][-0-9 ]{11,16}[0-9])/); if (m13) { const cleaned = m13[1].replace(/[-\s]/g, ''); if (/^97[89]\d{10}$/.test(cleaned)) return cleaned; } // ISBN-10 in product details const m10 = text.match(/ISBN-?10[\s:‎‏ ]+(\d{9}[\dX])/); if (m10 && /^\d{9}[\dX]$/.test(m10[1])) return isbn10to13(m10[1]); return null; } if (host.includes('bookshop.org')) { const el = document.querySelector('[itemprop="isbn"]'); if (el && /^97[89]\d{10}$/.test(el.textContent.trim())) return el.textContent.trim(); for (const meta of document.querySelectorAll('meta')) { const c = (meta.getAttribute('content') || '').trim(); if (/^97[89]\d{10}$/.test(c)) return c; } const m = (document.body?.innerText || '').match(/\b(97[89]\d{10})\b/); return m ? m[1] : null; } return null; } function extractTitleAuthor() { const host = window.location.hostname; if (host.includes('bookshop.org')) { const title = ( document.querySelector('[itemprop="name"]') || document.querySelector('h1') )?.textContent?.trim(); const author = ( document.querySelector('[itemprop="author"]') || document.querySelector('.book-author a') || document.querySelector('.contributors a') )?.textContent?.trim(); return { title, author }; } if (host.includes('amazon.')) { const title = ( document.querySelector('#productTitle') || document.querySelector('.product-title-word-break') )?.textContent?.trim(); const author = ( document.querySelector('#bylineInfo .author a') || document.querySelector('.contributorNameID') )?.textContent?.trim(); return { title, author }; } if (host.includes('waterstones.com')) { const title = ( document.querySelector('[itemprop="name"]') || document.querySelector('h1') )?.textContent?.trim(); const author = ( document.querySelector('[itemprop="author"]') || document.querySelector('.author-name a') || document.querySelector('.book-authors a') )?.textContent?.trim(); return { title, author }; } return { title: null, author: null }; } function gmFetch(url) { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url, timeout: 8000, onload: (r) => resolve({ ok: r.status >= 200 && r.status < 400, text: r.responseText, finalUrl: r.finalUrl || url, }), onerror: () => resolve({ ok: false, text: '', finalUrl: url }), ontimeout: () => resolve({ ok: false, text: '', finalUrl: url }), }); }); } async function checkAnna(isbn, title, author) { const query = (title && author) ? `${title} ${author}` : isbn; const url = `https://annas-archive.gl/search?q=${encodeURIComponent(query)}`; const res = await gmFetch(url); if (!res.ok) return { found: null, url }; const noResults = /no\s+results|0\s+results/i.test(res.text); if (noResults) return { found: false, url }; // /md5/ followed by a 32-char hex hash = an actual file link, not site chrome const hasFiles = /\/md5\/[0-9a-f]{32}/i.test(res.text); if (hasFiles) return { found: true, url }; return { found: null, url }; } async function checkWaterstones(isbn) { const searchUrl = `https://www.waterstones.com/books/search/term/${isbn}`; const res = await gmFetch(searchUrl); if (!res.ok) return { found: null, url: searchUrl }; // Redirect to a product page means direct hit if (res.finalUrl && /\/book\/[^/]+\/[^/]+\//.test(res.finalUrl)) { return { found: true, url: res.finalUrl }; } const noResults = /no results|didn.t find|0 results found/i.test(res.text); if (noResults) return { found: false, url: searchUrl }; const hasResults = /book-wrap|data-isbn|"@type"\s*:\s*"Book"|class="book/i.test(res.text); if (hasResults) return { found: true, url: searchUrl }; // Search results are JS-rendered — can't determine, show link anyway return { found: null, url: searchUrl }; } function renderRow(label, result) { if (result.found === true) { return `
${label}
`; } if (result.found === false) { return `
✗ ${label} — not found
`; } // null = couldn't check, show link anyway return `
? ${label} (couldn't verify)
`; } function buildWidget() { const div = document.createElement('div'); div.id = '__book-links'; div.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: #16213e; color: #e0e0e0; border: 1px solid #0f3460; border-radius: 8px; padding: 12px 14px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-size: 13px; line-height: 1.75; z-index: 2147483647; max-width: 270px; box-shadow: 0 4px 20px rgba(0,0,0,0.5); `; div.innerHTML = `
Book Links ×
`; document.body.appendChild(div); div.querySelector('#__bl-close').onclick = () => div.remove(); return div; } async function run() { await new Promise(r => setTimeout(r, 800)); const isbn = extractISBN(); const { title, author } = extractTitleAuthor(); const widget = buildWidget(); if (!isbn && !title) { widget.querySelector('#__bl-results').innerHTML = '
Couldn\'t find book info on this page.
'; return; } widget.querySelector('#__bl-isbn').textContent = isbn ? `ISBN ${isbn}` : (title || ''); widget.querySelector('#__bl-results').textContent = 'Checking…'; const isWaterstones = window.location.hostname.includes('waterstones.com'); const checksToRun = [checkAnna(isbn, title, author)]; if (!isWaterstones && isbn) checksToRun.push(checkWaterstones(isbn)); const results = await Promise.all(checksToRun); const annaResult = results[0]; const waterstonesResult = isWaterstones ? null : results[1]; let html = ''; if (!isWaterstones) html += renderRow('Waterstones', waterstonesResult); html += renderRow("Anna's Archive", annaResult); widget.querySelector('#__bl-results').innerHTML = html; } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', run); } else { run(); } })();