Audible Metadata to MAM JSON script (fully supports banner pages)

Copies audiobook metadata to JSON and opens MAM upload page. Supports multiple series and works on all Audible domains.

当前为 2024-10-04 提交的版本,查看 最新版本

// ==UserScript==
// @name         Audible Metadata to MAM JSON script (fully supports banner pages)
// @namespace    https://greasyfork.org/en/scripts/511491
// @version      1.1.0
// @license      MIT
// @description  Copies audiobook metadata to JSON and opens MAM upload page. Supports multiple series and works on all Audible domains.
// @author       SnowmanNurse (Parts from and inspired by script 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

const AVAILABLE_CATEGORIES = [
    "Art", "Biographical", "Business", "Crafts", "Fantasy", "Food", "History", "Horror", "Humor",
    "Instructional", "Juvenile", "Language", "Medical", "Mystery", "Nature", "Philosophy",
    "Recreation", "Romance", "Self-Help", "Western", "Young Adult", "Historical Fiction",
    "Literary Classics", "Science Fiction", "True Crime", "Urban Fantasy", "Action/Adventure",
    "Computer/Internet", "Crime/Thriller", "Home/Garden", "Math/Science/Tech", "Travel/Adventure",
    "Pol/Soc/Relig", "General Fiction", "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",
    "art": "Audiobooks - Art",
    "music": "Audiobooks - Art",
    "photography": "Audiobooks - Art",
    "craft": "Audiobooks - Crafts",
    "diy": "Audiobooks - Crafts",
    "humor": "Audiobooks - Humor",
    "comedy": "Audiobooks - Humor",
    "funny": "Audiobooks - Humor",
    "children": "Audiobooks - Juvenile",
    "kid": "Audiobooks - Juvenile",
    "young adult": "Audiobooks - Young Adult",
    "teen": "Audiobooks - Young Adult",
    "language": "Audiobooks - Language",
    "linguistics": "Audiobooks - Language",
    "nature": "Audiobooks - Nature",
    "environment": "Audiobooks - Nature",
    "western": "Audiobooks - Western"
};

function cleanName(name) {
    const titlesToRemove = [
        "PhD", "MD", "JD", "MBA", "MA", "MS", "MSc", "MFA", "MEd", "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.", "Mr.", "Mrs.", "Ms.", "Prof.", "Rev.", "Fr.", "Sr.", "Capt.", "Col.", "Gen.", "Lt.", "Cmdr.", "Adm.", "Sir", "Dame", "Hon.", "Amb.", "Gov.", "Sen.", "Rep.", "BSN", "MSN", "RN"
    ];

    let cleanedName = name.trim();

    titlesToRemove.forEach(title => {
        const regexBefore = new RegExp(`^${title}\\s+`, 'i');
        const regexAfter = new RegExp(`\\s*,?\\s*${title}$`, 'i');
        cleanedName = cleanedName.replace(regexBefore, '').replace(regexAfter, '');
    });

    return cleanedName.trim();
}

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

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

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 = [];

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

    return seriesInfo;
}

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 smartCategoryMatcher(audibleCategory, title, description) {
    let possibleCategories = {};

    // Start with the Audible category mapping
    if (AUDIBLE_TO_MAM_CATEGORY_MAP[audibleCategory]) {
        possibleCategories[AUDIBLE_TO_MAM_CATEGORY_MAP[audibleCategory]] = 3;
    }

    // Function to add scores based on keyword matches
    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;
            }
        }
    };

    // Check title (higher weight)
    addScoreForKeywords(title, 2);

    // Check description (lower weight)
    addScoreForKeywords(description, 1);

    // Find the category with the highest score
    let bestMatch = Object.entries(possibleCategories).reduce((a, b) => a[1] > b[1] ? a : b)[0];

    // If no match found, default to "General Fiction" or "General Non-Fic"
    return bestMatch || (audibleCategory.includes("Fiction") ? "Audiobooks - General Fiction" : "Audiobooks - General Non-Fic");
}

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

    return smartCategoryMatcher(audibleCategory, title, description);
}

function getTitle() {
    let title = document.getElementsByTagName("h1")[0].innerText;
    return title;
}

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 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());
    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 = authorName.replace(/ - (foreword|afterword|translator|editor)/gi, "");
            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;
}

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

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

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

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

    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">
                            Copy Book info to JSON
                        </span>
                    </button>
                </span>
            </div>
        </div>
    </div>`;

    buyBoxElement.insertAdjacentHTML('beforeend', buttonHtml);

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

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