Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save peolic/4237aaa60e08fcda8a6f8e68092cd5f2 to your computer and use it in GitHub Desktop.
Save peolic/4237aaa60e08fcda8a6f8e68092cd5f2 to your computer and use it in GitHub Desktop.
// ==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();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment