// ==UserScript==
// @name Audible Search Hub
// @namespace https://greasyfork.org/en/users/1370284
// @version 0.1.0
// @license MIT
// @description Add various search links to Audible (MyAnonaMouse, AudioBookBay, Mobilism, Goodreads, Anna's Archive, Z-Library)
// @match https://*.audible.*/pd/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// ==/UserScript==
GM_config.init({
id: 'audible-search-options',
title: 'Search Options',
fields: {
sectionMAM: {
label: '--- MyAnonaMouse 👇🏻 ---',
type: 'hidden'
},
enableMAM: {
label: 'Enable',
type: 'checkbox',
default: true
},
urlMAM: {
label: 'URL',
type: 'text',
default: 'https://www.myanonamouse.net'
},
sectionABB: {
label: '--- AudioBookBay 👇🏻 ---',
type: 'hidden'
},
enableABB: {
label: 'Enable',
type: 'checkbox',
default: true
},
urlABB: {
label: 'URL',
type: 'text',
default: 'https://audiobookbay.lu'
},
sectionMobilism: {
label: '--- Mobilism 👇🏻 ---',
type: 'hidden'
},
enableMobilism: {
label: 'Enable',
type: 'checkbox',
default: true
},
urlMobilism: {
label: 'URL',
type: 'text',
default: 'https://forum.mobilism.org'
},
sectionGoodreads: {
label: '--- Goodreads 👇🏻 ---',
type: 'hidden'
},
enableGoodreads: {
label: 'Enable',
type: 'checkbox',
default: true
},
urlGoodreads: {
label: 'URL',
type: 'text',
default: 'https://www.goodreads.com'
},
sectionAnna: {
label: "--- Anna's Archive 👇🏻 ---",
type: 'hidden'
},
enableAnna: {
label: 'Enable',
type: 'checkbox',
default: true
},
urlAnna: {
label: "URL",
type: 'text',
default: 'https://annas-archive.org'
},
sectionZLib: {
label: "--- Z-Library 👇🏻 ---",
type: 'hidden'
},
enableZLib: {
label: 'Enable',
type: 'checkbox',
default: true
},
urlZLib: {
label: 'URL',
type: 'text',
default: 'https://z-lib.gs'
},
}
});
GM_registerMenuCommand('Open Settings', () => {
GM_config.open();
});
const getSearchLinkMAM = (search) => {
const baseUrl = GM_config.get('urlMAM');
const url = new URL(`${baseUrl}/tor/browse.php`)
url.searchParams.set('tor[text]', search)
return url.href
}
const getSearchLinkABB = (search) => {
const baseUrl = GM_config.get('urlABB');
const url = new URL(baseUrl)
url.searchParams.set('s', search)
return url.href
}
const getSearchLinkMobilism = (search) => {
const baseUrl = GM_config.get('urlMobilism');
const url = new URL(`${baseUrl}/search.php`)
url.searchParams.set('keywords', search)
url.searchParams.set('sr', 'topics')
url.searchParams.set('sf', 'titleonly')
return url.href
}
const getSearchLinkGoodreads = (search) => {
const baseUrl = GM_config.get('urlGoodreads');
const url = new URL(`${baseUrl}/search`)
url.searchParams.set('q', search)
return url.href
}
const getSearchLinkAnna = (search) => {
const baseUrl = GM_config.get('urlAnna');
const url = new URL(`${baseUrl}/search`)
url.searchParams.set('q', search)
url.searchParams.set('lang', 'en')
return url.href
}
const getSearchLinkZLib = (search) => {
const baseUrl = GM_config.get('urlZLib');
const url = new URL(`${baseUrl}/s/${search}`)
return url.href
}
async function fetchJsonDL(document2) {
try {
const acceptedTypes = ['Audiobook', 'Product', 'BreadcrumbList']
const result = {}
const ldJsonScripts = document2.querySelectorAll(
'script[type="application/ld+json"]'
)
ldJsonScripts.forEach((script) => {
try {
const jsonLdData = JSON.parse(script.textContent?.trim() || '')
const items = Array.isArray(jsonLdData) ? jsonLdData : [jsonLdData]
items.forEach((item) => {
if (acceptedTypes.includes(item['@type'])) {
result[item['@type']] = { ...result[item['@type']], ...item }
}
})
} catch (error) {
console.error('Error parsing JSON-LD:', error)
}
})
return result
} catch (error) {
console.error(`Error parsing data: `, error)
return {}
}
}
const waitForLdJsonScripts = () => {
return new Promise((resolve, reject) => {
const checkLdJson = async () => {
const data = await fetchJsonDL(document)
if (!!data?.Audiobook) {
resolve(data)
}
}
// Use MutationObserver to monitor DOM changes for new <script> elements
const observer = new MutationObserver(async (mutationsList, observer) => {
for (const mutation of mutationsList) {
if (mutation.addedNodes) {
for (const node of mutation.addedNodes) {
if (
node.nodeType === 1 && // Only process element nodes
node.tagName === 'SCRIPT' &&
node.type === 'application/ld+json'
) {
await checkLdJson() // Process the new script tag
}
}
}
}
})
// Start observing the document for added script tags
observer.observe(document, {
childList: true,
subtree: true,
})
// Also check initially in case the scripts are already present
checkLdJson().then((data) => {
if (Object.keys(data).length > 0) {
observer.disconnect()
resolve(data)
}
})
setTimeout(() => {
observer.disconnect()
reject(new Error('Timeout: ld+json script not found'))
}, 2000)
})
}
const style = document.createElement('style');
style.textContent = `
.custom-bc-tag {
text-decoration: none;
transition: background-color 0.2s ease;
}
.custom-bc-tag:hover {
background-color: #f0f0f0;
text-decoration: none;
}
`;
document.head.appendChild(style);
const createLink = (text, href, title) => {
const link = document.createElement('a');
link.href = href;
link.textContent = text;
link.target = '_blank';
link.classList.add('bc-tag', 'bc-size-footnote', 'bc-tag-outline', 'bc-badge-tag', 'bc-badge', 'custom-bc-tag');
link.style.whiteSpace = 'nowrap';
link.title = title || text;
return link;
};
const createLinksContainer = () => {
const container = document.createElement('div');
container.style.marginTop = '8px'
container.style.display = 'flex'
container.style.alignItems = 'center'
container.style.flexWrap = 'wrap'
container.style.gap = '4px'
container.style.maxWidth = '300px'
return container;
}
waitForLdJsonScripts()
.then((data) => {
injectSearchLinks(data)
})
.catch((error) => {
console.error('Error:', error.message)
})
const injectSearchLinks = async (data) => {
const title = data.Audiobook?.name
const author = data.Audiobook?.author?.[0]?.name
const titleAuthor = `${title} ${author} `
const authorLabelEl = document.querySelector('.authorLabel')
const infoParentEl = authorLabelEl?.parentElement
if (!infoParentEl) {
console.warn("Can't find the parent element to inject links.")
return
}
const linksContainer = createLinksContainer()
if (GM_config.get('enableMAM')) {
const linkTitle = createLink('🐭 title', getSearchLinkMAM(title), 'Search MyAnonaMouse by title')
const linkTitleAuthor = createLink('🐭 title + author', getSearchLinkMAM(titleAuthor), 'Search MyAnonaMouse by title & author')
linksContainer.append(linkTitle, linkTitleAuthor)
}
if (GM_config.get('enableABB')) {
const linkTitleAuthor = createLink('🎧 ABB', getSearchLinkABB(titleAuthor.toLowerCase()), 'Search AudioBookBay')
linksContainer.append(linkTitleAuthor)
}
if (GM_config.get('enableMobilism')) {
const linkTitleAuthor = createLink('📱 Mobilism', getSearchLinkMobilism(titleAuthor), 'Search Mobilism')
linksContainer.append(linkTitleAuthor)
}
if (GM_config.get('enableGoodreads')) {
const linkTitleAuthor = createLink('🔖 Goodreads', getSearchLinkGoodreads(titleAuthor), 'Search Goodreads')
linksContainer.append(linkTitleAuthor)
}
if (GM_config.get('enableAnna')) {
const linkTitleAuthor = createLink('📚 Anna', getSearchLinkAnna(titleAuthor), "Search Anna's Archive")
linksContainer.append(linkTitleAuthor)
}
if (GM_config.get('enableZLib')) {
const linkTitleAuthor = createLink('📕 Z-Library', getSearchLinkZLib(titleAuthor), 'Search Z-Library')
linksContainer.append(linkTitleAuthor)
}
infoParentEl.parentElement.appendChild(linksContainer)
}