// ==UserScript== // @name GGn No-Intro Helper // @description A GGn user script to help with No-Intro uploads/trumps // @namespace http://tampermonkey.net/ // @version 2.4.4 // @author BestGrapeLeaves // @license MIT // @match *://gazellegames.net/upload.php?groupid=* // @match *://gazellegames.net/torrents.php?id=* // @grant unsafeWindow // @grant GM_xmlhttpRequest // @grant GM_listValues // @grant GM_deleteValue // @grant GM_setValue // @grant GM_getValue // @connect datomatic.no-intro.org // @icon https://i.imgur.com/UFOk0Iu.png // @downloadURL none // ==/UserScript== /******/ (() => { // webpackBootstrap /******/ "use strict"; var __webpack_exports__ = {}; ;// CONCATENATED MODULE: ./src/inserts/checkForTrumpsButton.ts function checkForTrumpsButton() { const existing = $("#check-for-no-intro-trumps-button"); const button = existing.length > 0 ? existing : $(``); const progress = (text)=>{ button.val(text); }; const disable = ()=>{ button.prop("disabled", true); button.css("background-color", "pink"); button.css("color", "darkslategray"); button.css("box-shadow", "none"); }; const insert = ()=>{ button.detach(); $(".torrent_table > tbody > tr:first-child > td:first-child").first().append(button); }; return { disable, progress, insert, button }; } ;// CONCATENATED MODULE: ./src/utils/dom/extractNoIntroLinkFromDescription.ts function extractNoIntroLinkFromDescription(torrentId) { const links = $(`#torrent_${torrentId} #description a`); return links.map(function() { return $(this).attr("href"); }).get().map((link)=>{ try { const url = new URL(link); url.protocol = "https:"; // Rarely descriptions have the http protocol return url.toString(); } catch { return ""; } }).find((link)=>link.startsWith("https://datomatic.no-intro.org/")); } ;// CONCATENATED MODULE: ./src/utils/dom/getNoIntroTorrentsOnPage.ts function notFalse(x) { return x !== false; } function getNoIntroTorrentsOnPage() { return $('a[title="Permalink"]').map(function() { const torrentId = new URLSearchParams($(this).attr("href").replace("torrents.php", "")).get("torrentid"); const noIntroLink = extractNoIntroLinkFromDescription(torrentId); if (!noIntroLink) { return false; } const reported = $(this).parent().parent().find(".reported_label").text() === "Reported"; return { torrentId, a: $(this), noIntroLink, reported, permalink: window.location.origin + "/" + $(this).attr("href") }; }).get().filter(notFalse); } ;// CONCATENATED MODULE: ./src/inserts/insertAddCopyHelpers.ts function insertAddCopyHelpers() { getNoIntroTorrentsOnPage().forEach((param)=>{ let { torrentId , a , noIntroLink } = param; // Extract edition information const editionInfo = a.parents(".group_torrent").parent().prev().find(".group_torrent > td > strong").text(); const [editionYear, ...rest] = editionInfo.split(" - "); const editionName = rest.join(" - "); const formatedEditionInfo = `${editionName} (${editionYear})`; // GroupId const groupId = new URLSearchParams(window.location.search).get("id"); // Create params const params = new URLSearchParams(); params.set("groupid", groupId); params.set("edition", formatedEditionInfo); params.set("no-intro", noIntroLink); // Insert button const addCopyButton = $(`AC`); $([ " | ", addCopyButton ]).insertAfter(a); }); } ;// CONCATENATED MODULE: ./src/utils/GMCache.ts class GMCache { getKeyName(key) { return `cache${this.name}.${key}`; } get(key) { const res = GM_getValue(this.getKeyName(key)); if (res === undefined) { return undefined; } const { value , expires } = res; if (expires && expires < Date.now()) { this.delete(key); return undefined; } 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); } } }); } clear() { const keys = GM_listValues(); keys.forEach((key)=>{ if (key.startsWith(this.getKeyName(""))) { GM_deleteValue(key); } }); } constructor(name){ this.name = name; } } ;// CONCATENATED MODULE: ./src/constants.ts // LISTS const GGN_REGIONS = [ "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", ]; // TABLES const REGION_TO_LANGUAGE = { World: "English", USA: "English", Europe: "English", Japan: "Japanese", Australia: "English", France: "French", Germany: "German", Italy: "Italian", UK: "English", Netherlands: "Other", Sweden: "Other", Russia: "Russian", China: "Chinese", Korea: "Korean", Taiwan: "Chinese", "Hong Kong": "Chinese", Brazil: "Portuguese", Canada: "English", "USA, Europe": "English", "Europe, Australia": "English", "UK, Australia": "English", Other: "Other" }; const TWO_LETTER_REGION_CODE_TO_NAME = { en: "English", de: "German", fr: "French", cz: "Czech", zh: "Chinese", it: "Italian", ja: "Japanese", ko: "Korean", pl: "Polish", pt: "Portuguese", ru: "Russian", es: "Spanish" }; ;// CONCATENATED MODULE: ./src/utils/identifyNoIntroTags.ts function identifyNoIntroTags(title) { const tags = title.match(/\(.+?\)/g).map((p)=>p.slice(1, -1)); let region = ""; let languages = []; let edition = []; let release = []; tags.forEach((tag)=>{ // Region if (GGN_REGIONS.includes(tag)) { region = tag; return; } // Language const maybeTwoLetterCodes = tag.split(",").map((l)=>l.trim().toLowerCase()); const isLanguages = maybeTwoLetterCodes.every((l)=>l.length === 2); if (isLanguages) { languages = maybeTwoLetterCodes; return; } // Edition if ([ "Proto", "Sample" ].includes(tag) || tag.startsWith("Beta") || tag.startsWith("Demo") || tag.endsWith("Virtual Console") || tag.includes("Edition") || tag.includes("Collection")) { edition.push(tag); return; } // None of the above release.push(tag); }); if (region === "") { release.shift(); region = "Other"; } let language; if (languages.length === 0) { language = REGION_TO_LANGUAGE[region] || "Other"; } else if (languages.length === 1) { language = TWO_LETTER_REGION_CODE_TO_NAME[languages[0]] || "Other"; } else { language = "Multi-Language"; } return { language: language, region: region, edition, release: release.map((t)=>`(${t})`).join(" ") }; } ;// CONCATENATED MODULE: ./src/utils/fetchNoIntro.ts const cache = new GMCache("no-intro"); // @ts-expect-error unsafeWindow.GGN_NO_INTRO_HELPER_CACHE = cache; function fetchNoIntro(url) { return new Promise((resolve, reject)=>{ if (url.endsWith("n=")) { return reject(new Error("Blacklist no-intro url. Fetch was aborted to prevent IP ban.")); } const cached = cache.get(url); if (cached) { resolve({ ...cached, cached: true }); return; } GM_xmlhttpRequest({ method: "GET", url, timeout: 5000, onload: (param)=>{ let { responseText } = param; 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) { // @ts-expect-error unsafeWindow.GMPARSER = scraped; console.error("GGn No-Intro Helper: dumps title not found, set parser as global: GMPARSER", responseText); throw 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(); // Region/Lang const { language , region , edition , release } = identifyNoIntroTags(title); // One hour seems appropriate const extension = filename.split(".").pop() || ""; const info = { // We stopped shipping entire filenames // when zibzab reported that it varies from dump to dump // like some can have a "bad" filename // title is 100% accurate, and filename shouldn't vary filename: title + "." + extension, extension, title, language, region, editionTags: edition, releaseTags: release, cached: false }; cache.set(url, info, 1000 * 60 * 60); resolve(info); } catch (err) { console.error("zibzab helper failed to parse no-intro:", err); reject(new Error("Failed to parse no-intro :/\nPlease report to BestGrapeLeaves,\nincluding the error that was logged to the browser console")); } }, ontimeout: ()=>{ reject(new Error("Request to No-Intro timed out after 5 seconds")); } }); }); } ;// CONCATENATED MODULE: ./src/utils/dom/fetchTorrentFilelist.ts // We are fetching files for checking, // might as well reduce load on servers and save to dom (like the button does) function fetchTorrentFilelist(torrentId) { const parseFromDom = ()=>$(`#files_${torrentId} > table > tbody > tr:not(.colhead_dark) > td:first-child`).map(function() { return $(this).text(); }).get(); return new Promise((resolve)=>{ // @ts-expect-error if ($("#files_" + torrentId).raw().innerHTML === "") { // $('#files_' + torrentId).gshow().raw().innerHTML = '
${text}`; } ;// CONCATENATED MODULE: ./src/inserts/insertTrumpNotice.ts function inditermintNoticeInfo(param) { let { inditermint } = param; return { title: "Couldn't determine if the torrent is trumpable:", details: inditermint, color: "pink" }; } function reportedNoticeInfo(param) { let { inditermint } = param; return { title: "Torrent was trumped and reported!", details: "", color: "var(--darkRed)" }; } function trumpableNoticeInfo(param) { let { actualFilename , desiredFilename } = param; return { title: "This torrent is trumpable!", details: `The filename in the torrent is: ${smallPre(actualFilename, "lightcoral")} but the desired filename, based on No-Intro is: ${smallPre(desiredFilename, "lightgreen")}`, color: "hotpink" }; } function reportableNoticeInfo(param) { let { fixedVersion , torrentId , actualFilename , desiredFilename , noIntroLink } = param; const form = $(""); const trumpingTorrentInput = $(``); const commentTextarea = $(``); commentTextarea.text(`ROM name changed on No-Intro. Reported filename : ${actualFilename} Trumping filename : ${desiredFilename} No-Intro for reference : ${noIntroLink}`); const submitInputButton = $(``); const errorMessage = $("").hide(); form.append(`
Trumping Torrent Permalink:
`, trumpingTorrentInput, `Report Comment:
`, commentTextarea, `Submit Report:
`, submitInputButton, errorMessage); submitInputButton.click(async ()=>{ // If it's disabled this should in theory not trigger, // but just in case jquery does some shenaningans // when you manually trigger a click event if (submitInputButton.prop("disabled") === true) { return; } errorMessage.hide(); submitInputButton.prop("disabled", true); const data = new FormData(); data.append("submit", "true"); data.append("torrentid", torrentId); data.append("categoryid", "1"); data.append("type", "trump"); data.append("sitelink", fixedVersion.permalink); data.append("extra", commentTextarea.val()); data.append("id_token", new Date().getTime().toString()); try { await fetch("/reportsv2.php?action=takereport", { method: "POST", body: data }); location.reload(); } catch (err) { console.error("Error submitting trump report", err); console.error("Form data sent:", Object.fromEntries([ ...data.entries() ])); errorMessage.text(`An error occurred while submitting the trump report. If you believe this is a problem with the script, please report to BestGrapeLeaves (including console logs if you can). Error Message: ${err.message}`); errorMessage.show(); submitInputButton.prop("disabled", false); } }); return { title: "Torrent needs to be reported for trump!", details: form, color: "#ff7600" }; } function insertTrumpNotice(torrent) { const { inditermint , fixedVersion , torrentId , reported } = torrent; // Settings let info; let type; if (inditermint) { type = "inditermint"; info = inditermintNoticeInfo(torrent); } else if (fixedVersion) { if (reported) { type = "reported"; info = reportedNoticeInfo(torrent); } else { type = "reportable"; info = reportableNoticeInfo(torrent); } } else { type = "trumpable"; info = trumpableNoticeInfo(torrent); } const { color , details , title } = info; // Elements const detailsDiv = $(``).hide(); detailsDiv.append(details); const titleSpan = $(` ${title}`); const actionsDiv = $(``); // Toggle Details if (type !== "reported") { const toggleDetailsActionSpan = $(`[Expand]`); toggleDetailsActionSpan.click(()=>{ const collapsed = toggleDetailsActionSpan.text() === "[Expand]"; if (collapsed) { toggleDetailsActionSpan.text("[Collapse]"); detailsDiv.show(); } else { toggleDetailsActionSpan.text("[Expand]"); detailsDiv.hide(); } }); actionsDiv.append(toggleDetailsActionSpan); } // Send Report if (type === "reportable") { const sendReportActionSpan = $(`[Send Report]`); sendReportActionSpan.click(()=>{ $(`#no-intro-helper-submit-trump-report-${torrentId}`).click(); }); actionsDiv.append(sendReportActionSpan); } // Cheer if (type === "reported") { const cheerActionSpan = $(`[Cheer]`); cheerActionSpan.click(()=>{ cheerActionSpan.text("HOORAY!"); cheerActionSpan.animate({ opacity: 0 }, // @ts-expect-error { duration: 2000, step: function(now) { cheerActionSpan.css({ transform: "rotate(" + now * 360 * 5 + "deg)" }); } }, "swing"); }); actionsDiv.append(cheerActionSpan); } // Tree const wrapper = $(``); titleSpan.append(actionsDiv); wrapper.append(titleSpan); wrapper.append(detailsDiv); // Place let currentlyAdaptedToSmallScreen; function placeTrumpNotice() { console.log("adapting", window.innerWidth); if (window.innerWidth <= 800) { if (currentlyAdaptedToSmallScreen) { return; } currentlyAdaptedToSmallScreen = true; $(`#torrent${torrentId}`).css("border-bottom", "none"); wrapper.css("margin-left", "25px"); wrapper.detach(); wrapper.insertAfter(`#torrent${torrentId}`); } else { if (currentlyAdaptedToSmallScreen === false) { return; } currentlyAdaptedToSmallScreen = false; $(`#torrent${torrentId}`).css("border-bottom", ""); wrapper.css("margin-left", "0px"); wrapper.detach(); wrapper.appendTo(`#torrent${torrentId} > td:first-child`); } } placeTrumpNotice(); $(window).resize(placeTrumpNotice); // Call global hook (for other scripts) // @ts-expect-error if (typeof unsafeWindow.GM_GGN_NOINTRO_HELPER_ADDED_LINKS === "function") { // @ts-expect-error unsafeWindow.GM_GGN_NOINTRO_HELPER_ADDED_LINKS({ ...torrent, links: actionsDiv }); } } ;// CONCATENATED MODULE: ./src/inserts/insertTrumpSuggestions.ts async function checkForImproperlyNamedTorrents(torrents) { const { disable , progress } = checkForTrumpsButton(); disable(); let prevCached = false; const results = []; for(let i = 0; i < torrents.length; i++){ const torrent = torrents[i]; progress(`Checking For Trumps ${i + 1}/${torrents.length}...`); // timeout to avoid rate limiting if (!prevCached) { await new Promise((resolve)=>setTimeout(resolve, 500)); } // Check trump const TrumpCheckResult = await checkIfTrumpable(torrent); const { cached } = TrumpCheckResult; prevCached = cached; results.push({ ...TrumpCheckResult, ...torrent }); } return results; } // Filter the torrents that have a trump uploaded function attachFixedVersionsToTorrents(torrents) { const trumpCanidates = []; const validTorrents = []; for (const torrent of torrents){ if (torrent.trumpable) { trumpCanidates.push(torrent); continue; } validTorrents.push(torrent); } // Efficiency is not my greatest of concerns, // if you want implement a graph theory solution in O(1) or something const processed = trumpCanidates.map((c)=>({ ...c, fixedVersion: validTorrents.find((v)=>!v.inditermint && v.noIntroLink === c.noIntroLink) })); return [ ...validTorrents, ...processed ]; } async function insertTrumpSuggestions(results) { const { progress } = checkForTrumpsButton(); let trumps = 0; results.forEach((torrent)=>{ if (!torrent.trumpable) { return; } if (!torrent.inditermint && !torrent.fixedVersion) { trumps++; } insertTrumpNotice(torrent); }); if (trumps === 0) { progress("No Trumps Found"); } else if (trumps === 1) { progress("1 Trump Found"); } else { progress(`${trumps} Trumps Found`); } } async function findAndDisplayTrumps() { const torrents = getNoIntroTorrentsOnPage(); const results = await checkForImproperlyNamedTorrents(torrents); const processed = attachFixedVersionsToTorrents(results); console.log("GGn No-Intro Helper: Trumps", processed); insertTrumpSuggestions(processed); } ;// CONCATENATED MODULE: ./src/pages/torrents.ts function trumpSuggestions() { const torrents = getNoIntroTorrentsOnPage(); if (torrents.length === 0) { return; } const { button , insert } = checkForTrumpsButton(); insert(); if (torrents.length <= 4) { findAndDisplayTrumps(); } button.click((e)=>{ e.stopImmediatePropagation(); findAndDisplayTrumps(); }); } function torrentsPageMain() { insertAddCopyHelpers(); trumpSuggestions(); } ;// CONCATENATED MODULE: ./src/inserts/uploadLinkParserUI.ts function uploadNoIntroLinkParserUI() { // elements const container = $(`Loading...
').hide(); const warning = $('').hide(); const goButton = $(''); // structure const td = $("