Audible Search Hub

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

目前为 2024-09-20 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Audible Search Hub
  3. // @namespace https://greasyfork.org/en/users/1370284
  4. // @version 0.2.0
  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. // @grant GM_getValue
  9. // @grant GM_setValue
  10. // @grant GM_registerMenuCommand
  11. // @require https://openuserjs.org/src/libs/sizzle/GM_config.js
  12. // ==/UserScript==
  13.  
  14. const sites = {
  15. mam: {
  16. label: '🐭 MAM',
  17. name: 'MyAnonaMouse',
  18. url: 'https://www.myanonamouse.net',
  19. searchBy: { title: true, titleAuthor: true },
  20. getLink: (search) => {
  21. const baseUrl = GM_config.get('url_mam');
  22. const url = new URL(`${baseUrl}/tor/browse.php`);
  23. url.searchParams.set('tor[text]', search);
  24. return url.href;
  25. }
  26. },
  27. abb: {
  28. label: '🎧 ABB',
  29. name: 'AudioBookBay',
  30. url: 'https://audiobookbay.lu',
  31. searchBy: { title: false, titleAuthor: true },
  32. getLink: (search) => {
  33. const baseUrl = GM_config.get('url_abb');
  34. const url = new URL(baseUrl);
  35. url.searchParams.set('s', search.toLowerCase());
  36. return url.href;
  37. }
  38. },
  39. mobilism: {
  40. label: '📱 Mobilism',
  41. name: 'Mobilism',
  42. url: 'https://forum.mobilism.org',
  43. searchBy: { title: false, titleAuthor: true },
  44. getLink: (search) => {
  45. const baseUrl = GM_config.get('url_mobilism');
  46. const url = new URL(`${baseUrl}/search.php`);
  47. url.searchParams.set('keywords', search);
  48. url.searchParams.set('sr', 'topics');
  49. url.searchParams.set('sf', 'titleonly');
  50. return url.href;
  51. }
  52. },
  53. goodreads: {
  54. label: '🔖 Goodreads',
  55. name: 'Goodreads',
  56. url: 'https://www.goodreads.com',
  57. searchBy: { title: false, titleAuthor: true },
  58. getLink: (search) => {
  59. const baseUrl = GM_config.get('url_goodreads');
  60. const url = new URL(`${baseUrl}/search`);
  61. url.searchParams.set('q', search);
  62. return url.href;
  63. }
  64. },
  65. anna: {
  66. label: '📚 Anna',
  67. name: "Anna's Archive",
  68. url: 'https://annas-archive.org',
  69. searchBy: { title: false, titleAuthor: true },
  70. getLink: (search) => {
  71. const baseUrl = GM_config.get('url_anna');
  72. const url = new URL(`${baseUrl}/search`);
  73. url.searchParams.set('q', search);
  74. url.searchParams.set('lang', 'en');
  75. return url.href;
  76. }
  77. },
  78. zlib: {
  79. label: '📕 zLib',
  80. name: 'Z-Library',
  81. url: 'https://z-lib.gs',
  82. searchBy: { title: false, titleAuthor: true },
  83. getLink: (search) => {
  84. const baseUrl = GM_config.get('url_zlib');
  85. const url = new URL(`${baseUrl}/s/${search}`);
  86. return url.href;
  87. }
  88. },
  89. libgen: {
  90. label: '📗 Libgen',
  91. name: 'Libgen',
  92. url: 'https://libgen.rs',
  93. searchBy: { title: false, titleAuthor: true },
  94. getLink: (search) => {
  95. const baseUrl = GM_config.get('url_libgen');
  96. const url = new URL(`${baseUrl}/search`);
  97. url.searchParams.set('req', search);
  98. return url.href;
  99. }
  100. },
  101. tgx: {
  102. label: '🌌 TGX',
  103. name: 'TorrentGalaxy',
  104. url: 'https://tgx.rs/torrents.php',
  105. searchBy: { title: false, titleAuthor: true },
  106. getLink: (search) => {
  107. const baseUrl = GM_config.get('url_tgx');
  108. const url = new URL(baseUrl);
  109. url.searchParams.set('search', search);
  110. return url.href;
  111. }
  112. },
  113. btdig: {
  114. label: '⛏️ BTDig',
  115. name: 'BTDig',
  116. url: 'https://btdig.com',
  117. searchBy: { title: false, titleAuthor: true },
  118. getLink: (search) => {
  119. const baseUrl = GM_config.get('url_btdig');
  120. const url = new URL(`${baseUrl}/search`);
  121. url.searchParams.set('q', search);
  122. return url.href;
  123. }
  124. }
  125. };
  126.  
  127. const searchByFields = {
  128. title: {
  129. label: 't',
  130. description: 'title',
  131. },
  132. titleAuthor: {
  133. label: 't+a',
  134. description: 'title + author',
  135. },
  136. };
  137.  
  138. const addSiteConfig = (site) => {
  139. return {
  140. [`section_${site}`]: {
  141. label: `-------------- ${sites[site].name} 👇🏻 --------------`,
  142. type: 'hidden'
  143. },
  144. [`enable_${site}`]: {
  145. label: 'Enable',
  146. type: 'checkbox',
  147. default: true
  148. },
  149. [`url_${site}`]: {
  150. label: 'URL',
  151. type: 'text',
  152. default: sites[site].url
  153. },
  154. [`enable_search_title_${site}`]: {
  155. label: 'Enable Search by Title',
  156. type: 'checkbox',
  157. default: sites[site].searchBy.title
  158. },
  159. [`enable_search_titleAuthor_${site}`]: {
  160. label: 'Enable Search by Title + Author',
  161. type: 'checkbox',
  162. default: sites[site].searchBy.titleAuthor
  163. },
  164. }
  165. }
  166.  
  167. const perSiteFields = Object.keys(sites).reduce((acc, siteKey) => {
  168. return {
  169. ...acc,
  170. ...addSiteConfig(siteKey, sites[siteKey])
  171. };
  172. }, {});
  173.  
  174. GM_config.init({
  175. id: 'audible-search-sites',
  176. title: 'Search Sites',
  177. fields: {
  178. open_in_new_tab: {
  179. label: 'Open Links in New Tab',
  180. type: 'checkbox',
  181. default: true
  182. },
  183. ...perSiteFields,
  184. }
  185. });
  186.  
  187. GM_registerMenuCommand('Open Settings', () => {
  188. GM_config.open();
  189. });
  190.  
  191. async function extractBookData(document2) {
  192. try {
  193. const acceptedTypes = ['Audiobook', 'Product', 'BreadcrumbList']
  194. const result = {}
  195. const ldJsonScripts = document2.querySelectorAll(
  196. 'script[type="application/ld+json"]'
  197. )
  198. ldJsonScripts.forEach((script) => {
  199. try {
  200. const jsonLdData = JSON.parse(script.textContent?.trim() || '')
  201. const items = Array.isArray(jsonLdData) ? jsonLdData : [jsonLdData]
  202. items.forEach((item) => {
  203. if (acceptedTypes.includes(item['@type'])) {
  204. result[item['@type']] = { ...result[item['@type']], ...item }
  205. }
  206. })
  207. } catch (error) {
  208. console.error('Error parsing JSON-LD:', error)
  209. }
  210. })
  211. return result
  212. } catch (error) {
  213. console.error(`Error parsing data: `, error)
  214. return {}
  215. }
  216. }
  217.  
  218. const waitForBookDataScripts = () =>
  219. new Promise((resolve, reject) => {
  220. const checkLdJson = async () => {
  221. const data = await extractBookData(document);
  222. if (data?.Audiobook) resolve(data);
  223. };
  224.  
  225. const observer = new MutationObserver(async (mutationsList) => {
  226. mutationsList.forEach((mutation) =>
  227. mutation.addedNodes.forEach(async (node) => {
  228. if (node.nodeType === 1 && node.tagName === 'SCRIPT' && node.type === 'application/ld+json') {
  229. await checkLdJson();
  230. }
  231. })
  232. );
  233. });
  234.  
  235. observer.observe(document, { childList: true, subtree: true });
  236. checkLdJson().then((data) => data.Audiobook && observer.disconnect() && resolve(data));
  237. setTimeout(() => {
  238. observer.disconnect();
  239. reject(new Error('Timeout: ld+json script not found'));
  240. }, 2000);
  241. });
  242.  
  243.  
  244. const style = document.createElement('style');
  245. style.textContent = `
  246. .custom-bc-tag {
  247. text-decoration: none;
  248. transition: background-color 0.2s ease;
  249. }
  250. .custom-bc-tag:hover {
  251. background-color: #f0f0f0;
  252. text-decoration: none;
  253. }
  254. `;
  255. document.head.appendChild(style);
  256.  
  257.  
  258.  
  259. waitForBookDataScripts()
  260. .then((data) => {
  261. injectSearchLinks(data)
  262. })
  263. .catch((error) => {
  264. console.error('Error:', error.message)
  265. })
  266.  
  267.  
  268. function createLink(text, href, title) {
  269. const link = document.createElement('a');
  270. link.href = href;
  271. link.textContent = text;
  272. link.target = GM_config.get('open_in_new_tab') ? '_blank' : '_self';
  273. link.target = '_blank';
  274. link.classList.add('bc-tag', 'bc-size-footnote', 'bc-tag-outline', 'bc-badge-tag', 'bc-badge', 'custom-bc-tag');
  275. link.style.whiteSpace = 'nowrap';
  276. link.title = title || text;
  277. return link;
  278. };
  279.  
  280. function createLinksContainer() {
  281. const container = document.createElement('div');
  282. container.style.marginTop = '8px'
  283. container.style.display = 'flex'
  284. container.style.alignItems = 'center'
  285. container.style.flexWrap = 'wrap'
  286. container.style.gap = '4px'
  287. container.style.maxWidth = '340px'
  288. return container;
  289. }
  290.  
  291. async function injectSearchLinks(data) {
  292. const title = data.Audiobook?.name
  293. const author = data.Audiobook?.author?.[0]?.name
  294. const titleAuthor = `${title} ${author} `
  295.  
  296. const authorLabelEl = document.querySelector('.authorLabel')
  297. const infoParentEl = authorLabelEl?.parentElement
  298.  
  299. if (!infoParentEl) {
  300. console.warn("Can't find the parent element to inject links.")
  301. return
  302. }
  303.  
  304. const linksContainer = createLinksContainer()
  305.  
  306. Object.keys(sites).forEach((siteKey) => {
  307. if (GM_config.get(`enable_${siteKey}`)) {
  308. const { label, name, getLink } = sites[siteKey];
  309.  
  310. const enabledSearchFields = Object.keys(searchByFields).filter((field) =>
  311. GM_config.get(`enable_search_${field}_${siteKey}`)
  312. );
  313. const isMultipleEnabled = enabledSearchFields.length > 1;
  314.  
  315. enabledSearchFields.forEach((field) => {
  316. const { label: searchLabel, description } = searchByFields[field];
  317.  
  318. const finalLabel = isMultipleEnabled ? `${label} (${searchLabel})` : label;
  319.  
  320. const searchValue = field === 'titleAuthor' ? titleAuthor : title;
  321. const link = createLink(finalLabel, getLink(searchValue), `Search ${name} by ${description}`);
  322. linksContainer.append(link);
  323. });
  324. }
  325. });
  326.  
  327. infoParentEl.parentElement.appendChild(linksContainer)
  328. }