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(); })