// How to use. // Log on to Audible, and open up your library. // Next, you want to run this entire file as a script on the page. // Chrome, Firefox and Internet Explorer does this differently. // For chrome, press F12 to open the Chrome Dev Tools, and click // on the Console. Then paste in this whole file in the console // window and press enter. // But first, set the following settings: // How frequentyly should the script ask for book information? // Too often and Audible might look at you angrily. // Too slow and it will take longer to grab the information. var requestBookDataFrequencySeconds = 3 // This number is in books per seconds. // Do you want the data shown on screen in a table or as raw JSON data? var exportFormat = 'html' // Options: 'html', 'json' or 'excel' // Note that even with 'excel', you may have to copy-and-paste the resulting text // first into a text file like a NotePad. Not sure why, but it seems to make it better for Excel. // If you want a HTML table, do you want it to include the book thumbnail? var exportHtmlIncludeImage = false // Helper methods to convert from 2h 15m to 135 var convertToMinutes = function (str) { var match = str.match(/.*?(\d+)\s*?h.*?/); var hours = (match != null) ? parseInt(match[1]) : 0; match = str.match(/.*?(\d+)\s*?m.*?/); var minutes = (match != null) ? parseInt(match[1]) : 0; return hours * 60 + minutes; }; async function tableFill(drawFn) { // const books = $('.bc-list:has(.authorLabel) .bc-list-item:first-child a') const books = $('.adbl-library-content-row') .map(function () { const headerLink = $(this).find('.bc-list-item:first-child a') const title = headerLink.text().trim() const link = headerLink.attr('href') const remaining = convertToMinutes($(this).find('[id^=time-remaining-]:not(.bc-pub-hidden)').text()) const isFavorite = $(this).find('.remove-from-favorites-button:not(.bc-pub-hidden)').length === 1 const rating = $(this).find('.adbl-prod-rate-review-bar-overall').data('star-count') return { title, link, remaining ,isFavorite, rating} } ).get() drawFn(books) for (var i = 0; i < books.length; i++) { const bookData = await fetch(books[i].link) .then(response => response.text()) .then(async html => { const book = $(html) const details = book.find('.bc-container:has(.runtimeLabel)') const image = details.find('img.bc-pub-block').attr('src') const title = details.find('h1.bc-heading:first').text() const authors = details.find('.authorLabel:first a').map((i, link) => ({ name: link.text, link: link.href })).get() const narrators = details.find('.narratorLabel:first a').map((i, link) => ({ name: link.text, link: link.href })).get() const series = details.find('.seriesLabel:first a').map((i, link) => { let ordinal = link.nextSibling.wholeText.match(/\d+\.?\d*/) let name = ordinal ? link.text + ', #' + ordinal[0] : link.text return { name, link: link.href } }).get() const minutes = convertToMinutes(details.find('.runtimeLabel:first').text()) const datePurchased = book.find('#adbl-buy-box-purchase-date .bc-color-secondary').text().trim() const reviewLink = book.find('#adbl-buybox-review-redirect-btn a').attr('href') let performance = '' let story = '' if (typeof reviewLink === 'string' && reviewLink.startsWith('/')) { try { await fetch(reviewLink) .then(response => response.text()) .then(html2 => { const reviewPage = $(html2) const reviews = reviewPage.find('#writeReviewForm .bc-rating-stars') if (reviews.length === 3) { performance = reviews[1].getAttribute('data-star-count') story = reviews[2].getAttribute('data-star-count') } }) } catch (error) { console.error(error) } } const result = { authors, narrators, series, minutes, datePurchased, performance, story, image } if (title) result.title = title; return result }) books[i] = { ...books[i], ...bookData } drawFn(books) await new Promise(resolve => setTimeout(resolve, requestBookDataFrequencySeconds * 1000)) } alert('done!') } /* ************************************** COMMON DRAWING STUFF ************************************** */ let drawCalledBefore = false; tableFill(exportFormat === 'html' ? drawHtml : exportFormat === 'excel' ? drawExcel : drawJson) /* ************************************** JSON TABLE DRAWING ************************************** */ function drawJson(data) { $('body').text(JSON.stringify(data)) drawCalledBefore = true } /* *************************************** EXCEL TABLE DRAWING ***************************************/ function drawExcel(tableData) { if (!drawCalledBefore) { var style = document.createElement('style'); style.innerHTML = ` p{overflow:hidden; white-space: nowrap;} p:first-child{background-color: #4CAF50;color: white;} p:nth-child(even){background-color: #f2f2f2;} `; document.head.appendChild(style); } const body = $('body') body.empty() const rows = [["Title", "Authors", "Narrators", "Series", "Length (min)", "Remaining (min)", "Date Purchased", "Is Favorite", "Overall Rating", "Performance", "Story", "Book URL",]] tableData.forEach(function (rowData) { rows.push([ rowData.title, makeExcelLinks(rowData.authors), makeExcelLinks(rowData.narrators), makeExcelLinks(rowData.series), rowData.minutes, rowData.remaining, rowData.datePurchased, rowData.isFavorite, rowData.rating, rowData.performance, rowData.story, window.location.origin + rowData.link, ]) }); body.append(`
${rows.map(row => row.map(str => !str ? '' : (str + '').replace(/[\t\n\r]/g, '').replace(/"/g, '""')).join("\t")).join("\n")}
`) drawCalledBefore = true } function makeExcelLinks(links) { if (!Array.isArray(links)) return '' return links.map(link => link.name).join(', ') } /* *************************************** HTML TABLE DRAWING ***************************************/ function drawHtml(tableData) { if (!drawCalledBefore) { var style = document.createElement('style'); style.innerHTML = ` table { font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; border-collapse: collapse; width: 100%; } table td, table th { border: 1px solid #ddd; padding: 8px; max-width: 200px; } table tr:nth-child(even){background-color: #f2f2f2;} table tr:hover {background-color: #ddd;} table th { padding-top: 12px; padding-bottom: 12px; text-align: left; background-color: #4CAF50; color: white; } `; document.head.appendChild(style); } $('body').empty() var table = document.createElement('table'); const imageTH = exportHtmlIncludeImage ? 'Image' : '' $(`${imageTH}TitleAuthorsNarratorsSeriesLength (min)Remaining (min)Date PurchasedIs FavoriteOverall RatingPerformanceStory`).appendTo(table) var tableBody = document.createElement('tbody'); tableData.forEach(function (rowData) { var row = document.createElement('tr'); if (exportHtmlIncludeImage) appendCell(row, makeImage(rowData.image)); appendCell(row, makeLink({ name: rowData.title, link: rowData.link })); appendCell(row, makeLinks(rowData.authors)); appendCell(row, makeLinks(rowData.narrators)); appendCell(row, makeLinks(rowData.series)); appendCell(row, rowData.minutes); appendCell(row, rowData.remaining); appendCell(row, rowData.datePurchased); appendCell(row, rowData.isFavorite); appendCell(row, rowData.rating); appendCell(row, rowData.performance); appendCell(row, rowData.story); tableBody.appendChild(row); }); table.appendChild(tableBody); document.body.appendChild(table); drawCalledBefore = true } function appendCell(row, child) { var cell = document.createElement('td'); if (typeof child === 'string' || typeof child === 'number') { cell.appendChild(document.createTextNode('' + child)); } else if (child instanceof Element) { cell.appendChild(child); } else if (Array.isArray(child)) { for (let i = 0; i < child.length; i++) { cell.appendChild(child[i]) } } else if (typeof child === 'boolean') { cell.appendChild(document.createTextNode(child ? '✔' : '')); } row.appendChild(cell); } function makeLinks(links) { if (!Array.isArray(links)) return null const result = links.map(link => makeLink(link)) for (let i = result.length - 1; i > 0; i--) { result.splice(i, 0, document.createTextNode(', ')); } return result; } function makeLink(nameLinkObj) { if (!nameLinkObj) return null const el = document.createElement('a') el.setAttribute('href', nameLinkObj.link) el.appendChild(document.createTextNode(nameLinkObj.name)) return el } function makeImage(url) { if (!url) return null const el = document.createElement('img') el.setAttribute('src', url) return el }