Shows Amazon UK prices for books in your Goodreads To Read list
Size
9.0 KB
Version
1.1.1
Created
Oct 16, 2025
Updated
7 days ago
1// ==UserScript==
2// @name Goodreads Amazon UK Price Checker
3// @description Shows Amazon UK prices for books in your Goodreads To Read list
4// @version 1.1.1
5// @match https://www.goodreads.com/review/list/*
6// @icon https://www.gstatic.com/images/branding/searchlogo/ico/favicon.ico
7// @grant GM.xmlhttpRequest
8// @connect amazon.co.uk
9// ==/UserScript==
10(function() {
11 'use strict';
12
13 console.log('Goodreads Amazon UK Price Checker started');
14
15 // Add styles for price display
16 const styles = `
17 .amazon-price-container {
18 margin-top: 5px;
19 font-size: 12px;
20 }
21 .amazon-price {
22 display: inline-block;
23 padding: 4px 8px;
24 background-color: #ff9900;
25 color: #111;
26 border-radius: 3px;
27 font-weight: bold;
28 text-decoration: none;
29 margin-right: 5px;
30 }
31 .amazon-price:hover {
32 background-color: #ffad33;
33 }
34 .amazon-loading {
35 color: #999;
36 font-style: italic;
37 }
38 .amazon-error {
39 color: #c00;
40 font-size: 11px;
41 }
42 .amazon-not-found {
43 color: #666;
44 font-size: 11px;
45 }
46 `;
47
48 TM_addStyle(styles);
49
50 // Function to extract price from Amazon UK page
51 function extractPriceFromAmazonPage(html) {
52 console.log('Extracting price from Amazon page');
53
54 // Try to find the actual selling price (not RRP)
55 // First try: Look for the main price display with a-price-whole
56 const priceWholeMatch = html.match(/<span class="a-price-whole">([^<]+)<\/span>/);
57 if (priceWholeMatch) {
58 let price = priceWholeMatch[1].replace(',', '').replace(/\.$/, '');
59 console.log('Found price from a-price-whole:', price);
60 return parseFloat(price);
61 }
62
63 // Second try: Look for price with data-a-color="base" (actual selling price, not RRP)
64 // This regex looks for a-price elements with data-a-color="base" and extracts the offscreen price
65 const basePriceRegex = /<span class="a-price"[^>]*data-a-color="base"[^>]*>[\s\S]*?<span class="a-offscreen">£([^<]+)<\/span>/;
66 const basePriceMatch = html.match(basePriceRegex);
67 if (basePriceMatch) {
68 const price = parseFloat(basePriceMatch[1].replace(',', ''));
69 console.log('Found price from base color price:', price);
70 return price;
71 }
72
73 // Third try: Look for a-offscreen prices (but skip RRP and strikethrough ones)
74 const offscreenMatches = html.matchAll(/<span class="a-price"[^>]*>[\s\S]*?<span class="a-offscreen">£([^<]+)<\/span>/g);
75 const prices = [];
76 for (const match of offscreenMatches) {
77 const fullMatch = match[0];
78 const priceText = match[1];
79
80 // Skip if it has strike-through or is marked as secondary color (RRP)
81 if (fullMatch.includes('data-a-strike="true"') ||
82 fullMatch.includes('data-a-color="secondary"') ||
83 priceText.includes('RRP')) {
84 continue;
85 }
86
87 const price = parseFloat(priceText.replace(',', ''));
88 if (!isNaN(price) && price > 0) {
89 prices.push(price);
90 }
91 }
92
93 // Return the first valid price found (usually the selling price)
94 if (prices.length > 0) {
95 console.log('Found price from filtered offscreen:', prices[0]);
96 return prices[0];
97 }
98
99 // Fourth try: Generic pound sign pattern
100 const genericMatch = html.match(/£(\d+\.?\d*)/);
101 if (genericMatch) {
102 const price = parseFloat(genericMatch[1].replace(',', ''));
103 console.log('Found price from generic pattern:', price);
104 return price;
105 }
106
107 console.log('No price found in page');
108 return null;
109 }
110
111 // Function to search Amazon UK for a book
112 async function searchAmazonUK(title, author) {
113 console.log(`Searching Amazon UK for: ${title} by ${author}`);
114
115 const searchQuery = encodeURIComponent(`${title} ${author}`);
116 const searchUrl = `https://www.amazon.co.uk/s?k=${searchQuery}&i=stripbooks`;
117
118 try {
119 const response = await GM.xmlhttpRequest({
120 method: 'GET',
121 url: searchUrl,
122 headers: {
123 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
124 }
125 });
126
127 if (response.status !== 200) {
128 console.error('Amazon search failed with status:', response.status);
129 return null;
130 }
131
132 const html = response.responseText;
133
134 // Extract first product link
135 const productLinkMatch = html.match(/href="(\/[^"]*\/dp\/[A-Z0-9]{10}[^"]*)"/);
136 if (!productLinkMatch) {
137 console.log('No product found in search results');
138 return null;
139 }
140
141 let productUrl = productLinkMatch[1];
142 // Clean up the URL
143 productUrl = productUrl.split('/ref=')[0];
144 productUrl = `https://www.amazon.co.uk${productUrl}`;
145
146 console.log('Found product URL:', productUrl);
147
148 // Fetch the product page to get the price
149 const productResponse = await GM.xmlhttpRequest({
150 method: 'GET',
151 url: productUrl,
152 headers: {
153 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
154 }
155 });
156
157 if (productResponse.status !== 200) {
158 console.error('Product page fetch failed with status:', productResponse.status);
159 return null;
160 }
161
162 const price = extractPriceFromAmazonPage(productResponse.responseText);
163
164 if (price) {
165 return {
166 price: price,
167 url: productUrl
168 };
169 }
170
171 return null;
172
173 } catch (error) {
174 console.error('Error searching Amazon:', error);
175 return null;
176 }
177 }
178
179 // Function to add price to a book row
180 async function addPriceToBook(bookRow) {
181 const titleElement = bookRow.querySelector('td.field.title a');
182 const authorElement = bookRow.querySelector('td.field.author a');
183
184 if (!titleElement || !authorElement) {
185 console.log('Could not find title or author for book row');
186 return;
187 }
188
189 const title = titleElement.textContent.trim();
190 const author = authorElement.textContent.trim();
191
192 console.log(`Processing book: ${title} by ${author}`);
193
194 // Create container for price
195 const priceContainer = document.createElement('div');
196 priceContainer.className = 'amazon-price-container';
197 priceContainer.innerHTML = '<span class="amazon-loading">Checking Amazon UK...</span>';
198
199 // Insert after the title
200 const titleCell = bookRow.querySelector('td.field.title .value');
201 titleCell.appendChild(priceContainer);
202
203 // Search Amazon
204 const result = await searchAmazonUK(title, author);
205
206 if (result && result.price) {
207 priceContainer.innerHTML = `
208 <a href="${result.url}" target="_blank" class="amazon-price">
209 £${result.price.toFixed(2)}
210 </a>
211 <span style="font-size: 11px; color: #666;">on Amazon UK</span>
212 `;
213 } else {
214 priceContainer.innerHTML = '<span class="amazon-not-found">Price not found on Amazon UK</span>';
215 }
216 }
217
218 // Function to process all books with delay to avoid rate limiting
219 async function processAllBooks() {
220 const bookRows = document.querySelectorAll('tr.bookalike.review');
221 console.log(`Found ${bookRows.length} books to process`);
222
223 for (let i = 0; i < bookRows.length; i++) {
224 await addPriceToBook(bookRows[i]);
225 // Add delay between requests to avoid overwhelming Amazon
226 if (i < bookRows.length - 1) {
227 await new Promise(resolve => setTimeout(resolve, 2000));
228 }
229 }
230
231 console.log('Finished processing all books');
232 }
233
234 // Initialize when page is ready
235 function init() {
236 console.log('Initializing Amazon UK price checker');
237
238 // Wait a bit for the page to fully load
239 setTimeout(() => {
240 processAllBooks();
241 }, 1000);
242 }
243
244 // Start the extension
245 if (document.readyState === 'loading') {
246 document.addEventListener('DOMContentLoaded', init);
247 } else {
248 init();
249 }
250
251})();