NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name GOG Checksum Lookup
// @description Use the GOG API to show MD5 checksums of owned games.
// @icon https://www.gog.com/apple-touch-icon.png
// @homepageURL https://gogapidocs.readthedocs.io/en/latest/
// @match https://www.gog.com/account
// @match https://www.gog.com/*/account
// @version 22w04b
// @license MIT
// @author xmanacollectorx
// @grant GM_addStyle
// @grant GM.xmlHttpRequest
// ==/UserScript==
// ------------------------------------------------------------------
// VARIABLES
// ------------------------------------------------------------------
// Strings for UserScript communication.
var s_us_enter_lookup = 'Starting lookup for game ID ';
var s_us_error_generic = 'Generic file handler not found.';
var s_us_error_chunk_url = 'Could not extract chunklist URL.';
var s_us_got_chunk_url = ', received chunklist URL: ';
var s_us_got_data = ', received final data: ';
var s_us_got_generic = ', extracted generic file handler: ';
var s_us_got_token = 'Successfully retrieved GOG access token.';
var s_us_id_disappeared = 'Product ID disappeared.';
var s_us_id_detected = 'Detected product ID: ';
var s_us_invalid_task = 'Script could not understand its task.';
var s_us_iteration = 'Iterating download link ';
var s_us_no_checksum = 'Unable to extract md5 checksum from XML.';
var s_us_no_dl_rows = 'No downloadable content was found.';
var s_us_no_token = 'GOG access token not found.';
var s_us_row = 'Row ';
var s_us_startup = 'GOG Checksum Lookup loaded.';
var s_us_text_button_0 = 'Checksums ✓';
var s_us_text_button_1 = 'Checksums 🗘';
var s_us_text_hash = 'md5';
var s_us_update_button = 'Added lookup button to product popup.';
// HTML target search strings.
var s_html_dl_rows = 'a.game-link.row[ng-href^="/downloads/"]';
var s_html_dl_version = 'span.game-link__info';
var s_html_product_head = 'header.game-details__header';
var s_html_product_id = 'gog-account-product';
var s_html_product_popup = 'section.game-details';
// GOG API snippets.
var s_api_url_bonus_d = 'product_bonus/';
var s_api_url_downlink_b = 'https://api.gog.com/products/';
var s_api_url_downlink_d = '/downlink/';
var s_api_url_chunklist = '';
var s_api_url_redirect = '';
// Other global variables used in the script.
var arr_dl_rows = '';
var bool_button_added = false;
var bool_button_enabled = true;
var num_product_id = '';
var s_accessToken = '';
var s_chunklist = '';
var s_dl_generic = '';
var s_fetch_urls = '';
var s_nextTask = 'waitProduct';
// ------------------------------------------------------------------
// FUNCTIONS
// ------------------------------------------------------------------
// Detect product selection in the GOG game library.
function detectProductChange() {
var oTarget = document.querySelector(s_html_product_popup);
var oConfig = {
attributes: true,
attributeFilter: [s_html_product_id]
};
var oCallback = function (mutationsList, observer) {
for (const mutation of mutationsList) {
num_product_id = document.querySelector(s_html_product_popup).getAttribute(s_html_product_id);
if (num_product_id == '' || isNaN(num_product_id)) {
console.log(s_us_id_disappeared);
}
else {
console.log(s_us_id_detected + num_product_id);
enableLookup();
enableButton();
}
}
};
var observer = new MutationObserver(oCallback);
observer.observe(oTarget, oConfig);
}
// Filter product popup for product header.
function detectProductHeader() {
return document.querySelector(s_html_product_head);
}
// Filter product popup for download links.
function detectDownloadLinks() {
return document.querySelectorAll(s_html_dl_rows);
}
// Display button for checksum lookup.
function enableLookup() {
// Only add button if is doesn't exist yet.
if (bool_button_added != true) {
// Build the actual HTML element.
var header = detectProductHeader();
var button = document.createElement('div');
button.setAttribute('class', 'game-details__section__dropdown module-header2__element');
button.innerHTML = `
<span class="module-header2__dropdown" id="lookupButton" style="cursor:pointer">
` + s_us_text_button_1 + `
</span>
`;
header.appendChild(button);
document.getElementById('lookupButton').addEventListener('click', retrieveHashes, false);
bool_button_added = true;
console.log(s_us_update_button);
}
}
// Routine for looking up hashes, duh.
function retrieveHashes() {
console.log('User wants to see hashes.');
disableButton();
lookupRows();
}
// Wait for a download link table to appear.
function lookupRows() {
var intv = setInterval(
function () {
// Select elements with specific properties.
arr_dl_rows = detectDownloadLinks();
// When no element is found, start all over again.
if (arr_dl_rows.length < 1) {
return false;
}
// When an element is found, clear the interval.
clearInterval(intv);
// Iterate through the discovered elements.
for (let i = 0; i < arr_dl_rows.length; i++) {
fetchInstallerURLs(i);
}
},
100);
}
// Have a closer look at a table row and extract a generic URL.
function extractGenericURL(i) {
var name = arr_dl_rows[i].getAttribute('href').split('/').pop();
if (name != null) {
console.log(s_us_row + i + s_us_got_generic + name);
if (name.replace(/\d+/, '') != '') {
var step1 = name.replace(/^[a-zA-Z]+\d+/, '');
var step2 = step1.replace(/\d+/, '');
var generic = step2 + '/' + name;
return generic;
}
else {
var generic = s_api_url_bonus_d + name;
return generic;
}
}
else {
throw new Error(s_us_error_generic);
}
}
// Retrieve the GOG user access token from the browsers local storage.
function getAccessToken() {
var json = localStorage.getItem('dataClient_menuData');
var obj = JSON.parse(json);
if (obj != null) {
return obj.accessToken;
}
else {
throw new Error(s_us_no_token);
}
}
// Call the GOG API to hand over installer download links, bypass CORS.
function fetchInstallerURLs(i) {
GM.xmlHttpRequest({
method: 'GET',
url: s_api_url_downlink_b + num_product_id + s_api_url_downlink_d + extractGenericURL(i),
headers: {
'Authorization': 'Bearer ' + getAccessToken()
},
onload: function (response) {
extractChunklistURL(i, response.responseText.substring(0, 2000));
}
});
}
// Extract URL for installer metadata from the first API response.
function extractChunklistURL(i, raw) {
var json = JSON.parse(raw);
var url = json.checksum;
if (url != null) {
console.log(s_us_row + i + s_us_got_chunk_url + url);
fetchChunklist(i, url);
}
else {
throw new Error(s_us_error_chunk_url);
}
}
// Call the GOG API to send installer metadata, bypass CORS.
function fetchChunklist(i, url) {
GM.xmlHttpRequest({
method: 'GET',
url: url,
headers: {
'Authorization': 'Bearer ' + getAccessToken()
},
onload: function (response) {
extractInstallerChecksum(i, response.responseText);
}
});
}
// Extract the installer checksum from the second API response.
function extractInstallerChecksum(i, chunklist) {
var parser = new DOMParser();
var xmlDoc = parser.parseFromString(chunklist, 'text/xml');
var checksum = xmlDoc.getElementsByTagName("file")[0].getAttribute('md5');
var date = xmlDoc.getElementsByTagName("file")[0].getAttribute('timestamp').replace(/\s.*/, '');
if (checksum.length < 1) {
throw new Error(s_us_no_checksum);
}
else {
console.log(s_us_row + i + s_us_got_data + checksum + ', ' + date);
updateRows(i, checksum, date);
}
}
// Display the received checksums.
function updateRows(i, checksum, date) {
var hash = document.createElement('a');
hash.setAttribute('class', 'game-link row');
hash.setAttribute('ng-repeat', 'item in details.main.currentDownloads');
hash.innerHTML = `
<span ng-bind="item.name" class="game-link__right row__column">
` + checksum + `
</span>
<span class="game-link__right game-link__info row__column">
` + date + `
</span>
<span class="game-link__right game-link__size row__column">
` + s_us_text_hash + `
</span>
`;
insertAfter(arr_dl_rows[i], hash);
}
// Extract the installer version from a row entry.
function extractInstallerVersion(i) {
var version = arr_dl_rows[i].querySelector(s_html_dl_version).innerText;
if (version != null) {
return version;
}
else {
return '?';
}
}
// UserScript injection routine, duh.
function insertAfter(referenceNode, newNode) {
referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
}
// Disable the lookup button to prevent spam.
function disableButton() {
var button = document.getElementById('lookupButton');
button.removeEventListener('click', retrieveHashes, false);
button.setAttribute('class', 'module-header2__element-label');
button.removeAttribute('style');
button.innerHTML = s_us_text_button_0;
}
// Enable the lookup button to allow API interaction.
function enableButton() {
var button = document.getElementById('lookupButton');
button.addEventListener('click', retrieveHashes, false);
button.setAttribute('class', 'module-header2__dropdown');
button.setAttribute('style', 'cursor:pointer');
button.innerHTML = s_us_text_button_1;
}
// ------------------------------------------------------------------
// MAIN
// ------------------------------------------------------------------
// Begin when the page has loaded.
window.addEventListener('load', function () {
console.log(s_us_startup);
detectProductChange();
})