您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Copies audiobook metadata to JSON and opens MAM upload page. Optionally offers multiple AI models for category selection.
当前为
// ==UserScript== // @name Audible Metadata to MAM JSON script (with multiple AI model options) // @namespace https://greasyfork.org/en/scripts/511491 // @version 1.4.2 // @license MIT // @description Copies audiobook metadata to JSON and opens MAM upload page. Optionally offers multiple AI models for category selection. // @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 // Category selection method: 1 for direct mapping, 2 for scoring method, 3 for AI-based selection const CATEGORY_SELECTION_METHOD = 3; // AI model selection (only used if CATEGORY_SELECTION_METHOD is 3) // 0: None (fall back to direct mapping), 1: ChatGPT (OpenAI), 2: Meta Llama, 3: Google Gemini 128K, // 4: Mistral AI, 5: Google Gemini 1M, 6: Anthropic Claude 3 const AI_MODEL = 1; // API keys for different AI models const CHATGPT_API_KEY = ""; const META_API_KEY = ""; const GOOGLE_API_KEY = ""; const MISTRAL_API_KEY = ""; const ANTHROPIC_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", "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", "ScD", "DrPH", "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", "MS", "MN" ]; let cleanedName = name.trim(); titlesToRemove.forEach(title => { const regexBefore = new RegExp(`^${title}\\b`, 'i'); const regexAfter = new RegExp(`\\b${title}$`, 'i'); cleanedName = cleanedName.replace(regexBefore, '').replace(regexAfter, ''); }); // Remove any remaining titles that might be in the middle of the name titlesToRemove.forEach(title => { const regexMiddle = new RegExp(`\\s${title}\\s`, 'gi'); cleanedName = cleanedName.replace(regexMiddle, ' '); }); // Remove any extra spaces cleanedName = cleanedName.replace(/\s+/g, ' ').trim(); return cleanedName; } 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 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]] = 5; } // 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; // Give extra weight to romance keywords if (category === "Audiobooks - Romance") { possibleCategories[category] += 2; } } } }; // Check title (higher weight) addScoreForKeywords(title, 3); // Check description (lower weight) addScoreForKeywords(description, 2); // 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"); } async function getCategoryFromAI(title, description, audibleCategory) { const prompt = `Given the following audiobook information, select the most appropriate category from this list: ${AVAILABLE_CATEGORIES.join(", ")} Title: ${title} Description: ${description} Audible Category: ${audibleCategory} Please respond with only the category name, nothing else.`; switch (AI_MODEL) { case 1: return await getCategoryFromChatGPT(prompt); case 2: return await getCategoryFromMetaLlama(prompt); case 3: return await getCategoryFromGoogleGemini(prompt, false); case 4: return await getCategoryFromMistralAI(prompt); case 5: return await getCategoryFromGoogleGemini(prompt, true); case 6: return await getCategoryFromAnthropicClaude(prompt); default: return null; } } async function getCategoryFromChatGPT(prompt) { if (!OPENAI_API_KEY) return null; const response = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${OPENAI_API_KEY}` }, body: JSON.stringify({ model: "gpt-4o-mini-2024-07-18", messages: [{ role: "user", content: prompt }], temperature: 0.7, max_tokens: 50 }) }); const data = await response.json(); return data.choices[0].message.content.trim(); } async function getCategoryFromChatGPT(prompt) { if (!CHATGPT_API_KEY) return null; 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-3.5-turbo", messages: [{ role: "user", content: prompt }], temperature: 0.7, max_tokens: 50 }) }); const data = await response.json(); return data.choices[0].message.content.trim(); } async function getCategoryFromMetaLlama(prompt) { if (!META_API_KEY) return null; const response = await fetch("https://api.meta.com/v1/text/completions", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${META_API_KEY}` }, body: JSON.stringify({ model: "llama-2-13b-chat", prompt: prompt, max_tokens: 50 }) }); const data = await response.json(); return data.choices[0].text.trim(); } async function getCategoryFromGoogleGemini(prompt, isLargeModel) { if (!GOOGLE_API_KEY) return null; const model = isLargeModel ? "gemini-1.5-flash-1m" : "gemini-1.5-flash"; const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${GOOGLE_API_KEY}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }) }); const data = await response.json(); return data.candidates[0].content.parts[0].text.trim(); } async function getCategoryFromMistralAI(prompt) { if (!MISTRAL_API_KEY) return null; const response = await fetch("https://api.mistral.ai/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${MISTRAL_API_KEY}` }, body: JSON.stringify({ model: "mistral-large-latest", messages: [{ role: "user", content: prompt }], max_tokens: 50 }) }); const data = await response.json(); return data.choices[0].message.content.trim(); } async function getCategoryFromOpenAI(prompt) { if (!OPENAI_API_KEY) return null; const response = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${OPENAI_API_KEY}` }, body: JSON.stringify({ model: "gpt-4", messages: [{ role: "user", content: prompt }], temperature: 0.7, max_tokens: 50 }) }); const data = await response.json(); return data.choices[0].message.content.trim(); } async function getCategoryFromAnthropicClaude(prompt) { if (!ANTHROPIC_API_KEY) return null; const response = await fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": ANTHROPIC_API_KEY }, body: JSON.stringify({ model: "claude-3-haiku-20240307", max_tokens: 50, messages: [{ role: "user", content: prompt }] }) }); const data = await response.json(); return data.content[0].text.trim(); } async 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"; switch (CATEGORY_SELECTION_METHOD) { case 1: // Direct mapping return AUDIBLE_TO_MAM_CATEGORY_MAP[audibleCategory] || ""; case 2: // Scoring method return smartCategoryMatcher(audibleCategory, title, description); case 3: // AI-based selection const aiCategory = await getCategoryFromAI(title, description, audibleCategory); if (aiCategory && AVAILABLE_CATEGORIES.includes(aiCategory)) { return aiCategory; } // If AI fails or no API key, fall back to direct mapping return AUDIBLE_TO_MAM_CATEGORY_MAP[audibleCategory] || ""; default: 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() { 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; } async 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": await getMAMCategory(), "isbn": getASIN() }; var strJson = JSON.stringify(json); await copyToClipboard(strJson); } function addOverlayAndButton() { const overlayHtml = ` <div id="mam-overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); backdrop-filter: blur(5px); z-index: 9999;"> <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 5px; text-align: center;"> <p style="font-size: 18px; margin: 0;">Processing JSON data...</p> <p style="font-size: 16px; margin: 10px 0 0;">Please wait...</p> <div id="ai-gif" style="display: none; margin-top: 20px;"> <img src="https://c.tenor.com/JDV9WN1QC3kAAAAC/tenor.gif" alt="AI Processing" style="max-width: 200px;"> </div> </div> </div>`; document.body.insertAdjacentHTML('beforeend', overlayHtml); const buyBoxElement = document.querySelector("#adbl-buy-box"); if (!buyBoxElement) return; const isAISelected = CATEGORY_SELECTION_METHOD === 3 && AI_MODEL !== 0; const buttonText = isAISelected ? "Copy Book info to JSON with AI" : "Copy Book info to JSON without AI"; 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"> ${buttonText} </span> </button> </span> </div> </div> </div>`; buyBoxElement.insertAdjacentHTML('beforeend', buttonHtml); document.getElementById("copyMetadataButton").addEventListener("click", async function (event) { event.preventDefault(); showOverlay(); await generateJson(); hideOverlay(); }); } function showOverlay() { document.getElementById("mam-overlay").style.display = "block"; // Show GIF if AI model is selected if (CATEGORY_SELECTION_METHOD === 3 && AI_MODEL !== 0) { document.getElementById("ai-gif").style.display = "block"; } } function hideOverlay() { document.getElementById("mam-overlay").style.display = "none"; // Hide GIF document.getElementById("ai-gif").style.display = "none"; } async function copyToClipboard(text) { try { await navigator.clipboard.writeText(text); console.log('Copied to clipboard successfully!'); window.open("https://www.myanonamouse.net/tor/upload.php", "_blank"); } catch (err) { console.error('Could not copy text: ', err); alert('Failed to copy metadata. Please check the console for errors.'); } } window.onload = function () { addOverlayAndButton(); };