// ==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();
};