Audible Metadata to MAM JSON script (with ChatGPT)

Copies audiobook metadata to JSON and opens MAM upload page. Uses ChatGPT for category and series detection.

目前為 2024-12-16 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Audible Metadata to MAM JSON script (with ChatGPT)
// @namespace    https://greasyfork.org/en/scripts/511491
// @version      1.5.0
// @license      MIT
// @description  Copies audiobook metadata to JSON and opens MAM upload page. Uses ChatGPT for category and series detection.
// @author       SnowmanNurse (Modified from original by Dr.Blank)
// @include      https://www.audible.*/pd/*
// @include      https://www.audible.*/ac/*
// @grant        none
// ==/UserScript==

const RIPPER = "MusicFab"; // yours can be Libation, OpenAudible, MusicFab, InAudible etc. or Blank if Encoded
const CHAPTERIZED = true; // yours will be false if not properly ripped

// Category selection method: 1 for direct mapping, 2 for scoring method, 3 for ChatGPT
const CATEGORY_SELECTION_METHOD = 3;

// API key for ChatGPT
const CHATGPT_API_KEY = "OPENAI Code goes here."; // Insert your ChatGPT API key here

const AVAILABLE_CATEGORIES = [
    "Audiobooks - Art", "Audiobooks - Biographical", "Audiobooks - Business", "Audiobooks - Crafts",
    "Audiobooks - Fantasy", "Audiobooks - Food", "Audiobooks - History", "Audiobooks - Horror",
    "Audiobooks - Humor", "Audiobooks - Instructional", "Audiobooks - Juvenile", "Audiobooks - Language",
    "Audiobooks - Medical", "Audiobooks - Mystery", "Audiobooks - Nature", "Audiobooks - Philosophy",
    "Audiobooks - Recreation", "Audiobooks - Romance", "Audiobooks - Self-Help", "Audiobooks - Western",
    "Audiobooks - Young Adult", "Audiobooks - Historical Fiction", "Audiobooks - Literary Classics",
    "Audiobooks - Science Fiction", "Audiobooks - True Crime", "Audiobooks - Urban Fantasy",
    "Audiobooks - Action/Adventure", "Audiobooks - Computer/Internet", "Audiobooks - Crime/Thriller",
    "Audiobooks - Home/Garden", "Audiobooks - Math/Science/Tech", "Audiobooks - Travel/Adventure",
    "Audiobooks - Pol/Soc/Relig", "Audiobooks - General Fiction", "Audiobooks - General Non-Fic"
];

// Direct mapping from Audible to MAM categories
const AUDIBLE_TO_MAM_CATEGORY_MAP = {
    "Arts & Entertainment": "Audiobooks - Art",
    "Biographies & Memoirs": "Audiobooks - Biographical",
    "Business & Careers": "Audiobooks - Business",
    "Children's Audiobooks": "Audiobooks - Juvenile",
    "Comedy & Humor": "Audiobooks - Humor",
    "Computers & Technology": "Audiobooks - Computer/Internet",
    "Education & Learning": "Audiobooks - Instructional",
    "Erotica": "Audiobooks - Romance",
    "Health & Wellness": "Audiobooks - Medical",
    "History": "Audiobooks - History",
    "Home & Garden": "Audiobooks - Home/Garden",
    "LGBTQ+": "Audiobooks - General Fiction",
    "Literature & Fiction": "Audiobooks - General Fiction",
    "Money & Finance": "Audiobooks - Business",
    "Mystery, Thriller & Suspense": "Audiobooks - Mystery",
    "Politics & Social Sciences": "Audiobooks - Pol/Soc/Relig",
    "Relationships, Parenting & Personal Development": "Audiobooks - Self-Help",
    "Religion & Spirituality": "Audiobooks - Pol/Soc/Relig",
    "Romance": "Audiobooks - Romance",
    "Science & Engineering": "Audiobooks - Math/Science/Tech",
    "Science Fiction & Fantasy": "Audiobooks - Science Fiction",
    "Sports & Outdoors": "Audiobooks - Recreation",
    "Teen & Young Adult": "Audiobooks - Young Adult",
    "Travel & Tourism": "Audiobooks - Travel/Adventure"
};

// Keyword mapping for smart category matching
const KEYWORD_TO_MAM_CATEGORY_MAP = {
    "science fiction": "Audiobooks - Science Fiction",
    "sci-fi": "Audiobooks - Science Fiction",
    "fantasy": "Audiobooks - Fantasy",
    "magic": "Audiobooks - Fantasy",
    "mystery": "Audiobooks - Mystery",
    "detective": "Audiobooks - Mystery",
    "crime": "Audiobooks - Crime/Thriller",
    "thriller": "Audiobooks - Crime/Thriller",
    "suspense": "Audiobooks - Crime/Thriller",
    "horror": "Audiobooks - Horror",
    "romance": "Audiobooks - Romance",
    "love story": "Audiobooks - Romance",
    "historical": "Audiobooks - Historical Fiction",
    "history": "Audiobooks - History",
    "biography": "Audiobooks - Biographical",
    "memoir": "Audiobooks - Biographical",
    "business": "Audiobooks - Business",
    "finance": "Audiobooks - Business",
    "self-help": "Audiobooks - Self-Help",
    "personal development": "Audiobooks - Self-Help",
    "science": "Audiobooks - Math/Science/Tech",
    "technology": "Audiobooks - Math/Science/Tech",
    "computer": "Audiobooks - Computer/Internet",
    "programming": "Audiobooks - Computer/Internet",
    "travel": "Audiobooks - Travel/Adventure",
    "adventure": "Audiobooks - Travel/Adventure",
    "cooking": "Audiobooks - Food",
    "recipe": "Audiobooks - Food",
    "health": "Audiobooks - Medical",
    "wellness": "Audiobooks - Medical",
    "fitness": "Audiobooks - Medical",
    "sports": "Audiobooks - Recreation",
    "outdoor": "Audiobooks - Recreation",
    "philosophy": "Audiobooks - Philosophy",
    "religion": "Audiobooks - Pol/Soc/Relig",
    "spirituality": "Audiobooks - Pol/Soc/Relig",
    "politics": "Audiobooks - Pol/Soc/Relig",
    "social science": "Audiobooks - Pol/Soc/Relig"
};

// Part of main script: Replace the existing extractEditionInfo function

function extractEditionInfo(title) {
    // Comprehensive patterns for edition detection
    const editionPatterns = [
        // Parenthetical editions
        /\(([^)]*?\b(?:edition|ed\.|edn\.)[^)]*?)\)/i,

        // Editions after punctuation
        /(?:[-–—:,]\s*)([^-–—:,]*?\b(?:edition|ed\.|edn\.)[^-–—:,]*?)(?=\s*[-–—:,]|\s*$)/i,

        // Standalone edition phrases
        /\b(\w+(?:\s+\w+)*\s+(?:edition|ed\.|edn\.)(?:\s*[-–—:,][^-–—:,]*)?)\b/i,

        // Numbered editions
        /\b(\d+(?:st|nd|rd|th)?\s+(?:edition|ed\.|edn\.)\b[^,]*)/i
    ];

    let cleanedTitle = title;
    let editionInfo = null;

    // Try each pattern until we find a match
    for (const pattern of editionPatterns) {
        const match = title.match(pattern);
        if (match) {
            editionInfo = match[1] ? match[1].trim() : match[0].trim();
            // Remove the matched edition text from the title
            cleanedTitle = title.replace(match[0], '');
            break;
        }
    }

    // Clean up the remaining title
    if (cleanedTitle) {
        cleanedTitle = cleanedTitle
            // Remove trailing punctuation
            .replace(/\s*[-–—:,]\s*$/, '')
            // Remove empty parentheses
            .replace(/\(\s*\)/g, '')
            // Remove trailing parenthetical content
            .replace(/\s*\([^)]*\)\s*$/, '')
            // Clean up multiple commas
            .replace(/,\s*,/g, ',')
            // Remove trailing comma
            .replace(/\s*,\s*$/, '')
            // Remove trailing colon
            .replace(/:\s*$/g, '')
            // Normalize spaces
            .replace(/\s+/g, ' ')
            .trim();
    }

    return {
        cleanedTitle: cleanedTitle || title,
        editionInfo: editionInfo
    };
}

function cleanName(name) {
    const titlesToRemove = [
        "PhD", "MD", "JD", "MBA", "MA", "MS", "MSc", "MFA", "MEd", "ScD", "DrPH", "MPH", "LLM",
        "DDS", "DVM", "EdD", "PsyD", "ThD", "DO", "PharmD", "DSc", "DBA", "RN", "CPA", "Esq.",
        "LCSW", "PE", "AIA", "FAIA", "CSP", "CFP", "Jr.", "Sr.", "I", "II", "III", "IV",
        "Dr.", "Rev.", "Msgr.", "STD", "Mr.", "Mrs.", "Ms.", "Prof.", "Sr.",
        "Capt.", "Col.", "Gen.", "Lt.", "Cmdr.", "Adm.", "Sir", "Dame", "Hon.", "Amb.", "Gov.", "Sen.", "Rep.", "BSN", "MSN"
    ];

    // Generate variations of the titles that might have or not have periods
    const titlesWithVariants = titlesToRemove.reduce((acc, title) => {
        // Remove the period if it exists, and add it as an additional variant
        const cleanTitle = title.replace('.', '');
        acc.push(title, cleanTitle);
        return acc;
    }, []);

    let cleanedName = name.trim();

    // Remove everything after a hyphen with space
    cleanedName = cleanedName.replace(/\s+-\s+.*$/, '');

    // Remove specific role patterns
    const rolesToRemove = [
        "foreword",
        "afterword",
        "translator",
        "editor",
        "introduction",
        "preface",
        "contributor",
        "with",
        "featuring",
        "feat[.]?",
        "illustrated by",
        "read by",
        "narrated by",
        "performed by",
        "presented by"
    ];

    // Create pattern for removing roles with various separators
    const rolePattern = new RegExp(`\\s*[-–—:,]\\s*(${rolesToRemove.join('|')}).*$`, 'i');
    cleanedName = cleanedName.replace(rolePattern, '');

    // Remove parenthetical information
    cleanedName = cleanedName.replace(/\s*\(.*?\)\s*$/g, '');

    // Sort titles by length in descending order to handle longer titles first
    const sortedTitles = titlesWithVariants.sort((a, b) => b.length - a.length);

    // Remove titles using a more comprehensive approach
    for (const title of sortedTitles) {
        // Escape special regex characters in the title
        const escapedTitle = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        // Match the title at start, end, or middle of string, with word boundaries and optional spaces
        const regex = new RegExp(`(^|\\s+)${escapedTitle}(\\s+|$)`, 'gi');
        cleanedName = cleanedName.replace(regex, ' ');
    }

    // Clean up any remaining artifacts
    cleanedName = cleanedName
        .replace(/\s+/g, ' ')    // Replace multiple spaces with single space
        .replace(/\s*,\s*$/, '') // Remove trailing comma
        .replace(/^\s*,\s*/, '') // Remove leading comma
        .trim();

    return cleanedName;
}

function cleanSeriesName(seriesName) {
    const wordsToRemove = ["series", "an", "the", "novel"];
    let cleanedName = seriesName.toLowerCase();

    wordsToRemove.forEach(word => {
        const regex = new RegExp(`\\b${word}\\b`, 'gi');
        cleanedName = cleanedName.replace(regex, '');
    });

    cleanedName = cleanedName.replace(/\s+/g, ' ').trim();
    return cleanedName.replace(/\b\w/g, l => l.toUpperCase());
}

async function detectSeriesWithChatGPT(title, author, description) {
    if (!CHATGPT_API_KEY) return { name: "", number: "" };

    const prompt = `Given the following book information, determine if it's part of a series. Only respond with series information if you are HIGHLY confident (90%+ certain) it's part of a series. Many books are standalone and not part of any series.

Title: ${title}
Author: ${author}
Description: ${description}

If the book is part of a series, respond in exactly this JSON format:
{"name": "Series Name", "number": "X"}
where X is the book number if known, or empty string if unknown.

If the book is not part of a series, or if you're not highly confident, respond with exactly:
{"name": "", "number": ""}

Response should be ONLY the JSON, nothing else.`;

    try {
        const response = await fetch("https://api.openai.com/v1/chat/completions", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Authorization": `Bearer ${CHATGPT_API_KEY}`
            },
            body: JSON.stringify({
                model: "gpt-4",
                messages: [{ role: "user", content: prompt }],
                temperature: 0.3,
                max_tokens: 100
            })
        });
        const data = await response.json();
        const result = JSON.parse(data.choices[0].message.content.trim());
        return {
            name: result.name || "",
            number: result.number || ""
        };
    } catch (error) {
        console.error("Error detecting series with ChatGPT:", error);
        return { name: "", number: "" };
    }
}

async function getCategoryFromChatGPT(title, description, audibleCategory) {
    if (!CHATGPT_API_KEY) return null;

    const prompt = `Given the following audiobook information, select the most appropriate category from this list: ${AVAILABLE_CATEGORIES.join(", ")}

Title: ${title}
Description: ${description}
Audible Category: ${audibleCategory}

Please respond with only the category name, nothing else.`;

    try {
        const response = await fetch("https://api.openai.com/v1/chat/completions", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Authorization": `Bearer ${CHATGPT_API_KEY}`
            },
            body: JSON.stringify({
                model: "gpt-4",
                messages: [{ role: "user", content: prompt }],
                temperature: 0.7,
                max_tokens: 50
            })
        });
        const data = await response.json();
        return data.choices[0].message.content.trim();
    } catch (error) {
        console.error("Error getting category from ChatGPT:", error);
        return null;
    }
}

function smartCategoryMatcher(audibleCategory, title, description) {
    let possibleCategories = {};

    if (AUDIBLE_TO_MAM_CATEGORY_MAP[audibleCategory]) {
        possibleCategories[AUDIBLE_TO_MAM_CATEGORY_MAP[audibleCategory]] = 5;
    }

    const addScoreForKeywords = (text, weight) => {
        text = text.toLowerCase();
        for (let [keyword, category] of Object.entries(KEYWORD_TO_MAM_CATEGORY_MAP)) {
            if (text.includes(keyword)) {
                possibleCategories[category] = (possibleCategories[category] || 0) + weight;
                if (category === "Audiobooks - Romance") {
                    possibleCategories[category] += 2;
                }
            }
        }
    };

    addScoreForKeywords(title, 3);
    addScoreForKeywords(description, 2);

    let bestMatch = Object.entries(possibleCategories).reduce((a, b) => a[1] > b[1] ? a : b)[0];
    return bestMatch || (audibleCategory.includes("Fiction") ? "Audiobooks - General Fiction" : "Audiobooks - General Non-Fic");
}

async function getMAMCategory() {
    let audibleCategory = getAudibleCategory();
    let title = getTitle();
    let description = document.querySelector(".productPublisherSummary>div>div>span") ?
        document.querySelector(".productPublisherSummary>div>div>span").textContent :
        "No description available";

    switch (CATEGORY_SELECTION_METHOD) {
        case 1: // Direct mapping
            return AUDIBLE_TO_MAM_CATEGORY_MAP[audibleCategory] || "";
        case 2: // Scoring method
            return smartCategoryMatcher(audibleCategory, title, description);
        case 3: // ChatGPT
            const chatGPTCategory = await getCategoryFromChatGPT(title, description, audibleCategory);
            if (chatGPTCategory && AVAILABLE_CATEGORIES.includes(chatGPTCategory)) {
                return chatGPTCategory;
            }
            // If ChatGPT fails or no API key, fall back to direct mapping
            return AUDIBLE_TO_MAM_CATEGORY_MAP[audibleCategory] || "";
        default:
            return "";
    }
}

function getLanguage() {
    let languageElement = document.querySelector(".languageLabel");
    let patt = /\s*(\w+)$/g;
    let matches = patt.exec(languageElement.innerHTML.trim());
    return matches[1];
}

function getSeriesInfo() {
    let seriesElements = document.querySelectorAll(".seriesLabel");
    let seriesInfo = [];

    // First check for series information on the Audible page
    seriesElements.forEach(element => {
        let seriesLink = element.querySelector("a");
        if (seriesLink) {
            let seriesName = cleanSeriesName(seriesLink.textContent);
            let bookNumberMatch = element.textContent.match(/Book\s*?(\d+\.?\d*-?\d*\.?\d*)/);
            let bookNumber = bookNumberMatch ? bookNumberMatch[1] : "";

            seriesInfo.push({ name: seriesName, number: bookNumber });
        }
    });

    // If no series found on page and ChatGPT is enabled, try AI detection
    if (seriesInfo.length === 0 && CATEGORY_SELECTION_METHOD === 3) {
        window.detectingSeriesWithAI = true;
    }

    return seriesInfo;
}

function getTitle() {
    let title = document.getElementsByTagName("h1")[0].innerText;
    const { cleanedTitle, editionInfo } = extractEditionInfo(title);

    // Store edition info for later use in tags
    if (editionInfo) {
        window.editionInfo = editionInfo;
    }

    return cleanedTitle;
}

function getSubtitle() {
    let sLoggedIn = document.querySelector(".subtitle");
    let sLoggedOut = document.querySelector("span.bc-size-medium");
    let subtitle = "";

    if (sLoggedIn) {
        subtitle = sLoggedIn.innerText;
    } else if (sLoggedOut) {
        subtitle = sLoggedOut.innerText;
    }

    if (!subtitle) return "";
    if (subtitle.trim() === "+More") return "";

    let seriesInfo = getSeriesInfo();
    let isSubtitleSeries = seriesInfo.some(series =>
        subtitle.toLowerCase().includes(series.name.toLowerCase())
    );

    if (isSubtitleSeries) return "";

    return subtitle;
}

function getTitleAndSubtitle() {
    let subtitle = getSubtitle();
    if (subtitle) {
        return `${getTitle()}: ${subtitle}`;
    }
    return getTitle();
}

function getReleaseDate() {
    let element = document.querySelector(".releaseDateLabel");
    let patt = /\d{2}-\d{2}-\d{2}/;
    let matches = patt.exec(element.innerText);
    return matches ? matches[0] : "";
}

function getPublisher() {
    let publisherElement = document.querySelector(".publisherLabel>a");
    return publisherElement ? publisherElement.innerText : "Unknown Publisher";
}

function getRunTime() {
    let runtimeElement = document.querySelector(".runtimeLabel");
    let runtime = runtimeElement ? runtimeElement.textContent : "Unknown";
    let patt = new RegExp("Length:\\n\\s+(\\d[^\n]+)");
    let matches = patt.exec(runtime);
    return matches ? matches[1] : "Unknown";
}

function getAudibleCategory() {
    let categoryElement = document.querySelector(".categoriesLabel");
    if (categoryElement) return categoryElement.innerText;

    categoryElement = document.querySelector("nav.bc-breadcrumb");
    if (categoryElement) return categoryElement.innerText;

    return "";
}

function getAdditionalTags() {
    let tags = [];
    tags.push(`Duration: ${getRunTime()}`);
    if (CHAPTERIZED) tags.push("Chapterized");
    if (RIPPER) tags.push(RIPPER);
    tags.push(`Audible Release: ${getReleaseDate()}`);
    tags.push(`Publisher: ${getPublisher()}`);
    tags.push(getAudibleCategory());

    // Add edition info to tags if it exists
    if (window.editionInfo) {
        tags.push(`Edition: ${window.editionInfo}`);
    }

    return tags.join(" | ");
}

function getASIN() {
    const urlMatch = window.location.pathname.match(/\/([A-Z0-9]{10})/);
    if (urlMatch && urlMatch[1]) {
        return "ASIN:" + urlMatch[1];
    }

    const productDetails = document.querySelector('#detailsproductInfoSection');
    if (productDetails) {
        const asinMatch = productDetails.textContent.match(/ASIN:\s*([A-Z0-9]{10})/);
        if (asinMatch && asinMatch[1]) {
            return "ASIN:" + asinMatch[1];
        }
    }

    return "";
}

function getAuthors() {
    var authorElements = document.querySelectorAll(".authorLabel a");
    var authors = [];
    for (let element of authorElements) {
        if (element) {
            let authorName = element.textContent.trim();
            authorName = cleanName(authorName);
            if (authorName &&
                !authors.includes(authorName) &&
                authorName !== "Prologue Projects" &&
                authorName.toLowerCase() !== "title" &&
                authorName.toLowerCase() !== "author + title") {
                authors.push(authorName);
            }
        }
    }
    return authors;
}

function getNarrators() {
    var narratorElements = document.querySelectorAll(".narratorLabel a");
    var narrators = [];
    for (let element of narratorElements) {
        if (element) {
            let narratorName = element.textContent.trim();
            narratorName = cleanName(narratorName);
            if (narratorName && !narrators.includes(narratorName) && narratorName.toLowerCase() !== "full cast") {
                narrators.push(narratorName);
            }
        }
    }
    return narrators;
}

async function generateJson() {
    var imgElement = document.querySelector(".bc-image-inset-border");
    var imageSrc = imgElement ? imgElement.src : "No image available";

    var descriptionElement = document.querySelector(".productPublisherSummary>div>div>span") ||
        document.querySelector("div.bc-col-6 span.bc-text") ||
        document.querySelector("div.bc-text.bc-color-secondary");
    var description = descriptionElement ? descriptionElement.innerHTML : "No description available";
    description = description.replace(/\s+/g, " ").replace(/"/g, '\\"').replace(/<p><\/p>/g, "").replace(/<\/p>/g, "</p><br>").replace(/<\/ul>/g, "</ul><br>");

    let authors = getAuthors();
    let seriesInfo = getSeriesInfo();

    // If AI series detection is needed, do it here
    if (window.detectingSeriesWithAI) {
        try {
            const aiSeriesInfo = await detectSeriesWithChatGPT(getTitle(), authors[0] || "", description);
            if (aiSeriesInfo.name) {
                seriesInfo = [aiSeriesInfo];
            }
        } catch (error) {
            console.error("Error in AI series detection:", error);
        }
        delete window.detectingSeriesWithAI;
    }

    var json = {
        "authors": authors,
        "description": description,
        "narrators": getNarrators(),
        "tags": getAdditionalTags(),
        "thumbnail": imageSrc,
        "title": getTitleAndSubtitle(),
        "language": getLanguage(),
        "series": seriesInfo,
        "category": await getMAMCategory(),
        "isbn": getASIN()
    };

    var strJson = JSON.stringify(json);
    await copyToClipboard(strJson);
}

function addOverlayAndButton() {
    const overlayHtml = `
    <div id="mam-overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); backdrop-filter: blur(5px); z-index: 9999;">
        <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 5px; text-align: center;">
            <p style="font-size: 18px; margin: 0;">Processing JSON data...</p>
            <p style="font-size: 16px; margin: 10px 0 0;">Please wait...</p>
            <div id="ai-gif" style="display: none; margin-top: 20px;">
                <img src="https://c.tenor.com/JDV9WN1QC3kAAAAC/tenor.gif" alt="AI Processing" style="max-width: 200px;">
            </div>
        </div>
    </div>`;

    document.body.insertAdjacentHTML('beforeend', overlayHtml);

    const buyBoxElement = document.querySelector("#adbl-buy-box");
    if (!buyBoxElement) return;

    const buttonText = CATEGORY_SELECTION_METHOD === 3 ? "Copy Book info to JSON with ChatGPT" : "Copy Book info to JSON";

    const buttonHtml = `
    <div id="mam-metadata-button" class="bc-row bc-spacing-top-s1">
        <div class="bc-row">
            <div class="bc-trigger bc-pub-block">
                <span class="bc-button bc-button-primary">
                    <button id="copyMetadataButton" class="bc-button-text" type="button" tabindex="0" title="Copy book details as JSON">
                        <span class="bc-text bc-button-text-inner bc-size-action-large">
                            ${buttonText}
                        </span>
                    </button>
                </span>
            </div>
        </div>
    </div>`;

    buyBoxElement.insertAdjacentHTML('beforeend', buttonHtml);

    document.getElementById("copyMetadataButton").addEventListener("click", async function (event) {
        event.preventDefault();
        showOverlay();
        await generateJson();
        hideOverlay();
    });
}

function showOverlay() {
    document.getElementById("mam-overlay").style.display = "block";
    if (CATEGORY_SELECTION_METHOD === 3) {
        document.getElementById("ai-gif").style.display = "block";
    }
}

function hideOverlay() {
    document.getElementById("mam-overlay").style.display = "none";
    document.getElementById("ai-gif").style.display = "none";
}

async function copyToClipboard(text) {
    try {
        await navigator.clipboard.writeText(text);
        console.log('Copied to clipboard successfully!');
        window.open("https://www.myanonamouse.net/tor/upload.php", "_blank");
    } catch (err) {
        console.error('Could not copy text: ', err);
        alert('Failed to copy metadata. Please check the console for errors.');
    }
}

window.onload = function () {
    addOverlayAndButton();
};