Audible to MAM JSON Converter (Full API Version)

Complete API-based solution with full metadata cleaning, category mapping, and MAM search

// ==UserScript==
// @name         Audible to MAM JSON Converter (Full API Version)
// @namespace    https://greasyfork.org/en/scripts/511491
// @version      2.1.3
// @license      MIT
// @description  Complete API-based solution with full metadata cleaning, category mapping, and MAM search
// @author       SnowmanNurse (Special thanks to Dr.Blank, DeepSpaceDark, and Audnexus for their contributions)
// @include      https://www.audible.*/pd/*
// @include      https://www.audible.*/ac/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @grant        GM_openInTab
// ==/UserScript==

// Configuration Constants
const RIPPER = "MusicFab";
const CHAPTERIZED = true;
const CATEGORY_SELECTION_METHOD = 2;
const CHATGPT_API_KEY = "your-api-key-here";
const ENABLE_SEARCH_BUTTONS = true;
const MAM_AUDIOBOOK_CATEGORIES = [
    39, 49, 50, 83, 51, 97, 40, 41, 106, 42, 52, 98, 54, 55, 43, 99, 84, 44,
    56, 45, 57, 85, 87, 119, 88, 58, 59, 46, 47, 53, 89, 100, 108, 48, 111, 0
];

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"
];

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"
};

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"
};

function isAllCaps(str) {
    return str === str.toUpperCase() && /[A-Z]/.test(str);
}

function toTitleCase(str) {
    return str.toLowerCase().replace(/\b\w/g, (c) => c.toUpperCase());
}

// Cached data variables
let cachedApiData = null;
let currentASIN = null;

(function() {
    'use strict';

    window.addEventListener('load', () => {
        currentASIN = extractASIN();
        if (currentASIN) {
            createFloatingButton();
            addKeyframeAnimations();
        }
    });

    function createFloatingButton() {
        const container = document.createElement('div');
        container.id = 'mam-button-container';
        Object.assign(container.style, {
            position: 'fixed',
            top: '100px',
            right: '20px',
            zIndex: 9999,
            display: 'flex',
            flexDirection: 'column',
            gap: '8px'
        });

        // JSON Copy Button (always visible)
        const jsonBtnText = CATEGORY_SELECTION_METHOD === 3
            ? '📚 Copy MAM JSON with AI'
            : '📚 Copy MAM JSON';
        const jsonBtn = createButton(jsonBtnText, 'mam-json-btn', handleButtonClick);
        container.appendChild(jsonBtn);

        // Conditionally add search buttons
        if (ENABLE_SEARCH_BUTTONS) {
            const searchTitleBtn = createButton('🔍 Search MAM (Title)', 'mam-search-title-btn', () => handleSearchClick(false));
            const searchAuthorBtn = createButton('🔍 Search MAM (Title+Author)', 'mam-search-author-btn', () => handleSearchClick(true));
            container.appendChild(searchTitleBtn);
            container.appendChild(searchAuthorBtn);
        }

        document.body.appendChild(container);
    }

    function createButton(text, id, onClickHandler) {
        const btn = document.createElement('button');
        btn.id = id;
        btn.textContent = text;
        Object.assign(btn.style, {
            padding: '12px 18px',
            backgroundColor: '#FF9900',
            color: '#1a1a1a',
            border: 'none',
            borderRadius: '8px',
            cursor: 'pointer',
            boxShadow: '0 4px 12px rgba(0,0,0,0.2)',
            fontSize: '14px',
            fontWeight: 'bold',
            transition: 'all 0.3s ease',
            whiteSpace: 'nowrap'
        });

        btn.addEventListener('mouseenter', () => {
            btn.style.transform = 'scale(1.05)';
            btn.style.boxShadow = '0 6px 16px rgba(0,0,0,0.3)';
        });

        btn.addEventListener('mouseleave', () => {
            btn.style.transform = 'scale(1)';
            btn.style.boxShadow = '0 4px 12px rgba(0,0,0,0.2)';
        });

        btn.addEventListener('click', onClickHandler);
        return btn;
    }

    async function handleSearchClick(includeAuthor) {
        const btnId = includeAuthor ? 'mam-search-author-btn' : 'mam-search-title-btn';
        const btn = document.getElementById(btnId);
        const originalText = btn.textContent;

        try {
            btn.textContent = '🔍 Searching...';
            btn.style.backgroundColor = '#ff8000';

            const apiData = await fetchAndCacheData(currentASIN);
            const editionData = extractEditionInfo(apiData.title);
            const authors = cleanNames(apiData.authors?.map(a => a.name) || []);
            const title = encodeURIComponent(editionData.title.trim());
            const author = authors.length > 0 ? encodeURIComponent(authors[0].trim()) : '';

            let query = title;
            if (includeAuthor && author) query += `%20${author}`;

            // Build category parameters
            const categoryParams = MAM_AUDIOBOOK_CATEGORIES
                .map(cat => `tor%5Bcat%5D%5B%5D=${cat}`)
                .join('&');

            const searchURL = `https://www.myanonamouse.net/tor/browse.php?` +
                `tor%5Btext%5D=${query}&` +
                `tor%5BsrchIn%5D%5Btitle%5D=true&` +
                `tor%5BsrchIn%5D%5Bdescription%5D=true&` +
                `tor%5BsrchIn%5D%5Btags%5D=true&` +
                `tor%5BsrchIn%5D%5Bauthor%5D=true&` +
                `tor%5BsrchIn%5D%5Bnarrator%5D=true&` +
                `tor%5BsrchIn%5D%5Bseries%5D=true&` +
                `tor%5BsearchType%5D=all&` +
                `tor%5BsearchIn%5D=torrents&` +
                `${categoryParams}&` +
                `tor%5BbrowseFlagsHideVsShow%5D=0&` +
                `tor%5BminSize%5D=0&` +
                `tor%5BmaxSize%5D=0&` +
                `tor%5Bunit%5D=1&` +
                `tor%5BminSeeders%5D=0&` +
                `tor%5BmaxSeeders%5D=0&` +
                `tor%5BminLeechers%5D=0&` +
                `tor%5BmaxLeechers%5D=0&` +
                `tor%5BminSnatched%5D=0&` +
                `tor%5BmaxSnatched%5D=0&` +
                `tor%5BsortType%5D=created&` +
                `tor%5BstartNumber%5D=0`;

            GM_openInTab(searchURL, { active: true });
            showTempAlert('🌐 Searching MAM audiobooks...', '#2196F3');
        } catch (error) {
            showTempAlert(`❌ Error: ${error.message}`, '#f44336');
            console.error('Search Error:', error);
        } finally {
            btn.textContent = originalText;
            btn.style.backgroundColor = '#FF9900';
        }
    }

    async function fetchAndCacheData(asin) {
        if (!cachedApiData) {
            cachedApiData = await fetchBookData(asin);
        }
        return cachedApiData;
    }

    async function handleButtonClick() {
        const btn = document.getElementById('mam-json-btn');
        const originalText = btn.textContent;

        try {
            btn.textContent = '🔄 Processing...';
            btn.style.backgroundColor = '#ff8000';

            const apiData = await fetchAndCacheData(currentASIN);
            const jsonData = await processApiData(apiData);

            GM_setClipboard(JSON.stringify(jsonData, null, 2), 'text');
            GM_openInTab('https://www.myanonamouse.net/tor/upload.php', { active: true });
            showTempAlert('✅ JSON copied to clipboard!', '#4CAF50');
        } catch (error) {
            showTempAlert(`❌ Error: ${error.message}`, '#f44336');
            console.error('JSON Error:', error);
        } finally {
            btn.textContent = originalText;
            btn.style.backgroundColor = '#FF9900';
        }
    }

    function extractASIN() {
        const pathMatch = window.location.pathname.match(/\/([A-Z0-9]{10})(?:[\/?]|$)/);
        return pathMatch ? pathMatch[1] : null;
    }

    function fetchBookData(asin) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `https://api.audnex.us/books/${asin}`,
                onload: function(response) {
                    if (response.status === 200) {
                        resolve(JSON.parse(response.responseText));
                    } else {
                        reject(new Error(`API Error: ${response.status}`));
                    }
                },
                onerror: reject
            });
        });
    }

    // Update the processApiData function to process title and subtitle
    async function processApiData(apiData) {
    const editionData = extractEditionInfo(apiData.title);
    
    // Process title for all caps
    let processedTitle = editionData.title;
    if (isAllCaps(processedTitle)) {
        processedTitle = toTitleCase(processedTitle);
    }

    // Process subtitle for all caps
    let processedSubtitle = apiData.subtitle || '';
    if (processedSubtitle && isAllCaps(processedSubtitle)) {
        processedSubtitle = toTitleCase(processedSubtitle);
    }

    return {
        authors: cleanNames(apiData.authors?.map(a => a.name) || []),
        narrators: cleanNames(apiData.narrators?.map(n => n.name) || []),
        title: `${processedTitle}${processedSubtitle ? ": " + processedSubtitle : ""}`,
        description: formatDescription(apiData),
        language: formatLanguage(apiData.language),
        series: processSeries(apiData.seriesPrimary),
        category: await determineCategory(apiData),
        thumbnail: apiData.image,
        tags: await generateTags(apiData, editionData.editionInfo),
        isbn: `ASIN:${apiData.asin}`,
        editionInfo: editionData.editionInfo
    };
   }

    function cleanNames(names) {
        const patterns = {
            prefixes: new RegExp(
                '^\\s*' +
                '(?:Dr|Mr|Mrs|Ms|Miss|Prof|Rev|Hon|Sir|Dame|Lady|Capt|Col|Gen|Lt|Cmdr|Adm|Maj|Sgt)' +
                '[\\s.]*',
                'i'
            ),
            suffixes: new RegExp(
                '[\\s,]+(?:' +
                'Ph\\.?D|M\\.?D|J\\.?D|MBA|MSc|BSc|MS|MA|BA|BS|RN|CPA|Esq|Jr|Sr|I{1,3}|IV|VI?|' +
                '1st|2nd|3rd|4th|FNP|APRN|DNP|PhD\\.?|MD\\.?|JD\\.?' +
                ')(?:\\.|,|$)|' +
                '\\(.*?(?:certified|licensed|registered|chartered).*?\\)',
                'gi'
            )
        };

        return names.map(name => {
            return name.replace(patterns.prefixes, '')
                       .replace(patterns.suffixes, '')
                       .replace(/\s{2,}/g, ' ')
                       .trim();
        }).filter(name => name.length > 1);
    }

    function extractEditionInfo(title) {
        const patterns = [
            /(\d+(?:st|nd|rd|th)?\s*ed(?:ition)?)\b/i,
            /\((?:unabridged|abridged|special edition)\)/i,
            /\[.*(?:edition|version).*\]/i
        ];

        for (const pattern of patterns) {
            const match = title.match(pattern);
            if (match) {
                return {
                    title: title.replace(match[0], '').trim(),
                    editionInfo: match[1] || match[0]
                };
            }
        }
        return {
            title: title.trim(),
            editionInfo: null
        };
    }

    function formatDescription(apiData) {
        let desc = apiData.summary || 'No description available';
        let paragraphs = desc.split(/<\/p>/gi);
        paragraphs = paragraphs.map(p =>
            p.replace(/<p>/gi, '')
             .replace(/<\/?span[^>]*>/gi, '')
             .trim()
        ).filter(p => p.length > 0);

        desc = paragraphs.join('<br><br>');

        if (apiData.translators?.length > 0) {
            desc += `<br><br><strong>Translators:</strong> ${apiData.translators.join(', ')}`;
        }

        return desc;
    }

    function formatLanguage(lang) {
        return lang ? lang.charAt(0).toUpperCase() + lang.slice(1) : 'English';
    }

    function processSeries(seriesData) {
        return seriesData ? [{
            name: seriesData.name,
            number: seriesData.position?.toString() || ""
        }] : [];
    }

    async function determineCategory(apiData) {
        const audibleCategories = apiData.genres?.map(g => g.name) || [];

        switch (CATEGORY_SELECTION_METHOD) {
            case 1:
                for (const category of audibleCategories) {
                    if (AUDIBLE_TO_MAM_CATEGORY_MAP[category]) {
                        return AUDIBLE_TO_MAM_CATEGORY_MAP[category];
                    }
                }
                return '';

            case 2:
                return smartCategoryMatcher(audibleCategories, apiData.title, apiData.summary);

            case 3:
                return await getChatGptCategory(apiData.title, apiData.summary, audibleCategories);

            default:
                return '';
        }
    }

    function smartCategoryMatcher(audibleCategories, title, description) {
        const text = `${title} ${description}`.toLowerCase();
        const scores = {};

        for (const [keyword, category] of Object.entries(KEYWORD_TO_MAM_CATEGORY_MAP)) {
            const regex = new RegExp(`\\b${keyword}\\b`, 'gi');
            const matches = text.match(regex);
            if (matches) {
                scores[category] = (scores[category] || 0) + matches.length;
            }
        }

        for (const audibleCat of audibleCategories) {
            const mappedCat = AUDIBLE_TO_MAM_CATEGORY_MAP[audibleCat];
            if (mappedCat) {
                scores[mappedCat] = (scores[mappedCat] || 0) + 3;
            }
        }

        const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
        return sorted[0]?.[0] || '';
    }

    async function getChatGptCategory(title, description, audibleCategories) {
        if (CHATGPT_API_KEY === "your-api-key-here") {
            throw new Error("Please put your ChatGPT API key in the script to use AI functionality or switch to the direct mapping or keyword matching methods.");
        }
        if (!CHATGPT_API_KEY) return '';

        const prompt = `Select the most appropriate category from this list: ${AVAILABLE_CATEGORIES.join(", ")}

        Title: ${title}
        Description: ${description}
        Audible Categories: ${audibleCategories.join(", ")}

        Respond only with the exact category name.`;

        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.5,
                    max_tokens: 50
                })
            });

            if (!response.ok) {
                if (response.status === 401) {
                    throw new Error("Invalid ChatGPT API key");
                }
                throw new Error(`API Error: ${response.statusText}`);
            }

            const data = await response.json();
            const category = data.choices[0].message.content.trim();
            return AVAILABLE_CATEGORIES.includes(category) ? category : '';

        } catch (error) {
            throw new Error(error.message.includes("API key")
                ? "Invalid ChatGPT API key"
                : "AI Category Error");
        }
    }

    async function generateTags(apiData, editionInfo) {
        const tags = [
            `${formatDate(apiData.releaseDate)}`,
            CHAPTERIZED && 'Chapterized',
            RIPPER
        ].filter(Boolean);

        if (CATEGORY_SELECTION_METHOD === 3) {
            const aiTags = await generateAITags(apiData.title, apiData.summary);
            if (aiTags.length > 0) {
                tags.push(...aiTags);
            }
        } else {
            const category = await determineCategory(apiData);
            if (category) {
                const categoryTag = category.replace('Audiobooks - ', '');
                tags.push(categoryTag);
            }
        }

        tags.push(
            `${formatRuntime(apiData.runtimeLengthMin)}`,
            `${apiData.publisherName}`
        );

        if (editionInfo) tags.push(`Edition: ${editionInfo}`);
        return tags.join(' | ');
    }

    async function generateAITags(title, description) {
        const prompt = `For the audiobook with title "${title}" and description "${description}", generate 3-4 relevant descriptive tags.
        Tags should be single words or short phrases that describe the book's themes, setting, or notable elements.
        Avoid generic terms like "fiction", "non-fiction", or basic genre names.
        Respond with just the tags, separated by commas.`;

        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
                })
            });

            if (!response.ok) {
                return [];
            }

            const data = await response.json();
            return data.choices[0].message.content.split(',').map(tag => tag.trim());

        } catch (error) {
            console.error('Error generating AI tags:', error);
            return [];
        }
    }

    function formatRuntime(minutes) {
        const hours = Math.floor(minutes / 60);
        const mins = minutes % 60;
        return `${hours} hrs ${mins} mins`;
    }

    function formatDate(dateString) {
        const date = new Date(dateString);
        return date.toLocaleDateString('en-US', {
            month: 'long',
            day: 'numeric',
            year: 'numeric'
        });
    }

    function showTempAlert(message, color) {
        const alert = document.createElement('div');
        Object.assign(alert.style, {
            position: 'fixed',
            top: '20px',
            right: '20px',
            padding: '15px 25px',
            backgroundColor: color,
            color: 'white',
            borderRadius: '8px',
            boxShadow: '0 4px 12px rgba(0,0,0,0.2)',
            zIndex: 10000,
            animation: 'slideIn 0.3s ease-out'
        });
        alert.textContent = message;

        document.body.appendChild(alert);
        setTimeout(() => {
            alert.style.animation = 'slideOut 0.3s ease-in';
            setTimeout(() => alert.remove(), 300);
        }, 3000);
    }

    function addKeyframeAnimations() {
        const style = document.createElement('style');
        style.textContent = `
            @keyframes slideIn {
                from { transform: translateX(100%); }
                to { transform: translateX(0); }
            }
            @keyframes slideOut {
                from { transform: translateX(0); }
                to { transform: translateX(100%); }
            }
        `;
        document.head.appendChild(style);
    }
})();