Audible to MAM JSON Converter (Full API Version)

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

目前為 2025-01-25 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Audible to MAM JSON Converter (Full API Version)
// @namespace    https://greasyfork.org/en/scripts/511491
// @version      2.0.0
// @license      MIT
// @description  Complete API-based solution with full metadata cleaning and category mapping
// @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"; // Name of the ripping software used
const CHAPTERIZED = true; // Whether the audiobook is chapterized (true/false)
const CATEGORY_SELECTION_METHOD = 1; // Category selection method: 1 = Direct mapping, 2 = Keyword matching, 3 = Use AI (ChatGPT)
const CHATGPT_API_KEY = "your-api-key-here"; // Replace with your OpenAI API key

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() {
    'use strict';

    // Main initialization
    window.addEventListener('load', () => {
        const asin = extractASIN();
        if (asin) {
            createFloatingButton();
            addKeyframeAnimations();
        }
    });

    function createFloatingButton() {
        const btn = document.createElement('button');
        btn.id = 'mam-float-btn';
        btn.textContent = CATEGORY_SELECTION_METHOD === 3
            ? '📚 Copy MAM JSON with AI'
            : '📚 Copy MAM JSON';
        Object.assign(btn.style, {
            position: 'fixed',
            top: '100px',
            right: '20px',
            zIndex: 9999,
            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'
        });

        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', handleButtonClick);
        document.body.appendChild(btn);
    }

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

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

            const asin = extractASIN();
            if (!asin) throw new Error('Could not find ASIN');

            const apiData = await fetchBookData(asin);
            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('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
            });
        });
    }

    async function processApiData(apiData) {
        const editionData = extractEditionInfo(apiData.title);

        return {
            authors: cleanNames(apiData.authors?.map(a => a.name) || []),
            narrators: cleanNames(apiData.narrators?.map(n => n.name) || []),
            title: editionData.title,
            description: formatDescription(apiData),
            language: formatLanguage(apiData.language),
            series: processSeries(apiData.seriesPrimary),
            category: await determineCategory(apiData),
            thumbnail: apiData.image,
            tags: 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')
            .replace(/<p>/g, '<br>')
            .replace(/<\/p>/g, '')
            .trim();

        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 = {};

        // Score based on keywords
        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;
            }
        }

        // Score based on direct category mappings
        for (const audibleCat of audibleCategories) {
            const mappedCat = AUDIBLE_TO_MAM_CATEGORY_MAP[audibleCat];
            if (mappedCat) {
                scores[mappedCat] = (scores[mappedCat] || 0) + 3;
            }
        }

        // Return highest scoring category
        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");
        }
    }

    function generateTags(apiData, editionInfo) {
        const tags = [
            `Duration: ${formatRuntime(apiData.runtimeLengthMin)}`,
            CHAPTERIZED && 'Chapterized',
            RIPPER,
            `Audible Release: ${new Date(apiData.releaseDate).toISOString().split('T')[0]}`,
            `Publisher: ${apiData.publisherName}`
        ].filter(Boolean);

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

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

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