Created
September 7, 2025 18:11
-
-
Save dbernheisel/2f48ad7efc8b4d748ab0aa11f65b2d7b to your computer and use it in GitHub Desktop.
Scrape your owned Nintendo and Playstation games
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Copy and paste when browsing your collection | |
| // https://www.nintendo.com/us/orders/ | |
| // https://library.playstation.com/recently-purchased/1 | |
| // | |
| // A textarea will be appended to the page that will have a list of your games | |
| // ready for copy/paste into a spreadsheet program; the columns are: | |
| // Title, System, isPhysical, isDigital, isReproduction, isSubscriptionOnly | |
| // isSubscriptionOnly may not be accurate, because you may also own it physically, but it also is availble on Playstation Plus. | |
| // isReproduction is always false | |
| // isPhysical is always false | |
| const gameListEl = { | |
| "nintendo": "main", | |
| "playstation": "[data-qa='collection-game-list-collection']" | |
| } | |
| const gameEl = { | |
| "nintendo": 'section.sc-aicnew-2', | |
| "playstation": "[data-qa='collection-game-list-product#store-link']" | |
| } | |
| let games = [] | |
| const skipNames = [ | |
| "F-ZERO™ 99", | |
| "Pokémon™ HOME", | |
| "Minecoin Pack", | |
| "Super Mario Bros.™ 35", | |
| "Animal Crossing™: New Horizons Nintendo Switch Online - Member-Exclusive Items", | |
| "Media Player", | |
| "Twitch", | |
| "Crunchyroll", | |
| "Crackle - Free Movies and TV", | |
| "YouTube", | |
| "Plex", | |
| "Hulu", | |
| "Amazon Prime Video", | |
| "Netflix", | |
| "VUDU™ HD Movies", | |
| "Indivisible Prototype", | |
| "HBO Max", | |
| "Disney+", | |
| "Minecraft Preview", | |
| "Horizon Zero Dawn™ Artbook", | |
| "PUBG - Public Test Server", | |
| ] | |
| const skipIncludes = [ | |
| "nso family membership", | |
| /realms .* subscription/, | |
| /minecoin pack/, | |
| "soundtrack", | |
| "demo", | |
| "soundtrack", | |
| "demo", | |
| "tech test", | |
| "test server", | |
| "arcade game series:", | |
| ] | |
| const removeFromTitle = [ | |
| "(PS4)", | |
| "(PS5)", | |
| "(English)", | |
| "™", | |
| "™", | |
| "®", | |
| ] | |
| const fixTitles = { | |
| "Minecraft Legends": "Minecraft: Legends", | |
| "Crysis3® Remastered": "Crysis 3 Remastered", | |
| "Crysis2® Remastered": "Crysis 2 Remastered", | |
| "LittleBigPlanet™3": "LittleBigPlanet 3", | |
| "Plants vs Zombies GW2": "Plants vs Zombies Garden Warfare 2", | |
| "DIRT5": "DIRT 5", | |
| "WATCH_DOGS™": "Watch Dogs", | |
| "WATCH_DOGS® 2": "Watch Dogs 2" | |
| } | |
| function cleanTitle(title) { | |
| let cleaned = fixTitles[title] || title | |
| removeFromTitle.forEach((t) => cleaned = cleaned.replace(t, "")) | |
| return cleaned.replace(" : ", ": ").trim() | |
| } | |
| function skipGame(title) { | |
| return ( | |
| skipNames.some((t) => title == t) || | |
| skipIncludes.some((t) => { | |
| if (typeof t === 'string') return title.toLowerCase().includes(t.toLowerCase()) | |
| if (t instanceof RegExp) return t.test(title.toLowerCase()) | |
| }) | |
| ) | |
| } | |
| function getSystem(g, system){ | |
| switch (system) { | |
| case "nintendo": | |
| const s2 = g.querySelector("[data-testid='NintendoSwitch2LogoOnlyIcon']") | |
| if (s2) return "Nintendo Switch 2" | |
| return "Nintendo Switch" | |
| case "playstation": | |
| const tags = g.querySelector("[data-qa='collection-game-list-product#platform-tags']") | |
| if (tags && tags.innerText.includes("PS4")) { | |
| return "PlayStation 4" | |
| } else if (tags && tags.innerText.includes("PS5")) { | |
| return "PlayStation 5" | |
| } | |
| } | |
| } | |
| function isSubscription(g, system) { | |
| switch (system) { | |
| case "nintendo": | |
| return false | |
| case "playstation": | |
| const el = g.querySelector("[data-qa='collection-game-list-product#service-upsell']") | |
| if (el) { | |
| return el.innerText.includes("PS PLUS") | |
| } else { | |
| return false | |
| } | |
| } | |
| } | |
| function getTitle(g, system) { | |
| switch (system) { | |
| case "playstation": | |
| return JSON.parse(g.dataset.telemetryMeta).titleName | |
| case "nintendo": | |
| const el = g.querySelector('div.cGwtLt h1.s954l') | |
| if(el) { | |
| return el.innerText | |
| } else { | |
| return false | |
| } | |
| } | |
| } | |
| function process(system){ | |
| const gameList = document.querySelector(gameListEl[system]) | |
| for (let g of gameList.querySelectorAll(gameEl[system])) { | |
| let game = {} | |
| let title = getTitle(g, system) | |
| if (!title) { | |
| console.error("Could not process", g) | |
| continue | |
| } | |
| if (skipGame(title)) continue | |
| game.title = cleanTitle(title) | |
| game.system = getSystem(g, system) | |
| game.physical = false | |
| game.digital = true | |
| game.reproduction = false | |
| game.subscription = isSubscription(g, system) | |
| games.push(game) | |
| } | |
| return [...new Set(games)].sort((e) => e.title) | |
| } | |
| function writeTextArea() { | |
| let text = "" | |
| games.forEach((e) => { | |
| text += `"${e.title}","${e.system}",${e.physical},${e.digital},${e.reproduction},${e.subscription}\n` | |
| }) | |
| textEl = document.createElement('textarea') | |
| textEl.name = "copyme" | |
| textEl.style = 'background-color: white; width: 100%;' | |
| textEl.rows = 10 | |
| textEl.value = text | |
| document.querySelector('main').append(textEl) | |
| textEl.scrollIntoView() | |
| } | |
| function getNintendoPages() { | |
| const digitalBtn = [...document.querySelectorAll("button.buovD")].find((e) => e.innerText.includes("Digital")) | |
| if (digitalBtn) digitalBtn.click() | |
| let loop | |
| return new Promise(resolve => { | |
| loop = setInterval(function() { | |
| const btn = document.querySelector("button.sc-1wh0o51-3") | |
| if (btn) { | |
| console.log("getting page") | |
| btn.click() | |
| } else { | |
| clearInterval(loop) | |
| console.log("Done getting pages") | |
| resolve() | |
| } | |
| }, 1000) | |
| }) | |
| } | |
| async function nextPlaystationPage() { | |
| return await new Promise(resolve => { | |
| let loop | |
| const nextBtn = document.querySelector("[data-qa='pagination#next']:not(:disabled)") | |
| if (nextBtn) { | |
| console.log("getting page") | |
| nextBtn.click() | |
| let loop | |
| loop = setInterval(function() { | |
| if (document.querySelector("[data-qa='collection-loading']")) { | |
| console.log("waiting for page...") | |
| } else { | |
| clearInterval(loop) | |
| resolve(true) | |
| } | |
| }, 1500) | |
| } else { | |
| console.log("Done getting pages") | |
| resolve(false) | |
| } | |
| }) | |
| } | |
| async function getPage(system) { | |
| switch (system) { | |
| case "nintendo": | |
| return false | |
| case "playstation": | |
| return await nextPlaystationPage() | |
| break; | |
| } | |
| } | |
| async function preprocess(system) { | |
| switch (system) { | |
| case "nintendo": | |
| await getNintendoPages() | |
| break; | |
| case "playstation": | |
| break; | |
| } | |
| } | |
| async function getGames() { | |
| let system | |
| switch (window.location.host) { | |
| case 'library.playstation.com': | |
| system = 'playstation' | |
| break; | |
| case 'www.nintendo.com': | |
| system = 'nintendo' | |
| break; | |
| default: | |
| console.error("Does not support this site") | |
| return | |
| } | |
| console.log("preprocess") | |
| await preprocess(system) | |
| while (true) { | |
| console.log("process") | |
| process(system) | |
| if (await getPage(system) === false) break | |
| } | |
| writeTextArea() | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment