Greasy Fork

GGn Steam Uploady (edited)

Fill upload form with Steam info. Edited from "GGn New Uploady"

目前为 2023-12-19 提交的版本。查看 最新版本

// ==UserScript==
// @name         GGn Steam Uploady (edited)
// @namespace    https://gazellegames.net/
// @version      3
// @description  Fill upload form with Steam info. Edited from "GGn New Uploady"
// @author       NeutronNoir, ZeDoCaixao, ingts
// @match        https://gazellegames.net/upload.php*
// @match        https://gazellegames.net/torrents.php?action=editgroup*
// @grant        GM_xmlhttpRequest
// @connect      store.steampowered.com
// @connect      steamcdn-a.akamaihd.net
// ==/UserScript==
const get_languages = true
const text_only = true
const format_about = true
const get_image_res = false

const allowedTags = new Set([
    // "Casual", allowed but too common
    // "Exploration", allowed but too common
    "Action",
    "Adventure",
    "Simulation",
    "Strategy",
    "RPG",
    "Puzzle",
    "Fantasy",
    "Shooter",
    "Platformer",
    "Horror",
    "Visual Novel",
    "Open World",
    "Survival",
    "Sports",
    "Comedy",
    "FPS",
    "Mystery",
    "Sandbox",
    "Fighting",
    "Racing",
    "Shoot 'Em Up",
    "Point & Click",
    "Building",
    "Management",
    "Turn-Based Strategy",
    "Drama",
    "Romance",
    "Interactive Fiction",
    "Hidden Object",
    "Survival Horror",
    "Hack and Slash",
    "Education",
    "Bullet Hell",
    "Dungeon Crawler",
    "Dating Sim",
    "Historical",
    "Walking Simulator",
    "Card Game",
    "Third-Person Shooter",
    "RTS",
    "Life Sim",
    "Clicker",
    "Board Game",
    "Driving",
    "Tower Defense",
    "Time Management",
    "City Builder",
    "Thriller",
    "Wargame",
    "Beat 'em up",
    "Runner",
    "Stealth",
    "Trivia",
    "Typing",
    "Minigames",
    "4X",
    "Cooking",
    "Match 3",
    "Rhythm",
    "Cricket",
    "Rugby",
    "Mahjong",
    "Snowboarding",
    "Hockey",
    "Bowling",
    "Skateboarding",
    "Tennis",
    "Cycling",
    "Wrestling",
    "Basketball",
    "Golf",
    "Chess",
    "Boxing",
    "Gambling",
    "Fishing",
    "Auto Battler",
    "Solitaire",
    "Hunting",
    "Grand Strategy",
    "Space Sim",
])

const tagMap = new Map ([
    ["Sci-fi", "science.fiction"],
    ["Adult Content", "adult"],
    ["Roguelike", "roguelike"],
    ["Roguelite", "roguelike"],
    ["Text-Based", "text.adventure"],
    ["Flight", "flight.simulation"],
    ["Party", "party"],
    ["Party Game", "party"],
    ["Football (American)", "american.football"],
    ["Football (Soccer)", "soccer"],
    ])


function html2bb(str) {
    if (!str) return ""
    str = str.replace(/< *br *\/*>/g, "\n\n") //*/
    str = str.replace(/< *b *>/g, "[b]")
    str = str.replace(/< *\/ *b *>/g, "[/b]")
    str = str.replace(/< *u *>/g, "[u]")
    str = str.replace(/< *\/ *u *>/g, "[/u]")
    str = str.replace(/< *i *>/g, "[i]")
    str = str.replace(/< *\/ *i *>/g, "[/i]")
    str = str.replace(/< *strong *>/g, "[b]")
    str = str.replace(/< *\/ *strong *>/g, "[/b]")
    str = str.replace(/< *em *>/g, "[i]")
    str = str.replace(/< *\/ *em *>/g, "[/i]")
    str = str.replace(/< *li *>/g, "[*]")
    str = str.replace(/< *\/ *li *>/g, "")
    str = str.replace(/< *ul *class=\\*\"bb_ul\\*\" *>/g, "")
    str = str.replace(/< *\/ *ul *>/g, "")
    str = str.replace(/< *h2 *class=\"bb_tag\" *>/g, "\n[align=center][u][b]")
    str = str.replace(/< *h[12] *>/g, "\n[align=center][u][b]")
    str = str.replace(/< *\/ *h[12] *>/g, "[/b][/u][/align]\n")
    str = str.replace(/\&quot;/g, "\"")
    str = str.replace(/\&amp;/g, "&")
    str = str.replace(/< *img *src="([^"]*)".*>/g, "\n")
    str = str.replace(/< *a [^>]*>/g, "")
    str = str.replace(/< *\/ *a *>/g, "")
    str = str.replace(/< *p *>/g, "\n\n")
    str = str.replace(/< *\/ *p *>/g, "")
    str = str.replace(/“/g, "\"")
    str = str.replace(/”/g, "\"")
    str = str.replace(/  +/g, " ")
    str = str.replace(/\n +/g, "\n")
    str = str.replace(/\n\n\n+/gm, "\n\n")
    str = str.replace(/\n\n\n+/gm, "\n\n")
    str = str.replace(/\[\/b]\[\/u]\[\/align]\n\n/g, "[/b][/u][/align]\n")
    str = str.replace(/\n\n\[\*]/g, "\n[*]")
    return str
}

function fix_emptylines(str) {
    const lst = str.split("\n")
    let result = ""
    let empty = 1
    lst.forEach(function (s) {
        if (s) {
            empty = 0
            result = result + s + "\n"
        } else if (empty < 1) {
            empty = empty + 1
            result = result + "\n"
        }
    })
    return result
}

function pretty_sr(str) {
    if (!str) return ""
    str = str.replace(/™/g, "")
    str = str.replace(/®/g, "")
    str = str.replace(/:\[\/b\] /g, "[/b]: ")
    str = str.replace(/:\n/g, "\n")
    str = str.replace(/:\[\/b\]\n/g, "[/b]\n")
    str = str.replace(/\n\n\[b\]/g, "\n[b]")
    return str
}

function toTitleCase(str) {
    str = str.replace(/\s/g, ' ')
    const smallWords = /^(a|an|and|as|at|but|by|en|for|if|in|nor|of|on|or|per|the|to|v.?|vs.?|via)$/i
    const alphanumericPattern = /([A-Za-z0-9\u00C0-\u00FF])/
    const wordSeparators = /([ :–—-])/
    const allUppercase = ['rpg', 'fps', 'tps', 'rts', 'tbs', 'mmo', 'mmorpg', 'arpg', 'jrpg', 'pvp', 'pve']

    return str.toLowerCase().split(wordSeparators)
        .map(function (current, index, array) {
            if (allUppercase.includes(current)) return current.toUpperCase()
            if (
                /* Check for small words */
                current.search(smallWords) > -1 &&
                /* Skip first and last word */
                index !== 0 &&
                index !== array.length - 1 &&
                /* Ignore title end and subtitle start */
                array[index - 3] !== ':' &&
                array[index + 1] !== ':' &&
                /* Ignore small words that start a hyphenated phrase */
                (array[index + 1] !== '-' ||
                    (array[index - 1] === '-' && array[index + 1] === '-'))
            ) {
                return current.toLowerCase()
            }

            /* Ignore URLs */
            if (array[index + 1] === ':' && array[index + 2] !== '') {
                return current
            }

            /* Capitalize the first letter */
            return current.replace(alphanumericPattern, function (match) {
                return match.toUpperCase()
            })
        })
        .join('')
}

function formatAbout(about) {
    if (!format_about) return about

    function fixSplitLinesInListItems(input) {
        let lines = input.split('\n')
        for (let i = 0; i < lines.length; i++) {
            if (lines[i].startsWith("[*]")) {
                while (i + 1 < lines.length && !lines[i].match(/[.?!。?!]$/)) {
                    if (lines[i + 1].startsWith("[*]")) {
                        lines[i] += '.'
                        break
                    } else if (lines[i + 1].trim() !== '') {
                        lines[i] += ' ' + lines.splice(i + 1, 1)[0]
                    } else {
                        lines.splice(i + 1, 1)
                    }
                }
            }
        }
        return lines.join('\n')
    }

    // If a line starts with [u], [i], or [b], there is no other text on that line, and it contains 'features', replace tags with [align=center][b][u]
    about = about.replace(/^(\[b]|\[u]|\[i])*(.*?)(\[\/b]|\[\/u]|\[\/i])*$/gm, (match, p1, p2, p3) => {
        return (p1 && p3 && /features/i.test(p2)) ? `[align=center][b][u]${p2}[/u][/b][/align]` : match
    })

    // Title case text inside [align=center][b][u]
    about = about.replace(/\[align=center]\[([bu])]\[([bu])]([\s\S]*?)\[\/\2]\[\/\1]\[\/align]/g, (match, p1, p2, p3) => {
        return `[align=center][b][u]${toTitleCase(p3)}[/u][/b][/align]`
    })

    // Add a newline before lines with [align=center] if there isn't already a double newline before it
    about = about.replace(/(?<!\n\n)(\[align=center])/g, '\n\$1')

    // Remove colons in text inside [align=center][b][u]
    about = about.replace(/\[align=center]\[b]\[u](.*?)\[\/u]\[\/b]\[\/align]/g, (match, p1) => {
        return match.replace(/:/g, '')
    })

    // Replace different list symbols at the start with [*]
    about = about.replace(/^[-•◦]\s*/gm, '[*]')

    // If a line starts with [u], [i], or [b] and it is not the only text on that line, add [*] at the start and replace tags with [b]
    about = about.replace(/^(\[b]|\[u]|\[i])*(.*?)(\[\/b]|\[\/u]|\[\/i])+(.*$)/gm, (match, p1, p2, p3, p4) => {
        if (p4.trim() === '') {
            return match
        }
        return p1 && p3 ? `[*][b]${p2}[/b]${p4}` : match
    })

    // Replace tags with [b] for lines that start with [*]
    about = about.replace(/\[\*][^\n]*\[\/?[ui]][^\n]*\n/g, function (match) {
        return match.replace(/[ui]/g, "b")
    })

    // Title case text inside tags for lines starting with [u], [i], or [b] and has nothing else after the closing tag
    about = about.replace(/(^|\n)(\[([uib])](.*?)\[\/([uib])]\s*$)/gm, (match, p1, p2, p3, p4) => `${p1}[${p3}]${toTitleCase(p4)}[/${p3}]`)

    // For lines that start with [*], replace newlines with spaces  until that line ends with ., ?, or !
    // and add a full stop if there is no punctuation before another [*]
    about = fixSplitLinesInListItems(about)

    // Remove double newlines between [*] lines
    about = about.replace(/(\[\*][^\n]*)(\n{2,})(?=\[\*])/g, '$1\n')

    // Add a newline when next line doesn't start with [*]
    about = about.replace(/(\[\*][^\n]*\n)([^\[*\]\n])/g, '$1\n$2')

    // Move : and . outside of closing tags
    about = about.replace(/(\[([bui])])(.*?)([:.])\[\/([bui])]/g, '$1\$3[/b]\$4')

    // Remove [u], [i], or [b] if the line starts with [*] followed by a [u], [i], or [b], and ends with a punctuation after the closing tag
    about = about.replace(/^\[\*]\[([bui])](.*?)\[\/([bui])]([.?!。?!])$/gm, "[*]$2$4")

    // If a line ends with [/align] replace double newlines with one newline
    about = about.replace(/(\[\/align])\n\n/g, '$1\n')

    return about
}

function parseSteamLanguage($document) {
    const langSelect = document.getElementById('language')

    let table = $document.querySelector('table.game_language_options')
    if (!table) return
    let languages = {}
    for (let r = 0; r < table.rows.length; r++) {
        for (let c = 0; c < table.rows[r].cells.length; c++) {
            if (table.rows[r].cells[c].textContent.trim() === '✔') {
                let header = table.rows[0].cells[c].textContent.trim()
                if (!languages[header]) {
                    languages[header] = []
                }
                languages[header].push(table.rows[r].cells[0].textContent.trim())
            }
        }
    }
    if (text_only) delete languages['Full Audio']
    const textLanguages = languages['Subtitles'] ? languages['Subtitles'] : languages['Interface']
    const audioLanguages = languages['Full Audio']
    const textMulti = textLanguages.length > 1 ? 's' : ''
    const audioMulti = audioLanguages && audioLanguages.length > 1 ? 's' : ''
    const joinedText = textLanguages.join(', ')
    const joinedAudio = audioLanguages && audioLanguages.join(', ')
    const languageList = [
        'English',
        'German',
        'French',
        'Czech',
        'Italian',
        'Japanese',
        'Korean',
        'Polish',
        'Portuguese',
        'Russian',
        'Spanish',
    ]
    const desc = document.getElementById('release_desc')
    const inLangList = !textMulti && languageList.some(lang => textLanguages[0].includes(lang))
    let langSelectValue = textMulti ? 'Multi-Language' : inLangList ? textLanguages[0] : 'Other'
    let descValue
    if (!textMulti && textLanguages[0].includes('Chinese')) {
        langSelectValue = 'Chinese'
        descValue = audioLanguages && areSame(textLanguages, audioLanguages)
            ? `[b]Text and Audio Language${textMulti}[/b]: ${joinedText}`
            : audioLanguages
                ? `[b]Text Language[/b]: ${joinedAudio}\n[b]Audio Language${audioMulti}[/b]: ${joinedAudio}`
                : `[b]Language[/b]: ${joinedText}`
    } else if (audioLanguages && areSame(textLanguages, audioLanguages)) {
        descValue = `[b]Text and Audio Language${textMulti}[/b]: ${joinedText}`
    } else {
        const addText = textMulti ? `[b]Languages[/b]: ${joinedText}` : ''
        descValue = audioLanguages
            ? `${addText ? 'Text ' + addText + '\n' : ''}[b]Audio Language${audioMulti}[/b]: ${joinedAudio}`
            : addText
    }
    langSelect.value = langSelectValue
    desc.value += descValue
}

function areSame(array1, array2) {
    return array1.length === array2.length && array1.sort().every((value, index) => value === array2.sort()[index])
}

const steamIdInput = document.getElementById('steamid')

function fill_form(response) {
    const gameInfo = response.response[steamIdInput.value].data
    let about = gameInfo.about_the_game
    if (about === '') {
        about = gameInfo.detailed_description
    }
    about = "[align=center][b][u]About the game[/u][/b][/align]\n" + formatAbout(html2bb(about)).trim()
    const year = gameInfo.release_date.date.split(", ").pop()
    const screens = document.getElementsByName("screens[]")
    const add_screen = $("#image_block a[href='#']").first()

    for (let i = 0; i < gameInfo.screenshots.length; i++) {
        if (i >= 4) add_screen.click()
        screens[i].value = gameInfo.screenshots[i].path_full.split("?")[0]
        if (get_image_res) {
            new Promise((resolve, reject) => {
                let img = new Image()
                img.src = gameInfo.screenshots[i].path_full.split("?")[0]
                img.onload = () => resolve(img)
                img.onerror = () => reject()
            }).then(img => {
                screens[i].parentElement.style.position = 'relative'
                screens[i].insertAdjacentHTML('afterend',
                    `<span style="position:absolute;top: -115%;right: 9.5%;">${img.naturalWidth}x${img.naturalHeight}</span>`)
            })
        }
    }

    let platform = "Windows"
    let cover_field = "input[name='image']"
    let desc_field = "textarea[name='body']"

    if (window.location.href.includes("action=editgroup")) {
        $("input[name='year']").val(year)
        $("input[name='name']").val(gameInfo.name)
        if ($("#trailer~a").attr("href").includes("Linux")) {
            platform = "Linux"
        } else if ($("#trailer~a").attr("href").includes("Mac")) {
            platform = "Mac"
        }
    } else {
        GM_xmlhttpRequest({
            url: `https://store.steampowered.com/app/${steamIdInput.value}`,
            onload: function (response) {
                const page = new DOMParser().parseFromString(response.responseText, "text/html")
                let uploadTags = new Set()

                for (const steamTag of page.querySelectorAll('.glance_tags a')) {
                    const text = steamTag.textContent.trim()
                    if (allowedTags.has(text)) {
                        uploadTags.add(text.toLowerCase()
                            .replace(/sim(?!\w)/, "simulation")
                            .replace(/'/g, '').replace(/&/g, 'and').replace(/-/g, ' ').replace(/ /g, '.'))
                        continue
                    }
                    const t = tagMap.get(text)
                    if (t) {
                        uploadTags.add(t)
                    }
                }
                document.getElementById('tags').value = Array.from(uploadTags).join(', ')

                if (get_languages && !document.getElementById('empty_group').checked) {
                    parseSteamLanguage(page)
                }
            }
        })
        $("#title").val(gameInfo.name)
        $("#gameswebsiteuri").val(gameInfo.website)
        $("#year").val(year)
        cover_field = "#image"
        desc_field = "#album_desc"
        platform = $("#platform").val()
    }

    let recfield = gameInfo.pc_requirements
    switch (platform) {
        case "Windows":
            recfield = gameInfo.pc_requirements
            break
        case "Linux":
            recfield = gameInfo.linux_requirements
            break
        case "Mac":
            recfield = gameInfo.mac_requirements
            break
    }
    let sr = ''
    if (typeof (recfield.minimum) !== "undefined") {
        sr += html2bb(recfield.minimum)
    }
    if (typeof (recfield.recommended) !== "undefined") {
        sr += "\n" + html2bb(recfield.recommended)
    }
    sr = "\n\n[quote][align=center][b][u]System Requirements[/u][/b][/align]\n\n" +
        pretty_sr(sr) +
        "[/quote]"
    $(desc_field).val(about)
    $(desc_field).val($(desc_field).val() + sr)
    $(cover_field).val(gameInfo.header_image.split("?")[0])
    const big_cover = "https://steamcdn-a.akamaihd.net/steam/apps/" + steamIdInput.value + "/library_600x900_2x.jpg"
    GM_xmlhttpRequest({
        method: "GET",
        url: big_cover,
        responseType: "json",
        onload: function (response) {
            if (response.status === 200) {
                $(cover_field).val(big_cover)
            }
        }
    })
    $(desc_field).val(fix_emptylines($(desc_field).val()))
    if (gameInfo.metacritic) {
        $("#meta").val(gameInfo.metacritic.score)
        $("#metauri").val(gameInfo.metacritic.url.split("?")[0] + "/critic-reviews")
    }
    if (gameInfo.hasOwnProperty('movies')) {
        const trailer = gameInfo.movies[0].webm.max.split("?")[0].replace("http:", "https:")
        $("#trailer").val(trailer)
    }
}

if (window.location.href.includes("action=editgroup")) {
    $("td.center").parent().after("<tr><td class='label'>Steam ID</td><td><input id='steamid' /></td></tr>")
} else {
    steamIdInput.type = 'text'
    steamIdInput.size = 20
    steamIdInput.removeAttribute('min')
    steamIdInput.insertAdjacentHTML('afterend',
        '<a href="javascript:;" id="fill_win">Win</a> <a href="javascript:;" id="fill_lin">Lin</a> <a href="javascript:;" id="fill_mac">Mac</a>')
    $('#fill_win').click(function () {
        $("#platform").val("Windows")
    })
    $('#fill_lin').click(function () {
        $("#platform").val("Linux")
    })
    $('#fill_mac').click(function () {
        $("#platform").val("Mac")
    })
}

steamIdInput.onblur = () => {
    if (steamIdInput.value.includes('store.steampowered')) {
        steamIdInput.value = /\d+/.exec(steamIdInput.value)[0]
    }
    GM_xmlhttpRequest({
        method: "GET",
        url: "https://store.steampowered.com/api/appdetails?l=en&appids=" + steamIdInput.value,
        responseType: "json",
        onload: fill_form
    })
}