// ==UserScript== // @name GGn No-Intro Helper // @description A GGn user script to help with No-Intro uploads/trumps // @namespace http://tampermonkey.net/ // @version 2.3.0 // @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)=>{ 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/")); } ;// 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/constants.ts // REGEXES const PARENS_TAGS_REGEX = /\(.*?\)/g; const NO_INTRO_TAGS_REGEX = /\((Unl|Proto|Sample|Aftermarket|Homebrew)\)|\(Rev \d+\)|\(v[\d\.]+\)|\(Beta(?: \d+)?\)/; // 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 = { USA: "English", Europe: "English", Japan: "Japanese", World: "English", "USA, Europe": "English", Other: "English", Korea: "Korean", Taiwan: "Chinese" }; 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/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/utils/noIntroToGGnLanguage.ts function noIntroToGGnLanguage(region, possiblyLanguages) { if (possiblyLanguages === undefined) { // @ts-expect-error return REGION_TO_LANGUAGE[region] || "Other"; } const twoLetterCodes = possiblyLanguages.split(",").map((l)=>l.trim().toLowerCase()); const isLanguages = twoLetterCodes.every((l)=>l.length === 2); if (!isLanguages || twoLetterCodes.length === 0) { // @ts-expect-error return REGION_TO_LANGUAGE[region] || "Other"; } if (twoLetterCodes.length > 1) { return "Multi-Language"; } return TWO_LETTER_REGION_CODE_TO_NAME[twoLetterCodes[0]] || "Other"; } ;// 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 [region, possiblyLanguages] = title.match(/\(.+?\)/g).map((p)=>p.slice(1, -1)); const matchedGGnRegion = GGN_REGIONS.find((r)=>r === region) || "Other"; const matchedGGnLanguage = noIntroToGGnLanguage(matchedGGnRegion, possiblyLanguages); // 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 extension, title, filename: title + "." + extension, language: matchedGGnLanguage, region: matchedGGnRegion, 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 = '

Loading...

'; ajax.get("torrents.php?action=torrentfilelist&torrentid=" + torrentId, function(response) { // @ts-expect-error $("#files_" + torrentId).ghide(); // @ts-expect-error $("#files_" + torrentId).raw().innerHTML = response; resolve(parseFromDom()); }); } else { resolve(parseFromDom()); } }); } ;// CONCATENATED MODULE: ./src/utils/dom/checkIfTrumpable.ts async function checkIfTrumpable(torrent) { try { const { title , cached } = await fetchNoIntro(torrent.noIntroLink); const desiredFilename = title + ".zip"; const files = await fetchTorrentFilelist(torrent.torrentId); if (files.length !== 1) { return { trumpable: true, desiredFilename, cached, inditermint: "Couldn't determine if the torrent is trumpable -\nMultiple/No zip files found in torrent" }; } const actualFilename = files[0]; return { trumpable: desiredFilename !== actualFilename, desiredFilename, actualFilename, cached }; } catch (err) { console.error("GGn No-Intro Helper: Error checking trumpability", err); return { trumpable: true, cached: false, inditermint: "Couldn't determine if the torrent is trumpable -\nFailed fetching No-Intro:\n" + err.message }; } } ;// CONCATENATED MODULE: ./src/inserts/smallPre.ts function smallPre(text, bgColor) { return `
${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 = $(`