Based on a bookmarklet by @deliciousnights.
Installation requires a browser extension such as Violentmonkey / Tampermonkey / Greasemonkey.
Based on a bookmarklet by @deliciousnights.
Installation requires a browser extension such as Violentmonkey / Tampermonkey / Greasemonkey.
// ==UserScript== | |
// @name Stash Tagger Colorizer | |
// @author peolic | |
// @version 1.2 | |
// @description Based on a bookmarklet by @deliciousnights | |
// @namespace https://github.com/peolic | |
// @match http://localhost:9999/* | |
// @match https://localhost:9999/* | |
// @grant none | |
// @homepageURL https://gist.github.com/peolic/4237aaa60e08fcda8a6f8e68092cd5f2 | |
// @downloadURL https://gist.github.com/peolic/4237aaa60e08fcda8a6f8e68092cd5f2/raw/stash-tagger-colorizer.user.js | |
// @updateURL https://gist.github.com/peolic/4237aaa60e08fcda8a6f8e68092cd5f2/raw/stash-tagger-colorizer.user.js | |
// ==/UserScript== | |
// Change if you have Stash in a different language: | |
const LABELS = { | |
ScrapeAllButton: 'Scrape All', | |
Studio: 'Studio', | |
Performer: 'Performer', | |
}; | |
const COLORS = { | |
true: '#0f9960', | |
false: '#ff7373', | |
}; | |
async function main() { | |
execute(); | |
window.addEventListener(locationChanged, execute); | |
} | |
async function execute() { | |
const [tagger, taggerHeader] = await Promise.race([ | |
Promise.all([ | |
elementReady('.tagger-container'), | |
elementReady('.tagger-container > .tagger-container-header'), | |
elementReady('.tagger-container > .tagger-container-header + div'), | |
]), | |
wait(1000).then(() => []), | |
]); | |
if (!tagger) | |
return; | |
const scrapeAllButton = await new Promise(async (resolve) => { | |
let button; | |
do { | |
const buttons = Array.from(taggerHeader.querySelectorAll('button')); | |
button = buttons.find((btn) => btn.innerText === LABELS.ScrapeAllButton); | |
await wait(50); | |
} while (!button); | |
resolve(button); | |
}); | |
if (!scrapeAllButton) { | |
console.error('[tagger colorizer] scrape all button not found'); | |
return; | |
} | |
scrapeAllButton.addEventListener('click', () => { | |
const mainButtonID = 'colorize-tagger-results'; | |
if (taggerHeader.querySelector(`#${mainButtonID}`)) | |
return; | |
const colorizeButtonContainer = document.createElement('div'); | |
colorizeButtonContainer.classList.add('ml-1', 'mr-1'); | |
const colorizeButton = scrapeAllButton.cloneNode(true); | |
colorizeButton.classList.replace('btn-primary', 'btn-success'); | |
colorizeButton.innerText = 'Colorize Results'; | |
colorizeButton.id = mainButtonID; | |
colorizeButton.addEventListener('click', () => colorizeScrapeResults(tagger)); | |
colorizeButtonContainer.appendChild(colorizeButton); | |
scrapeAllButton.closest('.d-flex').prepend(colorizeButtonContainer); | |
}); | |
} | |
function colorizeScrapeResults(tagger) { | |
const searchItems = Array.from(tagger.querySelectorAll('.search-item')); | |
if (searchItems.length === 0) | |
return; | |
searchItems.forEach((searchItem) => { | |
const localname = searchItem.querySelector('.scene-link > div').textContent.toLowerCase(); | |
const queryname = searchItem.querySelector('input').value.toLowerCase(); | |
const check = (fn) => [localname, queryname].some(fn); | |
const searchResults = Array.from(searchItem.querySelectorAll('.search-result')); | |
if (searchResults.length === 0) | |
return; | |
searchResults.forEach((searchResult) => { | |
const titleEl = searchResult.querySelector('.scene-metadata > h4'); | |
const dateEl = searchResult.querySelector('.scene-metadata > h5'); | |
const entityNames = Array.from(searchResult.querySelectorAll('.entity-name')) | |
.map((en) => ({ | |
type: en.firstChild.textContent, | |
name: en.lastChild.textContent, | |
nameEl: en.lastChild, | |
el: en, | |
})); | |
const studio = entityNames.find(({ type }) => type === LABELS.Studio); | |
const studioName = studio.name.toLowerCase().replaceAll(/[. -]/g, ''); // '21 .Naturals' -> '21naturals' | |
studio.nameEl.style.color = COLORS[check((n) => n.replaceAll(/[. -]/g, '').includes(studioName))]; | |
const performers = entityNames.filter(({ type }) => type === LABELS.Performer); | |
performers.forEach(({ name, nameEl }) => { | |
nameEl.style.color = COLORS[check((n) => n.includes(name.toLowerCase()))]; | |
}); | |
const title = titleEl.textContent.toLowerCase().replaceAll('!', ''); // 'Coming Home for Xmas!' -> 'coming home for xmas' | |
titleEl.querySelector('a').style.color = COLORS[check((n) => n.includes(title))]; | |
const date = dateEl.textContent; // 2022-01-31 | |
const dateMatch = check( | |
(n) => date === n.replace( | |
/.*(\d{2}|\d{4})[\D](\d{2})[\D](\d{2}).*/g, (_, year, month, day) => | |
[(year.length === 2 ? '20' : '') + year, month, day].join('-') | |
) | |
); | |
dateEl.querySelector('button + div').style.color = COLORS[dateMatch]; | |
}); | |
}); | |
}; | |
const wait = (/** @type {number} */ ms) => new Promise((resolve) => setTimeout(resolve, ms)); | |
// MIT Licensed | |
// Author: jwilson8767 | |
// https://gist.github.com/jwilson8767/db379026efcbd932f64382db4b02853e | |
/** | |
* 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 | |
}); | |
}); | |
} | |
// Based on: https://dirask.com/posts/JavaScript-on-location-changed-event-on-url-changed-event-DKeyZj | |
const locationChanged = (function() { | |
const { pushState, replaceState } = history; | |
// @ts-expect-error | |
const prefix = GM.info.script.name | |
.toLowerCase() | |
.replace(/[^a-z0-9 -]/g, '') | |
.trim() | |
.replace(/\s+/g, '-'); | |
const eventLocationChange = new Event(`${prefix}$locationchange`); | |
history.pushState = function(...args) { | |
pushState.apply(history, args); | |
window.dispatchEvent(new Event(`${prefix}$pushstate`)); | |
window.dispatchEvent(eventLocationChange); | |
} | |
history.replaceState = function(...args) { | |
replaceState.apply(history, args); | |
window.dispatchEvent(new Event(`${prefix}$replacestate`)); | |
window.dispatchEvent(eventLocationChange); | |
} | |
window.addEventListener('popstate', function() { | |
window.dispatchEvent(eventLocationChange); | |
}); | |
return eventLocationChange.type; | |
})(); | |
main(); |