- // ==UserScript==
- // @name Audible Search Hub
- // @namespace https://greasyfork.org/en/users/1370284
- // @version 0.2.0
- // @license MIT
- // @description Add various search links to Audible (MyAnonaMouse, AudioBookBay, Mobilism, Goodreads, Anna's Archive, Z-Library & more)
- // @match https://*.audible.*/pd/*
- // @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',
- },
- };
-
- const 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;
- }
-
- async function injectSearchLinks(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()
-
- 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)
- }