Add Chez Mon Libraire, ISBN-first search with title fallback, and result caching

Anna's Archive and Waterstones now search by ISBN first and only fall
back to title/author when that comes back with an explicit no-results,
since a specific edition's ISBN can be unindexed even though the book
exists under another edition. Chez Mon Libraire is added as a third
source (isbn-based search, French no-results/found text patterns).
Results are cached per site+isbn via GM_getValue/GM_setValue for 14
days to cut down on repeat requests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Alex 2026-07-27 19:41:13 +02:00
parent f1cf418f63
commit 9117841bb1

View file

@ -12,9 +12,12 @@
// @match https://www.amazon.com/*/dp/* // @match https://www.amazon.com/*/dp/*
// @connect annas-archive.gl // @connect annas-archive.gl
// @connect www.waterstones.com // @connect www.waterstones.com
// @connect www.chez-mon-libraire.fr
// @updateURL https://raw.githubusercontent.com/alexture/book-links/main/book-links.user.js // @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 // @downloadURL https://raw.githubusercontent.com/alexture/book-links/main/book-links.user.js
// @grant GM_xmlhttpRequest // @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript== // ==/UserScript==
(function () { (function () {
@ -136,8 +139,20 @@
}); });
} }
async function checkAnna(isbn, title, author) { const CACHE_TTL_MS = 14 * 24 * 60 * 60 * 1000; // 14 days
const query = (title && author) ? `${title} ${author}` : isbn;
async function withCache(key, fn) {
const entry = GM_getValue(key);
if (entry && Date.now() - entry.ts < CACHE_TTL_MS) return entry.value;
const value = await fn();
// Only cache confident answers — retry uncertain ones (network error,
// timeout, JS-rendered page) next visit instead of freezing them in.
if (value.found !== null) GM_setValue(key, { value, ts: Date.now() });
return value;
}
async function checkAnnaQuery(query) {
const url = `https://annas-archive.gl/search?q=${encodeURIComponent(query)}`; const url = `https://annas-archive.gl/search?q=${encodeURIComponent(query)}`;
const res = await gmFetch(url); const res = await gmFetch(url);
if (!res.ok) return { found: null, url }; if (!res.ok) return { found: null, url };
@ -152,8 +167,21 @@
return { found: null, url }; return { found: null, url };
} }
async function checkWaterstones(isbn) { async function checkAnna(isbn, title, author) {
const searchUrl = `https://www.waterstones.com/books/search/term/${isbn}`; if (isbn) {
const byIsbn = await checkAnnaQuery(isbn);
// A specific edition's ISBN may be unindexed even though the book exists
// under another edition — retry by title/author before calling it not found.
if (byIsbn.found === false && title && author) {
return checkAnnaQuery(`${title} ${author}`);
}
return byIsbn;
}
return checkAnnaQuery(`${title} ${author}`);
}
async function checkWaterstonesQuery(query) {
const searchUrl = `https://www.waterstones.com/books/search/term/${encodeURIComponent(query)}`;
const res = await gmFetch(searchUrl); const res = await gmFetch(searchUrl);
if (!res.ok) return { found: null, url: searchUrl }; if (!res.ok) return { found: null, url: searchUrl };
@ -172,6 +200,36 @@
return { found: null, url: searchUrl }; return { found: null, url: searchUrl };
} }
async function checkWaterstones(isbn, title, author) {
const byIsbn = await checkWaterstonesQuery(isbn);
if (byIsbn.found === false && title && author) {
return checkWaterstonesQuery(`${title} ${author}`);
}
return byIsbn;
}
async function checkChezMonLibraireQuery(query) {
const searchUrl = `https://www.chez-mon-libraire.fr/listeliv.php?base=allbooks&mots_recherche=${encodeURIComponent(query)}`;
const res = await gmFetch(searchUrl);
if (!res.ok) return { found: null, url: searchUrl };
const noResults = /n.avons pas trouv/i.test(res.text);
if (noResults) return { found: false, url: searchUrl };
const hasResults = /\/livre\/\d/.test(res.text) || /produit[s]?\s+trouv/i.test(res.text);
if (hasResults) return { found: true, url: searchUrl };
return { found: null, url: searchUrl };
}
async function checkChezMonLibraire(isbn, title, author) {
const byIsbn = await checkChezMonLibraireQuery(isbn);
if (byIsbn.found === false && title && author) {
return checkChezMonLibraireQuery(`${title} ${author}`);
}
return byIsbn;
}
function renderRow(label, result) { function renderRow(label, result) {
if (result.found === true) { if (result.found === true) {
return `<div>✓ <a href="${result.url}" target="_blank" style="color:#7eb8f7;text-decoration:none">${label}</a></div>`; return `<div>✓ <a href="${result.url}" target="_blank" style="color:#7eb8f7;text-decoration:none">${label}</a></div>`;
@ -232,15 +290,26 @@
widget.querySelector('#__bl-results').textContent = 'Checking…'; widget.querySelector('#__bl-results').textContent = 'Checking…';
const isWaterstones = window.location.hostname.includes('waterstones.com'); const isWaterstones = window.location.hostname.includes('waterstones.com');
const checksToRun = [checkAnna(isbn, title, author)]; const cacheKeyFor = (site) => `${site}:${isbn || `${title}|${author}`}`;
if (!isWaterstones && isbn) checksToRun.push(checkWaterstones(isbn)); const checksToRun = [
withCache(cacheKeyFor('anna'), () => checkAnna(isbn, title, author)),
];
if (!isWaterstones && isbn) {
checksToRun.push(withCache(cacheKeyFor('waterstones'), () => checkWaterstones(isbn, title, author)));
}
if (isbn) {
checksToRun.push(withCache(cacheKeyFor('chezmonlibraire'), () => checkChezMonLibraire(isbn, title, author)));
}
const results = await Promise.all(checksToRun); const results = await Promise.all(checksToRun);
const annaResult = results[0]; let idx = 0;
const waterstonesResult = isWaterstones ? null : results[1]; const annaResult = results[idx++];
const waterstonesResult = (!isWaterstones && isbn) ? results[idx++] : null;
const chezMonLibraireResult = isbn ? results[idx++] : null;
let html = ''; let html = '';
if (!isWaterstones) html += renderRow('Waterstones', waterstonesResult); if (waterstonesResult) html += renderRow('Waterstones', waterstonesResult);
if (chezMonLibraireResult) html += renderRow('Chez Mon Libraire', chezMonLibraireResult);
html += renderRow("Anna's Archive", annaResult); html += renderRow("Anna's Archive", annaResult);
widget.querySelector('#__bl-results').innerHTML = html; widget.querySelector('#__bl-results').innerHTML = html;