// ==UserScript==
// @name Audible Search Hub
// @namespace https://greasyfork.org/en/users/1370284
// @version 0.2.1
// @license MIT
// @description Add various search links to Audible (MyAnonaMouse, AudioBookBay, Mobilism, Goodreads, Anna's Archive, Z-Library & more)
// @match https://*.audible.*/pd/*
// @match https://*.audible.*/ac/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// ==/UserScript==
const sites = {
mam: {
label: '🐭 MAM',
name: 'MyAnonaMouse',
url: 'https://www.myanonamouse.net',
searchBy: { title: true, titleAuthor: true },
getLink: (search) => {
const baseUrl = GM_config.get('url_mam');
const url = new URL(`${baseUrl}/tor/browse.php`);
url.searchParams.set('tor[text]', search);
return url.href;
}
},
abb: {
label: '🎧 ABB',
name: 'AudioBookBay',
url: 'https://audiobookbay.lu',
searchBy: { title: false, titleAuthor: true },
getLink: (search) => {
const baseUrl = GM_config.get('url_abb');
const url = new URL(baseUrl);
url.searchParams.set('s', search.toLowerCase());
return url.href;
}
},
mobilism: {
label: '📱 Mobilism',
name: 'Mobilism',
url: 'https://forum.mobilism.org',
searchBy: { title: false, titleAuthor: true },
getLink: (search) => {
const baseUrl = GM_config.get('url_mobilism');
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;
}
},
goodreads: {
label: '🔖 Goodreads',
name: 'Goodreads',
url: 'https://www.goodreads.com',
searchBy: { title: false, titleAuthor: true },
getLink: (search) => {
const baseUrl = GM_config.get('url_goodreads');
const url = new URL(`${baseUrl}/search`);
url.searchParams.set('q', search);
return url.href;
}
},
anna: {
label: '📚 Anna',
name: "Anna's Archive",
url: 'https://annas-archive.org',
searchBy: { title: false, titleAuthor: true },
getLink: (search) => {
const baseUrl = GM_config.get('url_anna');
const url = new URL(`${baseUrl}/search`);
url.searchParams.set('q', search);
url.searchParams.set('lang', 'en');
return url.href;
}
},
zlib: {
label: '📕 zLib',
name: 'Z-Library',
url: 'https://z-lib.gs',
searchBy: { title: false, titleAuthor: true },
getLink: (search) => {
const baseUrl = GM_config.get('url_zlib');
const url = new URL(`${baseUrl}/s/${search}`);
return url.href;
}
},
libgen: {
label: '📗 Libgen',
name: 'Libgen',
url: 'https://libgen.rs',
searchBy: { title: false, titleAuthor: true },
getLink: (search) => {
const baseUrl = GM_config.get('url_libgen');
const url = new URL(`${baseUrl}/search`);
url.searchParams.set('req', search);
return url.href;
}
},
tgx: {
label: '🌌 TGX',
name: 'TorrentGalaxy',
url: 'https://tgx.rs/torrents.php',
searchBy: { title: false, titleAuthor: true },
getLink: (search) => {
const baseUrl = GM_config.get('url_tgx');
const url = new URL(baseUrl);
url.searchParams.set('search', search);
return url.href;
}
},
btdig: {
label: '⛏️ BTDig',
name: 'BTDig',
url: 'https://btdig.com',
searchBy: { title: false, titleAuthor: true },
getLink: (search) => {
const baseUrl = GM_config.get('url_btdig');
const url = new URL(`${baseUrl}/search`);
url.searchParams.set('q', search);
return url.href;
}
}
};
const searchByFields = {
title: {
label: 't',
description: 'title',
},
titleAuthor: {
label: 't+a',
description: 'title + author',
},
};
function addSiteConfig(site) {
return {
[`section_${site}`]: {
label: `-------------- ${sites[site].name} 👇🏻 --------------`,
type: 'hidden'
},
[`enable_${site}`]: {
label: 'Enable',
type: 'checkbox',
default: true
},
[`url_${site}`]: {
label: 'URL',
type: 'text',
default: sites[site].url
},
[`enable_search_title_${site}`]: {
label: 'Enable Search by Title',
type: 'checkbox',
default: sites[site].searchBy.title
},
[`enable_search_titleAuthor_${site}`]: {
label: 'Enable Search by Title + Author',
type: 'checkbox',
default: sites[site].searchBy.titleAuthor
},
}
}
const perSiteFields = Object.keys(sites).reduce((acc, siteKey) => {
return {
...acc,
...addSiteConfig(siteKey, sites[siteKey])
};
}, {});
GM_config.init({
id: 'audible-search-sites',
title: 'Search Sites',
fields: {
open_in_new_tab: {
label: 'Open Links in New Tab',
type: 'checkbox',
default: true
},
...perSiteFields,
}
});
GM_registerMenuCommand('Open Settings', () => {
GM_config.open();
});
async function extractBookData(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 waitForBookDataScripts = () =>
new Promise((resolve, reject) => {
const checkLdJson = async () => {
const data = await extractBookData(document);
if (data?.Audiobook) resolve(data);
};
const observer = new MutationObserver(async (mutationsList) => {
mutationsList.forEach((mutation) =>
mutation.addedNodes.forEach(async (node) => {
if (node.nodeType === 1 && node.tagName === 'SCRIPT' && node.type === 'application/ld+json') {
await checkLdJson();
}
})
);
});
observer.observe(document, { childList: true, subtree: true });
checkLdJson().then((data) => data.Audiobook && 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);
waitForBookDataScripts()
.then((data) => {
injectSearchLinks(data)
})
.catch((error) => {
console.error('Error:', error.message)
})
function createLink(text, href, title) {
const link = document.createElement('a');
link.href = href;
link.textContent = text;
link.target = GM_config.get('open_in_new_tab') ? '_blank' : '_self';
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;
};
function 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 = '340px'
return container;
}
function decodeHtmlEntities (str) {
if (str == null) {
return ''
}
const parser = new DOMParser();
const doc = parser.parseFromString(str, 'text/html');
return doc.documentElement.textContent;
};
async function injectSearchLinks(data) {
const title = decodeHtmlEntities(data.Audiobook?.name)
const author = decodeHtmlEntities(data.Audiobook?.author?.at(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()
Object.keys(sites).forEach((siteKey) => {
if (GM_config.get(`enable_${siteKey}`)) {
const { label, name, getLink } = sites[siteKey];
const enabledSearchFields = Object.keys(searchByFields).filter((field) =>
GM_config.get(`enable_search_${field}_${siteKey}`)
);
const isMultipleEnabled = enabledSearchFields.length > 1;
enabledSearchFields.forEach((field) => {
const { label: searchLabel, description } = searchByFields[field];
const finalLabel = isMultipleEnabled ? `${label} (${searchLabel})` : label;
const searchValue = field === 'titleAuthor' ? titleAuthor : title;
const link = createLink(finalLabel, getLink(searchValue), `Search ${name} by ${description}`);
linksContainer.append(link);
});
}
});
infoParentEl.parentElement.appendChild(linksContainer)
}