- // ==UserScript==
- // @name Audible Search Hub
- // @namespace https://greasyfork.org/en/users/1370284
- // @version 0.2.4
- // @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,
- titleAuthorNarrator: true,
- },
- getLink: (search, opts = {}) => {
- const baseUrl = GM_config.get('url_mam')
-
- const url = new URL(`${baseUrl}/tor/browse.php`)
-
- url.searchParams.set('tor[text]', search)
- url.searchParams.set('tor[searchType]', 'active')
- url.searchParams.set('tor[main_cat]', 13) // Audiobooks - not working tho...
- url.searchParams.set('tor[srchIn][title]', true)
- url.searchParams.set('tor[srchIn][author]', true)
- if (opts?.narrator) {
- url.searchParams.set('tor[srchIn][narrator]', true)
- }
- 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
- },
- },
- // TODO: add libby, pointing to your library
- }
-
- const sitesKeys = Object.keys(sites)
-
- const searchByFields = {
- title: {
- label: 't',
- description: 'title',
- },
- titleAuthor: {
- label: 't+a',
- description: 'title + author',
- },
- titleAuthorNarrator: {
- label: 't+a+n',
- description: 'title + author + narrator',
- },
- }
-
- 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 || false,
- },
- [`enable_search_titleAuthor_${site}`]: {
- label: 'Enable Search by Title + Author',
- type: 'checkbox',
- default: sites[site].searchBy?.titleAuthor || false,
- },
- [`enable_search_titleAuthorNarrator_${site}`]: {
- label: 'Enable Search by Title + Author + Narrator',
- type: 'checkbox',
- default: sites[site].searchBy?.titleAuthorNarrator || false,
- },
- }
- }
-
- const perSiteFields = sitesKeys.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()
- })
-
- 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.classList.add(
- 'bc-tag',
- 'bc-size-footnote',
- 'bc-tag-outline',
- 'bc-badge-tag',
- 'bc-badge',
- 'custom-bc-tag'
- )
- 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
- }
-
- const parser = new DOMParser()
-
- function decodeHtmlEntities(str) {
- if (str == null) return ''
- const domParser = parser || new DOMParser()
- const doc = domParser.parseFromString(str, 'text/html')
- return doc.documentElement.textContent
- }
-
- function cleanSeriesName(seriesName) {
- if (!seriesName) return ''
-
- const wordsToRemove = new Set(['series', 'an', 'the', 'novel'])
- return seriesName
- .toLowerCase()
- .split(' ')
- .filter((word) => !wordsToRemove.has(word))
- .join(' ')
- .trim()
- }
-
- function cleanQuery(str) {
- const decoded = decodeHtmlEntities(str)
- // Remove dashes only when surrounded by spaces
- const noSurroundingDashes = decoded.replace(/(?<=\s)-(?=\s)/g, '')
- // Remove other unwanted characters
- return noSurroundingDashes.replace(/[?!:+~]/g, '')
- }
-
- function removePersonTitles(str) {
- return str
- ?.replace(
- /\b(Dr\.?|Mr\.?|Mrs\.?|Ms\.?|Prof\.?|M\.?D\.?|Ph\.?D\.?|D\.?O\.?|D\.?C\.?|D\.?D\.?S\.?|D\.?M\.?D\.?|D\.?Sc\.?|Ed\.?D\.?|LLB|JD|Esq\.?)\b\.?/gi,
- ''
- ) // Remove common author-related titles
- .replace(/\b\w{1,2}\.\s*/g, '') // Remove any 1 or 2 letter abbreviations followed by a dot
- .replace(/\s+/g, ' ') // Condense multiple spaces into one
- .trim() // Trim any extra spaces at the start or end
- }
-
- function extractBookInfo(data) {
- return {
- title: cleanQuery(data?.name),
- author: removePersonTitles(cleanQuery(data?.author?.at(0)?.name)),
- narrator: removePersonTitles(cleanQuery(data?.readBy?.at(0)?.name)),
- }
- }
-
- async function injectSearchLinks(data) {
- const { title, author, narrator } = extractBookInfo(data)
- const titleAuthor = `${title} ${author} `
- const titleAuthorNarrator = `${title} ${author} ${narrator}`
-
- 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()
- const fragment = document.createDocumentFragment() // Use a DocumentFragment
-
- sitesKeys.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
-
- let searchValue
-
- if (field === 'titleAuthorNarrator') {
- searchValue = titleAuthorNarrator
- } else if (field === 'titleAuthor') {
- searchValue = titleAuthor
- } else {
- searchValue = title
- }
-
- const opts = narrator ? { narrator } : {}
-
- const link = createLink(
- finalLabel,
- getLink(searchValue, opts),
- `Search ${name} by ${description}`
- )
- fragment.appendChild(link)
- })
- }
- })
-
- linksContainer.appendChild(fragment)
- infoParentEl.parentElement.appendChild(linksContainer)
- }
-
- function injectStyles() {
- const style = document.createElement('style')
- style.textContent = `
- .custom-bc-tag {
- text-decoration: none;
- transition: background-color 0.2s ease;
- white-space: nowrap;
- }
- .custom-bc-tag:hover {
- background-color: #f0f0f0;
- text-decoration: none;
- }
- `
- document.head.appendChild(style)
- }
-
- function extractBookData(doc) {
- try {
- const acceptedType = 'Audiobook'
- const ldJsonScripts = doc.querySelectorAll(
- 'script[type="application/ld+json"]'
- )
-
- for (const script of ldJsonScripts) {
- try {
- const jsonLdData = JSON.parse(script.textContent?.trim() || '')
- const items = Array.isArray(jsonLdData) ? jsonLdData : [jsonLdData]
-
- for (const item of items) {
- if (item['@type'] === acceptedType) {
- return item
- }
- }
- } catch (error) {
- console.error('Error parsing JSON-LD:', error)
- }
- }
-
- return null
- } catch (error) {
- console.error(`Error parsing data: `, error)
- return null
- }
- }
-
- function waitForBookDataScripts() {
- return new Promise((resolve, reject) => {
- const data = extractBookData(document)
- if (data) return resolve(data)
-
- const observer = new MutationObserver(() => {
- const data = extractBookData(document)
- if (data) {
- observer.disconnect()
- resolve(data)
- }
- })
-
- observer.observe(document, { childList: true, subtree: true })
-
- setTimeout(() => {
- observer.disconnect()
- reject(new Error('Timeout: ld+json script not found'))
- }, 2000)
- })
- }
-
- injectStyles()
-
- waitForBookDataScripts()
- .then(injectSearchLinks)
- .catch((error) => console.error('Error:', error.message))