├── .gitignore ├── LICENSE ├── README.md ├── download-all-kindle-books.js └── kindle-deals-goodreads-ratings.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Ignore example html pages 133 | temp/ 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright 2024 Chris Hollindale 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | software and associated documentation files (the "Software"), to deal in the Software 7 | without restriction, including without limitation the rights to use, copy, modify, 8 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # greasemonkey-scripts -------------------------------------------------------------------------------- /download-all-kindle-books.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Amazon Kindle Book Downloader 3 | // @namespace http://tampermonkey.net/ 4 | // @version 0.2.1 5 | // @description Adds a button to trigger downloads of all Kindle books on the page 6 | // @author Chris Hollindale 7 | // @match https://www.amazon.com/hz/mycd/digital-console/contentlist/* 8 | // @grant GM_xmlhttpRequest 9 | // @grant GM_addStyle 10 | // @run-at document-idle 11 | // @license MIT 12 | // ==/UserScript== 13 | 14 | (function() { 15 | 'use strict'; 16 | 17 | // Wait until the page is fully loaded before injecting the button 18 | window.addEventListener('load', function() { 19 | // Create a button in the top right of the page to trigger the action 20 | const button = document.createElement('button'); 21 | button.innerText = 'Trigger Download'; 22 | button.style.position = 'fixed'; 23 | button.style.top = '20px'; 24 | button.style.right = '20px'; 25 | button.style.padding = '10px'; 26 | button.style.fontSize = '16px'; 27 | button.style.backgroundColor = '#4CAF50'; 28 | button.style.color = 'white'; 29 | button.style.border = 'none'; 30 | button.style.borderRadius = '5px'; 31 | button.style.cursor = 'pointer'; 32 | button.style.zIndex = 9999; 33 | 34 | // Add button to the body 35 | document.body.appendChild(button); 36 | 37 | // Function to simulate clicking an element 38 | function clickElement(selector) { 39 | clickElementWithin(document, selector); 40 | } 41 | 42 | // Function to simulate clicking an element within a specific selector 43 | function clickElementWithin(topElement, selector) { 44 | const element = topElement.querySelector(selector); 45 | if (element) { 46 | element.click(); 47 | console.log(`Clicked: ${selector}`); 48 | } else { 49 | console.log(`Element not found: ${selector}`); 50 | } 51 | } 52 | 53 | // Function to handle processing of each dropdown 54 | async function processDropdowns() { 55 | // Get all dropdowns with the class prefix 'Dropdown-module_container__' 56 | const dropdowns = document.querySelectorAll('[class^="Dropdown-module_container__"]'); 57 | 58 | for (let i = 0; i < dropdowns.length; i++) { 59 | // Open the dropdown 60 | const dropdown = dropdowns[i]; 61 | dropdown.click(); 62 | console.log(`Dropdown ${i+1} opened`); 63 | 64 | // Wait a moment for the dropdown to open and perform the actions 65 | await new Promise(resolve => setTimeout(resolve, 500)); 66 | 67 | // Now perform the actions on the opened dropdown using wildcard selectors 68 | await new Promise(resolve => setTimeout(() => { 69 | const topDiv = Array.from(dropdown.querySelector('[class^="Dropdown-module_dropdown_container__"]').querySelectorAll('div')) 70 | .find(div => div.textContent.includes('Download & transfer via USB')); // Download & transfer via USB 71 | topDiv.querySelector('div').click(); 72 | resolve(); 73 | }, 500)); 74 | 75 | await new Promise(resolve => setTimeout(() => { 76 | clickElementWithin(dropdown, 'span[id^="download_and_transfer_list_"]'); // Choose the first Kindle in list 77 | // If you want the second Kindle in the list, change the above line to this instead (for the third, you'd change the [1] to [2] and so on): 78 | // dropdown.querySelectorAll('span[id^="download_and_transfer_list_"]')[1].click(); 79 | resolve(); 80 | }, 500)); 81 | 82 | await new Promise(resolve => setTimeout(() => { 83 | Array.from(dropdown.querySelectorAll('[id$="_CONFIRM"]')) 84 | .find(div => div.textContent.includes('Download')).click(); // Download 85 | resolve(); 86 | }, 500)); 87 | 88 | await new Promise(resolve => setTimeout(() => { 89 | clickElement('span[id="notification-close"]'); // Close success screen 90 | resolve(); 91 | }, 500)); 92 | 93 | // Wait a little before processing the next dropdown 94 | // This is set to 5 seconds - you can speed this up even faster if you prefer 95 | await new Promise(resolve => setTimeout(resolve, 5000)); 96 | } 97 | 98 | console.log('All dropdowns processed'); 99 | } 100 | 101 | // Button click event to start processing all dropdowns 102 | button.addEventListener('click', function() { 103 | processDropdowns(); 104 | }); 105 | }); 106 | 107 | // Add some CSS to make the button look nice 108 | GM_addStyle(` 109 | button { 110 | font-family: Arial, sans-serif; 111 | box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); 112 | } 113 | `); 114 | })(); 115 | -------------------------------------------------------------------------------- /kindle-deals-goodreads-ratings.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Amazon Kindle Deals Goodreads Ratings (Per Section) 3 | // @license MIT-0 4 | // @namespace http://tampermonkey.net/ 5 | // @version 2.5.2 6 | // @description Add Goodreads ratings to Amazon Kindle deals page for specific sections with highlighting 7 | // @match https://www.amazon.com/* 8 | // @grant GM_xmlhttpRequest 9 | // @run-at document-idle 10 | // ==/UserScript== 11 | 12 | (function() { 13 | 'use strict'; 14 | 15 | // Configurable variables 16 | let debugMode = true; 17 | 18 | // Rating thresholds and colors 19 | const highRatingThresholds = [4.00, 4.30, 4.50]; 20 | const highRatingColors = ['#e6ffe6', '#ccffcc', '#99ff99']; // Light to dark green 21 | const lowRatingThresholds = [3.60, 3.30]; 22 | const lowRatingColors = ['#ffe6e6', '#ffcccc']; // Light to medium red 23 | 24 | // Review count thresholds and colors 25 | const highRatingsCountThresholds = [1000, 5000, 10000]; 26 | const highRatingsCountColors = ['#e6ffe6', '#ccffcc', '#99ff99']; // Light to dark green 27 | const lowRatingsCountThresholds = [100, 10]; 28 | const lowRatingsCountColors = ['#ffe6e6', '#ffcccc']; // Light to medium red 29 | 30 | const longTitleLength = 42; 31 | 32 | let bookData = []; 33 | let processedASINs = new Set(); 34 | let linksToProcess = []; 35 | let currentLinkIndex = 0; 36 | let isPaused = false; 37 | let isProcessing = false; 38 | 39 | function log(message) { 40 | if (debugMode) { 41 | console.log(`[Goodreads Ratings Debug]: ${message}`); 42 | } 43 | } 44 | 45 | function decodeHTMLEntities(text) { 46 | const textArea = document.createElement('textarea'); 47 | textArea.innerHTML = text; 48 | return textArea.value; 49 | } 50 | 51 | function getASIN(url) { 52 | const match = url.match(/\/([A-Z0-9]{10})(?:\/|\?|$)/); 53 | return match ? match[1] : null; 54 | } 55 | 56 | function extractYear(text) { 57 | const yearRegex = /\b\d{4}\b/; // Regular expression to match a four-digit year 58 | const match = text.match(yearRegex); 59 | return match ? parseInt(match[0]) : '-'; 60 | } 61 | 62 | function extractNumberOfPages(text) { 63 | 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" 64 | const match = text.match(pagesRegex); 65 | return match ? parseInt(match[1].replace(/,/g, ''), 10) : '-'; 66 | } 67 | 68 | function extractLastNumber(str) { 69 | // First, try to find a number after '#' 70 | const hashMatch = str.match(/#\s*(\d+)\s*$/); 71 | if (hashMatch) { 72 | return parseInt(hashMatch[1]); 73 | } 74 | 75 | // If no '#' found, find the last number in the string 76 | const allNumbers = str.match(/\d+/g); 77 | if (allNumbers) { 78 | return parseInt(allNumbers[allNumbers.length - 1]); 79 | } 80 | 81 | // If no numbers found at all 82 | return null; 83 | } 84 | 85 | function isShelved(container) { 86 | const buttons = container.querySelectorAll('button'); 87 | 88 | return Array.from(buttons).some(button => { 89 | const label = button.getAttribute('aria-label'); 90 | return label && label.includes('Shelved'); 91 | }); 92 | } 93 | 94 | function getLiteraryAwards(doc) { 95 | if (doc) { 96 | const scripts = doc.getElementsByTagName('script'); 97 | 98 | let awardsData = null; 99 | 100 | // Iterate through scripts to find the one containing awards data 101 | for (let script of scripts) { 102 | const content = script.textContent || script.innerText; 103 | if (content.includes('"awards":')) { 104 | // This script likely contains our data 105 | const match = content.match(/"awards":\s*"([^"]*)"/); 106 | if (match) { 107 | try { 108 | awardsData = match[1]; 109 | break; 110 | } catch (e) { 111 | console.error("Error parsing awards data:", e); 112 | } 113 | } 114 | } 115 | } 116 | 117 | return awardsData; 118 | } else { 119 | return null; 120 | } 121 | } 122 | 123 | function getUniqueBookLinks(container) { 124 | const uniqueLinks = []; 125 | const seenHrefs = new Set(); 126 | 127 | // Kindle Deals page 128 | const sections = container.querySelectorAll('div[data-testid="asin-face"], .ubf-book-info'); 129 | 130 | sections.forEach(asinFace => { 131 | const link = asinFace.querySelector('a'); 132 | if (link && !seenHrefs.has(link.href)) { 133 | seenHrefs.add(link.href); 134 | uniqueLinks.push(link); 135 | } 136 | }); 137 | 138 | 139 | // Regular Kindle page 140 | const bookFaceouts = container.querySelectorAll('bds-unified-book-faceout'); 141 | 142 | bookFaceouts.forEach(faceout => { 143 | const shadowRoot = faceout.shadowRoot; 144 | if (shadowRoot) { 145 | const link = shadowRoot.querySelector('a'); 146 | if (link && !seenHrefs.has(link.href)) { 147 | seenHrefs.add(link.href); 148 | uniqueLinks.push(link); 149 | } 150 | } 151 | }); 152 | 153 | log(`Unique link count: ${uniqueLinks.length}`); 154 | 155 | return uniqueLinks; 156 | } 157 | 158 | function fetchGoodreadsData(asin) { 159 | return new Promise((resolve, reject) => { 160 | setTimeout(() => { 161 | if (isPaused) { 162 | resolve(null); 163 | return; 164 | } 165 | log(`Fetching data for ASIN: ${asin}`); 166 | GM_xmlhttpRequest({ 167 | method: "GET", 168 | url: `https://www.goodreads.com/book/isbn/${asin}`, 169 | onload: function(response) { 170 | log(`Received response for ASIN: ${asin}`); 171 | const parser = new DOMParser(); 172 | const doc = parser.parseFromString(response.responseText, "text/html"); 173 | // log(doc.documentElement.outerHTML); 174 | 175 | const h1Element = doc.querySelector('h1[data-testid="bookTitle"]'); 176 | const metadataElement = doc.querySelector('.RatingStatistics__rating'); 177 | 178 | if (!h1Element || !metadataElement) { 179 | log(`No results found for ASIN: ${asin}`); 180 | resolve(null); 181 | return; 182 | } 183 | 184 | const fullTitle = h1Element.textContent.trim(); 185 | const title = fullTitle.length > longTitleLength ? fullTitle.slice(0, longTitleLength) + '...' : fullTitle; 186 | const rating = metadataElement.textContent.trim().replace(/\s*stars/, ''); 187 | const ratingsCountElement = doc.querySelector('[data-testid="ratingsCount"]'); 188 | const ratingsCount = ratingsCountElement ? ratingsCountElement.textContent.trim().split(' ')[0] : '0'; 189 | const reviewsCountElement = doc.querySelector('[data-testid="reviewsCount"]'); 190 | const reviewsCount = reviewsCountElement ? reviewsCountElement.textContent.trim().split(' ')[0] : '0'; 191 | 192 | // Is this part of a series? 193 | const seriesElement = doc.querySelector('.BookPageTitleSection h3 a'); 194 | const series = seriesElement ? seriesElement.textContent.trim() : null; 195 | const seriesLink = seriesElement ? seriesElement.href : null; 196 | 197 | // Show a small image of the cover 198 | const coverImageElement = doc.querySelector('.BookCover__image img'); 199 | const coverImage = coverImageElement ? coverImageElement.src : 'https://dryofg8nmyqjw.cloudfront.net/images/no-cover.png'; 200 | 201 | // Extract the first author 202 | const authorElement = doc.querySelector('span.ContributorLink__name'); 203 | const author = authorElement ? authorElement.textContent.trim() : '-'; 204 | 205 | // Extract the first genre 206 | const genreElement = doc.querySelector('.BookPageMetadataSection__genreButton a'); 207 | const genre = genreElement ? genreElement.textContent.trim() : '-'; 208 | 209 | // Extract the publication year 210 | const publicationElement = doc.querySelector('p[data-testid="publicationInfo"]'); 211 | const publicationYear = publicationElement ? extractYear(publicationElement.textContent.trim()) : ''; 212 | 213 | // Extract the number of pages 214 | const pagesElement = doc.querySelector('p[data-testid="pagesFormat"]'); 215 | const numberOfPages = pagesElement ? extractNumberOfPages(pagesElement.textContent.trim()) : ''; 216 | 217 | // Is it on a shelf already? 218 | const actionsElement = doc.querySelector('.BookPageMetadataSection__mobileBookActions'); 219 | const onShelf = isShelved(actionsElement); 220 | 221 | const awards = getLiteraryAwards(doc); 222 | 223 | const data = { 224 | asin: asin, 225 | coverImage: coverImage, 226 | title: title || "Unknown Title", 227 | fullTitle: fullTitle, 228 | longTitle: fullTitle.length > longTitleLength, 229 | author: author, 230 | series: series, 231 | seriesLink: seriesLink, 232 | rating: rating, 233 | ratingsCount: ratingsCount, 234 | reviewsCount: reviewsCount, 235 | genre: genre, 236 | goodreadsUrl: `https://www.goodreads.com/book/isbn/${asin}`, 237 | publicationYear: publicationYear, 238 | numberOfPages: numberOfPages, 239 | onShelf: onShelf, 240 | awards: awards 241 | }; 242 | log(`Parsed data for ${data.title}: ${JSON.stringify(data)}`); 243 | resolve(data); 244 | }, 245 | onerror: function(error) { 246 | log(`Error fetching data for ASIN: ${asin}`); 247 | reject(error); 248 | } 249 | }); 250 | }, 250); 251 | }); 252 | } 253 | 254 | function getRatingColor(rating) { 255 | rating = parseFloat(rating); 256 | for (let i = highRatingThresholds.length - 1; i >= 0; i--) { 257 | if (rating >= highRatingThresholds[i]) { 258 | return highRatingColors[i]; 259 | } 260 | } 261 | for (let i = lowRatingThresholds.length - 1; i >= 0; i--) { 262 | if (rating < lowRatingThresholds[i]) { 263 | return lowRatingColors[i]; 264 | } 265 | } 266 | return ''; 267 | } 268 | 269 | function getRatingsCountColor(count) { 270 | count = parseInt(count.replace(/,/g, '')); 271 | for (let i = highRatingsCountThresholds.length - 1; i >= 0; i--) { 272 | if (count >= highRatingsCountThresholds[i]) { 273 | return highRatingsCountColors[i]; 274 | } 275 | } 276 | for (let i = lowRatingsCountThresholds.length - 1; i >= 0; i--) { 277 | if (count < lowRatingsCountThresholds[i]) { 278 | return lowRatingsCountColors[i]; 279 | } 280 | } 281 | return ''; 282 | } 283 | 284 | function addUIElement(books, isLoading = false) { 285 | let container = document.getElementById('goodreads-ratings'); 286 | if (!container) { 287 | container = document.createElement('div'); 288 | container.id = 'goodreads-ratings'; 289 | container.style.position = 'fixed'; 290 | container.style.top = '10px'; 291 | container.style.right = '10px'; 292 | container.style.backgroundColor = 'white'; 293 | container.style.padding = '10px'; 294 | container.style.border = '1px solid black'; 295 | container.style.zIndex = '9999'; 296 | container.style.maxHeight = '80vh'; 297 | container.style.overflowY = 'auto'; 298 | document.body.appendChild(container); 299 | } 300 | 301 | // Clear previous content 302 | container.innerHTML = ''; 303 | 304 | const headerContainer = document.createElement('div'); 305 | headerContainer.style.display = 'flex'; 306 | headerContainer.style.justifyContent = 'space-between'; 307 | headerContainer.style.alignItems = 'center'; 308 | headerContainer.style.marginBottom = '10px'; 309 | 310 | const title = document.createElement('h3'); 311 | title.textContent = 'Goodreads Ratings'; 312 | title.style.margin = '0'; 313 | headerContainer.appendChild(title); 314 | 315 | // Add pause/resume button only if there are links to process 316 | if (linksToProcess.length > 0) { 317 | const pauseResumeButton = document.createElement('button'); 318 | pauseResumeButton.textContent = isPaused ? 'Resume' : 'Pause'; 319 | pauseResumeButton.addEventListener('click', togglePauseResume); 320 | headerContainer.appendChild(pauseResumeButton); 321 | } 322 | 323 | container.appendChild(headerContainer); 324 | 325 | const statusMessage = document.createElement('p'); 326 | if (isPaused) { 327 | statusMessage.textContent = 'Processing paused'; 328 | } else if (isLoading) { 329 | statusMessage.textContent = `Processing... (${currentLinkIndex} of ${linksToProcess.length} books processed)`; 330 | } else if (books.length === 0) { 331 | statusMessage.textContent = 'No books processed yet'; 332 | } else { 333 | statusMessage.textContent = 'Processing finished!'; 334 | } 335 | container.appendChild(statusMessage); 336 | 337 | const table = document.createElement('table'); 338 | table.style.borderCollapse = 'collapse'; 339 | table.style.width = '100%'; 340 | //table.style.tableLayout = 'fixed'; // This helps maintain consistent column widths 341 | 342 | // Add a style for all cells 343 | const cellStyle = ` 344 | border: 1px solid gray; 345 | padding: 5px; 346 | vertical-align: middle; 347 | `; 348 | 349 | // Create table header 350 | const thead = document.createElement('thead'); 351 | const headerRow = document.createElement('tr'); 352 | ['Cover', 'Title', 'Author', 'Price', 'Rating', 'Rating Count', 'Review Count', 'Genre', 'Year', 'Pages'].forEach(headerText => { 353 | const th = document.createElement('th'); 354 | th.textContent = headerText; 355 | th.style.cssText = cellStyle + ` 356 | font-weight: bold; 357 | background-color: #f2f2f2; 358 | `; 359 | headerRow.appendChild(th); 360 | }); 361 | thead.appendChild(headerRow); 362 | table.appendChild(thead); 363 | 364 | // Create table body 365 | const tbody = document.createElement('tbody'); 366 | books.forEach(book => { 367 | if (book) { 368 | const row = document.createElement('tr'); 369 | 370 | // Cover image cell 371 | const coverCell = document.createElement('td'); 372 | coverCell.style.cssText = cellStyle + ` 373 | text-align: center; 374 | width: 30px; 375 | `; 376 | const coverImg = document.createElement('img'); 377 | coverImg.src = book.coverImage; 378 | coverImg.alt = `${book.title} cover`; 379 | coverImg.style.width = '28px'; 380 | coverImg.style.height = 'auto'; 381 | coverImg.style.display = 'block'; 382 | coverImg.style.margin = '0 auto'; // Centers the image horizontally 383 | coverImg.onerror = function() { 384 | this.onerror = null; 385 | this.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII='; 386 | this.alt = 'Cover not available'; 387 | }; 388 | coverCell.appendChild(coverImg); 389 | row.appendChild(coverCell); 390 | 391 | // Title cell 392 | const titleCell = document.createElement('td'); 393 | if (book.series) { 394 | const seriesNumber = extractLastNumber(book.series); 395 | const seriesLink = document.createElement('a'); 396 | seriesLink.href = book.seriesLink; 397 | seriesLink.target = '_blank'; 398 | seriesLink.textContent = seriesNumber ? `📚${seriesNumber}` : '📚'; 399 | seriesLink.title = book.series; 400 | titleCell.appendChild(seriesLink); 401 | // Add small span separator 402 | const span = document.createElement('span'); 403 | span.textContent = ' | '; 404 | titleCell.appendChild(span); 405 | } 406 | const link = document.createElement('a'); 407 | link.href = book.goodreadsUrl; 408 | link.target = '_blank'; 409 | link.textContent = book.onShelf ? `⭐ ${book.title}` : book.title; 410 | if (book.longTitle) { 411 | link.title = decodeHTMLEntities(book.fullTitle); 412 | } 413 | titleCell.appendChild(link); 414 | titleCell.style.cssText = cellStyle; 415 | row.appendChild(titleCell); 416 | 417 | // Author cell 418 | const authorCell = document.createElement('td'); 419 | authorCell.textContent = book.author; 420 | authorCell.style.cssText = cellStyle; 421 | row.appendChild(authorCell); 422 | 423 | // Price cell 424 | const priceCell = document.createElement('td'); 425 | const priceLink = document.createElement('a'); 426 | priceLink.href = `https://www.amazon.com/dp/${book.asin}`; 427 | priceLink.target = '_blank'; 428 | priceLink.textContent = book.price.replace(/^(?!\$)/, '$') || 'N/A'; // Add leading $ sign 429 | if (book.awards) { 430 | priceLink.textContent = `🏅 ${priceLink.textContent}`; 431 | priceLink.title = decodeHTMLEntities(book.awards); 432 | } 433 | priceCell.appendChild(priceLink); 434 | priceCell.style.cssText = cellStyle + 'text-align: right;'; 435 | row.appendChild(priceCell); 436 | 437 | // Rating cell 438 | const ratingCell = document.createElement('td'); 439 | ratingCell.textContent = book.rating; 440 | ratingCell.style.cssText = cellStyle + ` 441 | text-align: right; 442 | background-color: ${getRatingColor(book.rating)}; 443 | 444 | `; 445 | row.appendChild(ratingCell); 446 | 447 | // Ratings count cell 448 | const ratingsCountCell = document.createElement('td'); 449 | ratingsCountCell.textContent = book.ratingsCount; 450 | ratingsCountCell.style.cssText = cellStyle + ` 451 | text-align: right; 452 | background-color: ${getRatingsCountColor(book.ratingsCount)}; 453 | 454 | `; 455 | row.appendChild(ratingsCountCell); 456 | 457 | // Reviews count cell 458 | const reviewsCountCell = document.createElement('td'); 459 | reviewsCountCell.textContent = book.reviewsCount; 460 | reviewsCountCell.style.cssText = cellStyle + 'text-align: right;'; 461 | row.appendChild(reviewsCountCell); 462 | 463 | // Genre cell 464 | const genreCell = document.createElement('td'); 465 | genreCell.textContent = book.genre; 466 | genreCell.style.cssText = cellStyle; 467 | row.appendChild(genreCell); 468 | 469 | // Genre cell 470 | const publicationCell = document.createElement('td'); 471 | publicationCell.textContent = book.publicationYear; 472 | publicationCell.style.cssText = cellStyle; 473 | row.appendChild(publicationCell); 474 | 475 | // Pages cell 476 | const pagesCell = document.createElement('td'); 477 | pagesCell.textContent = book.numberOfPages.toLocaleString('en-US'); 478 | pagesCell.style.cssText = cellStyle + 'text-align: right;'; 479 | row.appendChild(pagesCell); 480 | 481 | tbody.appendChild(row); 482 | } 483 | }); 484 | table.appendChild(tbody); 485 | 486 | container.appendChild(table); 487 | } 488 | 489 | function addBookAndSort(newBook) { 490 | if (newBook && !processedASINs.has(newBook.asin)) { 491 | bookData.push(newBook); 492 | processedASINs.add(newBook.asin); 493 | bookData.sort((a, b) => parseFloat(b.rating) - parseFloat(a.rating)); 494 | addUIElement(bookData, linksToProcess.length > currentLinkIndex); 495 | } 496 | } 497 | 498 | async function processBooks() { 499 | while (currentLinkIndex < linksToProcess.length && !isPaused) { 500 | const link = linksToProcess[currentLinkIndex]; 501 | const asin = getASIN(link.href); 502 | 503 | currentLinkIndex++; 504 | 505 | if (asin && !processedASINs.has(asin)) { 506 | try { 507 | log(`---- Processing book ${currentLinkIndex} of ${linksToProcess.length} ----`); 508 | const data = await fetchGoodreadsData(asin); 509 | if (data) { 510 | // Kindle Deals pages 511 | const asinFace = link.closest('[data-testid="asin-face"]'); 512 | if (asinFace) { 513 | const priceElement = asinFace && asinFace.querySelector('[data-testid="price"]'); 514 | if (priceElement) { 515 | const priceTextContent = priceElement.textContent; 516 | const priceMatch = priceTextContent.match(/Deal price: \$(\d+\.\d+)/); 517 | data.price = priceMatch ? priceMatch[1] : 'N/A'; 518 | } 519 | } 520 | // Regular Kindle page 521 | if (!data.price) { 522 | const sibling = link.nextElementSibling; 523 | if (sibling) { 524 | const bookPrice = sibling.querySelector('bds-book-price'); 525 | if (bookPrice) { 526 | // data.price = bookPrice.getAttribute('unstylizedprice'); -- old way 527 | const shadowRoot = bookPrice.shadowRoot; 528 | if (shadowRoot) { 529 | const priceDiv = shadowRoot.querySelector('.offscreen'); // .price -> .offscreen 530 | if (priceDiv) { 531 | data.price = priceDiv.textContent.trim(); 532 | } 533 | } 534 | } 535 | } 536 | } 537 | if (!data.price) { 538 | data.price = 'N/A'; 539 | } 540 | addBookAndSort(data); 541 | } 542 | } catch (error) { 543 | console.error('Error fetching Goodreads data:', error); 544 | } 545 | } 546 | } 547 | 548 | if (currentLinkIndex >= linksToProcess.length) { 549 | log('All books processed'); 550 | addUIElement(bookData, false); 551 | } 552 | } 553 | 554 | function togglePauseResume() { 555 | isPaused = !isPaused; 556 | if (!isPaused) { 557 | processBooks(); 558 | } 559 | addUIElement(bookData, !isPaused); 560 | } 561 | 562 | function addButtonToSection(section) { 563 | const button = document.createElement('button'); 564 | button.textContent = 'Get Goodreads Ratings'; 565 | button.style.margin = '10px'; 566 | button.addEventListener('click', async function() { 567 | this.disabled = true; 568 | 569 | const newLinks = getUniqueBookLinks(section); 570 | linksToProcess.push(...newLinks.filter(link => !processedASINs.has(getASIN(link.href)))); 571 | 572 | if (!isProcessing) { 573 | isProcessing = true; 574 | addUIElement(bookData, true); 575 | 576 | try { 577 | await processBooks(); 578 | } finally { 579 | isProcessing = false; 580 | } 581 | } 582 | }); 583 | section.insertBefore(button, section.firstChild); 584 | } 585 | 586 | function initializeScript() { 587 | const addButtonsToSections = () => { 588 | 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])'); 589 | sections.forEach(section => { 590 | addButtonToSection(section); 591 | section.setAttribute('data-goodreads-processed', 'true'); 592 | }); 593 | if (sections.length > 0) { 594 | log(`Buttons added to ${sections.length} new sections`); 595 | } 596 | }; 597 | 598 | // Initial run 599 | addButtonsToSections(); 600 | 601 | // Set up a MutationObserver to watch for new sections 602 | const observer = new MutationObserver((mutations) => { 603 | mutations.forEach((mutation) => { 604 | if (mutation.type === 'childList') { 605 | addButtonsToSections(); 606 | } 607 | }); 608 | }); 609 | 610 | observer.observe(document.body, { 611 | childList: true, 612 | subtree: true 613 | }); 614 | 615 | log('Script initialized and watching for new sections'); 616 | } 617 | 618 | // Run the script when the page is fully loaded 619 | if (document.readyState === 'complete') { 620 | initializeScript(); 621 | } else { 622 | window.addEventListener('load', initializeScript); 623 | } 624 | 625 | log('Script setup complete'); 626 | })(); 627 | --------------------------------------------------------------------------------