您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add Goodreads ratings to Amazon Kindle deals page for specific sections with highlighting
// ==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'); })();