Full-res cover images on Audible, with 'open' and 'download' actions.
// ==UserScript== // @name Audible HQ Cover // @namespace https://greasyfork.org/en/users/1370205 // @version 0.2.3 // @description Full-res cover images on Audible, with 'open' and 'download' actions. // @license MIT // @match https://*.audible.*/pd/* // @match https://*.audible.*/ac/* // ==/UserScript== const MARKETPLACE = { us: { tld: 'com', flag: '🇺🇸', name: 'US' }, uk: { tld: 'co.uk', flag: '🇬🇧', name: 'UK' }, ca: { tld: 'ca', flag: '🇨🇦', name: 'Canada' }, au: { tld: 'com.au', flag: '🇦🇺', name: 'Australia' }, de: { tld: 'de', flag: '🇩🇪', name: 'Germany' }, fr: { tld: 'fr', flag: '🇫🇷', name: 'France' }, es: { tld: 'es', flag: '🇪🇸', name: 'Spain' }, it: { tld: 'it', flag: '🇮🇹', name: 'Italy' }, in: { tld: 'in', flag: '🇮🇳', name: 'India' }, jp: { tld: 'co.jp', flag: '🇯🇵', name: 'Japan' }, br: { tld: 'com.br', flag: '🇧🇷', name: 'Brazil' }, } function extractRegionFromUrl(url) { try { const { hostname } = new URL(url) for (const [key, marketplace] of Object.entries(MARKETPLACE)) { if (hostname.endsWith(marketplace.tld)) { return key } } return null } catch (e) { console.error('Invalid URL:', e) return null } } function extractAsinFromUrl(url) { const asinMatch = url.pathname.match(/\/([A-Z0-9]{10})/) return asinMatch ? asinMatch[1] : null } function getTld(region) { return MARKETPLACE[region].tld || 'com' } function getAudibleApiUrl(region) { const tld = getTld(region) return new URL(`https://api.audible.${tld}/1.0`).href } const fetchProductData = async (region, asin, query) => { const baseUrl = getAudibleApiUrl(region) const url = new URL(`${baseUrl}/catalog/products/${asin}`) url.searchParams.append('num_r###lts', '1') Object.entries(query).forEach(([key, value]) => { url.searchParams.append(key, value) }) const response = await fetch(url.href, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } return await response.json() } async function fetchAudibleProductImages(region, asin) { const data = await fetchProductData(region, asin, { response_groups: 'media', image_sizes: 2400, num_r###lts: '1', }) return data.product.product_images } function extractParams() { const url = window.location const region = extractRegionFromUrl(url) const asin = extractAsinFromUrl(url) return { region, asin } } function downloadImage(imageUrl, fileName) { fetch(imageUrl) .then((response) => response.blob()) .then((blob) => { const url = window.URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = fileName link.style.display = 'none' document.body.appendChild(link) link.click() window.URL.revokeObjectURL(url) document.body.removeChild(link) }) .catch((error) => console.error('Error downloading image:', error)) } function createActionButton(iconUrl, title, onClick) { const button = document.createElement('button') button.title = title button.setAttribute('data-hidden', 'true') const icon = document.createElement('img') icon.src = iconUrl icon.alt = title button.appendChild(icon) button.addEventListener('click', (e) => { e.stopPropagation() e.preventDefault() onClick() }) return button } function wrapCoverWithLink(imageElement, coverUrl) { const linkElement = document.createElement('a') linkElement.href = coverUrl linkElement.className = 'cover-link' linkElement.title = 'Open cover' linkElement.target = '_blank' imageElement.parentNode.insertBefore(linkElement, imageElement) linkElement.appendChild(imageElement) } function enhanceCoverImage(coverUrl, asin) { const imageElement = document.querySelector( 'img.bc-pub-block.bc-image-inset-border.js-only-element' ) if (imageElement) { const containerElement = document.createElement('div') containerElement.className = 'container' imageElement.parentNode.insertBefore(containerElement, imageElement) containerElement.appendChild(imageElement) wrapCoverWithLink(imageElement, coverUrl) const coverActionsContainer = document.createElement('div') coverActionsContainer.className = 'cover-actions-container' const hqButton = createActionButton( 'https://api.iconify.design/solar:high-quality-bold.svg', 'Load HQ cover', () => { imageElement.src = coverUrl hqButton.classList.add('action-btn-disabled') hqButton.title = 'HQ cover loaded' } ) const downloadButton = createActionButton( 'https://api.iconify.design/solar:download-square-bold.svg', 'Download cover', () => downloadImage(coverUrl, `${asin}.jpg`) ) coverActionsContainer.appendChild(hqButton) coverActionsContainer.appendChild(downloadButton) containerElement.appendChild(coverActionsContainer) containerElement.addEventListener('mouseenter', () => { downloadButton.setAttribute('data-hidden', false) hqButton.setAttribute('data-hidden', false) }) containerElement.addEventListener('mouseleave', () => { downloadButton.setAttribute('data-hidden', true) hqButton.setAttribute('data-hidden', true) }) } else { console.warn('Cover image element not found.') } } const COVER_SIZE = 2400 const main = async () => { try { const { region, asin } = extractParams() if (!region || !asin) { throw new Error('Unable to extract region or ASIN from URL') } const images = await fetchAudibleProductImages(region, asin) if (!images || !images[COVER_SIZE]) { throw new Error('No images found') } const cover = images[COVER_SIZE] enhanceCoverImage(cover, asin) addStyles() } catch (error) { console.error('Error in main function:', error.message) } } main() function addStyles() { const style = document.createElement('style') style.textContent = ` .cover-actions-container { position: absolute; bottom: 10px; right: 10px; display: flex; gap: 6px; } .cover-actions-container button { background-color: rgba(0, 0, 0, 0.85); border: none; border-radius: 12px; padding: 6px; cursor: pointer; transition: opacity 200ms ease-in-out; } .cover-actions-container button img { width: 20px; height: 20px; filter: invert(1); display: block; } .cover-actions-container .action-btn-disabled { opacity: 0.5; cursor: not-allowed; } .container { position: relative; display: flex; align-items: center; justify-content: center; } [data-hidden="true"] { opacity: 0; visibility: hidden; } [data-hidden="false"] { opacity: 1; visibility: visible; } ` document.head.appendChild(style) }