您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Copies Goodreads book metadata to JSON format with multiple AI-powered category selection options
当前为
// ==UserScript== // @name Goodreads Book Metadata with Multiple AI Options // @namespace https://greasyfork.org/en/scripts/513471 // @version 1.0.1 // @license MIT // @description Copies Goodreads book metadata to JSON format with multiple AI-powered category selection options // @author SnowmanNurse // @match https://www.goodreads.com/book/show/* // @grant none // ==/UserScript== (function() { 'use strict'; const CATEGORY_SELECTION_METHOD = 3; // 1 for direct mapping, 2 for scoring method, 3 for AI-based selection // 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 (Replace with your actual API keys) const CHATGPT_API_KEY = "your_chatgpt_api_key_here"; const META_API_KEY = "your_meta_api_key_here"; const GOOGLE_API_KEY = "your_google_api_key_here"; const MISTRAL_API_KEY = "your_mistral_api_key_here"; const ANTHROPIC_API_KEY = "your_anthropic_api_key_here"; const AVAILABLE_CATEGORIES = [ "Ebooks - Art", "Ebooks - Biographical", "Ebooks - Business", "Ebooks - Computers", "Ebooks - Cooking", "Ebooks - Crafts", "Ebooks - Fantasy", "Ebooks - Fiction", "Ebooks - Health", "Ebooks - History", "Ebooks - Home & Garden", "Ebooks - Horror", "Ebooks - Humor", "Ebooks - Juvenile", "Ebooks - Language", "Ebooks - Literature", "Ebooks - Mathematics", "Ebooks - Medical", "Ebooks - Mystery", "Ebooks - Nature", "Ebooks - Philosophy", "Ebooks - Politics", "Ebooks - Reference", "Ebooks - Religion", "Ebooks - Romance", "Ebooks - Science", "Ebooks - Science Fiction", "Ebooks - Self-Help", "Ebooks - Social Science", "Ebooks - Sports", "Ebooks - Technology", "Ebooks - Travel", "Ebooks - True Crime", "Ebooks - Young Adult" ]; // Mapping from Goodreads shelves to categories const GOODREADS_TO_MAM_CATEGORY_MAP = { "Art": "Ebooks - Art", "Biography": "Ebooks - Biographical", "Business": "Ebooks - Business", "Computer Science": "Ebooks - Computers", "Cooking": "Ebooks - Cooking", "Crafts": "Ebooks - Crafts", "Fantasy": "Ebooks - Fantasy", "Fiction": "Ebooks - Fiction", "Health": "Ebooks - Health", "History": "Ebooks - History", "Horror": "Ebooks - Horror", "Humor": "Ebooks - Humor", "Children's": "Ebooks - Juvenile", "Reference": "Ebooks - Reference", "Romance": "Ebooks - Romance", "Science": "Ebooks - Science", "Self Help": "Ebooks - Self-Help", "Sports": "Ebooks - Sports", "Young Adult": "Ebooks - Young Adult" }; // SVG logo const svgLogo = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 59.16 69.57'> <g transform='translate(-110.73,-61.49)'> <path d='m 130.15,99.28 c 2.9,-1.3 4.93,-4.22 4.94,-7.6 0,-4.61 -3.74,-8.35 -8.35,-8.35 -4.61,0 -8.35,3.74 -8.35,8.35 0,4.6 3.74,8.34 8.35,8.34 0.72,0 1.41,-0.1 2.08,-0.27 0.41,-0.18 0.85,-0.33 1.32,-0.46 z m -7.55,-6.13 c 0,-2.3 1.87,-4.17 4.18,-4.17 0.58,0 1.15,0.12 1.65,0.34 -0.11,0.31 -0.18,0.65 -0.18,1.0 0,1.53 1.19,2.79 2.7,2.91 -0.04,2.26 -1.89,4.09 -4.17,4.09 -2.3,0 -4.18,-1.87 -4.18,-4.17' style='fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.03' /> <path d='m 146.01,83.31 c -4.61,0 -8.35,3.74 -8.35,8.35 0,3.35 1.97,6.23 4.82,7.56 0.55,0.14 1.08,0.32 1.56,0.54 0.63,0.15 1.28,0.24 1.96,0.24 4.6,0 8.34,-3.74 8.35,-8.35 0,-4.61 -3.74,-8.35 -8.35,-8.35 z m 0.04,14.01 c -2.3,0 -4.17,-1.87 -4.17,-4.17 0,-2.3 1.87,-4.17 4.17,-4.17 0.51,0 1.01,0.09 1.46,0.26 -0.13,0.33 -0.21,0.7 -0.21,1.08 0,1.61 1.3,2.92 2.92,2.92 -0.05,2.26 -1.9,4.08 -4.17,4.08' style='fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.03' /> </g> </svg>`; 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", "DNP", "CNE", "CNEcl", "ANEF", "FAADN", "COI" ]; // Start with the original name let cleanedName = name.trim(); // First remove credentials after commas cleanedName = cleanedName.split(',')[0].trim(); // Create pattern for space-separated credentials const credentialPattern = `\\s+(${titlesToRemove.join('|')})\\b`; const credentialRegex = new RegExp(credentialPattern, 'g'); // Remove space-separated credentials cleanedName = cleanedName.replace(credentialRegex, ''); // Final cleanup of any extra spaces while preserving internal spaces cleanedName = cleanedName.replace(/\s+/g, ' ').trim(); return cleanedName; } function cleanText(text) { return text .replace(/\u200E/g, '') // Remove LTR mark .replace(/\s+/g, ' ') .replace(/›.*$/g, '') .replace(/‹.*$/g, '') .trim(); } function cleanDescription(description) { return description .replace(/<div>/gi, '') .replace(/<\/div>/gi, '') .replace(/<p>(.*?)<\/p>/g, '$1\n\n') .replace(/<br\s*\/?>/gi, '\n') .replace(/<i>/gi, '[i]') .replace(/<\/i>/gi, '[/i]') .replace(/<b>/gi, '[b]') .replace(/<\/b>/gi, '[/b]') .replace(/<em>/gi, '[i]') .replace(/<\/em>/gi, '[/i]') .replace(/<strong>/gi, '[b]') .replace(/<\/strong>/gi, '[/b]') .replace(/\n\s*\n/g, '\n\n') .trim(); } function showOverlay() { 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>`; if (!document.getElementById('mam-overlay')) { document.body.insertAdjacentHTML('beforeend', overlayHtml); } document.getElementById("mam-overlay").style.display = "block"; if (CATEGORY_SELECTION_METHOD === 3 && AI_MODEL !== 0) { document.getElementById("ai-gif").style.display = "block"; } } function hideOverlay() { const overlay = document.getElementById("mam-overlay"); if (overlay) { overlay.style.display = "none"; const aiGif = document.getElementById("ai-gif"); if (aiGif) { aiGif.style.display = "none"; } } } function getAuthors() { let authorElements = document.querySelectorAll('.ContributorLink__name'); let authors = []; authorElements.forEach(element => { if (element) { let authorName = element.textContent.trim(); authorName = cleanName(authorName); if (authorName && !authors.includes(authorName)) { authors.push(authorName); } } }); return authors; } function getTitle() { let titleElement = document.querySelector('.Text.Text__title1'); let title = titleElement ? titleElement.textContent.trim() : ""; return title; } function getDescription() { let descriptionElement = document.querySelector('.DetailsLayoutRightParagraph__widthConstrained .Formatted'); if (!descriptionElement) return "No description available"; return cleanDescription(descriptionElement.innerHTML); } function getThumbnail() { let imgElement = document.querySelector('.BookCover__image .ResponsiveImage'); return imgElement ? imgElement.getAttribute('src') : ""; } function getLanguage() { let languageElement = document.querySelector('div.BookDetails [data-testid*="language"]'); if (languageElement) { return languageElement.textContent.replace('Language:', '').trim(); } return "English"; } function getTags() { let tags = []; // Get publication info let publisherInfo = document.querySelector('div.BookDetails [data-testid*="publisher"]'); if (publisherInfo) { const publisherText = publisherInfo.textContent .replace('Publisher:', '') .split('(')[0] // Remove anything in parentheses .trim(); if (publisherText) { tags.push(`Publisher: ${publisherText}`); } } // Get publication date let dateElement = document.querySelector('div.BookDetails [data-testid*="publicationInfo"]'); if (dateElement) { const dateText = dateElement.textContent .replace('Published', '') .replace(':', '') .trim(); if (dateText) { tags.push(`Published: ${dateText}`); } } // Get page count let pagesElement = document.querySelector('div.BookDetails [data-testid*="pagesFormat"]'); if (pagesElement) { const pagesMatch = pagesElement.textContent.match(/(\d+)\s*pages/); if (pagesMatch) { tags.push(`Pages: ${pagesMatch[1]}`); } } // Get formats let formatElement = document.querySelector('div.BookDetails [data-testid*="format"]'); if (formatElement) { const format = formatElement.textContent.replace('Format:', '').trim(); if (format) { tags.push(format); } } // Get genres/shelves let genreElements = document.querySelectorAll('.BookPageMetadataSection__genres a'); genreElements.forEach(element => { const genre = element.textContent.trim(); if (genre) { tags.push(genre); } }); // Get series info for tags if exists const seriesElement = document.querySelector('.BookPageTitleSection__title .Text__subdued'); if (seriesElement) { const seriesText = seriesElement.textContent.trim(); if (seriesText) { tags.push(seriesText); } } // Clean and format tags let cleanedTags = tags .filter(tag => tag && tag.trim()) // Remove empty tags .map(tag => tag.replace(/\s+/g, ' ').trim()) // Clean up whitespace .filter((tag, index, self) => self.indexOf(tag) === index); // Remove duplicates return cleanedTags.join(" | "); } function getSeriesInfo() { let seriesInfo = []; const seriesElement = document.querySelector('.BookPageTitleSection__title .Text__subdued'); if (seriesElement) { let seriesText = seriesElement.textContent.trim(); const seriesMatch = seriesText.match(/(.*?)(?:\s*#(\d+))?$/); if (seriesMatch) { seriesInfo.push({ name: seriesMatch[1].trim(), number: seriesMatch[2] || "" }); } } return seriesInfo; } function getBookData() { const script = document.querySelector("script[id=__NEXT_DATA__]"); const json = JSON.parse(script.textContent); return json.props.pageProps.apolloState; } function getISBN() { const bookData = getBookData(); for (const key in bookData) { if (key.startsWith("Book:") && bookData[key].hasOwnProperty("details")) { const details = bookData[key].details; return details.isbn13 || details.isbn10 || details.asin || ""; } } return ""; } async function getCategoryFromAI(title, description, goodreadsShelf) { const prompt = `Given the following book information, select the most appropriate category from this list: ${AVAILABLE_CATEGORIES.join(", ")} Title: ${title} Description: ${description} Goodreads Shelf: ${goodreadsShelf} 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: // Fallback to direct mapping if AI fails or is not configured return GOODREADS_TO_MAM_CATEGORY_MAP[goodreadsShelf] || "Ebooks - Fiction"; } } async function getMAMCategory(title, description, goodreadsShelf) { switch (CATEGORY_SELECTION_METHOD) { case 1: // Direct mapping return GOODREADS_TO_MAM_CATEGORY_MAP[goodreadsShelf] || ""; case 2: // Scoring method return smartCategoryMatcher(goodreadsShelf, title, description); case 3: // AI-based selection return await getCategoryFromAI(title, description, goodreadsShelf); default: return ""; } } 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 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(); } function smartCategoryMatcher(goodreadsShelf, title, description) { let scores = {}; AVAILABLE_CATEGORIES.forEach(category => { scores[category] = 0; // Check title if (title.toLowerCase().includes(category.toLowerCase().replace("Ebooks - ", ""))) { scores[category] += 2; } // Check description if (description.toLowerCase().includes(category.toLowerCase().replace("Ebooks - ", ""))) { scores[category] += 1; } // Check Goodreads shelf match if (GOODREADS_TO_MAM_CATEGORY_MAP[goodreadsShelf] === category) { scores[category] += 3; } }); return Object.entries(scores).reduce((a, b) => b[1] > a[1] ? b : a)[0]; } async function generateJson() { try { showOverlay(); const title = getTitle(); const authors = getAuthors(); const description = getDescription(); const thumbnail = getThumbnail(); const isbn = getISBN(); const language = getLanguage(); const tags = getTags(); const seriesInfo = getSeriesInfo(); // Get the first shelf as category const shelfElement = document.querySelector('.BookPageMetadataSection__genres a'); const goodreadsShelf = shelfElement ? shelfElement.textContent.trim() : ""; const mamCategory = await getMAMCategory(title, description, goodreadsShelf); const json = { "authors": authors, "description": description, "tags": tags, "thumbnail": thumbnail, "title": title, "language": language, "series": seriesInfo, "category": mamCategory, "isbn": isbn }; await navigator.clipboard.writeText(JSON.stringify(json, null, 2)); console.log('Copied to clipboard successfully!'); window.open("https://www.myanonamouse.net/tor/upload.php", "_blank"); } catch (error) { console.error('Error generating JSON:', error); } finally { hideOverlay(); } } function addButtonAndLogo() { const isAISelected = CATEGORY_SELECTION_METHOD === 3 && AI_MODEL !== 0; const buttonText = isAISelected ? "Copy Book info to JSON with AI" : "Copy Book info to JSON"; let container = document.createElement("div"); container.innerHTML = ` <div id="metadataButtonContainer" style="position:fixed;bottom:10px;right:10px;z-index:9999;display:flex;align-items:center;"> <div style="width:30px;height:30px;margin-right:10px;background-color:#027A48;border-radius:5px;padding:5px;">${svgLogo}</div> <button id="copyMetadataButton" style="padding:10px;background-color:#027A48;color:white;border:none;border-radius:5px;font-size:14px;cursor:pointer;transition:background-color 0.2s;"> ${buttonText} </button> </div>`; document.body.appendChild(container); // Add hover effect const button = document.getElementById("copyMetadataButton"); button.addEventListener("mouseover", function() { this.style.backgroundColor = "#025C36"; }); button.addEventListener("mouseout", function() { this.style.backgroundColor = "#027A48"; }); // Add click handler button.addEventListener("click", async function(event) { event.preventDefault(); await generateJson(); }); } function main() { // Only run on book pages if (window.location.pathname.includes('/book/show/')) { console.log("Goodreads Book Metadata script is running on a book page."); addButtonAndLogo(); } else { console.log("Not a book page. Goodreads Book Metadata script will not run."); } } // Run the script if (document.readyState === "complete" || document.readyState === "interactive") { setTimeout(main, 1000); // Delay execution by 1 second } else { window.addEventListener("DOMContentLoaded", function() { setTimeout(main, 1000); // Delay execution by 1 second }); } })();