Skip to content

Instantly share code, notes, and snippets.

@peolic
Last active December 27, 2023 16:28
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save peolic/6aa2cef8fafa377cb5848a473c0e3b30 to your computer and use it in GitHub Desktop.
Save peolic/6aa2cef8fafa377cb5848a473c0e3b30 to your computer and use it in GitHub Desktop.
indexxx helper userscript

Indexxx Helper Userscript

This userscript adds useful tools to indexxx.com

screenshot

Installation requires a browser extension such as Tampermonkey or Greasemonkey.

// ==UserScript==
// @name indexxx
// @author peolic
// @version 3.13
// @description Adds useful information and tools to indexxx pages
// @icon https://www.indexxx.com/apple-touch-icon.png
// @namespace https://github.com/peolic
// @match https://www.indexxx.com/*
// @grant none
// @homepageURL https://gist.github.com/peolic/6aa2cef8fafa377cb5848a473c0e3b30
// @downloadURL https://gist.github.com/peolic/6aa2cef8fafa377cb5848a473c0e3b30/raw/indexxx.user.js
// @updateURL https://gist.github.com/peolic/6aa2cef8fafa377cb5848a473c0e3b30/raw/indexxx.user.js
// ==/UserScript==
//@ts-check
function main() {
// Model page
if (/^\/m\/(.+)/.test(window.location.pathname)) {
new ModelPage();
}
}
const NO_ALIAS = '[no alias]';
class ModelPage {
constructor() {
/** @type {HTMLDivElement} */
this.websitesBox = document.querySelector('#model-websites-box');
/** @type {HTMLDivElement} */
this.modelHeader = document.querySelector('#model-header');
/** @type {HTMLHeadingElement} */
this.portfolioHeader = this.modelHeader.querySelector(':scope > .block1 > h2');
/** @type {IndexxxSet[]} */
this.allSets =
/** @type {HTMLDivElement[]} */
(Array.from(document.querySelectorAll('.pset.card'))).map(parseSetCard);
this.uniqueSets = this.allSets
.filter((set, i, self) => {
const { outUrl, display } = set.site;
const alias = set.models[0].name;
return i === self.findIndex((other) =>
(outUrl ? outUrl === other.site.outUrl : display === other.site.display)
&& alias === other.models[0].name
);
});
this.aliases = this.aliasesUsageCount();
const boxAliases = this.aliasUsageCountBox();
const boxSites = this.sitesForAliasBox();
this.websitesBox.after(boxAliases, boxSites);
tryResizable(boxAliases, { handles: 'e', minWidth: 130 });
tryResizable(boxSites, { handles: 'e', minWidth: 204 });
this.setUpToggle();
this.setUpExport();
}
style = {
box: {
borderColor: '#006ccc',
backgroundColor: '#8bbeff',
width: '204px',
},
title: {
backgroundColor: '#006ccc',
},
body: {
backgroundColor: '#e5eff9',
wordBreak: 'break-word',
padding: '0.1rem',
},
csv: {
userSelect: 'all',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontSize: '10px',
},
}
// Alias Usage Count
aliasUsageCountBox() {
const box = document.createElement('div');
box.classList.add('model-snippet-box');
setStyles(box, this.style.box);
const boxTitle = document.createElement('div');
boxTitle.classList.add('box-title', 'd-flex', 'px-1');
boxTitle.innerText = 'Alias Usage Count';
setStyles(boxTitle, this.style.title);
box.appendChild(boxTitle);
const boxBody = document.createElement('div');
boxBody.classList.add('box-body');
setStyles(boxBody, this.style.body);
this.aliases
.sort(({ count: a }, { count: b }) => {
if (a < b) return 1;
if (a > b) return -1;
return 0;
})
.forEach(({ alias, count, listed }) => {
const div = document.createElement('div');
const searchSpan = document.createElement('span');
setStyles(searchSpan, { marginRight: '.25rem', cursor: 'pointer', userSelect: 'none' });
searchSpan.innerText = '🔎';
searchSpan.addEventListener('click', () => {
const input = /** @type {HTMLInputElement} */ (document.querySelector('input#aliasSitesInput'));
input.value = alias;
input.dispatchEvent(new Event('input'));
});
const aliasSpan = document.createElement('span');
setStyles(aliasSpan, {
fontWeight: alias === NO_ALIAS ? 'bold' : undefined,
userSelect: 'all',
});
if (!listed) {
aliasSpan.classList.add('text-danger');
aliasSpan.title = `"${alias}" is not listed as an alias.`;
}
aliasSpan.innerText = alias;
div.append(searchSpan, aliasSpan, ` = ${count >= 0 ? count : '?'}`);
boxBody.appendChild(div);
});
box.appendChild(boxBody);
const csv = document.createElement('div');
setStyles(csv, this.style.csv);
csv.innerText = this.aliases
.reduce(
(r, { alias }) => alias !== NO_ALIAS ? r.concat(alias) : r,
/** @type {string[]} */ ([])
)
.sort()
.join(', ');
box.appendChild(csv);
return box;
}
// Sites For Alias
sitesForAliasBox() {
const box = document.createElement('div');
box.classList.add('model-snippet-box');
setStyles(box, this.style.box);
const boxTitle = document.createElement('div');
boxTitle.classList.add('box-title', 'd-flex', 'px-1');
setStyles(boxTitle, this.style.title);
box.appendChild(boxTitle);
const boxDefaultTitle = 'Websites By Alias';
const boxTitleNode = document.createTextNode(boxDefaultTitle);
boxTitle.appendChild(boxTitleNode);
const titleSetCount = document.createElement('span');
titleSetCount.classList.add('ml-auto');
boxTitle.appendChild(titleSetCount);
const boxBody = document.createElement('div');
boxBody.classList.add('box-body');
setStyles(boxBody, this.style.body);
box.appendChild(boxBody);
const results = document.createElement('div');
results.innerText = 'Results will show up here';
boxBody.appendChild(results);
const filterDiv = document.createElement('div');
filterDiv.classList.add('d-flex');
boxBody.prepend(filterDiv);
const input = document.createElement('input');
input.id = 'aliasSitesInput';
input.type = 'text';
input.setAttribute('placeholder', 'Click 🔎 or enter an alias...');
input.setAttribute('list', 'modelAliases');
setStyles(input, { flex: 'auto', marginBottom: '0.2rem' });
filterDiv.append(input);
const clearWrapper = document.createElement('span');
clearWrapper.style.position = 'relative';
const clear = document.createElement('span');
setStyles(clear, {
position: 'absolute',
top: '-0.22em',
right: '.1em',
padding: '0 .1em',
color: this.style.title.backgroundColor,
fontWeight: '800',
fontFamily: 'sans-serif',
fontSize: '1.2rem',
cursor: 'pointer',
userSelect: 'none',
})
clear.title = 'Clear';
clear.innerText = 'x';
clear.addEventListener('click', () => {
input.value = '';
input.dispatchEvent(new Event('input'));
});
clearWrapper.appendChild(clear);
filterDiv.append(clearWrapper);
const datalist = document.createElement('datalist');
datalist.id = 'modelAliases';
for (const alias of this.aliases.map(({ alias }) => alias).sort()) {
const option = document.createElement('option');
option.innerText = alias;
datalist.appendChild(option);
}
input.after(datalist);
const csv = document.createElement('div');
setStyles(csv, this.style.csv);
box.appendChild(csv);
const handleInput = () => {
const value = input.value.trim();
filterAll.disabled = !value;
filterAll.checked = false;
restoreAllSets();
let sites = this.sitesForName(value, true)
.sort((a, b) => (a.name || a.display).localeCompare((b.name || b.display), undefined, { sensitivity: 'accent' }));
if (!value)
sites = sites.filter(({ setCount }) => setCount > 0);
if (sites.length === 0) {
boxTitleNode.textContent = boxDefaultTitle;
titleSetCount.innerText = '';
csv.innerText = '';
if (value) {
results.innerText = 'No results';
filterAll.disabled = true;
filterAll.checked = false;
} else {
results.innerText = 'Results will show up here';
}
return;
}
const totalSetCount = sites.reduce((sum, { setCount }) => sum + setCount, 0);
// Disable if there are no sets
filterAll.disabled = totalSetCount === 0;
boxTitleNode.textContent = value ? boxDefaultTitle : 'Sets By Website';
results.innerText = '';
results.append(...sites.map(makeSiteDiv));
csv.innerText = sites
.map((s) => s.name || s.display)
.filter((s, i, self) => self.indexOf(s) === i)
.join(',');
titleSetCount.innerText = `${totalSetCount} sets`;
};
input.addEventListener('input', handleInput);
const filterAll = document.createElement('input');
filterAll.type = 'checkbox';
filterAll.checked = false;
filterAll.disabled = true;
filterAll.title = 'Select/unselect all sites below';
setStyles(filterAll, { margin: '0 2px 2px 0' });
filterDiv.prepend(filterAll);
filterAll.addEventListener('input', () => {
const checkboxes = getFilterCheckboxes();
checkboxes.forEach((cb) => {
cb.checked = filterAll.checked;
});
filterSets();
});
/** @param {SiteFilter} site */
const makeSiteDiv = (site) => {
const { name, display, listedCount, setCount } = site;
const div = document.createElement('div');
const filter = document.createElement('input');
filter.type = 'checkbox';
filter.name = 'siteSetFilter';
filter.value = JSON.stringify(site);
filter.disabled = setCount === 0;
filter.title = 'Filter sets by this sites';
setStyles(filter, { marginRight: '.25rem' });
filter.addEventListener('input', () => {
const checkboxes = getFilterCheckboxes();
filterAll.checked = checkboxes.every((cb) => cb.checked);
filterSets();
});
const siteSpan = document.createElement('abbr');
setStyles(siteSpan, { userSelect: 'all', wordBreak: 'break-all' });
siteSpan.innerText = name || display;
if (name && name !== display)
siteSpan.title = display;
const countEl = document.createElement('span');
countEl.innerText = ` (${setCount || listedCount})`;
const currentFilter = input.value.trim();
if (currentFilter && listedCount && setCount === 0) {
countEl.classList.add('text-danger');
countEl.title = `No sets as "${currentFilter}" found.`;
}
div.append(filter, siteSpan, countEl);
return div;
};
/** @type {IndexxxSet["el"][]} */
const filteredSets = [];
const restoreAllSets = () => {
filteredSets.forEach((el) => {
el.classList.toggle('d-none', false);
});
filteredSets.length = 0;
}
const getFilterCheckboxes = () =>
/** @type {HTMLInputElement[]} */
(Array.from(document.querySelectorAll('input[name="siteSetFilter"]:not(:disabled)')))
const filterSets = () => {
const checkboxes = getFilterCheckboxes();
/** @type {string[]} */
const selected = [];
for (const cb of checkboxes) {
if (cb.checked) {
/** @type {SiteFilter} */
const site = JSON.parse(cb.value);
selected.push(site.display);
}
}
// console.debug('selected sites', selected);
this.allSets.forEach((set) => {
const isSelected = selected.length === 0 || selected.includes(set.site.display);
const currentFilter = input.value.trim();
const isMatch = isSelected && (!currentFilter || set.models[0].name === currentFilter);
set.el.classList.toggle('d-none', !isMatch);
if (!isMatch) return filteredSets.push(set.el);
const index = filteredSets.indexOf(set.el);
if (index !== -1) filteredSets.splice(index, 1);
});
};
// initial state
handleInput();
return box;
}
_createActions() {
const modelTitleSection = document.querySelector('#modelTitleSection');
const atid = 'userscript-actions-top';
/** @type {HTMLDivElement} */
let actionsTop = modelTitleSection.querySelector(`#${atid}`);
if (!actionsTop) {
actionsTop = document.createElement('div');
actionsTop.id = atid;
setStyles(actionsTop, { fontSize: '2em', width: '3.3em', padding: '0 0.25rem' });
actionsTop.classList.add('d-flex', 'justify-content-between');
modelTitleSection.appendChild(actionsTop);
}
const abid = 'userscript-actions-bottom';
/** @type {HTMLSpanElement} */
let actionsBottom = this.portfolioHeader.querySelector(`#${abid}`);
if (!actionsBottom) {
actionsBottom = document.createElement('span');
actionsBottom.id = abid;
actionsBottom.classList.add('ml-2');
this.portfolioHeader.appendChild(actionsBottom);
}
return {
titleActions: actionsTop,
portfolioActions: actionsBottom,
};
}
setUpToggle() {
const { titleActions } = this._createActions();
const toggleButton = document.createElement('div');
setStyles(toggleButton, { cursor: 'pointer' });
toggleButton.title = 'Toggle all info boxes';
toggleButton.innerText = '🔘';
titleActions.append(toggleButton);
toggleButton.addEventListener('click', () => {
const anyHidden =
/** @type {HTMLDivElement[]} */
(Array.from(this.modelHeader.querySelectorAll('.box-title + .box-body')))
.some((el) => el.style.display === 'none');
/** @type {HTMLDivElement[]} */
(Array.from(this.modelHeader.querySelectorAll('.box-title + .box-body')))
.forEach((el) => el.style.display = anyHidden ? '' : 'none');
const twitter = /** @type {HTMLDivElement} */ (this.modelHeader.querySelector(':scope > .twitter'));
if (twitter)
twitter.classList.toggle('d-none', !anyHidden);
/** @type {HTMLImageElement} */
const modelImage = this.modelHeader.querySelector('img.model-img');
if (modelImage.dataset.maxHeight) {
modelImage.style.maxHeight = modelImage.dataset.maxHeight;
modelImage.dataset.maxHeight = '';
} else {
modelImage.dataset.maxHeight = modelImage.style.maxHeight;
modelImage.style.maxHeight = '350px';
}
});
}
/** @type {Map<string, (set: IndexxxSet) => HTMLTableCellElement>} */
ExportFields = new Map([
['URL', (set) => {
const cell = document.createElement('td');
cell.style.maxWidth = '500px';
const a = document.createElement('a');
a.href = a.innerText = set.url;
a.target = '_blank';
cell.appendChild(a);
return cell;
}],
['Site', (set) => {
const cell = document.createElement('td');
cell.innerText = set.site.name || set.site.display;
return cell;
}],
['Date', (set) => {
const cell = document.createElement('td');
cell.innerText = set.date;
return cell;
}],
['Title', (set) => {
const cell = document.createElement('td');
cell.style.maxWidth = '400px';
cell.innerText = set.title;
return cell;
}],
['Models', (set) => {
const cell = document.createElement('td');
cell.style.maxWidth = '400px';
set.models.forEach((model, idx) => {
if (idx > 0) cell.append(' | ');
const a = document.createElement('a');
a.classList.add('d-inline-block');
a.innerText =
model.actualName && model.actualName !== model.name
? `${model.name} (${model.actualName})`
: (model.actualName || model.name);
a.href = model.url;
cell.append(a);
});
return cell;
}],
]);
exportColumnOrder = Array.from(this.ExportFields.keys());
setUpExport() {
const { portfolioActions } = this._createActions();
const exportButton = document.createElement('span');
setStyles(exportButton, { cursor: 'pointer' });
exportButton.title = 'Export visible sets';
exportButton.innerText = '📤';
portfolioActions.append(exportButton);
this.exportTarget = document.createElement('div');
this.exportTarget.id = 'export-target';
this.portfolioHeader.after(this.exportTarget);
exportButton.addEventListener('click', () => this.onExport());
}
onExport() {
const table = document.createElement('table');
table.classList.add('table', 'table-sm', 'table-bordered', 'table-striped', 'w-auto');
const thead = document.createElement('thead');
const tbody = document.createElement('tbody');
table.append(thead, tbody);
const theadrow = document.createElement('tr');
theadrow.classList.add('table-primary');
this.exportColumnOrder.forEach((key, column) => {
const cell = document.createElement('th');
cell.innerText = key;
cell.style.cursor = 'pointer';
cell.title = 'Hide values in column';
cell.addEventListener('click', () => {
for (const row of tbody.rows) {
row.cells[column].classList.toggle('invisible');
}
});
theadrow.appendChild(cell);
});
thead.append(theadrow);
const visibleSets = this.allSets.filter(({ el }) => !el.classList.contains('d-none'));
const rows = visibleSets.map((set) => {
const row = document.createElement('tr');
for (const fieldFunc of this.ExportFields.values()) {
row.appendChild(fieldFunc(set));
}
return row;
});
tbody.append(...rows);
this.exportTarget.innerHTML = '';
this.exportTarget.append(table);
const { portfolioActions } = this._createActions();
const bodyRect = document.body.getBoundingClientRect();
const targetRect = this.exportTarget.getBoundingClientRect();
window.scrollTo({
behavior: 'smooth',
top: targetRect.top - bodyRect.top - 50,
left: 0,
});
/** @type {HTMLSpanElement} */
let buttons = this.portfolioHeader.querySelector('#userscript-export-buttons');
if (!buttons) {
buttons = document.createElement('span');
buttons.id = 'userscript-export-buttons';
buttons.classList.add('ml-2');
portfolioActions.append(buttons);
}
const scrollUp = () =>
window.scrollTo({ behavior: 'smooth', top: bodyRect.top, left: 0 });
const titleUp = 'Go up';
if (!buttons.querySelector(`[title="${titleUp}"]`)) {
const up = document.createElement('span');
setStyles(up, { cursor: 'pointer' });
up.title = titleUp;
up.innerText = '🔝';
up.addEventListener('click', scrollUp);
buttons.append(up);
}
const titleClose = 'Close';
if (!buttons.querySelector(`[title="${titleClose}"]`)) {
const close = document.createElement('span');
setStyles(close, { cursor: 'pointer' });
close.title = titleClose;
close.innerText = '❌';
close.addEventListener('click', () => {
this.exportTarget.innerHTML = '';
close.remove();
});
buttons.append(close);
}
}
/**
* Number of uses for each alias (sets or listed)
*/
aliasesUsageCount() {
/** @type {string[]} */
const sites = [];
/** @type {{ alias: string, count: number, listed: boolean }[]} */
const results = [];
/** @type {HTMLSpanElement[]} */
const aliasSpans = (Array.from(this.websitesBox.querySelectorAll('li span.alias')));
aliasSpans.forEach((aliasSpan) => {
let name = aliasSpan.innerText.trim();
if (!name) name = NO_ALIAS;
const siteEl = closestWebsiteLink(aliasSpan);
if (!siteEl) {
console.error('error getting site for', aliasSpan);
return;
}
const site = siteEl.textContent;
if (site) {
const key = [site, name].join('|');
if (sites.includes(key)) {
console.debug(`skipping duplicate site/alias: ${key}`);
return;
}
sites.push(key);
}
const { length: setCount } = this.setsBySite(siteEl, aliasSpan.innerText);
const listedCount = this.getAliasListedCount(aliasSpan);
if (listedCount === null && !setCount) return;
const minSetCount = setCount || listedCount || 1;
const result = results.find(({ alias }) => alias === name);
if (!result) results.push({ alias: name, count: minSetCount, listed: true });
else result.count += minSetCount;
});
this.uniqueSets
.forEach((set) => {
const { name } = set.models[0];
if (results.find(({ alias }) => alias === name) === undefined) {
const { display: siteDisplay, el: siteEl } = set.site;
const key = [siteDisplay, name].join('|');
if (sites.includes(key)) {
// console.debug(`skipping duplicate site/alias: ${key}`);
return;
}
sites.push(key);
const { length: count } = this.setsBySite(siteEl, name);
results.push({ alias: name, count, listed: false });
}
});
return results;
}
/**
* @typedef SiteFilter
* @property {string | null} name
* @property {string} display
* @property {number} listedCount
* @property {number} setCount
*/
/**
* List of sites for alias
* @param {string} [name]
* @param {boolean} [exact=false]
* @returns {SiteFilter[]}
*/
sitesForName(name, exact=false) {
/** @type {SiteFilter[]} */
const sites = [];
/**
* @param {SiteFilter} other
*/
const findExistingSite = (other) =>
sites.find((site) => site.name == other.name && site.display == other.display);
/**
* @param {string} name
* @param {string} display
* @param {number} count
* @param {boolean} fromList
*/
const push = (name, display, count, fromList) => {
const countTarget = (c = fromList) => c ? 'listedCount' : 'setCount';
const final = /** @type {SiteFilter} */ ({
name,
display,
[countTarget()]: count,
[countTarget(!fromList)]: 0,
});
const existingSite = findExistingSite(final);
if (existingSite)
existingSite[countTarget()] += count;
else
sites.push(final);
};
/**
* @param {string} alias
*/
const shouldHandle = (alias) => (
!name
|| (name === NO_ALIAS && !alias)
|| alias.localeCompare(name, undefined, { sensitivity: exact ? 'variant' : 'accent' }) === 0
);
/** @type {HTMLSpanElement[]} */
(Array.from(this.websitesBox.querySelectorAll('li span.alias')))
.forEach((aliasSpan) => {
const alias = aliasSpan.textContent.trim();
if (shouldHandle(alias)) {
const siteEl = closestWebsiteLink(aliasSpan);
if (!siteEl) {
console.error('error getting site for', aliasSpan);
sites.push({ name: null, display: '???', listedCount: 0, setCount: 0 });
return;
}
const siteSets = this.setsBySite(siteEl);
const siteName = siteSets[0]?.site.name || '';
const siteDisplay = siteEl.textContent;
const hasSetsAsAlias = !!siteSets.find((set) => set.models[0].name === alias);
if (!hasSetsAsAlias) {
const listedCount = (name ? this.getAliasListedCount(aliasSpan) : 0);
if (listedCount !== null)
push(siteName, siteDisplay, listedCount, true);
}
}
});
this.uniqueSets
.forEach((set) => {
const alias = set.models[0].name;
if (shouldHandle(alias)) {
const { name: siteName, display: siteDisplay, el: siteEl } = set.site;
const { length: setCount } = this.setsBySite(siteEl, alias);
push(siteName, siteDisplay, setCount, false);
}
});
return sites;
}
/**
* Get only the listed count of aliases.
* @param {HTMLSpanElement} aliasSpan
* @returns {number | null}
*/
getAliasListedCount(aliasSpan) {
let nextSpan = /** @type {HTMLElement} */ (aliasSpan.nextElementSibling);
if (!nextSpan || nextSpan.matches('a[title="edit"]')) {
// get parent site (multiple names for one site, set count on site row)
const site = closestWebsiteLink(aliasSpan);
if (!site) {
console.error('error getting site for', aliasSpan);
return 0;
}
nextSpan = site.parentElement.querySelector(':scope > span > span');
if (!nextSpan)
return 0;
}
if (nextSpan.classList.contains('count')) {
return Number(nextSpan.textContent.trim()) || 0;
}
if (nextSpan.classList.contains('descr')) {
const text = nextSpan.textContent.trim();
if (/\b(delete|remove)\b/i.test(text))
return null;
const match = text.match(/^(\d+) *;.+/);
if (match)
return Number(match[0]);
}
return 0;
}
/**
* @param {HTMLAnchorElement | HTMLSpanElement} siteEl
* @param {string} [alias]
* @returns {IndexxxSet[]}
*/
setsBySite(siteEl, alias) {
const site = siteEl.getAttribute('href') || siteEl.innerText;
if (!site)
return [];
const sets = this.allSets.filter((set) => {
if (alias && set.models[0].name !== alias)
return false;
if (site.startsWith('/')) // outUrl
return set.site.outUrl === site;
else
return set.site.display === site;
});
return sets;
}
} // ModelPage
/**
* @param {HTMLElement} start
* @returns {HTMLAnchorElement | null}
*/
function closestWebsiteLink(start) {
const selector = ':scope > a.websiteLink, :scope > span.websiteLink';
let current = start.parentElement.closest('li');
while (current && !current.querySelector(selector)) {
current = current.parentElement.closest('li');
}
if (!current) return null;
return current.querySelector(selector);
}
/**
* @typedef IndexxxSetModel
* @property {string} name
* @property {string} [actualName]
* @property {string} url
* @property {HTMLSpanElement} el
*/
/**
* @typedef IndexxxSetSite
* @property {string} name (title)
* @property {string} display (innerText)
* @property {string | null} outUrl (href)
* @property {HTMLAnchorElement | HTMLSpanElement} el
*/
/**
* @typedef IndexxxSet
* @property {string} title
* @property {string} date
* @property {string} url
* @property {[self: IndexxxSetModel, ...other: IndexxxSetModel[]]} models
* @property {IndexxxSetSite} site
* @property {HTMLDivElement} el
*/
/**
* @param {HTMLDivElement} setCard
* @returns {IndexxxSet}
*/
function parseSetCard(setCard) {
const setLink = /** @type {HTMLAnchorElement} */ (setCard.querySelector('.psetInfo > div:nth-of-type(2) > a'));
const url = setLink.href;
const date = setCard.querySelector('time').innerText.trim();
// alt="Lily Jordan, Kylie Page in Kylie Page fucking in the bed with her small tits, at 2chickssametime.com"
// alt="Lily Jordan in at amkingdom.com"
// alt="Michelle B (Lenka) in Michelle B 1, at domai.com"
// alt="Eva Fenix in , at atkexotics.com"
// https://regex101.com/r/SqnoyK/1
const alt = setCard.querySelector('img').alt.trim();
const altMatch = alt.match(/^(.*?) in (?:(.+)?, )? at .+$/);
if (!altMatch) {
console.debug(setCard);
throw new Error(`Failed to parse alt value: ${alt}`);
}
const [, rawActualNames, rawTitle] = altMatch;
const title = rawTitle || '[no title]';
setLink.title = title;
/** @type {{ [index: number]: string | undefined }} */
const actualNames = {};
rawActualNames.split(/, /g).forEach((name, i) => {
// https://regex101.com/r/fO11Ht/1
const nameMatch = name.match(/^(.+?)(?: \((.+)\))?$/);
if (!nameMatch) {
console.error(`Failed to parse name: ${name}`, setCard);
return;
}
const [, aliasOrName, actualName] = nameMatch;
actualNames[i] = actualName || aliasOrName || undefined;
});
/** @type {IndexxxSetModel[]} */
const allModels = (
/** @type {(HTMLAnchorElement | HTMLSpanElement)[]} */
(Array.from(setCard.querySelectorAll('.models li .modelLink')))
.map((modelLink, i) => ({
name: modelLink.innerText.trim(),
actualName: actualNames[i],
url: (modelLink instanceof HTMLAnchorElement
? modelLink
: /** @type {HTMLLinkElement} */ (modelLink.nextElementSibling)
).href,
el: modelLink,
}))
);
let thisModel = allModels.find(({ el }) => el instanceof HTMLSpanElement);
if (thisModel) {
allModels.splice(allModels.indexOf(thisModel), 1);
} else {
console.info('unexpected missing set model', setCard);
/** @type {IndexxxSetModel} */
thisModel = ({
name: /** @type {HTMLHeadingElement} */ (document.querySelector('h1#model-name')).innerText,
url: /** @type {HTMLLinkElement} */ (document.querySelector('link[rel="canonical"]')).href,
// @ts-expect-error
el: null,
});
}
/** @type {IndexxxSet["models"]} */
const models = [thisModel, ...allModels];
/** @type {HTMLAnchorElement | HTMLSpanElement} */
const siteEl = (setCard.querySelector('.psetInfo > .psWebsite > :first-child'));
const { innerText: display, title: siteTitle } = siteEl;
const outUrl = siteEl.getAttribute('href');
const siteName = siteTitle?.replace(/^Go to: /, '');
/** @type {IndexxxSet["site"]} */
const site = { name: siteName, display, outUrl, el: siteEl };
return { date, url, models, site, title, el: setCard };
}
/**
* @template {HTMLElement} E
* @param {E} el
* @param {Partial<CSSStyleDeclaration>} styles
* @returns {E}
*/
function setStyles(el, styles) {
Object.assign(el.style, styles);
return el;
}
/**
* @template {HTMLElement} E
* @param {E} el
* @param {Object} opts
* @returns {E}
*/
function tryResizable(el, opts) {
try {
//@ts-expect-error
jQuery(el).resizable(opts);
} catch (error) {}
return el;
}
main();
@janvier56
Copy link

janvier56 commented Oct 27, 2023

Useful script!
;-)
Can you add a related pornstar panel, with Thumbnail and links in indexxx ?
If you can find a way to link to EGAFD thumbnail when it possible - which are very useful for verification - it should be fine...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment