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.0.4
// @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 = "Libation"; // yours can be InAudible 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",
];

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

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

    // Remove extra spaces and trim
    cleanedName = cleanedName.replace(/\s+/g, ' ').trim();

    // Capitalize the first letter of each word
    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 getMAMCategory() {
    let audibleCategory = getAudibleCategory().toLowerCase();
    let guesses = [];
    AVAILABLE_CATEGORIES.forEach((category) => {
        if (audibleCategory.includes(category.toLowerCase())) {
            guesses.push(`Audiobooks - ${category}`);
            return;
        }

        let separators = ["/", " "];
        separators.forEach((separator) => {
            let splits = category.split(separator);
            splits.forEach((split) => {
                if (audibleCategory.includes(split.toLowerCase())) {
                    guesses.push(`Audiobooks - ${category}`);
                    return;
                }
            });
        });
    });
    if (guesses.length) return guesses[0];
    return "";
}

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() {
    // Try to get ASIN from URL first
    const urlMatch = window.location.pathname.match(/\/([A-Z0-9]{10})/);
    if (urlMatch && urlMatch[1]) {
        return "ASIN:" + urlMatch[1];
    }

    // If not in URL, try to find it in the page content
    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];
        }
    }

    // If ASIN is not found, return an empty string
    return "";
}

function generateJson() {
    var authorElements = document.querySelectorAll(".authorLabel a");
    var authors = [];
    for (let index = 0; index < authorElements.length; index++) {
        if (authorElements[index]) {
            let authorName = authorElements[index].innerHTML.replace(/ - (foreword|afterword|translator|editor)/gi, "");
            authorName = cleanName(authorName);
            authors.push(authorName);
        }
    }

    var narratorElements = document.querySelectorAll(".narratorLabel a");
    var narrators = [];
    for (let index = 0; index < narratorElements.length; index++) {
        if (narratorElements[index]) {
            let narratorName = narratorElements[index].innerHTML;
            narratorName = cleanName(narratorName);
            narrators.push(narratorName);
        }
    }

    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": authors,
        "description": description,
        "narrators": narrators,
        "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();
};