Audible Search Hub

Add various search links to Audible (MyAnonaMouse, AudioBookBay, Mobilism, Goodreads, Anna's Archive, Z-Library & more)

Εγκατάσταση αυτού του κώδικαΒοήθεια
Κώδικας προτεινόμενος από τον δημιιουργό

Μπορεί, επίσης, να σας αρέσει ο κώδικας Audible Search Marketplaces.

Εγκατάσταση αυτού του κώδικα
// ==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))