您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Check if Audible books are in the MAM database with match management and persistence
// ==UserScript== // @name Audible MAM Checker & Manager // @namespace https://gf.qytechs.cn/en/scripts/515127 // @version 1.0.2 // @description Check if Audible books are in the MAM database with match management and persistence // @author andromda // @match https://www.audible.com/pd/* // @match https://www.audible.*/pd/* // @match https://www.audible.com/library* // @match https://www.audible.*/library* // @license MIT // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @require https://cdn.jsdelivr.net/npm/@violentmonkey/dom@2 // ==/UserScript== (function(VM) { 'use strict'; // Storage configuration const STORAGE = { KEYS: { MATCHES: 'mam_matches', IGNORED: 'mam_ignored', SETTINGS: 'mam_settings' }, STATUS: { CONFIRMED: 'confirmed', NOT_FOUND: 'not_found', IGNORED: 'ignored' } }; // Utility class for managing stored matches class MatchStorage { static getMatches() { return GM_getValue(STORAGE.KEYS.MATCHES, {}); } static getIgnored() { return GM_getValue(STORAGE.KEYS.IGNORED, {}); } static saveMatch(asin, status, mamId = null) { const matches = this.getMatches(); matches[asin] = { status, mamId, timestamp: Date.now() }; GM_setValue(STORAGE.KEYS.MATCHES, matches); } static ignoreBook(asin) { const ignored = this.getIgnored(); ignored[asin] = { timestamp: Date.now() }; GM_setValue(STORAGE.KEYS.IGNORED, ignored); } static isIgnored(asin) { const ignored = this.getIgnored(); return !!ignored[asin]; } static getMatchStatus(asin) { const matches = this.getMatches(); return matches[asin] || null; } static removeMatch(asin) { const matches = this.getMatches(); delete matches[asin]; GM_setValue(STORAGE.KEYS.MATCHES, matches); } static removeIgnored(asin) { const ignored = this.getIgnored(); delete ignored[asin]; GM_setValue(STORAGE.KEYS.IGNORED, ignored); } } // Utility function to wait for elements to load function waitForElement(selector, timeout = 5000) { return new Promise((resolve, reject) => { const startTime = Date.now(); const checkElement = () => { const element = document.querySelector(selector); if (element) { resolve(element); return; } if (Date.now() - startTime > timeout) { reject(new Error(`Timeout waiting for ${selector}`)); return; } requestAnimationFrame(checkElement); }; checkElement(); }); } class Book { constructor(parentElement) { this.parent = parentElement; } static getBooks(type) { const books = document.querySelectorAll(type.mainSelector); books.forEach(book => { Book.library.add(new type(book)); }); return Book.library; } search(count = 0) { // Check if book is ignored if (MatchStorage.isIgnored(this.asin)) { return this.insertIgnored(); } // Check for saved match status const savedMatch = MatchStorage.getMatchStatus(this.asin); if (savedMatch) { switch (savedMatch.status) { case STORAGE.STATUS.CONFIRMED: return this.insertConfirmedMatch(savedMatch.mamId); case STORAGE.STATUS.NOT_FOUND: return this.insertConfirmedNotFound(); } } // Perform MAM search GM_xmlhttpRequest({ method: 'POST', url: 'https://www.myanonamouse.net/tor/js/loadSearchJSONbasic.php', headers: { 'Content-Type': 'application/json' }, data: this.stringify(), onloadend: response => { if (response.status != 200) return this.insertFailure(); response = JSON.parse(response.response); if (response.error) return this.withoutSubtitle(count); else if (response.data && count === 0) return this.insertFound(response.data[0].id); else if (response.data) return this.insertPossibleFound(response.data); } }); } matchPercentage(text1, text2) { const words1 = text1.split(/\s+/).sort(); const words2 = text2.split(/\s+/).sort(); let matchCount = 0; let i = 0, j = 0; while (i < words1.length && j < words2.length) { if (words1[i] === words2[j]) { matchCount++; i++; j++; } else if (words1[i] < words2[j]) { i++; } else { j++; } } const longestLength = Math.max(words1.length, words2.length); const percentage = matchCount / longestLength * 100; return percentage.toFixed(2).toString() + '%'; } createActionLink(text, onClick, color) { const link = document.createElement('a'); link.href = '#'; link.style.color = color; link.style.marginLeft = '10px'; link.style.textDecoration = 'underline'; link.textContent = text; link.onclick = (e) => { e.preventDefault(); onClick(); }; return link; } createListElement(...styles) { const li = document.createElement('li'); styles.forEach(style => { li.classList.add(style); }); return li; } createSpanElement(text, color, ...styles) { const span = document.createElement('span'); styles.forEach(style => { span.classList.add(style); }); span.style.color = color; span.textContent = text; return span; } createAnchorElement(text, href, color, ...styles) { const anchor = document.createElement('a'); styles.forEach(style => { anchor.classList.add(style); }); anchor.style.color = color; anchor.href = href; anchor.textContent = text; anchor.target = '_blank'; return anchor; } hasSubtitle(text) { const delimiters = [':', '-', '|']; for (const delimiter of delimiters) { if (text.includes(delimiter)) { return delimiter; } } return false; } withoutSubtitle(count) { const delimiter = this.hasSubtitle(this.title); if (!delimiter) return this.insertNotFound(); this.title = this.removeSubtitle(delimiter); this.searchLink = this.createSearchLink(); this.search(++count); } removeSubtitle(delimiter) { this.origTitle = this.title; return this.title.split(delimiter)[0].trim(); } createSearchLink() { const query = new URLSearchParams([ ['tor[text]', this.toString()], ['tor[srchIn][author]', 'true'], ['tor[srchIn][description]', 'true'], ['tor[srchIn][filenames]', 'true'], ['tor[srchIn][narrator]', 'true'], ['tor[srchIn][series]', 'true'], ['tor[srchIn][tags]', 'true'], ['tor[srchIn][title]', 'true'], ['tor[searchIn]', 'torrents'], ['tor[searchType]', 'active'], ['tor[main_cat]', '13'] ]); return new URL(`https://www.myanonamouse.net/tor/browse.php?${query}`).toString(); } toString() { return `${this.getAuthorString()} ${this.getTitleString()}`; } stringify() { return JSON.stringify({ tor: { text: this.toString(), srchIn: { author: 'true', description: 'true', filenames: 'true', narrator: 'true', series: 'true', tags: 'true', title: 'true' }, searchType: 'active', searchIn: 'torrents', main_cat: ['13'], browseFlagsHideVsShow: '0', startDate: '', endDate: '', hash: '', sortType: 'default', startNumber: '0' } }); } getTitleString() { const withSpace = new RegExp('(?<=\\S)[;:,.\\-—](?=\\S)', 'g'); const withoutSpace = new RegExp('[;:,.\\-—]', 'g'); const specialCharacters = new RegExp('[^a-zA-Z0-9\\s]', 'g'); return this.title.replace(withSpace, ' ').replace(withoutSpace, '').replace(specialCharacters, ''); } getAuthorString() { return this.authors; } 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, ''); }); titlesToRemove.forEach(title => { const regexMiddle = new RegExp(`\\s${title}\\s`, 'gi'); cleanedName = cleanedName.replace(regexMiddle, ' '); }); return cleanedName.replace(/\s+/g, ' ').trim(); } } Book.loginURL = 'https://www.myanonamouse.net/login.php'; Book.library = new Set(); class AudibleProduct extends Book { constructor(bookElement) { super(bookElement); this.initialize(); } async initialize() { try { await this.waitForPageContent(); this.title = await this.getTitle(); this.authors = await this.getAuthors(); this.asin = this.getASIN(); this.searchLink = this.createSearchLink(); this.search(); } catch (error) { console.error('Error initializing AudibleProduct:', error); } } async waitForPageContent() { await Promise.all([ waitForElement('h1'), waitForElement('.author-link, a[data-automation-id="author-link"], .authorLabel a, .product-author-link') ]); } async getTitle() { const titleSelectors = [ 'h1[data-automation-id="title"]', 'h1.bc-heading', 'h1' ]; for (const selector of titleSelectors) { const element = document.querySelector(selector); if (element) { return element.textContent.trim(); } } throw new Error('Could not find title element'); } async getAuthors() { const authorSelectors = [ 'a[data-automation-id="author-link"]', '.author-link', '.authorLabel a', '.product-author-link' ]; const authors = []; for (const selector of authorSelectors) { const elements = document.querySelectorAll(selector); if (elements.length > 0) { elements.forEach(author => { const authorName = this.cleanName(author.textContent.trim()); if (authorName && !authors.includes(authorName)) { authors.push(authorName); } }); break; } } return authors.join(", "); } getASIN() { const urlMatch = window.location.pathname.match(/\/([A-Z0-9]{10})/); if (urlMatch && urlMatch[1]) { return urlMatch[1]; } const productDetails = document.querySelector('[data-asin]'); if (productDetails) { return productDetails.getAttribute('data-asin'); } const urlParams = new URLSearchParams(window.location.search); const asin = urlParams.get('asin'); if (asin) { return asin; } return null; } createStatusDiv() { const div = document.createElement('div'); div.className = 'mam-status'; div.style.cssText = ` margin: 20px 0; padding: 10px; border-radius: 4px; font-size: 14px; font-weight: bold; background-color: #f8f8f8; border: 1px solid #e0e0e0; `; return div; } insertStatus(div, text, link, color, prefix = 'MAM Status: ') { div.innerHTML = ` <span style="color: ${color}"> ${prefix}<a href="${link}" target="_blank" style="color: ${color}; text-decoration: underline;">${text}</a> </span> `; const insertionPoints = [ '.buybox-regular-price', '.buybox-membership-price', '.merchandising-buybox', '#reviews-medley-header', '.about-this-audiobook' ]; for (const selector of insertionPoints) { const target = document.querySelector(selector); if (target) { target.parentNode.insertBefore(div, target); break; } } } insertActionLinks(div, actions) { const actionsDiv = document.createElement('div'); actionsDiv.style.marginTop = '5px'; actionsDiv.style.fontSize = '12px'; actions.forEach(action => { actionsDiv.appendChild(this.createActionLink(action.text, action.onClick, action.color)); }); div.appendChild(actionsDiv); } insertPossibleFound(data) { const statusDiv = this.createStatusDiv(); const text = `Possible Match Found (${this.matchPercentage(this.origTitle, data[0].title)})`; this.insertStatus(statusDiv, text, this.searchLink, 'orange'); this.insertActionLinks(statusDiv, [ { text: '✓ Confirm Match', onClick: () => { MatchStorage.saveMatch(this.asin, STORAGE.STATUS.CONFIRMED, data[0].id); this.insertConfirmedMatch(data[0].id); }, color: 'green' }, { text: '✗ Not Found', onClick: () => { MatchStorage.saveMatch(this.asin, STORAGE.STATUS.NOT_FOUND); this.insertConfirmedNotFound(); }, color: 'red' }, { text: '⌀ Ignore', onClick: () => { MatchStorage.ignoreBook(this.asin); this.insertIgnored(); }, color: 'gray' } ]); } insertIgnored() { const statusDiv = this.createStatusDiv(); this.insertStatus(statusDiv, 'Ignored', '#', 'gray'); this.insertActionLinks(statusDiv, [{ text: 'Unignore', onClick: () => { MatchStorage.removeIgnored(this.asin); this.search(); }, color: 'blue' }]); } insertConfirmedMatch(mamId) { const statusDiv = this.createStatusDiv(); const mamUrl = `https://www.myanonamouse.net/t/${mamId}`; this.insertStatus(statusDiv, 'Confirmed Match!', mamUrl, 'green'); this.insertActionLinks(statusDiv, [{ text: 'Remove Confirmation', onClick: () => { MatchStorage.removeMatch(this.asin); this.search(); }, color: 'orange' }]); } insertConfirmedNotFound() { const statusDiv = this.createStatusDiv(); this.insertStatus(statusDiv, 'Confirmed Not Found', this.searchLink, 'red'); this.insertActionLinks(statusDiv, [{ text: 'Remove Confirmation', onClick: () => { MatchStorage.removeMatch(this.asin); this.search(); }, color: 'orange' }]); } insertNotFound() { const statusDiv = this.createStatusDiv(); this.insertStatus(statusDiv, 'Not Found!', this.searchLink, 'red'); this.insertActionLinks(statusDiv, [{ text: '⌀ Ignore', onClick: () => { MatchStorage.ignoreBook(this.asin); this.insertIgnored(); }, color: 'gray' }]); } insertFound(mamId) { const statusDiv = this.createStatusDiv(); const mamUrl = `https://www.myanonamouse.net/t/${mamId}`; this.insertStatus(statusDiv, 'Found!', mamUrl, 'green'); this.insertActionLinks(statusDiv, [ { text: '✓ Confirm Match', onClick: () => { MatchStorage.saveMatch(this.asin, STORAGE.STATUS.CONFIRMED, mamId); this.insertConfirmedMatch(mamId); }, color: 'green' }, { text: '⌀ Ignore', onClick: () => { MatchStorage.ignoreBook(this.asin); this.insertIgnored(); }, color: 'gray' } ]); } insertFailure() { const statusDiv = this.createStatusDiv(); this.insertStatus(statusDiv, 'Are you logged in?', Book.loginURL, 'red', 'MAM ERROR: '); } } class AudibleLibrary extends Book { constructor(bookElement) { super(bookElement); this.isAudiobook = true; this.title = this.getTitle(AudibleLibrary); this.authors = this.getAuthors(AudibleLibrary); this.asin = this.getASIN(); this.searchLink = this.createSearchLink(); this.search(); } getTitle() { return this.parent.querySelector('li a span.bc-text').textContent.trim(); } getAuthors() { const authors = []; const authorElements = this.parent.querySelectorAll('li span.authorLabel a'); for (let element of authorElements) { if (element) { let authorName = this.cleanName(element.textContent.trim()); if (authorName && !authors.includes(authorName)) { authors.push(authorName); } } } return authors.join(", "); } getASIN() { const asinMatch = this.parent.querySelector('a[data-asin]'); if (asinMatch) { return asinMatch.getAttribute('data-asin'); } const linkElement = this.parent.querySelector('a[href*="/pd/"]'); if (linkElement) { const urlMatch = linkElement.href.match(/\/([A-Z0-9]{10})/); if (urlMatch && urlMatch[1]) { return urlMatch[1]; } } return null; } insertPossibleFound(data) { const li = this.createListElement('bc-list-item', 'bc-list-item'); const span = this.createSpanElement('MAM Status: ', 'orange', 'bc-text', 'authorLabel', 'bc-color-secondary'); const text = `Possible Match Found (${this.matchPercentage(this.origTitle, data[0].title)})`; const anchor = this.createAnchorElement(text, this.searchLink, 'orange', 'bc-link', 'bc-color-base'); li.appendChild(span.appendChild(anchor).parentElement); const actionsDiv = document.createElement('div'); actionsDiv.style.marginTop = '5px'; actionsDiv.style.fontSize = '12px'; const actions = [ { text: '✓ Confirm Match', onClick: () => { MatchStorage.saveMatch(this.asin, STORAGE.STATUS.CONFIRMED, data[0].id); this.insertConfirmedMatch(data[0].id); }, color: 'green' }, { text: '✗ Not Found', onClick: () => { MatchStorage.saveMatch(this.asin, STORAGE.STATUS.NOT_FOUND); this.insertConfirmedNotFound(); }, color: 'red' }, { text: '⌀ Ignore', onClick: () => { MatchStorage.ignoreBook(this.asin); this.insertIgnored(); }, color: 'gray' } ]; actions.forEach(action => { actionsDiv.appendChild(this.createActionLink(action.text, action.onClick, action.color)); }); li.appendChild(actionsDiv); return this.parent.children[2].insertAdjacentElement('afterend', li); } insertNotFound() { const li = this.createListElement(); const span = this.createSpanElement('MAM Status: ', 'red'); const anchor = this.createAnchorElement('Not Found!', this.searchLink, 'red'); li.appendChild(span.appendChild(anchor).parentElement); const actionsDiv = document.createElement('div'); actionsDiv.style.marginTop = '5px'; actionsDiv.style.fontSize = '12px'; actionsDiv.appendChild(this.createActionLink('⌀ Ignore', () => { MatchStorage.ignoreBook(this.asin); this.insertIgnored(); }, 'gray')); li.appendChild(actionsDiv); return this.parent.children[2].insertAdjacentElement('afterend', li); } insertFound(mamId) { const li = this.createListElement(); const span = this.createSpanElement('MAM Status: ', 'green'); const mamUrl = `https://www.myanonamouse.net/t/${mamId}`; const anchor = this.createAnchorElement('Found!', mamUrl, 'green'); li.appendChild(span.appendChild(anchor).parentElement); const actionsDiv = document.createElement('div'); actionsDiv.style.marginTop = '5px'; actionsDiv.style.fontSize = '12px'; const actions = [ { text: '✓ Confirm Match', onClick: () => { MatchStorage.saveMatch(this.asin, STORAGE.STATUS.CONFIRMED, mamId); this.insertConfirmedMatch(mamId); }, color: 'green' }, { text: '⌀ Ignore', onClick: () => { MatchStorage.ignoreBook(this.asin); this.insertIgnored(); }, color: 'gray' } ]; actions.forEach(action => { actionsDiv.appendChild(this.createActionLink(action.text, action.onClick, action.color)); }); li.appendChild(actionsDiv); return this.parent.children[2].insertAdjacentElement('afterend', li); } insertFailure() { const li = this.createListElement(); const span = this.createSpanElement('MAM ERROR: ', 'red'); const anchor = this.createAnchorElement('Are you logged in?', Book.loginURL, 'red'); li.appendChild(span.appendChild(anchor).parentElement); return this.parent.children[2].insertAdjacentElement('afterend', li); } insertIgnored() { const li = this.createListElement(); const span = this.createSpanElement('MAM Status: ', 'gray'); const anchor = this.createAnchorElement('Ignored', '#', 'gray'); li.appendChild(span.appendChild(anchor).parentElement); const actionsDiv = document.createElement('div'); actionsDiv.style.marginTop = '5px'; actionsDiv.style.fontSize = '12px'; actionsDiv.appendChild(this.createActionLink('Unignore', () => { MatchStorage.removeIgnored(this.asin); this.search(); }, 'blue')); li.appendChild(actionsDiv); return this.parent.children[2].insertAdjacentElement('afterend', li); } insertConfirmedMatch(mamId) { const li = this.createListElement(); const span = this.createSpanElement('MAM Status: ', 'green'); const mamUrl = `https://www.myanonamouse.net/t/${mamId}`; const anchor = this.createAnchorElement('Confirmed Match!', mamUrl, 'green'); li.appendChild(span.appendChild(anchor).parentElement); const actionsDiv = document.createElement('div'); actionsDiv.style.marginTop = '5px'; actionsDiv.style.fontSize = '12px'; actionsDiv.appendChild(this.createActionLink('Remove Confirmation', () => { MatchStorage.removeMatch(this.asin); this.search(); }, 'orange')); li.appendChild(actionsDiv); return this.parent.children[2].insertAdjacentElement('afterend', li); } insertConfirmedNotFound() { const li = this.createListElement(); const span = this.createSpanElement('MAM Status: ', 'red'); const anchor = this.createAnchorElement('Confirmed Not Found', this.searchLink, 'red'); li.appendChild(span.appendChild(anchor).parentElement); const actionsDiv = document.createElement('div'); actionsDiv.style.marginTop = '5px'; actionsDiv.style.fontSize = '12px'; actionsDiv.appendChild(this.createActionLink('Remove Confirmation', () => { MatchStorage.removeMatch(this.asin); this.search(); }, 'orange')); li.appendChild(actionsDiv); return this.parent.children[2].insertAdjacentElement('afterend', li); } } AudibleLibrary.mainSelector = '.adbl-library-content-row ul.bc-list.bc-list-nostyle'; // Initialize on page load and when content changes VM.observe(document.body, () => { const currentUrl = window.location.href.toLowerCase(); // Handle different page types if (currentUrl.includes('/pd/')) { new AudibleProduct(document.body); } else if (currentUrl.includes('/library')) { Book.getBooks(AudibleLibrary); } return true; }); })(VM);
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址