// ==UserScript== // @name GGn No-Intro Helper // @description A GGn user script to help with No-Intro uploads/trumps // @namespace http://tampermonkey.net/ // @version 2.0.1 // @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; } return { torrentId, a: $(this), noIntroLink }; }).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); } } }); } 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"); function fetchNoIntro(url) { return new Promise((resolve, reject)=>{ 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 info = { filename, title, 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(torrentId) { const url = extractNoIntroLinkFromDescription(torrentId); if (!url) { return { trumpable: false, cached: false }; } try { const { filename , cached } = await fetchNoIntro(url); const desiredFilename = filename.split(".").slice(0, -1).join(".") + ".zip"; const files = await fetchTorrentFilelist(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: true, 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 insertTrumpNotice(info) { const { inditermint , actualFilename , desiredFilename , torrentId } = info; // Settings const color = inditermint ? "pink" : "hotpink"; const title = inditermint ? "Couldn't determine if the torrent is trumpable:" : "This torrent is trumpable!"; const details = inditermint ?? `The filename in the torrent is: ${smallPre(actualFilename, "lightcoral")} but the desired filename, based on No-Intro is: ${smallPre(desiredFilename, "lightgreen")}`; // Elements const detailsDiv = $(`
${details}
`).hide(); const titleSpan = $(` ${title}`); const actionsDiv = $(``); // Toggle Details const toggleDetailsActionSpan = $(`[Expand]`); toggleDetailsActionSpan.click(()=>{ const collapsed = toggleDetailsActionSpan.text() === "[Expand]"; if (collapsed) { toggleDetailsActionSpan.text("[Collapse]"); detailsDiv.show(); } else { toggleDetailsActionSpan.text("[Expand]"); detailsDiv.hide(); } }); // Tree const wrapper = $(`
`); actionsDiv.append(toggleDetailsActionSpan); 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({ ...info, links: actionsDiv }); } } ;// CONCATENATED MODULE: ./src/inserts/insertTrumpSuggestions.ts async function insertTrumpSuggestions(torrents) { const { disable , progress } = checkForTrumpsButton(); disable(); let trumps = 0; let prevCached = false; 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.torrentId); const { trumpable , cached } = TrumpCheckResult; if (!trumpable) { continue; } // Follow up insertTrumpNotice({ ...TrumpCheckResult, ...torrent }); trumps++; prevCached = cached; } if (trumps === 0) { progress("No Trumps Found"); } else if (trumps === 1) { progress("1 Trump Found"); } else { progress(`${trumps} Trumps Found`); } } ;// 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) { insertTrumpSuggestions(torrents); } button.click((e)=>{ e.stopImmediatePropagation(); insertTrumpSuggestions(torrents); }); } function torrentsPageMain() { insertAddCopyHelpers(); trumpSuggestions(); } ;// CONCATENATED MODULE: ./src/inserts/uploadLinkParserUI.ts function uploadNoIntroLinkParserUI() { // elements const container = $(` No-Intro Link `); const input = $(''); const error = $('

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

Loading...

').hide(); // structure const td = $(""); td.append(input); td.append(error); td.append(loading); container.append(td); // utils const setError = (msg)=>{ error.text(msg); error.show(); }; const setLoading = (isLoading)=>{ if (isLoading) { loading.show(); } else { loading.hide(); } }; return { loading, error, container, input, setError, setLoading }; } ;// CONCATENATED MODULE: ./src/utils/dom/setUploadEdition.ts function setUploadEdition(edition) { try { $("#groupremasters").val(edition).change(); GroupRemaster(); } catch { // group remaster always throws (regardless of the userscript) } } ;// CONCATENATED MODULE: ./src/utils/generateTorrentDescription.ts const generateTorrentDescription = function() { let url = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : "xxx", filename = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : "xxx"; return `[align=center]${filename} matches [url=${url}]No-Intro checksum[/url] Compressed with [url=https://sourceforge.net/projects/trrntzip/]torrentzip.[/url][/align] `; }; ;// CONCATENATED MODULE: ./src/pages/upload.ts function linkParser() { // UI const { error , container , input , setError , setLoading } = uploadNoIntroLinkParserUI(); // watch link input let justChecked = ""; input.on("paste", (e)=>{ e.preventDefault(); const text = e.originalEvent.clipboardData.getData("text/plain"); input.val(text); submit(); }); input.change(submit); // React to release type change, and insert input $("select#miscellaneous").change(function() { const selected = $("select#miscellaneous option:selected").text(); if (selected === "ROM") { container.insertBefore("#regionrow"); $("textarea#release_desc").val(generateTorrentDescription()); /// xxx temporary } else { container.detach(); } }); // handle submit async function submit() { // Prechecks error.hide(); const url = input.val(); if (justChecked === url) { return; } if (!url.startsWith("https://datomatic.no-intro.org/")) { setError("Invalid URL"); return; } // Go justChecked = url; setLoading(true); try { const { filename , language , region } = await fetchNoIntro(url); $("textarea#release_desc").val(generateTorrentDescription(url, filename)); $("select#region").val(region); $("select#language").val(language); } catch (err) { setError(err.message || err || "An unexpected error has occurred"); } finally{ setLoading(false); } } } function magicNoIntroPress() { const filename = $("#file").val(); 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); // @ts-expect-error Update title updateReleaseTitle($("#title").raw().value + " " + tags); // Get url params const params = new URLSearchParams(window.location.search); // Set correct edition (fallback to guessing) const editionInfo = params.get("edition"); $("#groupremasters > option").each(function() { const title = $(this).text().toLowerCase(); console.log("checking", title); if (editionInfo && title === editionInfo.toLowerCase()) { setUploadEdition($(this).val()); return false; // This breaks out of the jquery loop } else { if (title.includes("no-intro") || title.includes("nointro")) { setUploadEdition($(this).val()); } } }); // Trigger no-intro link scraper const noIntroLink = params.get("no-intro"); if (noIntroLink) { $("#no-intro-url-input").val(noIntroLink).change(); } } function uploadPageMain() { // Insert No Intro magic button const noIntroMagicButton = $(''); noIntroMagicButton.click(()=>magicNoIntroPress()); noIntroMagicButton.insertAfter("#file"); linkParser(); } ;// CONCATENATED MODULE: ./src/index.ts async function main() { console.log("GGn No-Intro Helper: Starting..."); if (window.location.pathname === "/torrents.php") { torrentsPageMain(); } else if (window.location.pathname === "/upload.php") { uploadPageMain(); } } main().catch((e)=>{ console.log(e); }); /******/ })() ;