Improved Audiobook Metadata

Copies audiobook metadata to JSON and opens MAM upload page. Updated from DrBlank's script to work on pages that have a banner at the top.

目前為 2024-10-04 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Improved Audiobook Metadata
// @namespace    https://greasyfork.org/en/users/your-namespace
// @version      1.0.2
// @license      MIT
// @description  Copies audiobook metadata to JSON and opens MAM upload page. Updated from DrBlank's script to work on pages that have a banner at the top.
// @author       SnowmanNurse (Inspired by script by DrBlank)
// @include      https://www.audible.com/pd/*
// @include      https://www.audible.in/pd/*
// @include      https://www.audible.com/ac/*
// @include      https://www.audible.in/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 getSeriesName() {
    let series = "";
    let seriesElement = document.querySelector(".seriesLabel");
    if (seriesElement) {
        series = seriesElement.querySelector("a").innerHTML;
        series = cleanSeriesName(series);
    }
    return series;
}

function getSeriesBookNumber() {
    let bookNumber = "";
    if (!getSeriesName()) {
        return "";
    }
    let seriesElement = document.querySelector(".seriesLabel");
    let patt = /Book\s*?(\d+\.?\d*-?\d*\.?\d*)/g;
    bookNumber = patt.exec(seriesElement.innerHTML);

    if (!bookNumber) {
        return "";
    }
    return bookNumber[1];
}

function getSeries() {
    let seriesName = getSeriesName();
    if (seriesName) {
        let bookNumber = getSeriesBookNumber();
        return [{ name: seriesName, number: bookNumber }];
    }
    return [];
}

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 series = getSeriesName().toLowerCase();
    let isSubtitleSeries = Boolean(
        series && subtitle.toLocaleLowerCase().includes(series)
    );

    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 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": getSeries(),
        "category": getMAMCategory(),
    };

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

function addButton() {
    let buttonContainer = document.createElement("div");
    buttonContainer.innerHTML = `
    <div style="position:fixed;bottom:10px;right:10px;z-index:9999">
        <button id="copyMetadataButton" style="padding:10px;background-color:#008CBA;color:white;border:none;border-radius:5px;font-size:14px;">
            Copy Metadata & Open MAM
        </button>
    </div>`;
    document.body.appendChild(buttonContainer);

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

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