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 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Audible Metadata to MAM JSON script (with ChatGPT)
// @namespace    https://greasyfork.org/en/scripts/511491
// @version      1.5.2
// @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-4o-mini",
                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-4o-mini",
                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();
};