StoryGraph Plus: Search MAM Buttons

Add "Search MAM" buttons to TheStoryGraph book, series, and browse pages (Title/Series and Title/Series + Author)

// ==UserScript==
// @name         StoryGraph Plus: Search MAM Buttons
// @namespace    https://greasyfork.org/en/users/1457912
// @version      0.4.0
// @description  Add "Search MAM" buttons to TheStoryGraph book, series, and browse pages (Title/Series and Title/Series + Author)
// @author       WilliestWonka
// @match        https://app.thestorygraph.com/books/*
// @match        https://app.thestorygraph.com/series/*
// @match        https://app.thestorygraph.com/browse*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  const maxRetries = 2;
  let retryCount = 0;
  let retryIntervalId = null;

  function createMamButtons(title, author, isSeries = false) {
    console.log("[SG+] Creating MAM buttons for:", title, author, "isSeries:", isSeries);
    const container = document.createElement("div");
    container.className = "mam-button-container flex mt-2 mb-2 space-x-2 w-full";

    const createButton = (text, url) => {
      const button = document.createElement("a");
      button.href = url;
      button.target = "_blank";
      button.textContent = text;
      button.className =
        "py-2 px-2 border-x-2 border-x-darkGrey dark:border-x-darkerGrey " +
        "border-y border-y-darkGrey dark:border-y-darkerGrey border-b-2 " +
        "bg-grey dark:bg-darkestGrey hover:bg-darkGrey dark:hover:bg-darkerGrey " +
        "inline-flex items-center justify-center w-full text-center text-xs " +
        "text-darkerGrey dark:text-lightGrey";

      return button;
    };

    const searchUrl = (query) =>
      `https://www.myanonamouse.net/tor/browse.php?tor[text]=${encodeURIComponent(query)}`;

    if (isSeries) {
      container.appendChild(createButton("Search MAM Series", searchUrl(title)));
      container.appendChild(createButton("Search MAM Series + Author", searchUrl(`${title} ${author}`)));
    } else {
      container.appendChild(createButton("Search MAM Title", searchUrl(title)));
      container.appendChild(createButton("Search MAM Title + Author", searchUrl(`${title} ${author}`)));
    }

    return container;
  }

  function addButtonsIfReady() {
    console.log("[SG+] Checking if buttons should be added...");
    const pathParts = location.pathname.split('/').filter(Boolean);
    const isBookPage = pathParts[0] === "books";
    const isSeriesPage = pathParts[0] === "series";
    const isBrowsePage = pathParts[0] === "browse";

    if (!isBookPage && !isSeriesPage && !isBrowsePage) return false;

    if (isSeriesPage) {
      // Series page: title from h4.page-heading, author from nearby p > a
      const titleElement = document.querySelector("h4.page-heading");
      const authorElement = document.querySelector("p.font-body a[href^='/authors/']");
      const headingContainer = document.querySelector("div.flex.justify-between.items-center.px-1");

      const title = titleElement?.textContent.trim();
      const author = authorElement?.textContent.trim();

      if (title && author && headingContainer && !headingContainer.nextElementSibling?.classList.contains("mam-button-container")) {
        const topButtons = createMamButtons(title, author, true);
        headingContainer.insertAdjacentElement("afterend", topButtons);
        console.log("[SG+] 'Search MAM' series buttons added at top!");
      }
    }

    const containers = document.querySelectorAll("div.book-title-author-and-series");
    console.log("[SG+] Found book containers:", containers.length);
    if (!containers.length) return false;

    let allValid = true;

    containers.forEach(container => {
      if (container.querySelector(".mam-button-container")) return;

      let title = null;
      let author = null;

      const h3 = container.querySelector("h3");
      const h1 = container.querySelector("h1");

      if (isBrowsePage) {
        // Browse page structure: h1 > a (title), p.font-body > a (author)
        const titleLink = h1?.querySelector("a[href*='/books/']");
        const authorLink = container.querySelector("p.font-body a[href*='/authors/']");

        title = titleLink?.textContent.trim() ?? null;
        author = authorLink?.textContent.trim() ?? null;
      } else if (isBookPage) {
        // Book page structure: h3 has title text node, p inside h3 has author link
        if (h3) {
          const firstNode = h3.childNodes[0];
          title = firstNode?.nodeType === Node.TEXT_NODE ? firstNode.textContent.trim() : null;

          const authorLink = h3.querySelector("p.font-body a[href*='/authors/']");
          author = authorLink?.textContent.trim() ?? null;
        }
      } else if (isSeriesPage) {
        // Inside a series, containers are listings of books
        const titleLink = h3?.querySelector("a[href*='/books/']");
        const authorLink = container.querySelector("p.font-body a[href*='/authors/']");

        title = titleLink?.textContent.trim() ?? null;
        author = authorLink?.textContent.trim() ?? null;
      }

      if (!title || !author) {
        console.warn("[SG+] Missing title or author for a container:", container);
        allValid = false;
        return;
      }

      const buttons = createMamButtons(title, author, false);
      container.appendChild(buttons);
    });

    console.log("[SG+] 'Search MAM' buttons added to all containers.");
    return allValid;
  }

  function startUnifiedRetryLoop() {
    clearInterval(retryIntervalId);
    retryCount = 0;

    retryIntervalId = setInterval(() => {
      if (retryCount >= maxRetries) {
        clearInterval(retryIntervalId);
        console.log("[SG+] Max retries reached, stopping.");
        return;
      }

      if (!document.querySelector(".mam-button-container")) {
        console.log(`[SG+] Buttons missing, re-adding... Retry ${retryCount}`);
        const success = addButtonsIfReady();
        retryCount++;
        if (success) {
          clearInterval(retryIntervalId);
        }
      }
    }, 1000);
  }

  function setupNavigationListener() {
    const originalPushState = history.pushState;
    history.pushState = function (...args) {
      originalPushState.apply(this, args);
      window.dispatchEvent(new Event("locationchange"));
    };

    const originalReplaceState = history.replaceState;
    history.replaceState = function (...args) {
      originalReplaceState.apply(this, args);
      window.dispatchEvent(new Event("locationchange"));
    };

    window.addEventListener("popstate", () => {
      window.dispatchEvent(new Event("locationchange"));
    });

    window.addEventListener("locationchange", () => {
      setTimeout(() => {
        startUnifiedRetryLoop();
      }, 300);
    });
  }

  window.addEventListener("load", () => {
    console.log("[SG+] Script loaded.");
    startUnifiedRetryLoop();
    setupNavigationListener();
  });

})();