您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add extra utilities to iafd.com
当前为
// ==UserScript== // @name IAFD // @author peolic // @version 4.10 // @description Add extra utilities to iafd.com // @icon https://www.iafd.com/favicon-196x196.png // @namespace https://github.com/peolic // @match https://www.iafd.com/* // @grant GM.setClipboard // @grant GM.addStyle // @homepageURL https://gist.github.com/peolic/9e2981a8a14a49b9626cb277f878b157 // ==/UserScript== function main() { makeInternalLinksHTTPS(); fixIAFDLinks(); const { pathname } = window.location; if (/^\/title\.(asp|rme\/)/.test(pathname)) return titlePage(); if (/^\/person\.(asp|rme\/)/.test(pathname)) return personPage(); if (/^\/(([a-z]{4}_)?studio|distrib)\.(asp|rme\/)$/.test(pathname)) return studioDistribSelectPage(); if (/^\/(studio|distrib)\.(asp\?|rme\/)(studio|distrib)=\d+/.test(pathname)) return studioDistribPage(); if (/^\/(new|updated)(perfs|headshots)\.asp/.test(pathname)) return personUpdatePage(); if (/^\/lookuptat\.asp/.test(pathname)) return tattooLookupPage(); if (/^\/results\.asp/.test(pathname)) return setSearchField(); } const getBioDataElement = (headingText) => { return Array.from(document.querySelectorAll('p.bioheading')) .find(e => e.innerText.localeCompare(headingText, 'en', { sensitivity: 'accent' }) === 0) ?.nextElementSibling; }; const makeBioEntry = (heading, ...data) => { return [['heading', heading], ...data.map((d) => ['data', d])].map(([type, text]) => { const p = document.createElement('p'); p.classList.add(`bio${type}`); if (text instanceof Node) p.append(text); else p.innerText = text; return p; }); }; const makeQuickSelect = (text) => { const b = document.createElement('b'); b.style.userSelect = 'all'; b.innerText = text; return b; }; const makeISODateElement = (date) => { const isoDate = new Date(`${date} 0:00 UTC`).toISOString().slice(0, 10); return makeQuickSelect(isoDate); }; const slug = (text, char='-') => { return encodeURI(text.replace(/[^a-z0-9_.,:'-]+/gi, '___')) .toLowerCase() .replace(/___/g, char); }; const makeLegacyTitleLink = (title, year) => { const encodedTitle = slug(title, '+').replace(/^\+|\++$|^(a|an|the)\+/g, ''); return `https://www.iafd.com/title.rme/title=${encodedTitle}/year=${year}/${encodedTitle.replace(/\+/g, '-')}.htm`; }; const makeLegacyPerformerLink = (perfId, gender, name=undefined) => { const nameSlug = name ? `/${slug(name, '-')}.htm` : ''; return `https://www.iafd.com/person.rme/perfid=${perfId}/gender=${gender}${nameSlug}`; }; function makeInternalLinksHTTPS() { /** @type {NodeListOf<HTMLAnchorElement>} */ (document.querySelectorAll('a[href^="http://www.iafd.com/"]')).forEach((el) => { el.href = el.href.replace(/^http:/, 'https:'); }); }; function fixIAFDLinks() { const replacer = (_, offset, string) => { if (offset > string.indexOf('/year=')) return '-'; else return '+'; }; /** @type {NodeListOf<HTMLAnchorElement>} */ (document.querySelectorAll('a[href*="//www.iafd.com/"]')).forEach((el) => { el.href = el.href // upper-case UUID to lower-case UUID .replace(/(?<=\/id=)([A-F0-9-]+)$/g, (t) => t.toLowerCase()) // fix bad escaped characters .replace(/%2f/g, replacer); }); }; function titlePage() { const canonical = document.querySelector('.panel:last-of-type .padded-panel') ?.innerText?.match(/should be linked to:\n(.+)/)?.[1]?.replace(/^http:/, 'https:'); if (canonical) history.replaceState(null, '', canonical); const titleHeading = document.querySelector('h1'); // remove trailing space titleHeading.textContent = titleHeading.textContent.replace(/[ \t\n]+$/, ''); const correct = document.querySelector('#correct'); if (correct) { const filmIdBio = makeBioEntry('Film ID', correct.querySelector('input[name="FilmID"]')?.value); filmIdBio[0].style.marginTop = '4em'; correct.before(...filmIdBio); const links = []; if (canonical) { const link = document.createElement('a'); link.href = canonical; link.title = link.href; link.innerText = 'Title Page Link'; Object.assign(link.style, { color: '#337ab7', margin: '1em 0', }); links.push(link); } // Legacy link const titleAndYear = titleHeading.innerText.match(/^(.+) \((\d+)\)$/)?.slice(1) ?? []; const legacyLink = document.createElement('a'); legacyLink.href = makeLegacyTitleLink(...titleAndYear); legacyLink.title = legacyLink.href; legacyLink.innerText = 'Legacy Title Page Link'; Object.assign(legacyLink.style, { color: '#337ab7', margin: '1em 0', }); links.push(legacyLink); correct.before(...makeBioEntry('🔗 Links', ...links)); } const titleSeq = titleHeading.innerText.match(/\((X{0,3}(IX|IV|V?I{0,3}))\)/); if (titleSeq) { // Break up the text node for (let i=0; i < titleHeading.childNodes.length; i++) { if (i >= 10) break; const node = titleHeading.childNodes[i]; const open = node.textContent.indexOf('(', 1), close = node.textContent.indexOf(')'); if (open < 0 || close < 0) continue; if (close && close+1 >= node.length) break; node.splitText(open ?? close+1); } const seqNode = Array.from(titleHeading.childNodes).find((node) => node.textContent.includes(titleSeq?.[1])); const seq = document.createElement('small'); seq.style.color = 'lightgray'; seq.textContent = seqNode.textContent; titleHeading.replaceChild(seq, seqNode); } const releaseDateElement = getBioDataElement('RELEASE DATE'); const releaseDateNodes = Array.from(releaseDateElement.childNodes).filter((node) => node.nodeType === node.TEXT_NODE); releaseDateNodes.forEach((node) => { const text = node.textContent.trim(); const releaseDate = text.replace(/ \(.+\)$/, ''); if (!releaseDate || releaseDate === 'No Data') return; releaseDateElement.insertBefore(makeISODateElement(releaseDate), node); releaseDateElement.insertBefore(document.createElement('br'), node); }); setupScenePerformers(); } const setupScenePerformers = () => { const sceneInfo = document.querySelector('#sceneinfo'); if (!sceneInfo) return; const scenes = Array.from(sceneInfo.querySelectorAll('table tr')); if (scenes.length === 0) return; const castboxes = Array.from(document.querySelectorAll('.castbox')).map((cb) => ({ name: cb.querySelector('a').lastChild.textContent, cb, })); GM.addStyle(` .ext__scene-performers-toggle { white-space: nowrap; } .ext__scene-performers { display: flex; flex-wrap: wrap; column-gap: 1em; } .ext__scene-performers .multiple-matches { background: #bf0000cc; color: white; flex-basis: 100%; font-weight: 600; margin: 0.5em 1em 0 0; padding: 0.25em 0.6em; } .ext__scene-performers .castbox { max-width: 180px; min-height: unset; float: unset; margin-left: 0; } .ext__scene-performers .castbox img.headshot { margin-left: -14px; } `); const toggleStates = new Array(scenes.length).fill(false); const sceneToggles = scenes.map((sceneRow, sceneIndex) => { const sceneLabel = sceneRow.querySelector(':scope > td:first-of-type'); const sceneLabelText = sceneLabel.innerText.toLowerCase(); const scenePerformers = sceneLabel.nextElementSibling; const toggle = (newState = undefined) => { let sceneContainer = sceneRow.querySelector('.ext__scene-performers'); if (sceneContainer) { sceneContainer.style.display = sceneContainer.style.display === 'none' || newState === true ? '' : 'none'; toggleStates[sceneIndex] = sceneContainer.style.display !== 'none'; return; } else if (newState === false) { return; } const performers = scenePerformers.innerText .match(/^.+$/m)[0] // grab first row .replace(/ \[[a-z0-9 ]+\]/gi, '') // remove tags .split(/, /g); // split names sceneContainer = document.createElement('div'); sceneContainer.className = 'ext__scene-performers'; performers.forEach((performer) => { const matches = castboxes.filter(({ name }) => name === performer); if (matches.length === 0) { const [count, text] = performer.match(/^(\d+) (.+?)s?$/)?.slice(1, 3) ?? [1, performer]; for (let i = 1; i <= count; i++) { const node = document.createElement('div'); node.className = 'castbox'; const p = document.createElement('p'); const placeholder = document.createElement('div'); Object.assign(placeholder.style, { width: '170px', height: '200px', padding: '1em' }); const na = document.createElement('abbr'); na.title = `Performer "${performer}" was not found above`; na.innerText = '[N/A]'; p.append(placeholder, `${count == 1 ? performer : text} `, na); node.append(p); sceneContainer.append(node); } } else { if (matches.length > 1) { const warning = document.createElement('div'); warning.innerText = `multiple matches for "${performer}"`; warning.className = 'multiple-matches'; sceneContainer.prepend(warning); } matches.forEach(({ cb: performerCB }) => { const node = performerCB.cloneNode(true); // remove empty elements and extra line breaks from the inner paragraph element for (const cn of Array.from(node.firstElementChild.childNodes).reverse()) { if (cn.textContent.trim()) break; cn.remove(); } sceneContainer.append(node); }); } }); scenePerformers.append(sceneContainer); toggleStates[sceneIndex] = true; }; const sceneToggle = document.createElement('div'); sceneLabel.append(sceneToggle); sceneToggle.append(sceneLabel.firstChild); Object.assign(sceneToggle.style, { cursor: 'pointer', textDecoration: 'underline' }); sceneToggle.title = `View performers for ${sceneLabelText}`; sceneToggle.className = 'ext__scene-performers-toggle'; sceneToggle.addEventListener('click', () => { if (window.getSelection().type === 'Range') return; // Prevent unwanted action when selecting text toggle(); }); return toggle; }); // Toggle all scenes const sceneInfoHeading = sceneInfo.querySelector('.panel-heading h3'); Object.assign(sceneInfoHeading.style, { cursor: 'pointer', textDecoration: 'double underline' }); sceneInfoHeading.title = `View performers for all ${scenes.length} scenes\n [hold Alt to reset]`; sceneInfoHeading.addEventListener('click', (ev) => { if (window.getSelection().type === 'Range') return; // Prevent unwanted action when selecting text if (ev.altKey) { sceneInfo.querySelectorAll('.ext__scene-performers').forEach((el) => el.remove()); return toggleStates.fill(false); } const newState = toggleStates.filter(Boolean).length < scenes.length; sceneToggles.forEach((toggle) => toggle(newState)); }); }; function personPage() { makeExportButton(); const canonical = document.querySelector('#perfwith a[href^="/person.rme/"]')?.href; if (canonical) history.replaceState(null, '', canonical); const nameHeading = document.querySelector('h1'); // remove trailing space nameHeading.textContent = nameHeading.textContent.replace(/[ \n]$/, ''); // Director page if (/\/gender=d\//.test(window.location.pathname)) { const directorPage = document.createElement('b'); directorPage.innerText = 'Director-only page:'; const [maleLink, femaleLink] = ['male', 'female'].map((gender) => { const a = document.createElement('a'); a.href = (canonical ?? window.location.href).replace('/gender=d/', `/gender=${gender.charAt(0)}/`); a.innerText = gender; return a; }); const directorHelp = document.createElement('div'); directorHelp.append(directorPage, ' try the ', maleLink, ' or ', femaleLink, ' performer pages'); nameHeading.after(directorHelp); } const corrections = document.querySelector('#corrections'); if (corrections) { const perfIdCorrectionInput = corrections.querySelector('input[name="PerfID"]'); const gender = corrections.querySelector('input[name="Gender"]')?.value; const perfIdScenePair = document.querySelector('#scenepairings')?.dataset.src.match(/\/perfid=(.+)$/)[1]; const perfId = perfIdCorrectionInput?.value ?? perfIdScenePair; const perfIdBio = makeBioEntry('Performer ID', `${perfId} [${gender.toUpperCase()}]`); perfIdBio[0].style.marginTop = '4em'; corrections.before(...perfIdBio); if (!perfIdCorrectionInput.getAttribute('value')) { corrections.querySelector('input[type="submit"]').style.color = 'red'; perfIdCorrectionInput.setAttribute('value', perfId); } const links = []; if (canonical) { const link = document.createElement('a'); link.href = canonical; link.title = link.href; link.innerText = 'Performer Page Link'; Object.assign(link.style, { color: '#337ab7', margin: '1em 0', }); links.push(link); } // Legacy link const legacyLink = document.createElement('a'); legacyLink.href = makeLegacyPerformerLink(perfId, gender, nameHeading.innerText); legacyLink.title = legacyLink.href; legacyLink.innerText = 'Legacy Performer Page Link'; Object.assign(legacyLink.style, { color: '#337ab7', margin: '1em 0', }); links.push(legacyLink); corrections.before(...makeBioEntry('🔗 Links', ...links)); } const birthDateElement = getBioDataElement('BIRTHDAY'); const birthdayText = birthDateElement.innerText; const birthDate = birthdayText.trim().match(/([A-Z][a-z]+ \d{1,2}, \d{4})\b/); if (birthDate) { birthDateElement.prepend(makeISODateElement(birthDate[1]), document.createElement('br')); } else { const partialDate = birthdayText.trim().match(/(?<month>\?\?|\d{1,2})\/(?<day>\?\?|\d{1,2})\/(?<year>\d{2}[\d?]{2})/); if (partialDate) { const { year, month, day } = partialDate.groups; const exactYear = /^\d{4}$/.test(year); if (exactYear) { const dateParts = [year, month, day].join('-'); const firstQM = dateParts.indexOf('?', 4)-1; const [isoDate, remainder] = [dateParts.slice(0, firstQM), dateParts.slice(firstQM)]; birthDateElement.prepend(makeQuickSelect(isoDate), remainder, document.createElement('br')); } if (month !== '??') { const partialDateStr = (new Date(birthdayText.replace(/\?\?/g, '01') + ' 12:00')) .toLocaleString('en-us', { month: month === '??' ? undefined : 'long', day: day === '??' ? undefined : 'numeric', year: exactYear ? 'numeric' : undefined, }) + (exactYear ? '' : `, ${year}`); birthDateElement.lastChild.before(document.createTextNode(partialDateStr), document.createElement('br')); } } } const heightElement = getBioDataElement('HEIGHT'); const height = heightElement?.innerText.trim().match(/\((\d+) cm\)/); if (height && heightElement) { heightElement.prepend(makeQuickSelect(height[1]), document.createElement('br')); } // example: Lee Stone const akasDirectorElement = getBioDataElement('DIRECTOR AKA'); if (akasDirectorElement && akasDirectorElement.innerText.match(/^None No known aliases$/)) { akasDirectorElement.innerText = akasDirectorElement.innerText.replace(/^None /, ''); } const akasElement = getBioDataElement('AKA') ?? getBioDataElement('PERFORMER AKA'); const akas = akasElement?.innerText.trim(); // empty Performer AKA if (akas === '') { akasElement.innerText = 'No known aliases'; } if (akas && akas !== 'No known aliases') { const copyButtonDefaultText = '[copy names]'; const akasCopy = document.createElement('a'); akasCopy.innerText = copyButtonDefaultText; akasCopy.title = 'Copy only the names used (removes site names)' akasCopy.id = 'copy-akas'; Object.assign(akasCopy.style, { float: 'right', cursor: 'pointer', lineHeight: 1 }); akasCopy.addEventListener('click', async (ev) => { ev.stopPropagation(); ev.preventDefault(); // https://regex101.com/r/7Ad3U1/2 const names = akas.replace(/ \(.+?\)/g, '').split(/[,;\n] ?/g).map((s) => s.trim()).filter(Boolean); if (!names || names.length === 0) { akasCopy.innerText = '❌ Failed!'; akasCopy.style.color = 'red'; return; } const result = names.join(', '); GM.setClipboard(result); akasCopy.innerText = '✔ Copied to clipboard'; akasCopy.style.color = 'green'; akasElement.innerText = result; akasElement.style.backgroundColor = 'yellow'; await wait(1500); akasCopy.innerText = copyButtonDefaultText; akasCopy.style.color = null; akasElement.innerText = akas; akasElement.style.backgroundColor = ''; }) akasElement.previousElementSibling.append(akasCopy); } } function studioDistribSelectPage() { const select = document.querySelector('select[name="Studio"], select[name="Distrib"]'); const pageType = select.closest('form').getAttribute('action').replace(/^\/|\.rme\/$/g, '').toLowerCase(); const selectType = select.name.toLowerCase(); const fullType = selectType === 'distrib' ? 'distributor' : selectType; const listId = `${selectType}-list`; const submit = document.querySelector('form input[type="submit"]'); const input = document.createElement('input'); input.type = 'text'; input.placeholder = `Lookup ${fullType}...`; input.setAttribute('list', listId); Object.assign(input.style, { display: 'block', width: window.getComputedStyle(select).width, marginBottom: '.5rem', }); select.before(input); input.focus(); const datalist = document.createElement('datalist'); datalist.id = listId; for (const option of select.children) { const cloned = option.cloneNode(true); cloned.removeAttribute('value'); datalist.append(cloned); } select.before(datalist); const escapeRegex = (string) => { return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'); }; input.addEventListener('input', () => { const value = input.value.trim(); if (!value) return; const search = new RegExp(`^${escapeRegex(value)}`, 'i'); const found = Array.from(select.children).find((o) => search.test(o.innerText)); if (!found) return; select.value = found.value; }); const handleClick = (/** @type {Event} */ ev) => { if (!select.value) return; ev.preventDefault(); ev.stopPropagation(); const displayName = select.selectedOptions[0].textContent.replace(/[^a-z0-9.-]/ig, '-'); window.location = `https://www.iafd.com/${pageType}.rme/${selectType}=${select.value}/${displayName}.htm`; }; select.addEventListener('dblclick', (ev) => { if (ev.target instanceof HTMLOptionElement) return handleClick(ev); }); select.addEventListener('keyup', (/** @type {KeyboardEvent} */ ev) => { if (ev.key === 'Enter') return handleClick(ev); }); submit.addEventListener('click', handleClick); } function studioDistribPage() { makeExportButton(); } async function makeExportButton() { const filter = await elementReady('div[id$="_filter"]'); const type = filter.id.split('_')[0]; const exportButtonDefaultText = 'Export CSV'; const exportTimestamp = (new Date()).toISOString(); const tools = document.createElement('div'); Object.assign(tools.style, { marginRight: '.5em', display: 'inline-block', }); filter.prepend(tools); const button = document.createElement('button'); button.type = 'button'; button.innerText = exportButtonDefaultText; button.style.marginRight = '.5em'; tools.prepend(button); (async () => { const info = await Promise.race([ elementReady(`div#${type}_info`).then((el) => /** @type {HTMLDivElement} */ (el).innerText), wait(5000).then(() => null), ]); if (!info) return; /** @param {string} s */ const toNumber = (s) => Number(s?.replace(/,/, '')); const { start, end, total } = info .match(/Showing (?<start>[\d,]+) to (?<end>[\d,]+) of (?<total>[\d,]+) entries/i) ?.groups || {}; const count = toNumber(end) - Math.max(toNumber(start) - 1, 0); const totalCount = toNumber(total); let countLabel = `Count: ${count}`; if (count !== totalCount) countLabel += ` out of ${total}`; button.title = countLabel; })(); button.addEventListener('click', async () => { const output = makeOutput(type); if (!output) { button.innerText = '❌ Failed!'; button.style.backgroundColor = 'red'; return; } GM.setClipboard(output); button.innerText = '✔ Copied to clipboard'; button.style.backgroundColor = 'yellow'; await wait(1500); button.innerText = exportButtonDefaultText; button.style.backgroundColor = ''; }); /** @param {string} type */ const makeOutput = (type) => { const dataRows = Array.from(document.querySelectorAll(`div#${type}_wrapper .dataTable tbody > tr`)); let columns; let csv; if (type === 'studio' || type === 'distable') { const data = dataRows.map((tr) => ({ url: tr.children[0].querySelector('a').href, title: tr.children[0].innerText, studio: tr.children[1].innerText, year: Number(tr.children[2].innerText), })); columns = ['Title', 'Studio', 'Year', 'URL']; csv = data.map((d) => columns.map((c) => d[c.toLowerCase()]).join('\t')); } else if (type === 'personal') { const data = dataRows.map((tr) => ({ url: tr.children[0].querySelector('a').href, title: tr.children[0].innerText, year: Number(tr.children[1].innerText), distributor: tr.children[2].innerText, notes: tr.children[3].innerText, })); columns = ['Title', 'Distributor', 'Year', /*'Notes', */'URL']; csv = data.map((d) => columns.map((c) => d[c.toLowerCase()]).join('\t')); } else { return null; } return columns.join('\t') +`\t${exportTimestamp}` + '\n' + csv.join('\n') + '\n'; }; } function personUpdatePage() { } async function tattooLookupPage() { const info = await Promise.race([ elementReady(`div[id^="tat"][id$="_info"]`).then((el) => /** @type {HTMLDivElement} */ (el).innerText), wait(5000).then(() => null), ]); if (!info) return; } function setSearchField() { const field = document.querySelector('#the-basics > input[name="searchstring"]'); const params = new URLSearchParams(window.location.search); field.value = params.get('searchstring'); } const wait = (/** @type {number} */ ms) => new Promise((resolve) => setTimeout(resolve, ms)); /** * Waits for an element satisfying selector to exist, then resolves promise with the element. * Useful for resolving race conditions. * * @param {string} selector * @param {HTMLElement} [parentEl] * @returns {Promise<Element>} */ function elementReady(selector, parentEl) { return new Promise((resolve, reject) => { let el = (parentEl || document).querySelector(selector); if (el) {resolve(el);} new MutationObserver((mutationRecords, observer) => { // Query for elements matching the specified selector Array.from((parentEl || document).querySelectorAll(selector)).forEach((element) => { resolve(element); //Once we have resolved we don't need the observer anymore. observer.disconnect(); }); }) .observe(parentEl || document.documentElement, { childList: true, subtree: true }); }); } main();