diff --git a/README.md b/README.md index 262c74f..ab9b648 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,12 @@ Theme Name|Description | ## Utility Scripts +|Category|Userscript Name|Description| +---------|---------------|-----------| +StashDB |[StashDB Submission Helper](/userscripts/StashDB_Submission_Helper)|Adds handy functions for StashDB submissions like buttons to add aliases in bulk to a performer| + +## Utility Scripts + Category|Plugin Name|Description|Minimum Stash version --------|-----------|-----------|--------------------- Kodi|[Kodi Helper](scripts/kodi-helper)|Generates `nfo` and `strm` for use with Kodi.|v0.7 diff --git a/userscripts/StashDB_Submission_Helper/README.md b/userscripts/StashDB_Submission_Helper/README.md new file mode 100644 index 0000000..fdabf91 --- /dev/null +++ b/userscripts/StashDB_Submission_Helper/README.md @@ -0,0 +1,32 @@ +# StashDB Submission Helper + +- Adds button to add all unmatched aliases to performer +- Adds button to add all unmatched urls to performer +- Adds button to add all unmatched measurements to performer (if they match expected formats) +- Convert unmatched urls from regular strings to linked strings + +## [**INSTALL USERSCRIPT**](https://raw.githubusercontent.com/stashapp/CommunityScripts/main/userscripts/StashDB_Submission_Helper/stashdb_submission_helper.user.js) + +Installation requires a browser extension such as [Violentmonkey](https://violentmonkey.github.io/) / [Tampermonkey](https://www.tampermonkey.net/) / [Greasemonkey](https://www.greasespot.net/). + +### Screenshot +![script preview](https://user-images.githubusercontent.com/1358708/178110989-3bc33371-e3bb-4064-8851-a9356b5a4882.png) + +### Demo GIF: +![demo gif](https://monosnap.com/image/p4pkcqrKWYp3V5quHl5LWOAZUG3oAP) + +## Changelog + +### 0.7 +- Allow alias separator to also be `/` or ` or ` (space on either side of the or). +- Allow measurements to be added without the cup size +- Support full current list of sites for adding URLS (previously only IAFD, DATA18, Indexxx, and Twitter were supported because I forgot to add the others) + +### 0.6 +- Add input field / button to performer edit pages to add a comma separated list of aliases to a performer +![alias input](https://user-images.githubusercontent.com/1358708/179358258-89385345-36ed-42ea-8b71-4f7e84d3a253.png) +- Cleaned up code so that it doesn't run on non-performer drafts +- Added performer add and edit pages to the pages it runs on (since alias function isn't just draft related) + +### 0.5 +Public Release diff --git a/userscripts/StashDB_Submission_Helper/stashdb_submission_helper.user.js b/userscripts/StashDB_Submission_Helper/stashdb_submission_helper.user.js new file mode 100644 index 0000000..e125cf3 --- /dev/null +++ b/userscripts/StashDB_Submission_Helper/stashdb_submission_helper.user.js @@ -0,0 +1,360 @@ +// ==UserScript== +// @name StashDB Submission Helper +// @author halorrr +// @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/halorrr +// @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 "" + url + ""; +}; + +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; +}; + +function urlSite(url) { + let site; + if (/(^https?:\/\/(?:www\.)?adultfilmdatabase\.com\/(?:video|studio|actor)\/.+)\??/.test(url)) { + site = 'AFDB' + } else if (/(https?:\/\/www.babepedia.com\/babe\/[^?]+)\??/.test(url)) { + site = 'Babepedia' + } else if (/(^https?:\/\/(?:www\.)?bgafd\.co\.uk\/(?:films|actresses)\/details.php\/id\/[^?]+)\??/.test(url)) { + site = 'BGAFD' + } else if (/(https?:\/\/www.boobpedia.com\/boobs\/[^?]+)\??/.test(url)) { + site = 'Boobpedia' + } else if (/(https?:\/\/www.data18.com\/[^?]+)\??/.test(url)) { + site = 'DATA18' + } else if (/(^https?:\/\/(?:www\.)?egafd\.com\/(?:films|actresses)\/details.php\/id\/[^?]+)\??/.test(url)) { + site = 'EGAFD' + } else if (/(https?:\/\/(www\.)?eurobabeindex.com\/sbandoindex\/.*?.html)/.test(url)) { + site = 'Eurobabeindex' + } else if (/(^https?:\/\/(?:www.)?facebook\.com\/[^?]+)/.test(url)) { + site = 'Facebook' + } else if (/(https?:\/\/www.freeones.com\/[^/?]+)\??/.test(url)) { + site = 'FreeOnes' + } else if (/(https?:\/\/www.iafd.com\/[^?]+)\??/.test(url)) { + site = 'IAFD' + } else if (/(^https?:\/\/(?:www\.)?imdb\.com\/(?:name|title)\/[^?]+)\/?/.test(url)) { + site = 'IMDB' + } else if (/(https?:\/\/www.indexxx.com\/[^?]+)\??/.test(url)) { + site = 'Indexxx' + } else if (/(https?:\/\/www.instagram.com\/[^/?]+)\??/.test(url)) { + site = 'Instagram' + } else if (/(https?:\/\/www.manyvids.com\/[^?]+)\??/.test(url)) { + site = 'ManyVids' + } else if (/(^https?:\/\/(?:www.)?minnano-av\.com\/actress\d+.html)/.test(url)) { + site = 'Minnano-av' + } else if (/(^https?:\/\/(?:www.)?myspace\.com\/[^?]+)/.test(url)) { + site = 'Myspace' + } else if (/(https?:\/\/onlyfans.com\/[^?]+)\??/.test(url)) { + site = 'OnlyFans' + } else if (/(https?:\/\/www.thenude.com\/[^?]+\.htm)/.test(url)) { + site = 'theNude' + } else if (/(^https?:\/\/(?:www.)?tiktok\.com\/@[^?]+)/.test(url)) { + site = 'TikTok' + } else if (/(https?:\/\/twitter.com\/[^?]+)\??/.test(url)) { + site = 'Twitter' + } else if (/(^https?:\/\/(www\.)?wikidata.org\/wiki\/[^?]+)/.test(url)) { + site = 'Wikidata' + } else if (/(^https?:\/\/(?:\w+\.)?wikipedia\.org\/wiki\/[^?]+)/.test(url)) { + site = 'Wikipedia' + } else if (/(^https?:\/\/xslist\.org\/en\/model\/\d+\.html)/.test(url)) { + site = 'XsList' + } else if (/(^https?:\/\/(?:www.)?youtube\.com\/(?:c(?:hannel)?|user)\/[^?]+)/.test(url)) { + site = 'YouTube' + } else { + return; + }; + + return site; +}; + +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 = ''; + 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; + }; +});