您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Copies audiobook metadata to JSON and opens MAM upload page. Uses ChatGPT for category and series detection.
当前为
// ==UserScript== // @name Audible Metadata to MAM JSON script (with ChatGPT) // @namespace https://greasyfork.org/en/scripts/511491 // @version 1.6.0 // @license MIT // @description Copies audiobook metadata to JSON and opens MAM upload page. Uses ChatGPT for category and series detection. // @author SnowmanNurse (Modified from original 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 ChatGPT const CATEGORY_SELECTION_METHOD = 3; // API key for ChatGPT const CHATGPT_API_KEY = "OPENAI Code goes here."; // Insert your ChatGPT API key here 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" ]; // Direct mapping from Audible to MAM categories 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" }; // Keyword mapping for smart category matching 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" }; // Part of main script: Replace the existing extractEditionInfo function function extractEditionInfo(title) { // Comprehensive patterns for edition detection const editionPatterns = [ // Parenthetical editions /\(([^)]*?\b(?:edition|ed\.|edn\.)[^)]*?)\)/i, // Editions after punctuation /(?:[-–—:,]\s*)([^-–—:,]*?\b(?:edition|ed\.|edn\.)[^-–—:,]*?)(?=\s*[-–—:,]|\s*$)/i, // Standalone edition phrases /\b(\w+(?:\s+\w+)*\s+(?:edition|ed\.|edn\.)(?:\s*[-–—:,][^-–—:,]*)?)\b/i, // Numbered editions /\b(\d+(?:st|nd|rd|th)?\s+(?:edition|ed\.|edn\.)\b[^,]*)/i ]; let cleanedTitle = title; let editionInfo = null; // Try each pattern until we find a match for (const pattern of editionPatterns) { const match = title.match(pattern); if (match) { editionInfo = match[1] ? match[1].trim() : match[0].trim(); // Remove the matched edition text from the title cleanedTitle = title.replace(match[0], ''); break; } } // Clean up the remaining title if (cleanedTitle) { cleanedTitle = cleanedTitle // Remove trailing punctuation .replace(/\s*[-–—:,]\s*$/, '') // Remove empty parentheses .replace(/\(\s*\)/g, '') // Remove trailing parenthetical content .replace(/\s*\([^)]*\)\s*$/, '') // Clean up multiple commas .replace(/,\s*,/g, ',') // Remove trailing comma .replace(/\s*,\s*$/, '') // Remove trailing colon .replace(/:\s*$/g, '') // Normalize spaces .replace(/\s+/g, ' ') .trim(); } return { cleanedTitle: cleanedTitle || title, editionInfo: editionInfo }; } function cleanName(name) { // Helper function to escape regex special characters const escapeRegExp = (string) => { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }; // Get initial cleaned name let cleanedName = name.trim(); // Remove everything after a hyphen with space cleanedName = cleanedName.replace(/\s+-\s+.*$/, ''); // Comprehensive list of titles to remove const titlesToRemove = [ // Basic Professional Titles "Dr", "Dr.", "Doctor", "Prof", "Prof.", "Professor", "Rev", "Rev.", "Reverend", "Mr", "Mr.", "Mrs", "Mrs.", "Ms", "Ms.", "Miss", "Sir", "Dame", "Lady", "Capt", "Capt.", "Captain", "Col", "Col.", "Colonel", "Gen", "Gen.", "General", "Lt", "Lt.", "Lieutenant", "Cmdr", "Cmdr.", "Commander", "Adm", "Adm.", "Admiral", "Maj", "Maj.", "Major", "Sgt", "Sgt.", "Sergeant", "CPO", "Chief", "Hon", "Hon.", "Honorable", "Amb", "Amb.", "Ambassador", "Gov", "Gov.", "Governor", "Sen", "Sen.", "Senator", "Rep", "Rep.", "Representative", // Religious Titles "Fr", "Fr.", "Father", "Br", "Br.", "Brother", "Sr", "Sr.", "Sister", "Msgr", "Msgr.", "Monsignor", "Deacon", "Elder", "Bishop", "Archbishop", "Cardinal", "Pope", "Imam", "Rabbi", "Cantor", "Pastor", "Minister", // Associate Degrees "AA", "A.A.", "AS", "A.S.", "AAS", "A.A.S.", "AAB", "A.A.B.", "ABA", "A.B.A.", "ABE", "A.B.E.", "ABS", "A.B.S.", "AOS", "A.O.S.", "AGS", "A.G.S.", "AET", "A.E.T.", "AFA", "A.F.A.", "AB", "A.B.", "ASN", "A.S.N.", "ADN", "A.D.N.", "AES", "A.E.S.", "APS", "A.P.S.", // Bachelor's Degrees "BA", "B.A.", "BS", "B.S.", "BSc", "B.Sc.", "BFA", "B.F.A.", "BBA", "B.B.A.", "BSN", "B.S.N.", "BSW", "B.S.W.", "BLS", "B.L.S.", "BAE", "B.A.E.", "BArch", "B.Arch.", "BASc", "B.A.Sc.", "BCom", "B.Com.", "BCS", "B.C.S.", "BDS", "B.D.S.", "BEd", "B.Ed.", "BEng", "B.Eng.", "BES", "B.E.S.", "BHS", "B.H.S.", "BIS", "B.I.S.", "BMS", "B.M.S.", "BMus", "B.Mus.", "BN", "B.N.", "BPS", "B.P.S.", "BSD", "B.S.D.", "BSEE", "B.S.E.E.", "BSET", "B.S.E.T.", "BSBA", "B.S.B.A.", "BT", "B.T.", "BTech", "B.Tech.", "BTS", "B.T.S.", // Master's Degrees "MA", "M.A.", "MS", "M.S.", "MSc", "M.Sc.", "MBA", "M.B.A.", "MFA", "M.F.A.", "MEd", "M.Ed.", "MPH", "M.P.H.", "MPA", "M.P.A.", "MSW", "M.S.W.", "MSN", "M.S.N.", "MLS", "M.L.S.", "MAT", "M.A.T.", "MDiv", "M.Div.", "MArch", "M.Arch.", "MEng", "M.Eng.", "MMus", "M.Mus.", "MPP", "M.P.P.", "MAEd", "M.A.Ed.", "MSEd", "M.S.Ed.", "MLIS", "M.L.I.S.", "MPhil", "M.Phil.", "MTech", "M.Tech.", "MCA", "M.C.A.", // Doctoral Degrees "PhD", "Ph.D.", "MD", "M.D.", "JD", "J.D.", "EdD", "Ed.D.", "PsyD", "Psy.D.", "ThD", "Th.D.", "DO", "D.O.", "PharmD", "Pharm.D.", "DDS", "D.D.S.", "DMD", "D.M.D.", "DVM", "D.V.M.", "DPT", "D.P.T.", "DNP", "D.N.P.", "DSc", "D.Sc.", "DBA", "D.B.A.", "DMA", "D.M.A.", "DMin", "D.Min.", "DrPH", "Dr.P.H.", "DPhil", "D.Phil.", // International Medical Degrees "MBBS", "M.B.B.S.", "MBChB", "M.B.Ch.B.", "BMBS", "B.M.B.S.", // Nursing Credentials and Certifications "RN", "R.N.", "LPN", "L.P.N.", "LVN", "L.V.N.", "CNA", "C.N.A.", "APRN", "A.P.R.N.", "ARNP", "A.R.N.P.", "NP", "N.P.", "CNS", "C.N.S.", "CNM", "C.N.M.", "CRNA", "C.R.N.A.", "FNP", "F.N.P.", "FNP-BC", "F.N.P.-B.C.", "FNP-C", "F.N.P.-C", "ACNP", "A.C.N.P.", "ACNP-BC", "A.C.N.P.-B.C.", "ANP", "A.N.P.", "ANP-BC", "A.N.P.-B.C.", "CEN", "C.E.N.", "CPEN", "C.P.E.N.", "CCRN", "C.C.R.N.", "OCN", "O.C.N.", "CPON", "C.P.O.N.", "CPHON", "C.P.H.O.N.", // Professional Certifications "CPA", "C.P.A.", "CFP", "C.F.P.", "CFA", "C.F.A.", "CISSP", "C.I.S.S.P.", "PMP", "P.M.P.", "CISA", "C.I.S.A.", // Generational Suffixes "Jr", "Jr.", "Junior", "Sr", "Sr.", "Senior", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "1st", "2nd", "3rd", "4th", "5th", "First", "Second", "Third", "Fourth", "Fifth", // International Titles "Dato", "Dato'", "Datuk", "Tan Sri", "Drs.", "Ing.", "Dipl.-Ing." ]; // Handle compound titles with commas - but require word boundaries const compoundPattern = new RegExp( `\\b(?:,?\\s+)?(${titlesToRemove.map(escapeRegExp).join('|')})\\b\\s*,?\\s*`, 'gi' ); // Revised certification patterns that are more specific const certificationPatterns = [ // Match specific nursing/medical credentials /(?:,|\(|\s+)(?:(?:RN|ACNP|FNP|ANP|CNS|CNM|APRN)-[A-Z]{1,3})\b/g, // Professional certifications with -R suffix /(?:,|\(|\s+)[A-Z]{2,}-R\b/g, // General multi-letter certifications /(?:,|\(|\s+)[A-Z]{2,}(?:-[A-Z]+)*\b/g, // Dotted certifications must come after comma or in parentheses /(?:,|\()\s*[A-Z]{2,}(?:\.[A-Z]+)+\b/g, // Keep parenthetical credentials pattern /\([^)]*(?:certified|licensed|registered|chartered|accredited|fellow|member)[^)]*\)/gi, // Comma-separated credentials must start with comma /,\s*(?:[A-Z]{2,}\.?-?)+(?:\s|$)/g, // Fellowship/Membership phrases must be complete /\b(?:Fellow|Member)(?:\sof\s)?(?:the\s)?[A-Z][A-Za-z\s]+/g, // Role parentheticals /\((?:Principal|Senior|Chief|Lead|Head)\s+[^)]+\)/g, // Role suffixes with specific prefixes /\s*[-–—]\s*(?:Certified|Licensed|Registered|Chartered|Accredited)[^,]*/gi, // Credentials in parentheses /\([^)]*(?:RN|MD|PhD|DrPH|DNP|DDS)[^)]*\)/gi, // Board certification phrases must be complete /\b(?:Board\s+Certified\s+[A-Za-z\s]+)/gi ]; // Role patterns to remove - must be preceded by comma, hyphen, or parenthesis const rolesToRemove = [ "foreword", "afterword", "translator", "editor", "introduction", "preface", "contributor", "with", "featuring", "feat[.]?", "illustrated by", "read by", "narrated by", "performed by", "presented by", "lecturer", "instructor", "teacher", "professor", "researcher", "clinician", "practitioner", "specialist", "consultant", "advisor", "mentor", "supervisor", "coordinator", "manager", "director", "administrator", "executive", "president", "chair", "chief", "head", "lead", "senior", "junior", "associate", "assistant", "adjunct", "visiting", "emeritus" ]; // Create pattern for removing roles - must be preceded by separator const rolePattern = new RegExp( `(?:,|\\(|\\s+[-–—])\\s*(${rolesToRemove.join('|')}).*$`, 'i' ); // Apply cleaning patterns cleanedName = cleanedName // Remove roles .replace(rolePattern, '') // Remove parenthetical information at the end .replace(/\s*\([^)]*\)\s*$/, '') // Remove common degrees with or without commas .replace(/(?:,\s*|\s+)(?:PhD|MD|JD|MBA|MS|MA|BA|BS|DNP)(?:\s|$)/gi, '') // Remove leading titles .replace(/^(?:Dr\.?\s+|Prof\.?\s+|Rev\.?\s+|Hon\.?\s+)/i, '') // Remove generational suffixes - must be preceded by space or comma .replace(/(?:\s|,)(?:Sr\.?|Jr\.?|I{2,3}|IV)(?:\s|$)/g, ' ') // Remove status indicators .replace(/\s*\([^)]*(?:emeritus|retired|former|active|practicing)\s*\)/gi, '') // Remove degree sequences after commas .replace(/,\s*(?:MA|BA|BS|PhD|MD|JD|DNP|MSN|BSN)(?:\s|$)/gi, '') // Remove credentials with periods after commas .replace(/,\s*(?:[A-Z]\.)+(?:\s|$)/g, ''); // Apply certification patterns for (const pattern of certificationPatterns) { cleanedName = cleanedName.replace(pattern, ' '); } // Remove all variations of titles cleanedName = cleanedName.replace(compoundPattern, ' '); // Final cleanup cleanedName = cleanedName // Clean up separators and whitespace .replace(/^[.,\s]+|[.,\s]+$/g, '') .replace(/\s*,\s*$/, '') .replace(/\s*\.|,\s*$/, '') .replace(/^[,\s]+|[,\s]+$/g, '') .replace(/\s{2,}/g, ' ') .trim(); // Return cleaned name or original if empty return cleanedName || name; } async function getCategoryFromChatGPT(title, description, audibleCategory) { if (!CHATGPT_API_KEY) return null; 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.`; try { 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-4o-mini", messages: [{ role: "user", content: prompt }], temperature: 0.7, max_tokens: 50 }) }); const data = await response.json(); return data.choices[0].message.content.trim(); } catch (error) { console.error("Error getting category from ChatGPT:", error); return null; } } function smartCategoryMatcher(audibleCategory, title, description) { let possibleCategories = {}; if (AUDIBLE_TO_MAM_CATEGORY_MAP[audibleCategory]) { possibleCategories[AUDIBLE_TO_MAM_CATEGORY_MAP[audibleCategory]] = 5; } 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; if (category === "Audiobooks - Romance") { possibleCategories[category] += 2; } } } }; addScoreForKeywords(title, 3); addScoreForKeywords(description, 2); let bestMatch = Object.entries(possibleCategories).reduce((a, b) => a[1] > b[1] ? a : b)[0]; return bestMatch || (audibleCategory.includes("Fiction") ? "Audiobooks - General Fiction" : "Audiobooks - General Non-Fic"); } 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: // ChatGPT const chatGPTCategory = await getCategoryFromChatGPT(title, description, audibleCategory); if (chatGPTCategory && AVAILABLE_CATEGORIES.includes(chatGPTCategory)) { return chatGPTCategory; } // If ChatGPT fails or no API key, fall back to direct mapping return AUDIBLE_TO_MAM_CATEGORY_MAP[audibleCategory] || ""; default: return ""; } } 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 = []; // First check for series information on the Audible page 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 }); } }); // If no series found on page and ChatGPT is enabled, try AI detection if (seriesInfo.length === 0 && CATEGORY_SELECTION_METHOD === 3) { window.detectingSeriesWithAI = true; } return seriesInfo; } function getTitle() { let title = document.getElementsByTagName("h1")[0].innerText; const { cleanedTitle, editionInfo } = extractEditionInfo(title); // Store edition info for later use in tags if (editionInfo) { window.editionInfo = editionInfo; } return cleanedTitle; } 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 getAudibleCategory() { let categoryElement = document.querySelector(".categoriesLabel"); if (categoryElement) return categoryElement.innerText; categoryElement = document.querySelector("nav.bc-breadcrumb"); if (categoryElement) return categoryElement.innerText; return ""; } 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()); // Add edition info to tags if it exists if (window.editionInfo) { tags.push(`Edition: ${window.editionInfo}`); } 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 = 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>"); let authors = getAuthors(); let seriesInfo = getSeriesInfo(); // If AI series detection is needed, do it here if (window.detectingSeriesWithAI) { try { const aiSeriesInfo = await detectSeriesWithChatGPT(getTitle(), authors[0] || "", description); if (aiSeriesInfo.name) { seriesInfo = [aiSeriesInfo]; } } catch (error) { console.error("Error in AI series detection:", error); } delete window.detectingSeriesWithAI; } var json = { "authors": authors, "description": description, "narrators": getNarrators(), "tags": getAdditionalTags(), "thumbnail": imageSrc, "title": getTitleAndSubtitle(), "language": getLanguage(), "series": seriesInfo, "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 buttonText = CATEGORY_SELECTION_METHOD === 3 ? "Copy Book info to JSON with ChatGPT" : "Copy Book info to JSON"; 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"; if (CATEGORY_SELECTION_METHOD === 3) { document.getElementById("ai-gif").style.display = "block"; } } function hideOverlay() { document.getElementById("mam-overlay").style.display = "none"; 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(); };