Audible Search Hub

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

目前为 2024-09-19 提交的版本。查看 最新版本

// ==UserScript==
// @name         Audible Search Hub
// @namespace    https://greasyfork.org/en/users/1370284
// @version      0.1.0
// @license      MIT
// @description  Add various search links to Audible (MyAnonaMouse, AudioBookBay, Mobilism, Goodreads, Anna's Archive, Z-Library)
// @match        https://*.audible.*/pd/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @require      https://openuserjs.org/src/libs/sizzle/GM_config.js
// ==/UserScript==

GM_config.init({
  id: 'audible-search-options',
  title: 'Search Options',
  fields: {
    sectionMAM: {
      label: '--- MyAnonaMouse 👇🏻 ---',
      type: 'hidden'
    },
    enableMAM: {
      label: 'Enable',
      type: 'checkbox',
      default: true
    },
    urlMAM: {
      label: 'URL',
      type: 'text',
      default: 'https://www.myanonamouse.net'
    },

    sectionABB: {
      label: '--- AudioBookBay 👇🏻 ---',
      type: 'hidden'
    },
    enableABB: {
      label: 'Enable',
      type: 'checkbox',
      default: true
    },
    urlABB: {
      label: 'URL',
      type: 'text',
      default: 'https://audiobookbay.lu'
    },

    sectionMobilism: {
      label: '--- Mobilism 👇🏻 ---',
      type: 'hidden'
    },
    enableMobilism: {
      label: 'Enable',
      type: 'checkbox',
      default: true
    },
    urlMobilism: {
      label: 'URL',
      type: 'text',
      default: 'https://forum.mobilism.org'
    },

    sectionGoodreads: {
      label: '--- Goodreads 👇🏻 ---',
      type: 'hidden'
    },
    enableGoodreads: {
      label: 'Enable',
      type: 'checkbox',
      default: true
    },
    urlGoodreads: {
      label: 'URL',
      type: 'text',
      default: 'https://www.goodreads.com'
    },

    sectionAnna: {
      label: "--- Anna's Archive 👇🏻 ---",
      type: 'hidden'
    },
    enableAnna: {
      label: 'Enable',
      type: 'checkbox',
      default: true
    },
    urlAnna: {
      label: "URL",
      type: 'text',
      default: 'https://annas-archive.org'
    },

    sectionZLib: {
      label: "--- Z-Library 👇🏻 ---",
      type: 'hidden'
    },
    enableZLib: {
      label: 'Enable',
      type: 'checkbox',
      default: true
    },
    urlZLib: {
      label: 'URL',
      type: 'text',
      default: 'https://z-lib.gs'
    },
  }
});

GM_registerMenuCommand('Open Settings', () => {
  GM_config.open();
});

const getSearchLinkMAM = (search) => {
  const baseUrl = GM_config.get('urlMAM');
  const url = new URL(`${baseUrl}/tor/browse.php`)
  url.searchParams.set('tor[text]', search)
  return url.href
}

const getSearchLinkABB = (search) => {
  const baseUrl = GM_config.get('urlABB');
  const url = new URL(baseUrl)
  url.searchParams.set('s', search)
  return url.href

}

const getSearchLinkMobilism = (search) => {
  const baseUrl = GM_config.get('urlMobilism');
  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
}

const getSearchLinkGoodreads = (search) => {
  const baseUrl = GM_config.get('urlGoodreads');
  const url = new URL(`${baseUrl}/search`)
  url.searchParams.set('q', search)
  return url.href
}

const getSearchLinkAnna = (search) => {
  const baseUrl = GM_config.get('urlAnna');
  const url = new URL(`${baseUrl}/search`)
  url.searchParams.set('q', search)
  url.searchParams.set('lang', 'en')
  return url.href
}

const getSearchLinkZLib = (search) => {
  const baseUrl = GM_config.get('urlZLib');
  const url = new URL(`${baseUrl}/s/${search}`)
  return url.href
}

async function fetchJsonDL(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 waitForLdJsonScripts = () => {
  return new Promise((resolve, reject) => {
    const checkLdJson = async () => {
      const data = await fetchJsonDL(document)
      if (!!data?.Audiobook) {
        resolve(data)
      }
    }

    // Use MutationObserver to monitor DOM changes for new <script> elements
    const observer = new MutationObserver(async (mutationsList, observer) => {
      for (const mutation of mutationsList) {
        if (mutation.addedNodes) {
          for (const node of mutation.addedNodes) {
            if (
              node.nodeType === 1 && // Only process element nodes
              node.tagName === 'SCRIPT' &&
              node.type === 'application/ld+json'
            ) {
              await checkLdJson() // Process the new script tag
            }
          }
        }
      }
    })

    // Start observing the document for added script tags
    observer.observe(document, {
      childList: true,
      subtree: true,
    })

    // Also check initially in case the scripts are already present
    checkLdJson().then((data) => {
      if (Object.keys(data).length > 0) {
        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);


const createLink = (text, href, title) => {
  const link = document.createElement('a');
  link.href = href;
  link.textContent = text;
  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;
};

const 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 = '300px'
  return container;
}

waitForLdJsonScripts()
  .then((data) => {
    injectSearchLinks(data)
  })
  .catch((error) => {
    console.error('Error:', error.message)
  })

const injectSearchLinks = async (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()

  if (GM_config.get('enableMAM')) {
    const linkTitle = createLink('🐭 title', getSearchLinkMAM(title), 'Search MyAnonaMouse by title')
    const linkTitleAuthor = createLink('🐭 title + author', getSearchLinkMAM(titleAuthor), 'Search MyAnonaMouse by title & author')
    linksContainer.append(linkTitle, linkTitleAuthor)
  }

  if (GM_config.get('enableABB')) {
    const linkTitleAuthor = createLink('🎧 ABB', getSearchLinkABB(titleAuthor.toLowerCase()), 'Search AudioBookBay')
    linksContainer.append(linkTitleAuthor)
  }

  if (GM_config.get('enableMobilism')) {
    const linkTitleAuthor = createLink('📱 Mobilism', getSearchLinkMobilism(titleAuthor), 'Search Mobilism')
    linksContainer.append(linkTitleAuthor)
  }

  if (GM_config.get('enableGoodreads')) {
    const linkTitleAuthor = createLink('🔖 Goodreads', getSearchLinkGoodreads(titleAuthor), 'Search Goodreads')
    linksContainer.append(linkTitleAuthor)
  }

  if (GM_config.get('enableAnna')) {
    const linkTitleAuthor = createLink('📚 Anna', getSearchLinkAnna(titleAuthor), "Search Anna's Archive")
    linksContainer.append(linkTitleAuthor)
  }

  if (GM_config.get('enableZLib')) {
    const linkTitleAuthor = createLink('📕 Z-Library', getSearchLinkZLib(titleAuthor), 'Search Z-Library')
    linksContainer.append(linkTitleAuthor)
  }

  infoParentEl.parentElement.appendChild(linksContainer)
}