|
// ==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); |
|
|
|
} |
|
} |
|
} |
|
} |
|
})(); |
Hello, I'm trying to use the script and in tampermonkey it doesn't load the url and says "can't connect to archive.org at the moment." Violentmonkey works fine on showing the url but when I click on it, the archive oage says item has been removed.