您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Show unique ASINs for episodes and movies/seasons on Amazon Prime Video
- // ==UserScript==
- // @name Amazon Video ASIN Display
- // @namespace sac@libidgel.com
- // @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 });
- })();