Improved Audiobook Metadata

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

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

// ==UserScript==
// @name         Improved Audiobook Metadata
// @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       YourName
// @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();
};