Greasy Fork is available in English.

Audible Search Hub

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

  1. // ==UserScript==
  2. // @name Audible Search Hub
  3. // @namespace https://greasyfork.org/en/users/1370284
  4. // @version 0.2.4
  5. // @license MIT
  6. // @description Add various search links to Audible (MyAnonaMouse, AudioBookBay, Mobilism, Goodreads, Anna's Archive, Z-Library & more)
  7. // @match https://*.audible.*/pd/*
  8. // @match https://*.audible.*/ac/*
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_registerMenuCommand
  12. // @require https://openuserjs.org/src/libs/sizzle/GM_config.js
  13. // ==/UserScript==
  14.  
  15. const sites = {
  16. mam: {
  17. label: '🐭 MAM',
  18. name: 'MyAnonaMouse',
  19. url: 'https://www.myanonamouse.net',
  20. searchBy: {
  21. title: true,
  22. titleAuthor: true,
  23. titleAuthorNarrator: true,
  24. },
  25. getLink: (search, opts = {}) => {
  26. const baseUrl = GM_config.get('url_mam')
  27.  
  28. const url = new URL(`${baseUrl}/tor/browse.php`)
  29.  
  30. url.searchParams.set('tor[text]', search)
  31. url.searchParams.set('tor[searchType]', 'active')
  32. url.searchParams.set('tor[main_cat]', 13) // Audiobooks - not working tho...
  33. url.searchParams.set('tor[srchIn][title]', true)
  34. url.searchParams.set('tor[srchIn][author]', true)
  35. if (opts?.narrator) {
  36. url.searchParams.set('tor[srchIn][narrator]', true)
  37. }
  38. return url.href
  39. },
  40. },
  41. abb: {
  42. label: '🎧 ABB',
  43. name: 'AudioBookBay',
  44. url: 'https://audiobookbay.lu',
  45. searchBy: { title: false, titleAuthor: true },
  46. getLink: (search) => {
  47. const baseUrl = GM_config.get('url_abb')
  48. const url = new URL(baseUrl)
  49. url.searchParams.set('s', search.toLowerCase())
  50. return url.href
  51. },
  52. },
  53. mobilism: {
  54. label: '📱 Mobilism',
  55. name: 'Mobilism',
  56. url: 'https://forum.mobilism.org',
  57. searchBy: { title: false, titleAuthor: true },
  58. getLink: (search) => {
  59. const baseUrl = GM_config.get('url_mobilism')
  60. const url = new URL(`${baseUrl}/search.php`)
  61. url.searchParams.set('keywords', search)
  62. url.searchParams.set('sr', 'topics')
  63. url.searchParams.set('sf', 'titleonly')
  64. return url.href
  65. },
  66. },
  67. goodreads: {
  68. label: '🔖 Goodreads',
  69. name: 'Goodreads',
  70. url: 'https://www.goodreads.com',
  71. searchBy: { title: false, titleAuthor: true },
  72. getLink: (search) => {
  73. const baseUrl = GM_config.get('url_goodreads')
  74. const url = new URL(`${baseUrl}/search`)
  75. url.searchParams.set('q', search)
  76. return url.href
  77. },
  78. },
  79. anna: {
  80. label: '📚 Anna',
  81. name: "Anna's Archive",
  82. url: 'https://annas-archive.org',
  83. searchBy: { title: false, titleAuthor: true },
  84. getLink: (search) => {
  85. const baseUrl = GM_config.get('url_anna')
  86. const url = new URL(`${baseUrl}/search`)
  87. url.searchParams.set('q', search)
  88. url.searchParams.set('lang', 'en')
  89. return url.href
  90. },
  91. },
  92. zlib: {
  93. label: '📕 zLib',
  94. name: 'Z-Library',
  95. url: 'https://z-lib.gs',
  96. searchBy: { title: false, titleAuthor: true },
  97. getLink: (search) => {
  98. const baseUrl = GM_config.get('url_zlib')
  99. const url = new URL(`${baseUrl}/s/${search}`)
  100. return url.href
  101. },
  102. },
  103. libgen: {
  104. label: '📗 Libgen',
  105. name: 'Libgen',
  106. url: 'https://libgen.rs',
  107. searchBy: { title: false, titleAuthor: true },
  108. getLink: (search) => {
  109. const baseUrl = GM_config.get('url_libgen')
  110. const url = new URL(`${baseUrl}/search`)
  111. url.searchParams.set('req', search)
  112. return url.href
  113. },
  114. },
  115. tgx: {
  116. label: '🌌 TGX',
  117. name: 'TorrentGalaxy',
  118. url: 'https://tgx.rs/torrents.php',
  119. searchBy: { title: false, titleAuthor: true },
  120. getLink: (search) => {
  121. const baseUrl = GM_config.get('url_tgx')
  122. const url = new URL(baseUrl)
  123. url.searchParams.set('search', search)
  124. return url.href
  125. },
  126. },
  127. btdig: {
  128. label: '⛏️ BTDig',
  129. name: 'BTDig',
  130. url: 'https://btdig.com',
  131. searchBy: { title: false, titleAuthor: true },
  132. getLink: (search) => {
  133. const baseUrl = GM_config.get('url_btdig')
  134. const url = new URL(`${baseUrl}/search`)
  135. url.searchParams.set('q', search)
  136. return url.href
  137. },
  138. },
  139. // TODO: add libby, pointing to your library
  140. }
  141.  
  142. const sitesKeys = Object.keys(sites)
  143.  
  144. const searchByFields = {
  145. title: {
  146. label: 't',
  147. description: 'title',
  148. },
  149. titleAuthor: {
  150. label: 't+a',
  151. description: 'title + author',
  152. },
  153. titleAuthorNarrator: {
  154. label: 't+a+n',
  155. description: 'title + author + narrator',
  156. },
  157. }
  158.  
  159. function addSiteConfig(site) {
  160. return {
  161. [`section_${site}`]: {
  162. label: `-------------- ${sites[site].name} 👇 --------------`,
  163. type: 'hidden',
  164. },
  165. [`enable_${site}`]: {
  166. label: 'Enable',
  167. type: 'checkbox',
  168. default: true,
  169. },
  170. [`url_${site}`]: {
  171. label: 'URL',
  172. type: 'text',
  173. default: sites[site].url,
  174. },
  175. [`enable_search_title_${site}`]: {
  176. label: 'Enable Search by Title',
  177. type: 'checkbox',
  178. default: sites[site].searchBy?.title || false,
  179. },
  180. [`enable_search_titleAuthor_${site}`]: {
  181. label: 'Enable Search by Title + Author',
  182. type: 'checkbox',
  183. default: sites[site].searchBy?.titleAuthor || false,
  184. },
  185. [`enable_search_titleAuthorNarrator_${site}`]: {
  186. label: 'Enable Search by Title + Author + Narrator',
  187. type: 'checkbox',
  188. default: sites[site].searchBy?.titleAuthorNarrator || false,
  189. },
  190. }
  191. }
  192.  
  193. const perSiteFields = sitesKeys.reduce((acc, siteKey) => {
  194. return {
  195. ...acc,
  196. ...addSiteConfig(siteKey, sites[siteKey]),
  197. }
  198. }, {})
  199.  
  200. GM_config.init({
  201. id: 'audible-search-sites',
  202. title: 'Search Sites',
  203. fields: {
  204. open_in_new_tab: {
  205. label: 'Open Links in New Tab',
  206. type: 'checkbox',
  207. default: true,
  208. },
  209. ...perSiteFields,
  210. },
  211. })
  212.  
  213. GM_registerMenuCommand('Open Settings', () => {
  214. GM_config.open()
  215. })
  216.  
  217. function createLink(text, href, title) {
  218. const link = document.createElement('a')
  219. link.href = href
  220. link.textContent = text
  221. link.target = GM_config.get('open_in_new_tab') ? '_blank' : '_self'
  222. link.classList.add(
  223. 'bc-tag',
  224. 'bc-size-footnote',
  225. 'bc-tag-outline',
  226. 'bc-badge-tag',
  227. 'bc-badge',
  228. 'custom-bc-tag'
  229. )
  230. link.title = title || text
  231. return link
  232. }
  233.  
  234. function createLinksContainer() {
  235. const container = document.createElement('div')
  236. container.style.marginTop = '8px'
  237. container.style.display = 'flex'
  238. container.style.alignItems = 'center'
  239. container.style.flexWrap = 'wrap'
  240. container.style.gap = '4px'
  241. container.style.maxWidth = '340px'
  242. return container
  243. }
  244.  
  245. const parser = new DOMParser()
  246.  
  247. function decodeHtmlEntities(str) {
  248. if (str == null) return ''
  249. const domParser = parser || new DOMParser()
  250. const doc = domParser.parseFromString(str, 'text/html')
  251. return doc.documentElement.textContent
  252. }
  253.  
  254. function cleanSeriesName(seriesName) {
  255. if (!seriesName) return ''
  256.  
  257. const wordsToRemove = new Set(['series', 'an', 'the', 'novel'])
  258. return seriesName
  259. .toLowerCase()
  260. .split(' ')
  261. .filter((word) => !wordsToRemove.has(word))
  262. .join(' ')
  263. .trim()
  264. }
  265.  
  266. function cleanQuery(str) {
  267. const decoded = decodeHtmlEntities(str)
  268. // Remove dashes only when surrounded by spaces
  269. const noSurroundingDashes = decoded.replace(/(?<=\s)-(?=\s)/g, '')
  270. // Remove other unwanted characters
  271. return noSurroundingDashes.replace(/[?!:+~]/g, '')
  272. }
  273.  
  274. function removePersonTitles(str) {
  275. return str
  276. ?.replace(
  277. /\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,
  278. ''
  279. ) // Remove common author-related titles
  280. .replace(/\b\w{1,2}\.\s*/g, '') // Remove any 1 or 2 letter abbreviations followed by a dot
  281. .replace(/\s+/g, ' ') // Condense multiple spaces into one
  282. .trim() // Trim any extra spaces at the start or end
  283. }
  284.  
  285. function extractBookInfo(data) {
  286. return {
  287. title: cleanQuery(data?.name),
  288. author: removePersonTitles(cleanQuery(data?.author?.at(0)?.name)),
  289. narrator: removePersonTitles(cleanQuery(data?.readBy?.at(0)?.name)),
  290. }
  291. }
  292.  
  293. async function injectSearchLinks(data) {
  294. const { title, author, narrator } = extractBookInfo(data)
  295. const titleAuthor = `${title} ${author} `
  296. const titleAuthorNarrator = `${title} ${author} ${narrator}`
  297.  
  298. const authorLabelEl = document.querySelector('.authorLabel')
  299. const infoParentEl = authorLabelEl?.parentElement
  300.  
  301. if (!infoParentEl) {
  302. console.warn("Can't find the parent element to inject links.")
  303. return
  304. }
  305.  
  306. const linksContainer = createLinksContainer()
  307. const fragment = document.createDocumentFragment() // Use a DocumentFragment
  308.  
  309. sitesKeys.forEach((siteKey) => {
  310. if (GM_config.get(`enable_${siteKey}`)) {
  311. const { label, name, getLink } = sites[siteKey]
  312.  
  313. const enabledSearchFields = Object.keys(searchByFields).filter((field) =>
  314. GM_config.get(`enable_search_${field}_${siteKey}`)
  315. )
  316. const isMultipleEnabled = enabledSearchFields.length > 1
  317.  
  318. enabledSearchFields.forEach((field) => {
  319. const { label: searchLabel, description } = searchByFields[field]
  320.  
  321. const finalLabel = isMultipleEnabled
  322. ? `${label} (${searchLabel})`
  323. : label
  324.  
  325. let searchValue
  326.  
  327. if (field === 'titleAuthorNarrator') {
  328. searchValue = titleAuthorNarrator
  329. } else if (field === 'titleAuthor') {
  330. searchValue = titleAuthor
  331. } else {
  332. searchValue = title
  333. }
  334.  
  335. const opts = narrator ? { narrator } : {}
  336.  
  337. const link = createLink(
  338. finalLabel,
  339. getLink(searchValue, opts),
  340. `Search ${name} by ${description}`
  341. )
  342. fragment.appendChild(link)
  343. })
  344. }
  345. })
  346.  
  347. linksContainer.appendChild(fragment)
  348. infoParentEl.parentElement.appendChild(linksContainer)
  349. }
  350.  
  351. function injectStyles() {
  352. const style = document.createElement('style')
  353. style.textContent = `
  354. .custom-bc-tag {
  355. text-decoration: none;
  356. transition: background-color 0.2s ease;
  357. white-space: nowrap;
  358. }
  359. .custom-bc-tag:hover {
  360. background-color: #f0f0f0;
  361. text-decoration: none;
  362. }
  363. `
  364. document.head.appendChild(style)
  365. }
  366.  
  367. function extractBookData(doc) {
  368. try {
  369. const acceptedType = 'Audiobook'
  370. const ldJsonScripts = doc.querySelectorAll(
  371. 'script[type="application/ld+json"]'
  372. )
  373.  
  374. for (const script of ldJsonScripts) {
  375. try {
  376. const jsonLdData = JSON.parse(script.textContent?.trim() || '')
  377. const items = Array.isArray(jsonLdData) ? jsonLdData : [jsonLdData]
  378.  
  379. for (const item of items) {
  380. if (item['@type'] === acceptedType) {
  381. return item
  382. }
  383. }
  384. } catch (error) {
  385. console.error('Error parsing JSON-LD:', error)
  386. }
  387. }
  388.  
  389. return null
  390. } catch (error) {
  391. console.error(`Error parsing data: `, error)
  392. return null
  393. }
  394. }
  395.  
  396. function waitForBookDataScripts() {
  397. return new Promise((resolve, reject) => {
  398. const data = extractBookData(document)
  399. if (data) return resolve(data)
  400.  
  401. const observer = new MutationObserver(() => {
  402. const data = extractBookData(document)
  403. if (data) {
  404. observer.disconnect()
  405. resolve(data)
  406. }
  407. })
  408.  
  409. observer.observe(document, { childList: true, subtree: true })
  410.  
  411. setTimeout(() => {
  412. observer.disconnect()
  413. reject(new Error('Timeout: ld+json script not found'))
  414. }, 2000)
  415. })
  416. }
  417.  
  418. injectStyles()
  419.  
  420. waitForBookDataScripts()
  421. .then(injectSearchLinks)
  422. .catch((error) => console.error('Error:', error.message))