// ==UserScript==
// @name Amazon Kindle Deals Goodreads Ratings (Per Section)
// @license MIT-0
// @namespace http://tampermonkey.net/
// @version 2.5.2
// @description Add Goodreads ratings to Amazon Kindle deals page for specific sections with highlighting
// @match https://www.amazon.com/*
// @grant GM_xmlhttpRequest
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// Configurable variables
let debugMode = true;
// Rating thresholds and colors
const highRatingThresholds = [4.00, 4.30, 4.50];
const highRatingColors = ['#e6ffe6', '#ccffcc', '#99ff99']; // Light to dark green
const lowRatingThresholds = [3.60, 3.30];
const lowRatingColors = ['#ffe6e6', '#ffcccc']; // Light to medium red
// Review count thresholds and colors
const highRatingsCountThresholds = [1000, 5000, 10000];
const highRatingsCountColors = ['#e6ffe6', '#ccffcc', '#99ff99']; // Light to dark green
const lowRatingsCountThresholds = [100, 10];
const lowRatingsCountColors = ['#ffe6e6', '#ffcccc']; // Light to medium red
const longTitleLength = 42;
let bookData = [];
let processedASINs = new Set();
let linksToProcess = [];
let currentLinkIndex = 0;
let isPaused = false;
let isProcessing = false;
function log(message) {
if (debugMode) {
console.log(`[Goodreads Ratings Debug]: ${message}`);
}
}
function decodeHTMLEntities(text) {
const textArea = document.createElement('textarea');
textArea.innerHTML = text;
return textArea.value;
}
function getASIN(url) {
const match = url.match(/\/([A-Z0-9]{10})(?:\/|\?|$)/);
return match ? match[1] : null;
}
function extractYear(text) {
const yearRegex = /\b\d{4}\b/; // Regular expression to match a four-digit year
const match = text.match(yearRegex);
return match ? parseInt(match[0]) : '-';
}
function extractNumberOfPages(text) {
const pagesRegex = /(\d{1,3}(?:,\d{3})*)\s*(?:page|pages)/i; // This regex looks for one or more digits (with optional commas) followed by "page" or "pages"
const match = text.match(pagesRegex);
return match ? parseInt(match[1].replace(/,/g, ''), 10) : '-';
}
function extractLastNumber(str) {
// First, try to find a number after '#'
const hashMatch = str.match(/#\s*(\d+)\s*$/);
if (hashMatch) {
return parseInt(hashMatch[1]);
}
// If no '#' found, find the last number in the string
const allNumbers = str.match(/\d+/g);
if (allNumbers) {
return parseInt(allNumbers[allNumbers.length - 1]);
}
// If no numbers found at all
return null;
}
function isShelved(container) {
const buttons = container.querySelectorAll('button');
return Array.from(buttons).some(button => {
const label = button.getAttribute('aria-label');
return label && label.includes('Shelved');
});
}
function getLiteraryAwards(doc) {
if (doc) {
const scripts = doc.getElementsByTagName('script');
let awardsData = null;
// Iterate through scripts to find the one containing awards data
for (let script of scripts) {
const content = script.textContent || script.innerText;
if (content.includes('"awards":')) {
// This script likely contains our data
const match = content.match(/"awards":\s*"([^"]*)"/);
if (match) {
try {
awardsData = match[1];
break;
} catch (e) {
console.error("Error parsing awards data:", e);
}
}
}
}
return awardsData;
} else {
return null;
}
}
function getUniqueBookLinks(container) {
const uniqueLinks = [];
const seenHrefs = new Set();
// Kindle Deals page
const sections = container.querySelectorAll('div[data-testid="asin-face"], .ubf-book-info');
sections.forEach(asinFace => {
const link = asinFace.querySelector('a');
if (link && !seenHrefs.has(link.href)) {
seenHrefs.add(link.href);
uniqueLinks.push(link);
}
});
// Regular Kindle page
const bookFaceouts = container.querySelectorAll('bds-unified-book-faceout');
bookFaceouts.forEach(faceout => {
const shadowRoot = faceout.shadowRoot;
if (shadowRoot) {
const link = shadowRoot.querySelector('a');
if (link && !seenHrefs.has(link.href)) {
seenHrefs.add(link.href);
uniqueLinks.push(link);
}
}
});
log(`Unique link count: ${uniqueLinks.length}`);
return uniqueLinks;
}
function fetchGoodreadsData(asin) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (isPaused) {
resolve(null);
return;
}
log(`Fetching data for ASIN: ${asin}`);
GM_xmlhttpRequest({
method: "GET",
url: `https://www.goodreads.com/book/isbn/${asin}`,
onload: function(response) {
log(`Received response for ASIN: ${asin}`);
const parser = new DOMParser();
const doc = parser.parseFromString(response.responseText, "text/html");
// log(doc.documentElement.outerHTML);
const h1Element = doc.querySelector('h1[data-testid="bookTitle"]');
const metadataElement = doc.querySelector('.RatingStatistics__rating');
if (!h1Element || !metadataElement) {
log(`No results found for ASIN: ${asin}`);
resolve(null);
return;
}
const fullTitle = h1Element.textContent.trim();
const title = fullTitle.length > longTitleLength ? fullTitle.slice(0, longTitleLength) + '...' : fullTitle;
const rating = metadataElement.textContent.trim().replace(/\s*stars/, '');
const ratingsCountElement = doc.querySelector('[data-testid="ratingsCount"]');
const ratingsCount = ratingsCountElement ? ratingsCountElement.textContent.trim().split(' ')[0] : '0';
const reviewsCountElement = doc.querySelector('[data-testid="reviewsCount"]');
const reviewsCount = reviewsCountElement ? reviewsCountElement.textContent.trim().split(' ')[0] : '0';
// Is this part of a series?
const seriesElement = doc.querySelector('.BookPageTitleSection h3 a');
const series = seriesElement ? seriesElement.textContent.trim() : null;
const seriesLink = seriesElement ? seriesElement.href : null;
// Show a small image of the cover
const coverImageElement = doc.querySelector('.BookCover__image img');
const coverImage = coverImageElement ? coverImageElement.src : 'https://dryofg8nmyqjw.cloudfront.net/images/no-cover.png';
// Extract the first author
const authorElement = doc.querySelector('span.ContributorLink__name');
const author = authorElement ? authorElement.textContent.trim() : '-';
// Extract the first genre
const genreElement = doc.querySelector('.BookPageMetadataSection__genreButton a');
const genre = genreElement ? genreElement.textContent.trim() : '-';
// Extract the publication year
const publicationElement = doc.querySelector('p[data-testid="publicationInfo"]');
const publicationYear = publicationElement ? extractYear(publicationElement.textContent.trim()) : '';
// Extract the number of pages
const pagesElement = doc.querySelector('p[data-testid="pagesFormat"]');
const numberOfPages = pagesElement ? extractNumberOfPages(pagesElement.textContent.trim()) : '';
// Is it on a shelf already?
const actionsElement = doc.querySelector('.BookPageMetadataSection__mobileBookActions');
const onShelf = isShelved(actionsElement);
const awards = getLiteraryAwards(doc);
const data = {
asin: asin,
coverImage: coverImage,
title: title || "Unknown Title",
fullTitle: fullTitle,
longTitle: fullTitle.length > longTitleLength,
author: author,
series: series,
seriesLink: seriesLink,
rating: rating,
ratingsCount: ratingsCount,
reviewsCount: reviewsCount,
genre: genre,
goodreadsUrl: `https://www.goodreads.com/book/isbn/${asin}`,
publicationYear: publicationYear,
numberOfPages: numberOfPages,
onShelf: onShelf,
awards: awards
};
log(`Parsed data for ${data.title}: ${JSON.stringify(data)}`);
resolve(data);
},
onerror: function(error) {
log(`Error fetching data for ASIN: ${asin}`);
reject(error);
}
});
}, 250);
});
}
function getRatingColor(rating) {
rating = parseFloat(rating);
for (let i = highRatingThresholds.length - 1; i >= 0; i--) {
if (rating >= highRatingThresholds[i]) {
return highRatingColors[i];
}
}
for (let i = lowRatingThresholds.length - 1; i >= 0; i--) {
if (rating < lowRatingThresholds[i]) {
return lowRatingColors[i];
}
}
return '';
}
function getRatingsCountColor(count) {
count = parseInt(count.replace(/,/g, ''));
for (let i = highRatingsCountThresholds.length - 1; i >= 0; i--) {
if (count >= highRatingsCountThresholds[i]) {
return highRatingsCountColors[i];
}
}
for (let i = lowRatingsCountThresholds.length - 1; i >= 0; i--) {
if (count < lowRatingsCountThresholds[i]) {
return lowRatingsCountColors[i];
}
}
return '';
}
function addUIElement(books, isLoading = false) {
let container = document.getElementById('goodreads-ratings');
if (!container) {
container = document.createElement('div');
container.id = 'goodreads-ratings';
container.style.position = 'fixed';
container.style.top = '10px';
container.style.right = '10px';
container.style.backgroundColor = 'white';
container.style.padding = '10px';
container.style.border = '1px solid black';
container.style.zIndex = '9999';
container.style.maxHeight = '80vh';
container.style.overflowY = 'auto';
document.body.appendChild(container);
}
// Clear previous content
container.innerHTML = '';
const headerContainer = document.createElement('div');
headerContainer.style.display = 'flex';
headerContainer.style.justifyContent = 'space-between';
headerContainer.style.alignItems = 'center';
headerContainer.style.marginBottom = '10px';
const title = document.createElement('h3');
title.textContent = 'Goodreads Ratings';
title.style.margin = '0';
headerContainer.appendChild(title);
// Add pause/resume button only if there are links to process
if (linksToProcess.length > 0) {
const pauseResumeButton = document.createElement('button');
pauseResumeButton.textContent = isPaused ? 'Resume' : 'Pause';
pauseResumeButton.addEventListener('click', togglePauseResume);
headerContainer.appendChild(pauseResumeButton);
}
container.appendChild(headerContainer);
const statusMessage = document.createElement('p');
if (isPaused) {
statusMessage.textContent = 'Processing paused';
} else if (isLoading) {
statusMessage.textContent = `Processing... (${currentLinkIndex} of ${linksToProcess.length} books processed)`;
} else if (books.length === 0) {
statusMessage.textContent = 'No books processed yet';
} else {
statusMessage.textContent = 'Processing finished!';
}
container.appendChild(statusMessage);
const table = document.createElement('table');
table.style.borderCollapse = 'collapse';
table.style.width = '100%';
//table.style.tableLayout = 'fixed'; // This helps maintain consistent column widths
// Add a style for all cells
const cellStyle = `
border: 1px solid gray;
padding: 5px;
vertical-align: middle;
`;
// Create table header
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
['Cover', 'Title', 'Author', 'Price', 'Rating', 'Rating Count', 'Review Count', 'Genre', 'Year', 'Pages'].forEach(headerText => {
const th = document.createElement('th');
th.textContent = headerText;
th.style.cssText = cellStyle + `
font-weight: bold;
background-color: #f2f2f2;
`;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// Create table body
const tbody = document.createElement('tbody');
books.forEach(book => {
if (book) {
const row = document.createElement('tr');
// Cover image cell
const coverCell = document.createElement('td');
coverCell.style.cssText = cellStyle + `
text-align: center;
width: 30px;
`;
const coverImg = document.createElement('img');
coverImg.src = book.coverImage;
coverImg.alt = `${book.title} cover`;
coverImg.style.width = '28px';
coverImg.style.height = 'auto';
coverImg.style.display = 'block';
coverImg.style.margin = '0 auto'; // Centers the image horizontally
coverImg.onerror = function() {
this.onerror = null;
this.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=';
this.alt = 'Cover not available';
};
coverCell.appendChild(coverImg);
row.appendChild(coverCell);
// Title cell
const titleCell = document.createElement('td');
if (book.series) {
const seriesNumber = extractLastNumber(book.series);
const seriesLink = document.createElement('a');
seriesLink.href = book.seriesLink;
seriesLink.target = '_blank';
seriesLink.textContent = seriesNumber ? `📚${seriesNumber}` : '📚';
seriesLink.title = book.series;
titleCell.appendChild(seriesLink);
// Add small span separator
const span = document.createElement('span');
span.textContent = ' | ';
titleCell.appendChild(span);
}
const link = document.createElement('a');
link.href = book.goodreadsUrl;
link.target = '_blank';
link.textContent = book.onShelf ? `⭐ ${book.title}` : book.title;
if (book.longTitle) {
link.title = decodeHTMLEntities(book.fullTitle);
}
titleCell.appendChild(link);
titleCell.style.cssText = cellStyle;
row.appendChild(titleCell);
// Author cell
const authorCell = document.createElement('td');
authorCell.textContent = book.author;
authorCell.style.cssText = cellStyle;
row.appendChild(authorCell);
// Price cell
const priceCell = document.createElement('td');
const priceLink = document.createElement('a');
priceLink.href = `https://www.amazon.com/dp/${book.asin}`;
priceLink.target = '_blank';
priceLink.textContent = book.price.replace(/^(?!\$)/, '$') || 'N/A'; // Add leading $ sign
if (book.awards) {
priceLink.textContent = `🏅 ${priceLink.textContent}`;
priceLink.title = decodeHTMLEntities(book.awards);
}
priceCell.appendChild(priceLink);
priceCell.style.cssText = cellStyle + 'text-align: right;';
row.appendChild(priceCell);
// Rating cell
const ratingCell = document.createElement('td');
ratingCell.textContent = book.rating;
ratingCell.style.cssText = cellStyle + `
text-align: right;
background-color: ${getRatingColor(book.rating)};
`;
row.appendChild(ratingCell);
// Ratings count cell
const ratingsCountCell = document.createElement('td');
ratingsCountCell.textContent = book.ratingsCount;
ratingsCountCell.style.cssText = cellStyle + `
text-align: right;
background-color: ${getRatingsCountColor(book.ratingsCount)};
`;
row.appendChild(ratingsCountCell);
// Reviews count cell
const reviewsCountCell = document.createElement('td');
reviewsCountCell.textContent = book.reviewsCount;
reviewsCountCell.style.cssText = cellStyle + 'text-align: right;';
row.appendChild(reviewsCountCell);
// Genre cell
const genreCell = document.createElement('td');
genreCell.textContent = book.genre;
genreCell.style.cssText = cellStyle;
row.appendChild(genreCell);
// Genre cell
const publicationCell = document.createElement('td');
publicationCell.textContent = book.publicationYear;
publicationCell.style.cssText = cellStyle;
row.appendChild(publicationCell);
// Pages cell
const pagesCell = document.createElement('td');
pagesCell.textContent = book.numberOfPages.toLocaleString('en-US');
pagesCell.style.cssText = cellStyle + 'text-align: right;';
row.appendChild(pagesCell);
tbody.appendChild(row);
}
});
table.appendChild(tbody);
container.appendChild(table);
}
function addBookAndSort(newBook) {
if (newBook && !processedASINs.has(newBook.asin)) {
bookData.push(newBook);
processedASINs.add(newBook.asin);
bookData.sort((a, b) => parseFloat(b.rating) - parseFloat(a.rating));
addUIElement(bookData, linksToProcess.length > currentLinkIndex);
}
}
async function processBooks() {
while (currentLinkIndex < linksToProcess.length && !isPaused) {
const link = linksToProcess[currentLinkIndex];
const asin = getASIN(link.href);
currentLinkIndex++;
if (asin && !processedASINs.has(asin)) {
try {
log(`---- Processing book ${currentLinkIndex} of ${linksToProcess.length} ----`);
const data = await fetchGoodreadsData(asin);
if (data) {
// Kindle Deals pages
const asinFace = link.closest('[data-testid="asin-face"]');
if (asinFace) {
const priceElement = asinFace && asinFace.querySelector('[data-testid="price"]');
if (priceElement) {
const priceTextContent = priceElement.textContent;
const priceMatch = priceTextContent.match(/Deal price: \$(\d+\.\d+)/);
data.price = priceMatch ? priceMatch[1] : 'N/A';
}
}
// Regular Kindle page
if (!data.price) {
const sibling = link.nextElementSibling;
if (sibling) {
const bookPrice = sibling.querySelector('bds-book-price');
if (bookPrice) {
// data.price = bookPrice.getAttribute('unstylizedprice'); -- old way
const shadowRoot = bookPrice.shadowRoot;
if (shadowRoot) {
const priceDiv = shadowRoot.querySelector('.offscreen'); // .price -> .offscreen
if (priceDiv) {
data.price = priceDiv.textContent.trim();
}
}
}
}
}
if (!data.price) {
data.price = 'N/A';
}
addBookAndSort(data);
}
} catch (error) {
console.error('Error fetching Goodreads data:', error);
}
}
}
if (currentLinkIndex >= linksToProcess.length) {
log('All books processed');
addUIElement(bookData, false);
}
}
function togglePauseResume() {
isPaused = !isPaused;
if (!isPaused) {
processBooks();
}
addUIElement(bookData, !isPaused);
}
function addButtonToSection(section) {
const button = document.createElement('button');
button.textContent = 'Get Goodreads Ratings';
button.style.margin = '10px';
button.addEventListener('click', async function() {
this.disabled = true;
const newLinks = getUniqueBookLinks(section);
linksToProcess.push(...newLinks.filter(link => !processedASINs.has(getASIN(link.href))));
if (!isProcessing) {
isProcessing = true;
addUIElement(bookData, true);
try {
await processBooks();
} finally {
isProcessing = false;
}
}
});
section.insertBefore(button, section.firstChild);
}
function initializeScript() {
const addButtonsToSections = () => {
const sections = document.querySelectorAll('div[data-testid="asin-faceout-shoveler.card-cont"]:not([data-goodreads-processed]), div[data-testid="mfs-container.hor-scroll"]:not([data-goodreads-processed])');
sections.forEach(section => {
addButtonToSection(section);
section.setAttribute('data-goodreads-processed', 'true');
});
if (sections.length > 0) {
log(`Buttons added to ${sections.length} new sections`);
}
};
// Initial run
addButtonsToSections();
// Set up a MutationObserver to watch for new sections
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
addButtonsToSections();
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
log('Script initialized and watching for new sections');
}
// Run the script when the page is fully loaded
if (document.readyState === 'complete') {
initializeScript();
} else {
window.addEventListener('load', initializeScript);
}
log('Script setup complete');
})();