Greasy Fork

Greasy Fork is available in English.

GGn No-Intro Helper

A GGn user script to help with No-Intro uploads/trumps

当前为 2022-09-05 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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.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;
        }
        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 = $(`<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);
                }
            }
        });
    }
    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 = '<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(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 `<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 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 <i>No-Intro</i> is: ${smallPre(desiredFilename, "lightgreen")}`,
        color: "hotpink"
    };
}
function reportableNoticeInfo(param) {
    let { fixedVersion , torrentId , actualFilename , desiredFilename , noIntroLink  } = param;
    const form = $("<div/></div>");
    const trumpingTorrentInput = $(`<input type="text" value="${fixedVersion.permalink}" placeholder="https://gazellegames.net/torrents.php?id=xxxxx&torrentid=yyyyyy" style="width: 75%; background: #ffb952; color: black;"/>`);
    const commentTextarea = $(`<textarea placeholder="Previous upload didn't like hummus" style="height: 100px; width: 90%; background: #ffb952; color: black; margin: 0;"/>`);
    commentTextarea.text(`ROM name changed on No-Intro.
Reported filename        : ${actualFilename}
Trumping filename       : ${desiredFilename}
No-Intro for reference : ${noIntroLink}`);
    const submitInputButton = $(`<input id="no-intro-helper-submit-trump-report-${torrentId}" type="button" value="REPORT" style="width: 70px; margin: 0; background: #ffb952; color: black; font-weight: bolder;"/>`);
    const errorMessage = $("<div style='color: red; font-weight: bold; font-size: 13px; white-space: pre-line;'></div>").hide();
    form.append(`<p style="font-size: 12px;">Trumping Torrent Permalink:</p>`, trumpingTorrentInput, `<p style="font-size: 12px;">Report Comment:</p>`, commentTextarea, `<p style="font-size: 12px;">Submit Report:</p>`, 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 = $(`<div style="font-weight: normal; color: white;"></div>`).hide();
    detailsDiv.append(details);
    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
    if (type !== "reported") {
        const toggleDetailsActionSpan = $(`<span style="cursor: pointer; margin-right: 5px;">[Expand]</span>`);
        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 = $(`<span style="cursor: pointer; margin-right: 5px;">[Send Report]</span>`);
        sendReportActionSpan.click(()=>{
            $(`#no-intro-helper-submit-trump-report-${torrentId}`).click();
        });
        actionsDiv.append(sendReportActionSpan);
    }
    // Cheer
    if (type === "reported") {
        const cheerActionSpan = $(`<span style="cursor: pointer; margin-right: 5px; position: absolute;">[Cheer]</span>`);
        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 = $(`<div></div>`);
    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 = $(`<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();
    }
    // Blacklist no-intro URLs that lead to ban
    $("a").click(function() {
        if (/https?:\/\/datomatic\.no-intro\.org\/index\.php\?page=show_record&s=(.*?)&n=$/i.test($(this).attr("href"))) {
            $(this).text("BLACKLISTED");
            return false;
        }
    });
}
main().catch((e)=>{
    console.log(e);
});

/******/ })()
;