// ==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 // @downloadURL none // ==/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 = '

Loading...

'; 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 = $( `AC` ); $([" | ", 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 = $( `
` ); const trumpNoticeDetails = $( `
${details}
` ).hide(); const trumpNoticeTitle = $(` ${ inditermint ? "Unable to determine if torrent is trumpable:" : "This torrent can be trumped!" }`); const trumpNoticeLinks = $( `` ); const trumpNoticeToggleDetailsLink = $( `[Expand]` ); 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) => `
${text}
`; insert( `The filename in the torrent is: ${pre( res.actualFilename, "lightcoral" )} but the desired filename, based on No-Intro 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 = $( `` ); $(".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 = $( ` No-Intro Link ` ); const noIntroInput = $( '' ); const noIntroError = $( '

' ).hide(); const noIntroLoading = $( '

Loading...

' ).hide(); // structure const 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 = $(''); 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(); } })();