NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name PSPrices Show Product SKU
// @namespace https://github.com/XxUnkn0wnxX/Scripts
// @version 1.0.1.3
// @description Displays and copies the public PlayStation product SKU on PSPrices product pages, adding a native-style SKU panel below buy, checkout, or unavailable-store sections only when PSPrices does not already show one. Vibe coded with OpenAI.
// @homepageURL https://github.com/XxUnkn0wnxX/Scripts
// @supportURL https://discord.gg/slayersicerealm
// @author XxUnkn0wnxX
// @license AGPL-3.0-or-later
// @updateURL https://raw.githubusercontent.com/XxUnkn0wnxX/Scripts/master/userscripts/PSPrices-Show-Product-SKU.user.js
// @downloadURL https://raw.githubusercontent.com/XxUnkn0wnxX/Scripts/master/userscripts/PSPrices-Show-Product-SKU.user.js
// @match https://psprices.com/region-*/game/*
// @match https://www.psprices.com/region-*/game/*
// @run-at document-idle
// @grant none
// @noframes
// ==/UserScript==
(() => {
'use strict';
const CARD_ID = 'psprices-product-sku-userscript';
const UNAVAILABLE_STORE_TEXT =
'This item is no longer available for purchase on the PlayStation Store';
const PRODUCT_PATH = /^\/region-[a-z0-9-]+\/game\/\d+(?:\/[^/]+)?\/?$/i;
let mountScheduled = false;
function isProductPage() {
return PRODUCT_PATH.test(window.location.pathname);
}
function hasProductType(type) {
if (Array.isArray(type)) {
return type.some(hasProductType);
}
return String(type || '').toLowerCase() === 'product';
}
function findProductSku(value) {
if (Array.isArray(value)) {
for (const entry of value) {
const sku = findProductSku(entry);
if (sku) return sku;
}
return null;
}
if (!value || typeof value !== 'object') {
return null;
}
if (hasProductType(value['@type']) && typeof value.sku === 'string') {
const sku = value.sku.trim();
if (sku) return sku;
}
if (value['@graph']) {
return findProductSku(value['@graph']);
}
return null;
}
function readProductSku() {
const scripts = document.querySelectorAll('script[type="application/ld+json"]');
for (const script of scripts) {
try {
const sku = findProductSku(JSON.parse(script.textContent || ''));
if (sku) return sku;
} catch (_) {
// Ignore unrelated or malformed structured-data blocks.
}
}
return null;
}
function copyWithFallback(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
let copied = false;
try {
copied = document.execCommand('copy');
} catch (_) {}
textarea.remove();
return copied;
}
async function copySku(sku) {
try {
await navigator.clipboard.writeText(sku);
return true;
} catch (_) {
return copyWithFallback(sku);
}
}
function createSkuCard(sku) {
const card = document.createElement('div');
card.id = CARD_ID;
card.dataset.sku = sku;
card.dataset.testId = 'userscript-product-sku';
card.className =
'rounded-[var(--game-detail-radius-card)] border border-primary/30 bg-primary/5 p-4 space-y-2';
const heading = document.createElement('div');
heading.className = 'text-xs text-base-content font-medium uppercase tracking-wider';
heading.textContent = 'SKU';
const value = document.createElement('div');
value.className =
'overflow-x-auto font-mono text-sm text-base-content font-semibold whitespace-nowrap select-all';
value.textContent = sku;
const button = document.createElement('button');
button.type = 'button';
button.dataset.testId = 'userscript-product-sku-copy';
button.className =
'inline-flex items-center gap-1.5 text-sm text-primary transition-colors hover:text-primary/80 cursor-pointer';
button.title = 'Copy ID to clipboard';
const icon = document.createElement('span');
icon.className = 'material-symbols-outlined text-base';
icon.textContent = 'content_copy';
const buttonText = document.createElement('span');
buttonText.className = 'copy-text';
buttonText.textContent = 'Copy SKU';
button.append(icon, buttonText);
button.addEventListener('click', async () => {
const copied = await copySku(sku);
icon.textContent = copied ? 'check' : 'error';
buttonText.textContent = copied ? 'Copied' : 'Copy failed';
window.setTimeout(() => {
icon.textContent = 'content_copy';
buttonText.textContent = 'Copy SKU';
}, 1000);
});
card.append(heading, value, button);
return card;
}
function isUnavailableStoreAlert(element) {
return Boolean(
element?.matches?.('.alert.alert-warning') &&
element.textContent.replace(/\s+/g, ' ').trim().includes(UNAVAILABLE_STORE_TEXT)
);
}
function findUnavailableMountTarget(gameDetail) {
const checkoutCard = gameDetail.querySelector(
'[data-test-id="psprices-checkout-card"][data-psprices-checkout-target="unavailable"]'
);
if (checkoutCard?.parentElement) {
return { element: checkoutCard.parentElement, position: 'append-spaced' };
}
const alert = [...gameDetail.querySelectorAll('.alert.alert-warning')].find(
isUnavailableStoreAlert
);
if (alert?.parentElement) {
return { element: alert.parentElement, position: 'append-spaced' };
}
return null;
}
function findMountTarget(gameDetail) {
const avatarBuyBlock = gameDetail.querySelector('[data-avatar-buy-block]');
if (avatarBuyBlock) {
return { element: avatarBuyBlock, position: 'append' };
}
const unavailableTarget = findUnavailableMountTarget(gameDetail);
if (unavailableTarget) {
return unavailableTarget;
}
const gameActions = gameDetail.querySelector('.game-detail-actions');
if (gameActions) {
return { element: gameActions, position: 'prepend-spaced' };
}
const hero = gameDetail.querySelector('[data-test-id="game-detail-hero"]');
if (hero) {
return { element: hero, position: 'after-spaced' };
}
return null;
}
function mountSkuCard() {
const injectedCard = document.getElementById(CARD_ID);
if (!isProductPage()) {
injectedCard?.remove();
return;
}
// Avatar pages already provide the same native SKU card.
if (document.querySelector('[data-test-id="avatar-sku"]')) {
injectedCard?.remove();
return;
}
const sku = readProductSku();
if (!sku) {
return;
}
const gameDetail = document.getElementById('game-detail');
if (!gameDetail) {
return;
}
const target = findMountTarget(gameDetail);
if (!target) {
return;
}
if (injectedCard?.dataset.sku === sku && injectedCard.parentElement === target.element) {
if (target.position === 'append-spaced') {
injectedCard.style.marginTop = '0.75rem';
}
return;
}
injectedCard?.remove();
const card = createSkuCard(sku);
if (target.position === 'append') {
target.element.append(card);
} else if (target.position === 'append-spaced') {
card.style.marginTop = '0.75rem';
target.element.append(card);
} else if (target.position === 'prepend-spaced') {
card.style.marginBottom = '0.75rem';
target.element.prepend(card);
} else {
card.style.margin = '0 1rem 1.5rem';
target.element.insertAdjacentElement('afterend', card);
}
}
function scheduleMount() {
if (mountScheduled) return;
mountScheduled = true;
window.requestAnimationFrame(() => {
mountScheduled = false;
mountSkuCard();
});
}
scheduleMount();
const observer = new MutationObserver(scheduleMount);
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
window.addEventListener('pageshow', scheduleMount);
window.addEventListener('popstate', scheduleMount);
document.addEventListener('htmx:afterSwap', scheduleMount);
})();