// ==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
// ==/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 : $(`<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;"/>`);
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 = $(`<a href="upload.php?${params.toString()}" title="Add Copy" id="ac_${torrentId}">AC</a>`);
$([
" | ",
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 = '<h4>Loading...</h4>';
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 `<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>`;
}
;// 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 <i>No-Intro</i> is: ${smallPre(desiredFilename, "lightgreen")}`;
// Elements
const detailsDiv = $(`<div style="font-weight: normal; color: white;">${details}</div>`).hide();
const titleSpan = $(`
<span style="color: ${color}; font-size: 14px; font-weight: bold;">${title}</span>`);
const actionsDiv = $(`<div id="trump-notice-links-${torrentId}" style="font-weight: normal; font-size: 11px; display: inline; margin: 5px; user-select: none;"></div>`);
// Toggle Details
const toggleDetailsActionSpan = $(`<span style="cursor: pointer;">[Expand]</span>`);
toggleDetailsActionSpan.click(()=>{
const collapsed = toggleDetailsActionSpan.text() === "[Expand]";
if (collapsed) {
toggleDetailsActionSpan.text("[Collapse]");
detailsDiv.show();
} else {
toggleDetailsActionSpan.text("[Expand]");
detailsDiv.hide();
}
});
// Tree
const wrapper = $(`<div></div>`);
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 = $(`<tr id="no-intro-url" name="no-intro-url">
<td class="label">No-Intro Link</td>
</tr>`);
const input = $('<input type="text" id="no-intro-url-input" name="no-intro-url-input" size="70%" class="input_tog" value="">');
const error = $('<p id="no-intro-url-error" name="no-intro-url-error" style="color: red; white-space:pre-line;"></p>').hide();
const loading = $('<p id="no-intro-url-loading" name="no-intro-url-loading" style="color: green;">Loading...</p>').hide();
// structure
const td = $("<td></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 = $('<input type="button" value="No-Intro"></input>');
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);
});
/******/ })()
;