Greasy Fork

Amazon Video ASIN Display

Show unique ASINs for episodes and movies/seasons on Amazon Prime Video

// ==UserScript==
// @name         Amazon Video ASIN Display
// @namespace    [email protected]
// @version      0.3.6
// @description  Show unique ASINs for episodes and movies/seasons on Amazon Prime Video
// @author       ReiDoBrega
// @license      MIT
// @match        https://www.amazon.com/*
// @match        https://www.amazon.co.uk/*
// @match        https://www.amazon.de/*
// @match        https://www.amazon.co.jp/*
// @match        https://www.primevideo.com/*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
    "use strict";

    // Add styles for ASIN display and pop-up
    let style = document.createElement("style");
    style.textContent = `
        .x-asin-container {
            margin: 0.5em 0 1em 0;
        }
        .x-asin-item, .x-episode-asin {
            color: #1399FF; /* Green color */
            cursor: pointer;
            margin: 5px 0;
        }
        .x-copy-popup {
            position: fixed;
            bottom: 20px;
            right: 20px;
            background-color: rgba(0, 0, 0, 0); /* Transparent background */
            color: #1399FF; /* Green text */
            padding: 10px 20px;
            border-radius: 5px;
            font-family: Arial, sans-serif;
            font-size: 14px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0);
            z-index: 1000;
            animation: fadeInOut 2.5s ease-in-out;
        }
        @keyframes fadeOut {
            0% { opacity: 1; }
            100% { opacity: 0; }
        }
    `;
    document.head.appendChild(style);

    // Function to extract ASIN from URL
    function extractASINFromURL() {
        const url = window.location.href;
        const asinRegex = /\/gp\/video\/detail\/([A-Z0-9]{10})/;
        const match = url.match(asinRegex);
        return match ? match[1] : null;
    }

    // Function to find and display unique ASINs
    function findUniqueASINs() {
        // Extract ASIN from URL first
        const urlASIN = extractASINFromURL();
        if (urlASIN) {
            return { urlASIN };
        }

        // Object to store one unique ASIN/ID for each type
        let uniqueIds = {};

        // List of ID patterns to find
        const idPatterns = [
            {
                name: 'titleID',
                regex: /"titleID":"([^"]+)"/
            },
            // {
            //     name: 'pageTypeId',
            //     regex: /pageTypeId: "([^"]+)"/
            // },
            // {
            //     name: 'pageTitleId',
            //     regex: /"pageTitleId":"([^"]+)"/
            // },
            // {
            //     name: 'catalogId',
            //     regex: /catalogId":"([^"]+)"/
            // }
        ];

        // Search through patterns
        idPatterns.forEach(pattern => {
            let match = document.body.innerHTML.match(pattern.regex);
            if (match && match[1]) {
                uniqueIds[pattern.name] = match[1];
            }
        });

        return uniqueIds;
    }

    // Function to find ASINs from JSON response
    function findUniqueASINsFromJSON(jsonData) {
        let uniqueIds = {};

        // Comprehensive search paths for ASINs
        const searchPaths = [
            { name: 'titleId', paths: [
                ['titleID'],
                ['page', 0, 'assembly', 'body', 0, 'args', 'titleID'],
                ['titleId'],
                ['detail', 'titleId'],
                ['data', 'titleId']
            ]},
        ];

        // Deep object traversal function
        function traverseObject(obj, paths) {
            for (let pathSet of paths) {
                try {
                    let value = obj;
                    for (let key of pathSet) {
                        value = value[key];
                        if (value === undefined) break;
                    }

                    if (value && typeof value === 'string' && value.trim() !== '') {
                        return value;
                    }
                } catch (e) {
                    // Silently ignore traversal errors
                }
            }
            return null;
        }

        // Search through all possible paths
        searchPaths.forEach(({ name, paths }) => {
            const value = traverseObject(jsonData, paths);
            if (value) {
                uniqueIds[name] = value;
                console.log(`[ASIN Display] Found ${name} in JSON: ${value}`);
            }
        });

        return uniqueIds;
    }

    // Function to add episode ASINs
    function addEpisodeASINs() {
        try {
            document.querySelectorAll("[id^='selector-'], [id^='av-episode-expand-toggle-']").forEach(el => {
                // Skip if ASIN already added
                if (el.parentNode.querySelector(".x-episode-asin")) {
                    return;
                }

                // Extract ASIN from the element ID
                let asin = el.id.replace(/^(?:selector|av-episode-expand-toggle)-/, "");

                // Create ASIN element
                let asinEl = document.createElement("div");
                asinEl.className = "x-episode-asin";
                asinEl.textContent = asin;
                asinEl.addEventListener("click", () => copyToClipboard(asin));

                // Insert ASIN element after the episode title
                let epTitle = el.parentNode.querySelector("[data-automation-id^='ep-title']");
                if (epTitle) {
                    epTitle.parentNode.insertBefore(asinEl, epTitle.nextSibling);
                }
            });
            return true; // Episode ASINs added successfully
        } catch (e) {
            console.error("ASIN Display - Error in addEpisodeASINs:", e);
            return false; // Error occurred
        }
    }

    // Function to add ASIN display
    function addASINDisplay(uniqueIds = null) {
        try {
            // If no IDs provided, find them from HTML
            if (!uniqueIds) {
                uniqueIds = findUniqueASINs();
            }

            // Remove existing ASIN containers
            document.querySelectorAll(".x-asin-container").forEach(el => el.remove());

            // If no IDs found, return
            if (Object.keys(uniqueIds).length === 0) {
                console.log("ASIN Display: No ASINs found");
                return false;
            }

            // Create ASIN container
            let asinContainer = document.createElement("div");
            asinContainer.className = "x-asin-container";

            // Add each unique ID as a clickable element
            Object.entries(uniqueIds).forEach(([type, id]) => {
                let asinEl = document.createElement("div");
                asinEl.className = "x-asin-item";
                asinEl.textContent = id;
                asinEl.addEventListener("click", () => copyToClipboard(id));
                asinContainer.appendChild(asinEl);
            });

            // Insert the ASIN container after the synopsis
            let after = document.querySelector(".dv-dp-node-synopsis, .av-synopsis");
            if (!after) {
                console.log("ASIN Display: Could not find element to insert after");
                return false;
            }

            after.parentNode.insertBefore(asinContainer, after.nextSibling);
            return true;
        } catch (e) {
            console.error("ASIN Display - Error in addASINDisplay:", e);
            return false;
        }
    }

    // Function to copy text to clipboard and show pop-up
    function copyToClipboard(text) {
        const input = document.createElement("textarea");
        input.value = text;
        document.body.appendChild(input);
        input.select();
        document.execCommand("copy");
        document.body.removeChild(input);

        // Show pop-up
        const popup = document.createElement("div");
        popup.className = "x-copy-popup";
        popup.textContent = `Copied: ${text}`;
        document.body.appendChild(popup);

        // Remove pop-up after 1.5 seconds
        setTimeout(() => {
            popup.remove();
        }, 1500);
    }

    // Intercept fetch requests for JSON responses
    const originalFetch = window.fetch;
    window.fetch = function(...args) {
        const [url] = args;

        // Check if the URL matches the detail page pattern
        if (url.includes('/detail/') && url.includes('primevideo.com')) {
            return originalFetch.apply(this, args).then(response => {
                try {
                    const contentType = response.headers.get('content-type');

                    if (contentType?.includes('application/json')) {
                        return response.clone().json().then(jsonResponse => {
                            // Find unique IDs using comprehensive search paths
                            const jsonIds = findUniqueASINsFromJSON(jsonResponse);

                            if (Object.keys(jsonIds).length > 0) {
                                // Wait for the page to settle before displaying ASINs
                                setTimeout(() => addASINDisplay(jsonIds), 1000);
                            }

                            return response;
                        });
                    }
                    return response;
                } catch (error) {
                    console.error('Error in fetch interception:', error);
                    return response;
                }
            });
        }

        return originalFetch.apply(this, args);
    };

    // Track the current URL
    let currentURL = window.location.href;

    // Function to check for URL changes
    function checkForURLChange() {
        if (window.location.href !== currentURL) {
            currentURL = window.location.href;
            console.log("URL changed. Updating IDs...");
            // Wait for the page to settle before displaying ASINs
            setTimeout(() => {
                addASINDisplay(); // Display main ASINs
                addEpisodeASINs(); // Display episode ASINs
            }, 1000);
        }
    }

    // Run the URL change checker every 500ms
    setInterval(checkForURLChange, 500);

    // Initial run after the page has fully loaded
    window.addEventListener("load", () => {
        setTimeout(() => {
            addASINDisplay(); // Display main ASINs
            addEpisodeASINs(); // Display episode ASINs
        }, 1000);
    });
})();