|
// ==UserScript== |
|
// @name TamperMonkeyRetroachievements |
|
// @namespace https://archive.org/details/retroachievements_collection_v5 |
|
// @updateURL https://gist.github.com/SavageCore/2e96af3c572ec27e9db7182cd06683d8/raw/TamperMonkeyRetroachievements.user.js |
|
// @downloadURL https://gist.github.com/SavageCore/2e96af3c572ec27e9db7182cd06683d8/raw/TamperMonkeyRetroachievements.user.js |
|
// @version 1.0.03 |
|
// @description Add download links to retroachievements.org Supported Game Files page e.g. https://retroachievements.org/game/19339/hashes |
|
// @author wholee |
|
// @match https://retroachievements.org/game/*/hashes |
|
// @icon https://archive.org/images/glogo.jpg |
|
// @grant none |
|
// @run-at document-end |
|
// ==/UserScript== |
|
|
|
// |
|
// 0.7: Updated archiveOrgLastModified URL |
|
// 0.8: Don't call archive.org with every page refresh |
|
// 0.9: Refactor code |
|
// 0.9.1: Use {cache: 'no-cache'} for retroachievementsHashList download |
|
// 0.9.2: Updated disclaimer |
|
// 0.9.3: Split PS2 to new archive.org collection |
|
// 0.9.4: Refactor PS2 |
|
// 0.9.5: Add note for FLYCAST ROMs |
|
// 0.9.6: Added descriptive error messages |
|
// 0.9.7: Added FBNeoZipLink |
|
// 0.9.8: Due to page changes, updated disclaimer position |
|
// 0.9.9: Cosmetic code changes, FBNeo link updates and disclaimer text |
|
// 0.9.91: Cosmetic code changes, fix typo in PS2 download link |
|
// 0.9.92: Separated NES and SNES to their own archive items |
|
// 0.9.93: Separated Playstation |
|
// 0.9.94: Separated Playstation Portable |
|
// 0.9.95: Small code refactor |
|
// 0.9.96: Added GameCube |
|
// 0.9.97: HTML-encode links to archive.org |
|
// 0.9.98: Remove HTML-encode links |
|
// 1.0.00: Six months hiatus updates |
|
// Update download link position due to site changes |
|
// Add missing and Paid Hash info |
|
// Split PlayStation 2 in two due to the size |
|
// Rename NES and SNES collections to match ConsoleName update |
|
// 1.0.01: wrapped download links in <div> |
|
// 1.0.02: Fix some querySelector's and wait for full page load before modifying the DOM (SavageCore) |
|
// 1.0.03: Re-try code injection to workaround React Hydration issues (SavageCore) |
|
|
|
(async function () { |
|
'use strict'; |
|
|
|
const collectionName = 'retroachievements_collection'; |
|
const mainCollectionItem = 'v5'; |
|
const separateCollectionItems = ['NES-Famicom', 'SNES-Super Famicom', 'PlayStation', 'PlayStation 2', 'PlayStation Portable', 'GameCube']; |
|
|
|
const collectionDownloadURL = 'https://archive.org/download/' + collectionName; |
|
const collectionDetailsURL = 'https://archive.org/details/' + collectionName + '_' + mainCollectionItem; |
|
const collectionLastModifiedURL = 'https://archive.org/metadata/' + collectionName + '_' + mainCollectionItem + '/item_last_updated'; |
|
const FBNeoROMSDownloadURL = 'https://archive.org/download/2020_01_06_fbn/roms/'; |
|
const FBNeoROMSDetailsURL = 'https://archive.org/details/2020_01_06_fbn/'; |
|
|
|
const retroachievementsHashList = 'TamperMonkeyRetroachievements.json'; |
|
|
|
const updateInterval = 86400; // 24 hours |
|
const currentUnixTimestamp = Math.floor(Date.now() / 1000); |
|
const collectionLastUpdated = parseInt(localStorage.getItem('collectionLastUpdated')); |
|
const collectionLastModified = parseInt(localStorage.getItem('collectionLastModified')); |
|
|
|
function addDisclaimer() { |
|
// add disclaimer |
|
const disclaimer = '<b>Downloads are provided through <a href="' + collectionDetailsURL + '">' + collectionDetailsURL + '</a> TamperMonkey script</br>and are not endorsed or supported by retroachievements.org</br></br>Please respect retroachievements.org\'s policies and do not post links to ROMs on their website or Discord.</b>'; |
|
document.querySelector("#app > div > main > article > div > div.flex.flex-col.gap-5 > div.-mx-3.rounded.bg-embed.px-3.py-4.sm\\:mx-0.sm\\:px-4.flex.flex-col.gap-4").insertAdjacentHTML('afterend', '<p class="embedded" id="ia_disclaimer>' + disclaimer + '</p>'); |
|
} |
|
|
|
if (isNaN(collectionLastUpdated) || currentUnixTimestamp > collectionLastUpdated + updateInterval) { |
|
|
|
fetch(collectionLastModifiedURL) |
|
.then(response => response.json()) |
|
.then(output => { |
|
|
|
if (output.result === undefined) { // archive.org returns 200/OK and {"error" : "*error description*"} on errors |
|
|
|
throw 'Can\'t get last modified date from archive.org. ' + output.error; |
|
|
|
} else { |
|
|
|
localStorage.setItem('collectionLastModified', output.result); |
|
|
|
} |
|
|
|
if (parseInt(output.result) === collectionLastModified) { // don't download retroachievementsHashList if we already have the latest |
|
|
|
localStorage.setItem('collectionLastUpdated', currentUnixTimestamp); |
|
injectArchiveGames(JSON.parse(localStorage.getItem('collectionROMList'))); |
|
|
|
} else { |
|
fetch(collectionDownloadURL + '_' + mainCollectionItem + '/' + retroachievementsHashList, { cache: 'no-cache' }) |
|
.then(response => response.json()) |
|
.then(output => { |
|
injectArchiveGames(output); |
|
localStorage.setItem('collectionROMList', JSON.stringify(output)); |
|
localStorage.setItem('collectionLastUpdated', currentUnixTimestamp); |
|
}) |
|
.catch(error => { |
|
|
|
// if we can't download retroachievementsHashList |
|
injectArchiveGames(null, true, 'Can\'t get retroachievements hash list from archive.org. Please try again later.'); |
|
localStorage.removeItem('collectionLastModified'); |
|
localStorage.removeItem('collectionLastUpdated'); |
|
localStorage.removeItem('collectionROMList'); |
|
}); |
|
} |
|
|
|
addDisclaimer(); |
|
}) |
|
.catch(() => { |
|
// we still have to let the end user know that script is working but archive.org is not |
|
injectArchiveGames(null, true, 'Can\'t get required information from archive.org. Please try again later.'); |
|
localStorage.removeItem('collectionLastModified'); |
|
localStorage.removeItem('collectionLastUpdated'); |
|
localStorage.removeItem('collectionROMList'); |
|
}); |
|
|
|
} else { |
|
window.addEventListener('load', () => { |
|
addDisclaimer(); |
|
injectArchiveGames(JSON.parse(localStorage.getItem('collectionROMList'))); |
|
|
|
// After half a second, check if the page now includes the injected disclaimer and download links, retry if not |
|
// Fix for React Hydration failing resulting in removed links and disclaimer (https://reactjs.org/docs/error-decoder.html?invariant=418 in console) |
|
let attempts = 0; |
|
const maxAttempts = 10; |
|
const interval = 500; |
|
|
|
const tryInjection = () => { |
|
if (document.querySelector("#ia_disclaimer") === null) { |
|
addDisclaimer(); |
|
injectArchiveGames(JSON.parse(localStorage.getItem('collectionROMList'))); |
|
} else if (attempts < maxAttempts) { |
|
attempts++; |
|
setTimeout(tryInjection, interval); |
|
} |
|
}; |
|
|
|
setTimeout(tryInjection, interval); |
|
}); |
|
} |
|
|
|
function injectArchiveGames(gameData, boolArchiveOrgDown = false, message = '') { |
|
|
|
let hashLists = document.querySelector("#app > div > main > article > div > div.flex.flex-col.gap-5 > div.flex.flex-col.gap-1 > div > ul").getElementsByTagName('li'); // get hash list |
|
let gameId = window.location.pathname.split("/")[2]; // get gameID from URL |
|
|
|
for (let x = 0; x < hashLists.length; ++x) { |
|
let retroHashNode = hashLists[x].childNodes[1]; |
|
let retroHash = retroHashNode.childNodes[0].innerText.trim().toUpperCase(); |
|
retroHashNode.childNodes[0].innerText = retroHash;// fix hash capitalization on the page |
|
|
|
if (boolArchiveOrgDown) { |
|
retroHashNode.insertAdjacentHTML("beforeend", '<b>' + message + '</b>'); |
|
|
|
} else { |
|
try { |
|
if (gameData[gameId] != undefined && gameData[gameId][0][retroHash] != undefined) { |
|
let hashData = gameData[gameId][0][retroHash]; // for now, we only have one item in the gameData[gameId] array |
|
let link, appendExtraInfo = ''; |
|
|
|
let ROMdataArray = hashData.split('/'); |
|
let system = ROMdataArray[0]; |
|
let fileName = ROMdataArray[ROMdataArray.length - 1]; |
|
|
|
switch (true) { |
|
case hashData.indexOf('\\') !== -1: // '\' is used to easily identify FBNeo ROMs in retroachievementsHashList, 'arcade\10yard.zip', 'nes\finalfaniii.zip' |
|
|
|
ROMdataArray = hashData.split('\\'); |
|
system = ROMdataArray[0].replace('megadriv', 'megadrive'); |
|
fileName = ROMdataArray[ROMdataArray.length - 1]; |
|
|
|
// example link: https://archive.org/download/2020_01_06_fbn/roms/nes.zip/nes/finalfaniii.zip |
|
link = FBNeoROMSDownloadURL + system + '.zip/' + system + '/' + fileName; |
|
appendExtraInfo = '<u><b>FBNeo ' + system.toUpperCase() + ' ROM set maintained by a 3rd party at</u></b> <a href="' + FBNeoROMSDetailsURL + '">' + FBNeoROMSDetailsURL + '</a></br>Download FULL ' + system.toUpperCase() + ' SET: <a href="' + FBNeoROMSDownloadURL + system + '.zip">' + system + '.zip</a>'; // add a note for FBNeo ROMs |
|
|
|
retroHashNode.insertAdjacentHTML("beforeend", '<div><b><a href="' + link + '">Download ' + fileName + '</a></b></br>' + appendExtraInfo + '</div>'); |
|
break; |
|
|
|
case hashData.startsWith('Dreamcast/!_flycast/'): |
|
|
|
link = collectionDownloadURL + '_' + mainCollectionItem + '/' + hashData; |
|
appendExtraInfo = '<b>Use <a href="https://github.com/flyinghead/flycast">https://github.com/flyinghead/flycast</a> or <a href="https://github.com/libretro/flycast">https://github.com/libretro/flycast</a> to run this ROM.</b>'; // add a note for FLYCAST ROMs |
|
|
|
retroHashNode.insertAdjacentHTML("beforeend", '<div><b><a href="' + link + '">Download ' + fileName + '</a></b></br>' + appendExtraInfo + '</div>'); |
|
break; |
|
|
|
case separateCollectionItems.includes(system): |
|
|
|
// PlayStation 2 is split based on filename over two archive.org items due to it's size |
|
if (system == 'PlayStation 2') { |
|
/^[n-z].*$/gim.test(fileName) ? system = 'PlayStation_2_N-Z' : system = 'PlayStation_2_A-M'; |
|
} |
|
|
|
link = collectionDownloadURL + '_' + system.replace(' ', '_') + '/' + hashData; // archive.org is not allowing spaces in item name |
|
// appendExtraInfo = '<b>Download provided through <a href=' + collectionDetailsURL + '>' + collectionDetailsURL + '</a></b>'; |
|
|
|
retroHashNode.insertAdjacentHTML("beforeend", '<div><b><a href="' + link + '">Download ' + fileName + '</a></b></br>' + appendExtraInfo + '</div>'); |
|
break; |
|
|
|
case hashData.startsWith('missing'): |
|
|
|
// TODO |
|
retroHashNode.insertAdjacentHTML("beforeend", ''); |
|
break; |
|
|
|
case hashData.startsWith('paid'): |
|
|
|
//TODO |
|
retroHashNode.insertAdjacentHTML("beforeend", ''); |
|
break; |
|
|
|
case hashData.startsWith('ignore'): |
|
|
|
//TODO |
|
retroHashNode.insertAdjacentHTML("beforeend", ''); |
|
break; |
|
|
|
default: |
|
link = collectionDownloadURL + '_' + mainCollectionItem + '/' + hashData; |
|
//appendExtraInfo = '<b>Download provided through <a href=' + collectionDetailsURL + '>' + collectionDetailsURL + '</a></b>'; |
|
|
|
retroHashNode.insertAdjacentHTML("beforeend", '<div><b><a href="' + link + '">Download ' + fileName + '</a></b></br>' + appendExtraInfo + '</div>'); |
|
break; |
|
} |
|
|
|
} else { |
|
console.log('Hash not found: ' + retroHash); |
|
|
|
retroHashNode.insertAdjacentHTML("beforeend", '<div><b>Download not available.</b></div>'); |
|
|
|
} |
|
|
|
} catch (error) { |
|
console.log('Error processing hashData: ' + hashData); |
|
|
|
console.log(error); |
|
|
|
} |
|
} |
|
} |
|
} |
|
})(); |
Checked again and the links are being properly inserted now.

In regards to the disclaimer, there was a small syntax error on line 68. missing a closing quote for the id attribute. Disclaimer is now being displayed how it was intended.
ps. thank you for spending the time and fixing this script, with my lack of skill I tried and failed many times, but found yours and has saved me a lot of time. Cool to see it working.