// ==UserScript== // @name StashDB Submission Helper // @author mmenanno // @version 0.7 // @description Adds button to add all unmatched aliases, measurements, and urls to a performer. // @icon https://raw.githubusercontent.com/stashapp/stash/develop/ui/v2.5/public/favicon.png // @namespace https://github.com/mmenanno // @match https://stashdb.org/drafts/* // @match https://stashdb.org/performers/*/edit // @match https://stashdb.org/performers/add // @homepageURL https://github.com/stashapp/CommunityScripts/tree/main/userscripts/StashDB_Submission_Helper // @downloadURL https://raw.githubusercontent.com/stashapp/CommunityScripts/main/userscripts/StashDB_Submission_Helper/stashdb_submission_helper.user.js // @updateURL https://raw.githubusercontent.com/stashapp/CommunityScripts/main/userscripts/StashDB_Submission_Helper/stashdb_submission_helper.user.js // ==/UserScript== function setNativeValue(element, value) { const valueSetter = Object.getOwnPropertyDescriptor(element, "value")?.set; const prototype = Object.getPrototypeOf(element); const prototypeValueSetter = Object.getOwnPropertyDescriptor( prototype, "value" )?.set; if (prototypeValueSetter && valueSetter !== prototypeValueSetter) { prototypeValueSetter.call(element, value); } else if (valueSetter) { valueSetter.call(element, value); } else { throw new Error("The given element does not have a value setter"); } const eventName = element instanceof HTMLSelectElement ? "change" : "input"; element.dispatchEvent(new Event(eventName, { bubbles: true })); } function waitForElm(selector) { return new Promise((resolve) => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const observer = new MutationObserver((mutations) => { if (document.querySelector(selector)) { resolve(document.querySelector(selector)); observer.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true, }); }); } const aliasInputSelector = 'label[for="aliases"] + div input'; function unmatchedTargetElement(targetProperty) { var targetRegex = '//h6/following-sibling::ul/li[b[contains(text(), "' + targetProperty + '")]]/span/text()'; var targetElement = document.evaluate( targetRegex, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue; return targetElement; } function unmatchedTargetValue(targetProperty) { var targetElement = unmatchedTargetElement(targetProperty); if (targetElement == null) { return; } return targetElement.data; } function unmatchedTargetButton(targetProperty) { var targetRegex = '//h6/following-sibling::ul/li[b[contains(text(), "' + targetProperty + '")]]'; var targetElement = document.evaluate( targetRegex, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue; return targetElement; } function wrapUrlTag(url) { return "<a href='" + url + "' target='_blank'>" + url + "</a>"; } function makeUrlLink(element) { const currentUrls = element.data.split(", "); const wrappedUrls = currentUrls.map((url) => { return wrapUrlTag(url); }); element.parentElement.innerHTML = wrappedUrls.join(", "); } function formTab(tabName) { const tabRegex = '//ul[@role="tablist"]/li/button[contains(text(), "' + tabName + '")]'; return document.evaluate( tabRegex, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue; } function addAlias(alias) { alias = alias.trim(); const existingAliases = Array.from( document.querySelectorAll( 'label[for="aliases"] + div .react-select__multi-value__label' ) ); let aliasMatch = existingAliases.find((element) => { return element.innerText == alias; }); if (typeof aliasMatch !== "undefined") { console.warn( "Skipping alias '" + alias + "' as it is already added to this performer." ); return; } const aliasInput = document.querySelector(aliasInputSelector); setNativeValue(aliasInput, alias); var addButton = document.querySelector( 'label[for="aliases"] + div .react-select__option' ); formTab("Personal Information").click(); addButton.click(); } function existingUrlObjects() { const existingUrls = Array.from( document.querySelectorAll(".URLInput ul .input-group") ); const urlObjects = existingUrls.map((urlGroup) => { let site = urlGroup.childNodes[1].innerText; let url = urlGroup.childNodes[2].innerText; let urlObject = { site: site, url: url, }; return urlObject; }); return urlObjects; } const urlPatterns = [ { pattern: /(^https?:\/\/(?:www\.)?adultfilmdatabase\.com\/(?:video|studio|actor)\/.+)\??/, site: "AFDB", }, // AllMyLinks // APClips // ashemale Tube { pattern: /(https?:\/\/www.babepedia.com\/babe\/[^?]+)\??/, site: "Babepedia", }, // Babes and Stars { pattern: /(^https?:\/\/(?:www\.)?bgafd\.co\.uk\/(?:films|actresses)\/details.php\/id\/[^?]+)\??/, site: "BGAFD", }, { pattern: /(https?:\/\/www.boobpedia.com\/boobs\/[^?]+)\??/, site: "Boobpedia", }, // CamSoda // Chaturbate // Clips4Sale // Cocksuckers Guide { pattern: /(https?:\/\/www.data18.com\/[^?]+)\??/, site: "DATA18", }, // dbNaked // DefineFetish // DMM / FANZA { pattern: /(^https?:\/\/(?:www\.)?egafd\.com\/(?:films|actresses)\/details.php\/id\/[^?]+)\??/, site: "EGAFD", }, { pattern: /(https?:\/\/(www\.)?eurobabeindex.com\/sbandoindex\/.*?.html)/, site: "Eurobabeindex", }, // EuroPornstar { pattern: /(^https?:\/\/(?:www.)?facebook\.com\/[^?]+)/, site: "Facebook", }, // Fancentro // FansDB // Fansly { pattern: /(https?:\/\/www.freeones.com\/[^/?]+)\??/, site: "FreeOnes", }, { pattern: /^https:\/\/gayeroticvideoindex\.com\/performer\/\d+$/, site: "GEVI", }, // GravureFit { pattern: /(https?:\/\/www.iafd.com\/[^?]+)\??/, site: "IAFD", }, // Idol Erotic { pattern: /(^https?:\/\/(?:www\.)?imdb\.com\/(?:name|title)\/[^?]+)\/?/, site: "IMDB", }, { pattern: /(https?:\/\/www.indexxx.com\/[^?]+)\??/, site: "Indexxx", }, { pattern: /(https?:\/\/www.instagram.com\/[^/?]+)\??/, site: "Instagram", }, // iWantClips // JustFor.Fans // Kick // Linktree // Lnk.Bio // LoyalFans { pattern: /(https?:\/\/www.manyvids.com\/[^?]+)\??/, site: "ManyVids", }, // MFC Share { pattern: /(^https?:\/\/(?:www.)?minnano-av\.com\/actress\d+.html)/, site: "Minnano-av", }, // Modeling Agency // Model Mayhem // MSIN // MyDirtyHobby // MyFreeCams { pattern: /(^https?:\/\/(?:www.)?myspace\.com\/[^?]+)/, site: "Myspace", }, // Official Website { pattern: /(https?:\/\/onlyfans.com\/[^?]+)\??/, site: "OnlyFans", }, // Peach // PMV Stash // Pornhub // Pornopedia // PornPics // PornTeenGirl // R18.dev // Reddit User // Shemale Model Database // Snapchat // Sougouwiki // Stripchat { pattern: /(https?:\/\/www.thenude.com\/[^?]+\.htm)/, site: "theNude", }, // ThePornDB { pattern: /(^https?:\/\/(?:www.)?tiktok\.com\/@[^?]+)/, site: "TikTok", }, // Twitch { pattern: /(https?:\/\/twitter.com\/[^?]+)\??/, site: "Twitter", }, { pattern: /(https?:\/\/x.com\/[^?]+)\??/, site: "Twitter", }, // UViU // WAPdB // WAYBIG { pattern: /(^https?:\/\/(www\.)?wikidata.org\/wiki\/[^?]+)/, site: "Wikidata", }, // wikiFeet X { pattern: /(^https?:\/\/(?:\w+\.)?wikipedia\.org\/wiki\/[^?]+)/, site: "Wikipedia", }, // Wikiporno // XCITY { pattern: /(^https?:\/\/xslist\.org\/en\/model\/\d+\.html)/, site: "XsList", }, // XVideos { pattern: /(^https?:\/\/(?:www.)?youtube\.com\/(?:c(?:hannel)?|user)\/[^?]+)/, site: "YouTube", }, { pattern: /^https?:\/\/gayeroticvideoindex\.com\/performer\/\d+$/, site: "GEVI", }, { pattern: /^https:\/\/www\.gaybabeindex\.com\/[^?]+$/, site: "GBI", }, ]; function urlSite(url) { for (const { pattern, site } of urlPatterns) { if (pattern.test(url)) { return site; } } return "Studio Profile"; } function siteMatch(url, selections) { const match = Array.from(selections.options).find( (option) => option.text == urlSite(url) ); return match; } function addUrl(url) { const existingUrls = existingUrlObjects(); let urlMatch = existingUrls.find((element) => { return element.url == url; }); if (typeof urlMatch !== "undefined") { console.warn( "Skipping url '" + url + "' as it is already added to this performer." ); return; } const urlForm = document.querySelector("form .URLInput"); const urlInput = urlForm.querySelector(":scope > .input-group"); const selections = urlInput.children[1]; const inputField = urlInput.children[2]; const addButton = urlInput.children[3]; const selection = siteMatch(url, selections); setNativeValue(selections, selection.value); setNativeValue(inputField, url); if (addButton.disabled) { console.warn("Unable to add url (Add button is disabled)"); } formTab("Links").click(); addButton.click(); } function setStyles(element, styles) { Object.assign(element.style, styles); return element; } function baseButtonContainer() { const container = document.createElement("span"); return container; } function baseButtonSet(name) { const set = document.createElement("a"); set.innerText = "add " + name; set.classList.add("fw-bold"); setStyles(set, { color: "var(--bs-yellow)", cursor: "pointer", "margin-left": "0.5em", }); return set; } function insertButton(action, element, name) { const container = baseButtonContainer(); const set = baseButtonSet(name); set.addEventListener("click", action); container.append(set); element.appendChild(container); } function addMeasurements(measurements) { const splitMeasurements = measurements.split("-"); if (splitMeasurements.length > 0) { const braSize = splitMeasurements[0].trim(); const braInput = document.querySelector('input[name="braSize"]'); setNativeValue(braInput, braSize); } if (splitMeasurements.length > 1) { const waistSize = splitMeasurements[1].trim(); const waistInput = document.querySelector('input[name="waistSize"]'); setNativeValue(waistInput, waistSize); } if (splitMeasurements.length > 2) { const hipSize = splitMeasurements[2].trim(); const hipInput = document.querySelector('input[name="hipSize"]'); setNativeValue(hipInput, hipSize); } formTab("Personal Information").click(); } function createAliasButton(unmatched, element) { const addAliases = () => unmatched.forEach(addAlias); insertButton(addAliases, element, "aliases"); } function createMeasurementsButton(unmatched, element) { const insertMeasurements = () => addMeasurements(unmatched); insertButton(insertMeasurements, element, "measurements"); } function createUrlsButton(unmatched, element) { const addUrls = () => unmatched.forEach(addUrl); insertButton(addUrls, element, "urls"); } function isValidMeasurements(measurements) { const measurementsRegex = /(\d\d\w?\w?\w?\s?)(-\s?\d\d\s?)?(-\s?\d\d)?/; const isValid = measurementsRegex.test(measurements); if (!isValid) { console.warn( "Measurement format '" + measurements + "' is invalid and cannot be automatically added." ); } return measurementsRegex.test(measurements); } function addAliasInputContainer() { const performerForm = document.querySelector(".PerformerForm"); const aliasContainer = document.createElement("div"); aliasContainer.innerHTML = '<button id="aliasButton">Add Aliases</button>'; aliasContainer.setAttribute("id", "aliasContainer"); performerForm.prepend(aliasContainer); const aliasButton = document.createElement("input"); aliasButton.innerText = "Add Aliases"; aliasButton.setAttribute("id", "aliasButton"); aliasButton.setAttribute("style", "border-radius: 0.25rem;"); const aliasField = document.createElement("input"); aliasField.setAttribute("id", "aliasField"); aliasField.setAttribute("placeholder", " Comma separated aliases"); aliasField.setAttribute("size", "50px"); aliasField.setAttribute( "style", "border-radius: 0.25rem; margin-right: 0.5rem;" ); document.getElementById("aliasContainer").prepend(aliasField); const enteredAliases = document.getElementById("aliasField").value; document .getElementById("aliasButton") .addEventListener("click", function handleClick(event) { event.preventDefault(); const aliasField = document.getElementById("aliasField"); if (aliasField.value != "") { aliasField.value.split(/,|\/|\sor\s/).forEach(addAlias); aliasField.value = ""; } }); } function performerEditPage() { const aliasValues = unmatchedTargetValue("Aliases"); if (aliasValues != null) { const unmatchedAliases = aliasValues.split(/,|\/|\sor\s/); const aliasElement = unmatchedTargetButton("Aliases"); createAliasButton(unmatchedAliases, aliasElement); } const urlsValues = unmatchedTargetValue("URLs"); if (urlsValues != null) { const unmatchedUrls = urlsValues.split(", "); if (unmatchedUrls) { const umatchedUrlsElement = unmatchedTargetElement("URLs"); makeUrlLink(umatchedUrlsElement); } const urlsElement = unmatchedTargetButton("URLs"); createUrlsButton(unmatchedUrls, urlsElement); } const unmatchedMeasurements = unmatchedTargetValue("Measurements"); if (unmatchedMeasurements != null) { if (isValidMeasurements(unmatchedMeasurements)) { const measurementsElement = unmatchedTargetButton("Measurements"); createMeasurementsButton(unmatchedMeasurements, measurementsElement); } } addAliasInputContainer(); } function sceneEditPage() { return; } function pageType() { return document .querySelector(".NarrowPage form") .className.replace("Form", ""); } waitForElm(aliasInputSelector).then(() => { if (pageType() == "Performer") { performerEditPage(); } else if (pageType() == "Scene") { sceneEditPage(); } else { return; } });