// ==UserScript==
// @name GGn Epic Games Store Cover Replacer
// @namespace none
// @version 3
// @description Easily replace cover using Epic Games Store images
// @author ingts
// @match https://gazellegames.net/torrents.php?id=*
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @connect store.epicgames.com
// @connect store-content.ak.epicgames.com
// @connect litterbox.catbox.moe
// ==/UserScript==
function fillOptions(options = {
method: "GET",
responseType: "json",
onSuccess: (response) => {
return JSON.parse(response.responseText)
}
}) {
options.method = options.method || "GET"
options.responseType = options.responseType || "json"
if (!options.onSuccess) {
options.onSuccess = (response) => {
return JSON.parse(response.responseText)
}
}
return options
}
function doFetch(url, options = {
method: "GET",
responseType: "json",
onSuccess: (response) => {
return JSON.parse(response.responseText)
}
}) {
const fullOptions = fillOptions(options)
let resolve, reject
let responsePromise = new Promise((promiseResolve, promiseReject) => {
resolve = promiseResolve
reject = promiseReject
})
GM_xmlhttpRequest({
url: url,
method: fullOptions.method,
responseType: fullOptions.responseType,
body: fullOptions.body,
onload: (response) => {
if (response.status < 200 || response.status >= 400) {
console.error(response.responseText, url)
reject(response)
} else {
resolve(fullOptions.onSuccess(response))
}
}
})
return responsePromise
}
function graphql(query, variables, extensions) {
const jsonVariables = JSON.stringify(variables)
const jsonExtensions = JSON.stringify(extensions)
const url = `https://store.epicgames.com/graphql?operationName=${query}&variables=${jsonVariables}&extensions=${jsonExtensions}`
return doFetch(url).then(response => response.data)
}
function getMappingByPageSlug(slug) {
const variables = {
pageSlug: slug,
locale: "en-US",
}
const extensions = {
persistedQuery: {
version: 1,
sha256Hash: "781fd69ec8116125fa8dc245c0838198cdf5283e31647d08dfa27f45ee8b1f30",
}
}
return graphql("getMappingByPageSlug", variables, extensions)
}
function getProductMapping(slug) {
return doFetch(`https://store-content.ak.epicgames.com/api/en-US/content/products/${slug}`)
}
function getCatalogOffer(identifiers) {
const variables = {
country: "US",
locale: "en-US",
sandboxId: identifiers.sandboxId,
offerId: identifiers.offerId,
}
const extensions = {
persistedQuery: {
version: 1,
sha256Hash: "abafd6e0aa80535c43676f533f0283c7f5214a59e9fae6ebfb37bed1b1bb2e9b", // if this changes again, need to get it from page source
}
}
return graphql("getCatalogOffer", variables, extensions).then(data => data?.Catalog.catalogOffer)
}
GM_registerMenuCommand('Run', () => {
const egsUrl = document.querySelector('a[title=EpicGames]')
if (!egsUrl) {
alert('No Epic Games Store link found')
return
}
const slug = egsUrl.href.split("/").pop()
const mainDiv = document.createElement('div')
mainDiv.id = 'egs-cover-main'
mainDiv.style.cssText = `
position: absolute;
width: 250px;
left: 20%;
top: 0.1%;
background-color: #2a2b36;
z-index: 99999;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding: 3px 0
`
const loading = document.createElement('p')
loading.textContent = 'Loading'
loading.style.fontSize = '2em'
mainDiv.append(loading)
const coverDiv = document.getElementById('group_cover')
coverDiv.after(mainDiv)
getProductMapping(slug).then(mapping => { // some games don't have this. {"error":true,"message":"Page was not found"}
return {
sandboxId: mapping.namespace,
offerId: mapping.pages.find(o => o.type === "productHome").offer.id // could find from data.editions.editions but doesn't work for games that have only 1 edition
}
}).catch(() => { //
return getMappingByPageSlug(slug).then(mapping => ({
sandboxId: mapping.StorePageMapping.mapping.sandboxId,
offerId: mapping.StorePageMapping.mapping.mappings.offerId
}))
})
.then(async identifiers => {
let tries = 4
while (tries > 0) {
const catalog = await getCatalogOffer(identifiers)
if (!catalog) {
console.warn('Retrying getCatalogOffer')
await new Promise(resolve => setTimeout(resolve, 1000))
tries--
continue
}
return catalog
}
mainDiv.remove()
}).then(catalog => {
console.log("catalog", catalog)
const coverImage = catalog.keyImages.find(image => image.type === "OfferImageTall")?.url
if (!coverImage) {
alert('No cover image found')
return
}
new Promise((resolve, reject) => {
let img = new Image()
img.src = coverImage
img.style.maxWidth = '250px'
img.style.maxHeight = '350px'
img.onload = () => resolve(img)
img.onerror = () => reject()
}).then(img => {
loading.remove()
const currentCover = coverDiv.querySelector('img')
const closeBtn = document.createElement('button')
closeBtn.textContent = 'Close'
closeBtn.addEventListener('click', () => mainDiv.style.display = 'none')
closeBtn.style.alignSelf = 'end'
mainDiv.append(closeBtn)
mainDiv.insertAdjacentHTML('beforeend', `
<span style="font-size: 1.1em;">Current: ${currentCover.naturalWidth} x ${currentCover.naturalHeight}</span>
<span style="font-size: 1.1em;">New: ${img.naturalWidth} x ${img.naturalHeight}</span>
`)
mainDiv.append(img)
mainDiv.insertAdjacentHTML('beforeend', `
<input type="text" style="width: 80%;" id="egs-cover-input">
<button id="egs-cover-submit" type="button">Submit</button>
`)
const body = new URLSearchParams(`action=takeimagesedit&groupid=${new URL(location.href).searchParams.get('id')}&categoryid=1`)
document.querySelectorAll('#group_screenshots a').forEach(a => body.append('screens[]', a.href))
function addText(text) {
const p = document.createElement('p')
p.style.textAlign = 'center'
p.textContent = text
mainDiv.append(p)
return p
}
function done() {
const p = document.createElement('p')
p.style.textAlign = 'center'
p.textContent = 'Done'
p.style.cssText = "font-size: 1.5em;color: lightgreen;"
mainDiv.append(p)
setTimeout(() => {
mainDiv.remove()
}, 1000)
}
document.getElementById('egs-cover-submit').onclick = () => {
const input = document.getElementById('egs-cover-input')
body.append('image', input.value)
submitCover(body).then(() => {
done()
})
}
mainDiv.insertAdjacentHTML('beforeend', `<button id="egs-cover-ptpimg" type="button">PTPImg and Submit</button>`)
document.getElementById('egs-cover-ptpimg').onclick = () => {
function finish(url, text) {
ptpimg(url)
.then(ptpimgLink => {
body.append('image', ptpimgLink)
submitCover(body).then(() => {
text.remove()
done()
})
})
}
if (coverImage.includes('.jpg') || coverImage.includes('.png')) {
const text = addText("Uploading to PTPimg")
finish(coverImage, text)
} else {
const text = addText('URL has no extension. Uploading to litterbox first')
promiseXHR(coverImage, {responseType: 'blob'})
.then(r => {
const blob = r.response
const fd = new FormData()
fd.append('time', '1h')
fd.append('reqtype', 'fileupload')
fd.append('fileToUpload', new File([blob], 'a.' + blob.type.split('/')[1]))
promiseXHR('https://litterbox.catbox.moe/resources/internals/api.php', {
method: 'POST',
data: fd,
}).then(r => {
text.textContent = "Uploading to PTPimg"
finish(r.response, text)
})
})
}
}
})
})
})
function ptpimg(url) {
return fetch(`imgup.php?img=${url}`)
.then(res => res.text())
.then(text => {
if (text === "https://ptpimg.me/.") {
throw new Error()
}
return text
})
.catch(() => {
alert('PTPimg upload failed')
})
}
function submitCover(body) {
return fetch('torrents.php', {
method: 'post',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: body
})
.then(r => {
if (!r.redirected) {
throw Error
}
})
.catch(() => {
alert(`Failed to submit`)
})
}
function promiseXHR(url, options) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
url,
...options,
onabort: (response) => {
reject(response)
},
onerror: (response) => {
reject(response)
},
ontimeout: (response) => {
reject(response)
},
onload: (response) => {
resolve(response)
},
})
})
}