Greasy Fork

Greasy Fork is available in English.

Zibzab's GameDox/Rom Upload Helper

try to take over the world :)

当前为 2022-09-03 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Zibzab's GameDox/Rom Upload Helper
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  try to take over the world :)
// @author       BestGrapeLeaves
// @match        https://gazellegames.net/upload.php?groupid=*
// @match        https://gazellegames.net/torrents.php?id=*
// @icon         https://i.imgur.com/UFOk0Iu.png
// @grant        GM_xmlhttpRequest
// @grant        GM_listValues
// @grant        GM_deleteValue
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      datomatic.no-intro.org
// @license      MIT
// ==/UserScript==

// cache class based on GM_setValue, GM_getValue, GM_deleteValue, GM_listValues global functions. With expiration.
class GMCache {
  constructor(name) {
    this.cache = {};
    this.name = name;
  }

  getKeyName(key) {
    return `zibzabhelper.cache${this.name}.${key}`;
  }

  get(key, fallback) {
    const whenNotFound = () =>
      typeof fallback === "function" ? fallback() : fallback;

    const res = GM_getValue(this.getKeyName(key));
    if (res === undefined) {
      return whenNotFound();
    }
    const { value, expires } = res;
    if (expires && expires < Date.now()) {
      this.delete(key);
      return whenNotFound();
    }

    return value;
  }

  set(key, value, ttl) {
    const expires = Date.now() + ttl;
    GM_setValue(this.getKeyName(key), { value, expires });
  }

  delete(key) {
    GM_deleteValue(this.getKeyName(key));
  }

  cleanUp() {
    const keys = GM_listValues();
    keys.forEach((key) => {
      if (key.startsWith(this.getKeyName(""))) {
        const { expires } = GM_getValue(key);
        if (expires < Date.now()) {
          GM_deleteValue(key);
        }
      }
    });
  }
}

// Code is a spaghetti mess, don't read it. Do something else with your time.
(function () {
  "use strict";
  const noIntroCache = new GMCache("no-intro");

  const PARENS_TAGS_REGEX = /\(.*?\)/g;
  const NO_INTRO_TAGS_REGEX =
    /\((Unl|Proto|Sample|Aftermarket|Homebrew)\)|\(Rev \d+\)|\(v[\d\.]+\)|\(Beta(?: \d+)?\)/;
  const GAME_DOX_INSERT = `[align=center] pdf pages
[/align]
`;
  const genRomInsert = (
    url = "xxx",
    filename = "xxx"
  ) => `[align=center]${filename} matches [url=${url}]No-Intro checksum[/url]
Compressed with [url=https://sourceforge.net/projects/trrntzip/]torrentzip.[/url][/align]
`;

  const regionToLanguage = {
    USA: "English",
    Europe: "English",
    Japan: "Japanese",
    World: "English",
    "USA, Europe": "English",
    Other: "English",
    Korea: "Korean",
    Taiwan: "Chinese",
  };

  const twoLetterLanguageCodeToGGn = {
    en: "English",
    de: "German",
    fr: "French",
    cz: "Czech",
    zh: "Chinese",
    it: "Italian",
    ja: "Japanese",
    ko: "Korean",
    pl: "Polish",
    pt: "Portuguese",
    ru: "Russian",
    es: "Spanish",
  };

  const parseLanguage = (region, possiblyLanguages) => {
    if (possiblyLanguages === undefined) {
      return regionToLanguage[region] || "Other";
    }

    const twoLetterCodes = possiblyLanguages
      .split(",")
      .map((l) => l.trim().toLowerCase());
    const isLanguages = twoLetterCodes.every((l) => l.length === 2);
    if (!isLanguages || twoLetterCodes.length === 0) {
      return regionToLanguage[region] || "Other";
    }

    if (twoLetterCodes.length > 1) {
      return "Multi-Language";
    }

    return twoLetterLanguageCodeToGGn[twoLetterCodes[0]] || "Other";
  };

  function noIntroLinkForTorrentId(torrentId) {
    const links = $(`#torrent_${torrentId} #description a`);
    return links
      .map(function () {
        return $(this).attr("href");
      })
      .get()
      .map((link) => {
        const url = new URL(link);
        url.protocol = "https:"; // Rarely descriptions have the http protocol
        return url.toString();
      })
      .find(link => link.startsWith("https://datomatic.no-intro.org/"))
  }

  function fetchNoIntro(url) {
    return new Promise((resolve, reject) => {
      const cached = noIntroCache.get(url);
      if (cached) {
        console.log("Using cached no-intro data", url, cached);
        resolve({ ...cached, cached: true });
        return;
      }

      GM_xmlhttpRequest({
        method: "GET",
        url,
        timeout: 5000,
        onload: ({ responseText }) => {
          try {
            const parser = new DOMParser();
            const scraped = parser.parseFromString(responseText, "text/html");

            // HTML is great
            const dumpsTitle = [
              ...scraped.querySelectorAll("td.TableTitle"),
            ].find((td) => td.innerText.trim() === "Dump(s)");

            if (!dumpsTitle) {
              window.GMPARSER = scraped;
              console.err('zibzab dumps title not found, set parser as global: GMPARSER', responseText);
              new Error("No dump's title found");
            }

            const filename =
              dumpsTitle.parentElement.parentElement.parentElement.nextElementSibling
                .querySelector(
                  "table > tbody > tr:nth-child(2) > td:last-child"
                )
                .innerText.trim();
            const title = scraped
              .querySelector("tr.romname_section > td")
              .innerText.trim();
            const parenMatches = title
              .match(/\(.+?\)/g)
              .map((p) => p.slice(1, -1));
            const [region, possiblyLanguages] = parenMatches;
            const matchedGGnRegion =
              [
                "USA",
                "Europe",
                "Japan",
                "Asia",
                "Australia",
                "France",
                "Germany",
                "Spain",
                "Italy",
                "UK",
                "Netherlands",
                "Sweden",
                "Russia",
                "China",
                "Korea",
                "Hong Kong",
                "Taiwan",
                "Brazil",
                "Canada",
                "Japan, USA",
                "Japan, Europe",
                "USA, Europe",
                "Europe, Australia",
                "Japan, Asia",
                "UK, Australia",
                "World",
                "Region-Free",
                "Other",
              ].find((r) => r === region) || "Other";
            const matchedGGnLanguage = parseLanguage(
              matchedGGnRegion,
              possiblyLanguages
            );
            const res = { filename, matchedGGnRegion, matchedGGnLanguage };
            // One hour seems appropriate
            noIntroCache.set(url, res, 1000 * 60 * 60);
            resolve({ ...res, cached: false });
          } catch (err) {
            console.error("zibzab helper failed to parse no-intro:", err);
            reject(
              new Error(
                "Failed to parse no-intro :/\nPlease report to BestGrapeLeaves,\nthe error was logged to the browser console"
              )
            );
          }
        },
        ontimeout: () => {
          reject(new Error("Request to no-intro timed out after 5 seconds"));
        },
      });
    });
  }

  // We are fetching files for checking, might as well reduce load and save to dom
  function fetchTorrentFilesWithoutShowing(torrentId) {
    const fromDOM = () =>
      $(
        `#files_${torrentId} > table > tbody > tr:not(.colhead_dark) > td:first-child`
      )
        .map(function () {
          return $(this).text();
        })
        .get();

    return new Promise((resolve) => {
      if ($("#files_" + torrentId).raw().innerHTML === "") {
        // $('#files_' + torrentId).gshow().raw().innerHTML = '<h4>Loading...</h4>';
        ajax.get(
          "torrents.php?action=torrentfilelist&torrentid=" + torrentId,
          function (response) {
            $("#files_" + torrentId).ghide();
            $("#files_" + torrentId).raw().innerHTML = response;

            resolve(fromDOM());
          }
        );
      } else {
        resolve(fromDOM());
      }
    });
  }

  async function checkForTrumpPossibility(torrentId) {
    const url = noIntroLinkForTorrentId(torrentId);
    if (!url) {
      return { trumpable: false, cached: false };
    }

    let info;
    try {
      info = await fetchNoIntro(url);
    } catch (err) {
      return {
        trumpable: true,
        cached: true, // Might as well wait if an error occurred
        inditermint:
          "Couldn't determine if the torrent is trumpable -\nFailed fetching No-Intro:\n" +
          err.message,
      };
    }

    const expectedFilename =
      info.filename.split(".").slice(0, -1).join(".") + ".zip";

    const files = await fetchTorrentFilesWithoutShowing(torrentId);
    if (files.length !== 1) {
      return {
        trumpable: true,
        expectedFilename,
        cached: info.cached,
        inditermint:
          "Couldn't determine if the torrent is trumpable -\nMultiple/No zip files found in torrent",
      };
    }

    const actualFilename = files[0];

    if (expectedFilename !== actualFilename) {
      return {
        trumpable: true,
        expectedFilename,
        actualFilename,
        cached: info.cached,
      };
    }

    return {
      trumpable: false,
      expectedFilename,
      actualFilename,
      cached: info.cached,
    };
  }

  function getNoIntroTorrentsOnPage() {
    return $('a[title="Permalink"]')
      .map(function () {
        const torrentId = $(this)
          .attr("href")
          .replace(/.*?\?torrentid=/, "");
        console.log("ac", torrentId);

        const noIntroLink = noIntroLinkForTorrentId(torrentId);
        if (!noIntroLink) {
          return false;
        }

        return { torrentId, a: $(this), noIntroLink };
      })
      .get()
      .filter((x) => x);
  }

  // Add Copy button, stolen boilerplate shamelessly from trump helper script
  function insertAddCopyHelpers() {
    getNoIntroTorrentsOnPage().forEach(({ torrentId, a, noIntroLink }) => {
      const editionInfo = a
        .parents(".group_torrent")
        .parent()
        .prev()
        .find(".group_torrent > td > strong")
        .text();
      // Convert to upload format of edition info
      const [editionYear, ...rest] = editionInfo.split(" - ");
      const editionName = rest.join(" - ");
      const formatedEditionInfo = `${editionName} (${editionYear})`;

      const groupId = window.location.href.replace(/.*?\?id=/, "");
      const params = new URLSearchParams(url.search);
      params.set("groupid", groupId);
      params.set("edition", formatedEditionInfo);
      params.set("no-intro", noIntroLink);

      const addCopyButton = $(
        `<a href="upload.php?${params.toString()}" title="Add Copy" id="ac_${torrentId}">AC</a>`
      );
      $([" | ", addCopyButton]).insertAfter(a);
    });
  }

  async function insertTrumpSuggestions(torrents) {
    const checkForTrumpsButton = $("#check-for-no-intro-trumps-button");
    checkForTrumpsButton.prop("disabled", true);
    checkForTrumpsButton.css("background-color", "pink");
    checkForTrumpsButton.css("color", "darkslategray");
    checkForTrumpsButton.css("box-shadow", "none");

    let trumps = 0;
    let prevCached = false;
    for (let i = 0; i < torrents.length; i++) {
      // timeout to avoid rate limiting
      if (!prevCached) {
        await new Promise((resolve) => setTimeout(resolve, 500));
      }

      const torrent = torrents[i];
      checkForTrumpsButton.val(
        `Checking For Trumps ${i + 1}/${torrents.length}...`
      );

      const insert = (details, inditermint) => {
        const trumpNotice = $(
          `<div>
          </div>`
        );
        const trumpNoticeDetails = $(
          `<div style="font-weight: normal; color: white;">${details}</div>`
        ).hide();
        const trumpNoticeTitle = $(`
            <span style="color: ${
              inditermint ? "pink" : "hotpink"
            }; font-size: 14px; font-weight: bold;">${
          inditermint
            ? "Unable to determine if torrent is trumpable:"
            : "This torrent can be trumped!"
        }</span>`);
        const trumpNoticeLinks = $(
          `<div class="trump-notice-links" id="trump-notice-links-${torrent.torrentId}" style="font-weight: normal; font-size: 11px; display: inline; margin: 5px;"></div>`
        );
        const trumpNoticeToggleDetailsLink = $(
          `<span style="cursor: pointer;">[Expand]</span>`
        );

        trumpNoticeToggleDetailsLink.click(() => {
          const collapsed = trumpNoticeToggleDetailsLink.text() === "[Expand]";

          if (collapsed) {
            trumpNoticeToggleDetailsLink.text("[Collapse]");
            trumpNoticeDetails.show();
          } else {
            trumpNoticeToggleDetailsLink.text("[Expand]");
            trumpNoticeDetails.hide();
          }
        });

        trumpNoticeLinks.append(trumpNoticeToggleDetailsLink);
        trumpNoticeTitle.append(trumpNoticeLinks);
        trumpNotice.append(trumpNoticeTitle);
        trumpNotice.append(trumpNoticeDetails);

        let currentlyAdaptedToSmallScreen;
        const placeTrumpNotice = () => {
          console.log("adapting", window.innerWidth);
          if (window.innerWidth <= 800) {
            if (currentlyAdaptedToSmallScreen) {
              return;
            }

            currentlyAdaptedToSmallScreen = true;
            $(`#torrent${torrent.torrentId}`).css("border-bottom", "none");
            trumpNotice.css("margin-left", "25px");
            trumpNotice.detach();
            trumpNotice.insertAfter(`#torrent${torrent.torrentId}`);
          } else {
            if (currentlyAdaptedToSmallScreen === false) {
              return;
            }

            currentlyAdaptedToSmallScreen = false;
            $(`#torrent${torrent.torrentId}`).css("border-bottom", "");
            trumpNotice.css("margin-left", "0px");
            trumpNotice.detach();
            trumpNotice.appendTo(
              `#torrent${torrent.torrentId} > td:first-child`
            );
          }
        };
        placeTrumpNotice();
        $(window).resize(placeTrumpNotice);
      };

      const res = await checkForTrumpPossibility(torrent.torrentId);
      if (!res.trumpable) {
        continue;
      }

      if (res.inditermint) {
        insert(res.inditermint, true);
        continue;
      }

      trumps++;
      const pre = (text, bgColor) =>
        `<pre style="
          padding: 0px;
          margin: 0;
          background-color: ${bgColor};
          color: black;
          font-weight: bold;
          font-size: 12px;
          padding-left: 3px;
          padding-right: 3px;
          width: fit-content;
        ">${text}</pre>`;
      insert(
        `The filename in the torrent is: ${pre(
          res.actualFilename,
          "lightcoral"
        )} but the desired filename, based on <i>No-Intro</i> is: ${pre(
          res.expectedFilename,
          "lightgreen"
        )}`
      );

      prevCached = res.cached;
    }

    if (trumps === 0) {
      checkForTrumpsButton.val("No Trumps Found");
    } else if (trumps === 1) {
      checkForTrumpsButton.val("1 Trump Found");
    } else {
      checkForTrumpsButton.val(`${trumps} Trumps Found`);
    }
  }

  function insertTrumpButtonAndMaybeCheck() {
    const torrents = getNoIntroTorrentsOnPage();

    if (torrents.length === 0) {
      return;
    }

    const checkForTrumpsButton = $(
      `<input id="check-for-no-intro-trumps-button" type="button" value="Check for No-Intro Trumps" style="background: hotpink; color: black; font-weight: bold; margin-left: 10px;"/>`
    );
    $(".torrent_table > tbody > tr:first-child > td:first-child")
      .first()
      .append(checkForTrumpsButton);

    if (torrents.length <= 4) {
      insertTrumpSuggestions(torrents);
    }

    checkForTrumpsButton.click((e) => {
      e.stopImmediatePropagation();
      insertTrumpSuggestions(torrents);
    });
  }

  // No Intro Button
  function makeNoIntro(filename) {
    const tags = filename
      ? filename
          .match(PARENS_TAGS_REGEX)
          .filter((p) => NO_INTRO_TAGS_REGEX.test(p))
          .join(" ")
      : "";

    // Release type = ROM
    $("select#miscellaneous").val("ROM").change();
    // It is a special edition
    if (!$("input#remaster").prop("checked")) {
      $("input#remaster").prop("checked", true);
      Remaster();
    }
    // Not a scene release
    $("#ripsrc_home").prop("checked", true);
    // Update title
    updateReleaseTitle($("#title").raw().value + " " + tags);

    // Get url params
    const params = new URLSearchParams(window.location.search);

    // Set correct edition (fallback to guessing)
    const setEdition = (edition) => {
      try {
        $("#groupremasters").val(edition).change();
        GroupRemaster();
      } catch {
        // group remaster always throws (regardless of the userscript)
      }
    };
    const editionInfo = params.get("edition");
    $("#groupremasters > option").each(function () {
      const title = $(this).text().toLowerCase();
      console.log("checking", title);
      if (editionInfo && title === editionInfo.toLowerCase()) {
        setEdition($(this).val());
        return false; // This breaks out of the jquery loop
      } else {
        if (title.includes("no-intro") || title.includes("nointro")) {
          setEdition($(this).val());
        }
      }
    });

    // Trigger no-intro link scraper
    const noIntroLink = params.get("no-intro");
    if (noIntroLink) {
      $("#no-intro-url-input").val(noIntroLink).change();
    }
  }

  function noIntroUI() {
    // elements
    const noIntroContainer = $(
      `<tr id="no-intro-url" name="no-intro-url">
      <td class="label">No-Intro Link</td>
    </tr>`
    );
    const noIntroInput = $(
      '<input type="text" id="no-intro-url-input" name="no-intro-url-input" size="70%" class="input_tog" value="">'
    );
    const noIntroError = $(
      '<p id="no-intro-url-error" name="no-intro-url-error" style="color: red; white-space:pre-line;"></p>'
    ).hide();
    const noIntroLoading = $(
      '<p id="no-intro-url-loading" name="no-intro-url-loading" style="color: green;">Loading...</p>'
    ).hide();
    // structure
    const td = $("<td></td>");
    td.append(noIntroInput);
    td.append(noIntroError);
    td.append(noIntroLoading);
    noIntroContainer.append(td);
    // utils
    const error = (msg) => {
      noIntroError.text(msg);
      noIntroError.show();
    };
    const loading = (isLoading) => {
      if (isLoading) {
        noIntroLoading.show();
      } else {
        noIntroLoading.hide();
      }
    };
    return { loading, error, noIntroContainer, noIntroInput, noIntroError };
  }

  function torrentViewPage() {
    insertAddCopyHelpers();
    insertTrumpButtonAndMaybeCheck();
  }

  function uploadPage() {
    // Insert No Intro button
    const nointro = $('<input type="button" value="No-Intro"></input>');
    nointro.click(() => makeNoIntro($("#file").val()));
    nointro.insertAfter("#file");

    // Link parser UI
    const { noIntroContainer, noIntroInput, noIntroError, error, loading } =
      noIntroUI();

    async function submitNoInput() {
      noIntroError.hide();
      const url = noIntroInput.val();

      if (justChecked === url) {
        return;
      }

      if (!url.startsWith("https://datomatic.no-intro.org/")) {
        error("Invalid URL");
        return;
      }

      justChecked = url;
      loading(true);

      try {
        const { filename, matchedGGnLanguage, matchedGGnRegion } =
          await fetchNoIntro(url);
        $("textarea#release_desc").val(genRomInsert(url, filename));
        $("select#region").val(matchedGGnRegion);
        $("select#language").val(matchedGGnLanguage);
      } catch (err) {
        error(err.message || err || "An unexpected error has occurred");
      } finally {
        loading(false);
      }
    }

    // watch link input
    let justChecked = "";
    noIntroInput.on("paste", (e) => {
      e.preventDefault();
      const text = e.originalEvent.clipboardData.getData("text/plain");
      noIntroInput.val(text);
      submitNoInput();
    });
    noIntroInput.change(submitNoInput);
    // React to release type change
    $("select#miscellaneous").change(function () {
      const selected = $("select#miscellaneous option:selected").text();
      if (selected === "GameDOX") {
        noIntroContainer.detach();
        $("input#release_title").val(
          $("input#release_title").val() + " - Manual"
        );
        $("select#gamedox").val("Guide").change();
        $("select#format").val("PDF").change();
        $("input#scan").click();
        Scan();
        $("textarea#release_desc").val(
          $("textarea#release_desc").val() + GAME_DOX_INSERT
        );
      } else if (selected === "ROM") {
        noIntroContainer.insertBefore("#regionrow");
        $("textarea#release_desc").val(genRomInsert());
      } else {
        noIntroContainer.detach();
      }
    });
  }

  if (window.location.pathname === "/torrents.php") {
    torrentViewPage();
  } else if (window.location.pathname === "/upload.php") {
    uploadPage();
  }
})();