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.4.0
// @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 = `
        // Modify your style.textContent by adding this rule:
        .x-asin-container ._3ra7oO {
            font-size: 0.2em;
            opacity: 0.75;
            margin-top: 2px;
        }
        .x-asin-item, .x-episode-asin {
            color: #1399FF; /* Blue 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; /* Blue 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; }
        }
        .x-asin-display {
            font-size: 5px; /* Absolute size in pixels */
            opacity: 0.7;
            margin-top: 12px;
            cursor: pointer;
        }
    `;
    document.head.appendChild(style);

    // Store for captured episode data
    let capturedEpisodeData = [];

    // Flag to indicate if we've already processed episodes from API
    let episodesProcessed = false;

    // 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 extract episodes from JSON data
    function extractEpisodes(jsonData) {
        try {
            // Possible paths to episode data
            const episodePaths = [
                ['items'],
                ['widgets', 0, 'data', 'items'],
                ['widgets', 0, 'items'],
                ['data', 'widgets', 0, 'items'],
                ['page', 0, 'assembly', 'body', 0, 'items'],
                ['data', 'items']
            ];

            // Try each path
            for (const path of episodePaths) {
                let current = jsonData;
                let valid = true;

                // Navigate through the path
                for (const key of path) {
                    if (current && current[key] !== undefined) {
                        current = current[key];
                    } else {
                        valid = false;
                        break;
                    }
                }

                // If we found a valid path and it's an array of items
                if (valid && Array.isArray(current)) {
                    return current.filter(item =>
                        item &&
                        (item.titleId || item.id || item.episodeID || item.asin)
                    );
                }
            }
        } catch (e) {
            console.error("Error extracting episodes:", e);
        }
        return [];
    }

    // Function to add episode ASINs from captured API data
    function addAPIEpisodeASINs() {
        try {
            if (capturedEpisodeData.length === 0 || episodesProcessed) {
                return false;
            }

            console.log(`[ASIN Display] Processing ${capturedEpisodeData.length} captured episodes`);

            // Process and display episode ASINs
            capturedEpisodeData.forEach(episode => {
                const episodeId = episode.titleId || episode.id || episode.episodeID || episode.asin;
                const episodeNumber = episode.episodeNumber || episode.number;
                const seasonNumber = episode.seasonNumber;

                if (!episodeId) return;

                // Find episode element to attach ASIN to
                const selector = `[data-automation-id="ep-${episodeNumber}"], [id^="selector-${episodeId}"], [id^="av-episode-expand-toggle-${episodeId}"]`;
                let episodeElement = document.querySelector(selector);

                // If can't find by direct ID, try to find by episode number
                if (!episodeElement && episodeNumber) {
                    episodeElement = document.querySelector(`[data-automation-id*="ep-${episodeNumber}"]`);

                    // Try alternative approaches for finding episode elements
                    if (!episodeElement) {
                        // This will try to match elements that might contain the episode number visually
                        const possibleElements = [...document.querySelectorAll('[data-automation-id*="ep-"]')];
                        episodeElement = possibleElements.find(el => {
                            const text = el.textContent.trim();
                            return text.includes(`Episode ${episodeNumber}`) ||
                                  text.includes(`Ep. ${episodeNumber}`) ||
                                  text.match(new RegExp(`\\b${episodeNumber}\\b`));
                        });
                    }
                }

                // If we found an element to attach to
                if (episodeElement) {
                    // Skip if ASIN already added
                    if (episodeElement.parentNode.querySelector("._3ra7oO")) {
                        return;
                    }

                    // Create ASIN element
                    let asinEl = document.createElement("div");
                    asinEl.className = "_3ra7oO x-asin-display"; // Add your custom class alongside the original
                    asinEl.textContent = asin;
                    asinEl.addEventListener("click", () => copyToClipboard(asin));

                    // Insert ASIN element after the episode title
                    let epTitle = episodeElement.parentNode.querySelector("[data-automation-id^='ep-title']");
                    if (epTitle) {
                        epTitle.parentNode.insertBefore(asinEl, epTitle.nextSibling);
                    } else {
                        // If can't find specific title element, just append to the episode element's parent
                        episodeElement.parentNode.appendChild(asinEl);
                    }
                } else {
                    console.log(`[ASIN Display] Could not find element for episode ${episodeNumber} with ID ${episodeId}`);
                }
            });

            // Mark as processed to avoid duplicate processing
            episodesProcessed = true;

            return true; // API episode ASINs added successfully
        } catch (e) {
            console.error("ASIN Display - Error in addAPIEpisodeASINs:", e);
            return false; // Error occurred
        }
    }

    // Function to add episode ASINs using DOM
    function addEpisodeASINs() {
        try {
            document.querySelectorAll("[id^='selector-'], [id^='av-episode-expand-toggle-']").forEach(el => {
                // Skip if ASIN already added
                if (el.parentNode.querySelector("._3ra7oO")) {
                    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 = "_3ra7oO x-asin-display"; // Add your custom class alongside the original
                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 = "_1jWggM v2uvTa fbl-btn _2Pw7le";
                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;
        const isString = typeof url === 'string';

        // Create a promise for the original fetch
        const fetchPromise = originalFetch.apply(this, args);

        // Check if this is a URL we're interested in
        if (isString &&
           ((url.includes('/detail/') && url.includes('primevideo.com')) ||
            (url.includes('api/getDetailWidgets')))) {

            // Process the response without blocking the original fetch
            fetchPromise.then(async response => {
                try {
                    // Only process JSON responses
                    const contentType = response.headers.get('content-type');
                    if (contentType?.includes('application/json')) {
                        // Clone the response to avoid consuming it
                        const clonedResponse = response.clone();
                        const jsonResponse = await clonedResponse.json();

                        // Find unique IDs from the response
                        const jsonIds = findUniqueASINsFromJSON(jsonResponse);

                        // For Detail API calls, extract episodes
                        if (url.includes('getDetailWidgets')) {
                            const episodes = extractEpisodes(jsonResponse);
                            if (episodes && episodes.length > 0) {
                                console.log(`[ASIN Display] Intercepted API response with ${episodes.length} episodes`);

                                // Store episode data for later use
                                capturedEpisodeData = episodes;

                                // Reset the processed flag to allow reprocessing on new data
                                episodesProcessed = false;

                                // Wait for the page to settle before updating
                                setTimeout(() => {
                                    addAPIEpisodeASINs();
                                }, 1000);
                            }
                        }

                        // Update ASIN display with any findings
                        if (Object.keys(jsonIds).length > 0) {
                            setTimeout(() => {
                                addASINDisplay(jsonIds);
                            }, 1000);
                        }
                    }
                } catch (error) {
                    console.error('[ASIN Display] Error processing fetch response:', error);
                }
            }).catch(error => {
                console.error('[ASIN Display] Error in fetch intercept:', error);
            });
        }

        // Return the original fetch promise so the page works normally
        return fetchPromise;
    };

    // Also intercept XHR requests to capture any non-fetch API calls
    const originalXHROpen = XMLHttpRequest.prototype.open;
    const originalXHRSend = XMLHttpRequest.prototype.send;

    XMLHttpRequest.prototype.open = function(method, url, ...rest) {
        // Store the URL if it's a detail API call
        if (typeof url === 'string' && url.includes('api/getDetailWidgets')) {
            this._asinDisplayUrl = url;
        }
        return originalXHROpen.apply(this, [method, url, ...rest]);
    };

    XMLHttpRequest.prototype.send = function(...args) {
        if (this._asinDisplayUrl) {
            // Add a response handler
            this.addEventListener('load', function() {
                try {
                    if (this.responseType === 'json' ||
                        (this.getResponseHeader('content-type')?.includes('application/json'))) {

                        let jsonResponse;
                        if (this.responseType === 'json') {
                            jsonResponse = this.response;
                        } else {
                            jsonResponse = JSON.parse(this.responseText);
                        }

                        // Extract episodes and IDs
                        const episodes = extractEpisodes(jsonResponse);
                        if (episodes && episodes.length > 0) {
                            console.log(`[ASIN Display] Intercepted XHR with ${episodes.length} episodes`);
                            capturedEpisodeData = episodes;
                            episodesProcessed = false;

                            setTimeout(() => {
                                addAPIEpisodeASINs();
                            }, 1000);
                        }

                        // Update main ASIN display
                        const jsonIds = findUniqueASINsFromJSON(jsonResponse);
                        if (Object.keys(jsonIds).length > 0) {
                            setTimeout(() => {
                                addASINDisplay(jsonIds);
                            }, 1000);
                        }
                    }
                } catch (error) {
                    console.error('[ASIN Display] Error processing XHR response:', error);
                }
            });
        }
        return originalXHRSend.apply(this, args);
    };

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

    // Function to update all ASINs
    function updateAllASINs() {
        // Display main ASINs
        addASINDisplay();

        // Try to add episode ASINs from DOM first
        addEpisodeASINs();

        // Try to add episode ASINs from captured API data
        addAPIEpisodeASINs();

        // Reset the episodesProcessed flag on page change
        episodesProcessed = false;
    }

    // Function to check for URL changes
    function checkForURLChange() {
        if (window.location.href !== currentURL) {
            currentURL = window.location.href;
            console.log("[ASIN Display] URL changed. Updating IDs...");

            // Clear captured data on page change
            capturedEpisodeData = [];
            episodesProcessed = false;

            // Wait for the page to settle before displaying ASINs
            setTimeout(() => {
                updateAllASINs();
            }, 1000);
        }
    }

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

    // Initial run after the page has fully loaded
    window.addEventListener("load", () => {
        setTimeout(() => {
            updateAllASINs();
        }, 1000);
    });

    // Additional MutationObserver to detect DOM changes that might indicate new episodes loaded
    const observer = new MutationObserver((mutations) => {
        // Look for mutations that might indicate new episode content
        const episodeContentChanged = mutations.some(mutation => {
            // Check if any added nodes contain episode selectors
            return Array.from(mutation.addedNodes).some(node => {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    return node.querySelector?.('[id^="selector-"], [id^="av-episode-expand-toggle-"]') ||
                           node.id?.startsWith('selector-') ||
                           node.id?.startsWith('av-episode-expand-toggle-');
                }
                return false;
            });
        });

        if (episodeContentChanged) {
            console.log("[ASIN Display] Detected new episode content, updating ASINs...");
            setTimeout(() => {
                updateAllASINs();
            }, 1000);
        }
    });

    // Start observing the document body for episode content changes
    observer.observe(document.body, { childList: true, subtree: true });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址