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