// ==UserScript== // @name Book Links // @namespace http://tampermonkey.net/ // @version 1.1 // @description Show Anna's Archive and Waterstones links when browsing books // @license MIT // @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 `