您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Complete API-based solution with full metadata cleaning, category mapping, and MAM search
// ==UserScript== // @name Audible to MAM JSON Converter (Full API Version) // @namespace https://greasyfork.org/en/scripts/511491 // @version 2.1.5 // @license MIT // @description Complete API-based solution with full metadata cleaning, category mapping, and MAM search // @author SnowmanNurse (Special thanks to Dr.Blank, DeepSpaceDark, BareMetal, and Audnexus for their contributions) // @include https://www.audible.*/pd/* // @include https://www.audible.*/ac/* // @grant GM_xmlhttpRequest // @grant GM_openInTab // @connect api.audnex.us // ==/UserScript== // Configuration Constants const RIPPER = "MusicFab"; const CHAPTERIZED = true; const CATEGORY_SELECTION_METHOD = 2; const CHATGPT_API_KEY = "your-api-key-here"; const ENABLE_SEARCH_BUTTONS = true; const MAM_AUDIOBOOK_CATEGORIES = [ 39, 49, 50, 83, 51, 97, 40, 41, 106, 42, 52, 98, 54, 55, 43, 99, 84, 44, 56, 45, 57, 85, 87, 119, 88, 58, 59, 46, 47, 53, 89, 100, 108, 48, 111, 0 ]; 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" }; function isAllCaps(str) { return str === str.toUpperCase() && /[A-Z]/.test(str); } function toTitleCase(str) { return str.toLowerCase().replace(/\b\w/g, (c) => c.toUpperCase()); } // Cached data variables let cachedApiData = null; let currentASIN = null; (function() { 'use strict'; window.addEventListener('load', () => { currentASIN = extractASIN(); if (currentASIN) { createFloatingButton(); addKeyframeAnimations(); } }); function createFloatingButton() { const container = document.createElement('div'); container.id = 'mam-button-container'; Object.assign(container.style, { position: 'fixed', top: '100px', right: '20px', zIndex: 9999, display: 'flex', flexDirection: 'column', gap: '8px' }); // JSON Copy Button (always visible) const jsonBtnText = CATEGORY_SELECTION_METHOD === 3 ? '📚 Copy MAM JSON with AI' : '📚 Copy MAM JSON'; const jsonBtn = createButton(jsonBtnText, 'mam-json-btn', handleButtonClick); container.appendChild(jsonBtn); // Conditionally add search buttons if (ENABLE_SEARCH_BUTTONS) { const searchTitleBtn = createButton('🔍 Search MAM (Title)', 'mam-search-title-btn', () => handleSearchClick(false)); const searchAuthorBtn = createButton('🔍 Search MAM (Title+Author)', 'mam-search-author-btn', () => handleSearchClick(true)); container.appendChild(searchTitleBtn); container.appendChild(searchAuthorBtn); } document.body.appendChild(container); } function createButton(text, id, onClickHandler) { const btn = document.createElement('button'); btn.id = id; btn.textContent = text; Object.assign(btn.style, { padding: '12px 18px', backgroundColor: '#FF9900', color: '#1a1a1a', border: 'none', borderRadius: '8px', cursor: 'pointer', boxShadow: '0 4px 12px rgba(0,0,0,0.2)', fontSize: '14px', fontWeight: 'bold', transition: 'all 0.3s ease', whiteSpace: 'nowrap' }); btn.addEventListener('mouseenter', () => { btn.style.transform = 'scale(1.05)'; btn.style.boxShadow = '0 6px 16px rgba(0,0,0,0.3)'; }); btn.addEventListener('mouseleave', () => { btn.style.transform = 'scale(1)'; btn.style.boxShadow = '0 4px 12px rgba(0,0,0,0.2)'; }); btn.addEventListener('click', onClickHandler); return btn; } async function handleSearchClick(includeAuthor) { const btnId = includeAuthor ? 'mam-search-author-btn' : 'mam-search-title-btn'; const btn = document.getElementById(btnId); const originalText = btn.textContent; try { btn.textContent = '🔍 Searching...'; btn.style.backgroundColor = '#ff8000'; const apiData = await fetchAndCacheData(currentASIN); const editionData = extractEditionInfo(apiData.title); const authors = cleanNames(apiData.authors?.map(a => a.name) || []); const title = encodeURIComponent(editionData.title.trim()); const author = authors.length > 0 ? encodeURIComponent(authors[0].trim()) : ''; let query = title; if (includeAuthor && author) query += `%20${author}`; // Build category parameters const categoryParams = MAM_AUDIOBOOK_CATEGORIES .map(cat => `tor%5Bcat%5D%5B%5D=${cat}`) .join('&'); const searchURL = `https://www.myanonamouse.net/tor/browse.php?` + `tor%5Btext%5D=${query}&` + `tor%5BsrchIn%5D%5Btitle%5D=true&` + `tor%5BsrchIn%5D%5Bdescription%5D=true&` + `tor%5BsrchIn%5D%5Btags%5D=true&` + `tor%5BsrchIn%5D%5Bauthor%5D=true&` + `tor%5BsrchIn%5D%5Bnarrator%5D=true&` + `tor%5BsrchIn%5D%5Bseries%5D=true&` + `tor%5BsearchType%5D=all&` + `tor%5BsearchIn%5D=torrents&` + `${categoryParams}&` + `tor%5BbrowseFlagsHideVsShow%5D=0&` + `tor%5BminSize%5D=0&` + `tor%5BmaxSize%5D=0&` + `tor%5Bunit%5D=1&` + `tor%5BminSeeders%5D=0&` + `tor%5BmaxSeeders%5D=0&` + `tor%5BminLeechers%5D=0&` + `tor%5BmaxLeechers%5D=0&` + `tor%5BminSnatched%5D=0&` + `tor%5BmaxSnatched%5D=0&` + `tor%5BsortType%5D=created&` + `tor%5BstartNumber%5D=0`; GM_openInTab(searchURL, { active: true }); showTempAlert('🌐 Searching MAM audiobooks...', '#2196F3'); } catch (error) { showTempAlert(`❌ Error: ${error.message}`, '#f44336'); console.error('Search Error:', error); } finally { btn.textContent = originalText; btn.style.backgroundColor = '#FF9900'; } } async function fetchAndCacheData(asin) { if (!cachedApiData) { cachedApiData = await fetchBookData(asin); } return cachedApiData; } async function handleButtonClick() { const btn = document.getElementById('mam-json-btn'); const originalText = btn.textContent; try { btn.textContent = '🔄 Processing...'; btn.style.backgroundColor = '#ff8000'; const apiData = await fetchAndCacheData(currentASIN); const jsonData = await processApiData(apiData); try { await navigator.clipboard.writeText(JSON.stringify(jsonData, null, 2)); } catch (error) { // maybe show a dialogue to user console.error(error.message); } GM_openInTab('https://www.myanonamouse.net/tor/upload.php', { active: true }); showTempAlert('✅ JSON copied to clipboard!', '#4CAF50'); } catch (error) { showTempAlert(`❌ Error: ${error.message}`, '#f44336'); console.error('JSON Error:', error); } finally { btn.textContent = originalText; btn.style.backgroundColor = '#FF9900'; } } function extractASIN() { const pathMatch = window.location.pathname.match(/\/([A-Z0-9]{10})(?:[\/?]|$)/); return pathMatch ? pathMatch[1] : null; } function fetchBookData(asin) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `https://api.audnex.us/books/${asin}`, onload: function(response) { if (response.status === 200) { resolve(JSON.parse(response.responseText)); } else { reject(new Error(`API Error: ${response.status}`)); } }, onerror: reject }); }); } // Update the processApiData function to process title and subtitle async function processApiData(apiData) { const editionData = extractEditionInfo(apiData.title); // Process title for all caps let processedTitle = editionData.title; if (isAllCaps(processedTitle)) { processedTitle = toTitleCase(processedTitle); } // Process subtitle for all caps let processedSubtitle = apiData.subtitle || ''; if (processedSubtitle && isAllCaps(processedSubtitle)) { processedSubtitle = toTitleCase(processedSubtitle); } return { authors: cleanNames(apiData.authors?.map(a => a.name) || []), narrators: cleanNames(apiData.narrators?.map(n => n.name) || []), title: `${processedTitle}${processedSubtitle ? ": " + processedSubtitle : ""}`, description: formatDescription(apiData), language: formatLanguage(apiData.language), series: processSeries(apiData.seriesPrimary), category: await determineCategory(apiData), thumbnail: apiData.image, tags: await generateTags(apiData, editionData.editionInfo), isbn: `ASIN:${apiData.asin}`, editionInfo: editionData.editionInfo }; } function cleanNames(names) { const patterns = { prefixes: new RegExp( '^\\s*' + '(?:Dr|Mr|Mrs|Ms|Miss|Prof|Rev|Hon|Sir|Dame|Lady|Capt|Col|Gen|Lt|Cmdr|Adm|Maj|Sgt)' + '[\\s.]*', 'i' ), suffixes: new RegExp( '[\\s,]+(?:' + 'Ph\\.?D|M\\.?D|J\\.?D|MBA|MSc|BSc|MS|MA|BA|BS|RN|CPA|Esq|Jr|Sr|I{1,3}|IV|VI?|' + '1st|2nd|3rd|4th|FNP|APRN|DNP|PhD\\.?|MD\\.?|JD\\.?' + ')(?:\\.|,|$)|' + '\\(.*?(?:certified|licensed|registered|chartered).*?\\)', 'gi' ) }; return names.map(name => { return name.replace(patterns.prefixes, '') .replace(patterns.suffixes, '') .replace(/\s{2,}/g, ' ') .trim(); }).filter(name => name.length > 1); } function extractEditionInfo(title) { const patterns = [ /(\d+(?:st|nd|rd|th)?\s*ed(?:ition)?)\b/i, /\((?:unabridged|abridged|special edition)\)/i, /\[.*(?:edition|version).*\]/i ]; for (const pattern of patterns) { const match = title.match(pattern); if (match) { return { title: title.replace(match[0], '').trim(), editionInfo: match[1] || match[0] }; } } return { title: title.trim(), editionInfo: null }; } function formatDescription(apiData) { let desc = apiData.summary || 'No description available'; let paragraphs = desc.split(/<\/p>/gi); paragraphs = paragraphs.map(p => p.replace(/<p>/gi, '') .replace(/<\/?span[^>]*>/gi, '') .trim() ).filter(p => p.length > 0); desc = paragraphs.join('<br><br>'); if (apiData.translators?.length > 0) { desc += `<br><br><strong>Translators:</strong> ${apiData.translators.join(', ')}`; } return desc; } function formatLanguage(lang) { return lang ? lang.charAt(0).toUpperCase() + lang.slice(1) : 'English'; } function processSeries(seriesData) { return seriesData ? [{ name: seriesData.name, number: seriesData.position?.toString() || "" }] : []; } async function determineCategory(apiData) { const audibleCategories = apiData.genres?.map(g => g.name) || []; switch (CATEGORY_SELECTION_METHOD) { case 1: for (const category of audibleCategories) { if (AUDIBLE_TO_MAM_CATEGORY_MAP[category]) { return AUDIBLE_TO_MAM_CATEGORY_MAP[category]; } } return ''; case 2: return smartCategoryMatcher(audibleCategories, apiData.title, apiData.summary); case 3: return await getChatGptCategory(apiData.title, apiData.summary, audibleCategories); default: return ''; } } function smartCategoryMatcher(audibleCategories, title, description) { const text = `${title} ${description}`.toLowerCase(); const scores = {}; for (const [keyword, category] of Object.entries(KEYWORD_TO_MAM_CATEGORY_MAP)) { const regex = new RegExp(`\\b${keyword}\\b`, 'gi'); const matches = text.match(regex); if (matches) { scores[category] = (scores[category] || 0) + matches.length; } } for (const audibleCat of audibleCategories) { const mappedCat = AUDIBLE_TO_MAM_CATEGORY_MAP[audibleCat]; if (mappedCat) { scores[mappedCat] = (scores[mappedCat] || 0) + 3; } } const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]); return sorted[0]?.[0] || ''; } async function getChatGptCategory(title, description, audibleCategories) { if (CHATGPT_API_KEY === "your-api-key-here") { throw new Error("Please put your ChatGPT API key in the script to use AI functionality or switch to the direct mapping or keyword matching methods."); } if (!CHATGPT_API_KEY) return ''; const prompt = `Select the most appropriate category from this list: ${AVAILABLE_CATEGORIES.join(", ")} Title: ${title} Description: ${description} Audible Categories: ${audibleCategories.join(", ")} Respond only with the exact category name.`; 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.5, max_tokens: 50 }) }); if (!response.ok) { if (response.status === 401) { throw new Error("Invalid ChatGPT API key"); } throw new Error(`API Error: ${response.statusText}`); } const data = await response.json(); const category = data.choices[0].message.content.trim(); return AVAILABLE_CATEGORIES.includes(category) ? category : ''; } catch (error) { throw new Error(error.message.includes("API key") ? "Invalid ChatGPT API key" : "AI Category Error"); } } async function generateTags(apiData, editionInfo) { const tags = [ `${formatDate(apiData.releaseDate)}`, CHAPTERIZED && 'Chapterized', RIPPER ].filter(Boolean); if (CATEGORY_SELECTION_METHOD === 3) { const aiTags = await generateAITags(apiData.title, apiData.summary); if (aiTags.length > 0) { tags.push(...aiTags); } } else { const category = await determineCategory(apiData); if (category) { const categoryTag = category.replace('Audiobooks - ', ''); tags.push(categoryTag); } } tags.push( `${formatRuntime(apiData.runtimeLengthMin)}`, `${apiData.publisherName}` ); if (editionInfo) tags.push(`Edition: ${editionInfo}`); return tags.join(' | '); } async function generateAITags(title, description) { const prompt = `For the audiobook with title "${title}" and description "${description}", generate 3-4 relevant descriptive tags. Tags should be single words or short phrases that describe the book's themes, setting, or notable elements. Avoid generic terms like "fiction", "non-fiction", or basic genre names. Respond with just the tags, separated by commas.`; 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 }) }); if (!response.ok) { return []; } const data = await response.json(); return data.choices[0].message.content.split(',').map(tag => tag.trim()); } catch (error) { console.error('Error generating AI tags:', error); return []; } } function formatRuntime(minutes) { const hours = Math.floor(minutes / 60); const mins = minutes % 60; return `${hours} hrs ${mins} mins`; } function formatDate(dateString) { const date = new Date(dateString); return date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); } function showTempAlert(message, color) { const alert = document.createElement('div'); Object.assign(alert.style, { position: 'fixed', top: '20px', right: '20px', padding: '15px 25px', backgroundColor: color, color: 'white', borderRadius: '8px', boxShadow: '0 4px 12px rgba(0,0,0,0.2)', zIndex: 10000, animation: 'slideIn 0.3s ease-out' }); alert.textContent = message; document.body.appendChild(alert); setTimeout(() => { alert.style.animation = 'slideOut 0.3s ease-in'; setTimeout(() => alert.remove(), 300); }, 3000); } function addKeyframeAnimations() { const style = document.createElement('style'); style.textContent = ` @keyframes slideIn { from { transform: translateX(100%); } to { transform: translateX(0); } } @keyframes slideOut { from { transform: translateX(0); } to { transform: translateX(100%); } } `; document.head.appendChild(style); } })();