CommunityScripts/userscripts/StashDB_Submission_Helper/stashdb_submission_helper.user.js
DogmaDragon 43feb5909d Add URLs to plugins for better access and support
- Updated FileMonitor plugin URL to Discourse link
- Updated LocalVisage plugin URL to Discourse link
- Updated PlexSync plugin URL to Discourse link
- Updated PythonDepManager plugin URL to Discourse link
- Updated PythonToolsInstaller plugin URL to Discourse link
- Updated RenameFile plugin URL to Discourse link
- Updated SFW Switch plugin URL to Discourse link
- Updated SecondaryPerformerImage plugin URL to Discourse link
- Updated StashRandomButton plugin URL to Discourse link
- Updated TPDBMarkers plugin URL to Discourse link
- Updated ThumbPreviews plugin URL to Discourse link
- Updated VideoBanner plugin URL to Discourse link
- Updated VideoScrollWheel plugin URL to Discourse link
- Updated additionalFilesDeleter plugin URL to Discourse link
- Updated audio-transcodes plugin URL to Discourse link
- Updated bulkImageScrape plugin URL to Discourse link
- Updated chooseYourAdventurePlayer plugin URL to Discourse link
- Updated cjCardTweaks plugin URL to Discourse link
- Updated comicInfoExtractor plugin URL to Discourse link
- Updated defaultDataForPath plugin URL to Discourse link
- Updated dupeMarker plugin URL to Discourse link
- Updated e621_tagger plugin URL to Discourse link
- Updated externalLinksEnhanced plugin URL to Discourse link
- Updated filenameParser plugin URL to Discourse link
- Updated funscriptMarkers plugin URL to Discourse link
- Updated hotCards plugin URL to Discourse link
- Updated imageGalleryNavigation plugin URL to Discourse link
- Updated image_date_from_metadata plugin URL to Discourse link
- Updated markerDeleteButton plugin URL to Discourse link
- Updated markerTagToScene plugin URL to Discourse link
- Updated miscTags plugin URL to Discourse link
- Updated nfoSceneParser plugin URL to Discourse link
- Updated pathParser plugin URL to Discourse link
- Updated performerStashboxUrlToID plugin URL to Discourse link
- Updated sceneCoverCropper plugin URL to Discourse link
- Updated scenePageRememberStates plugin URL to Discourse link
- Updated setPerformersFromTags plugin URL to Discourse link
- Updated setSceneCoverFromFile plugin URL to Discourse link
- Updated starIdentifier plugin URL to Discourse link
- Updated stashAI plugin URL to Discourse link
- Updated stashAppAndroidTvCompanion plugin URL to Discourse link
- Updated stashNotes plugin URL to Discourse link
- Updated stashNotifications plugin URL to Discourse link
- Updated stashdb-performer-gallery plugin URL to Discourse link
- Updated stats plugin URL to Discourse link
- Updated tagCopyPaste plugin URL to Discourse link
- Updated tagGalleriesFromImages plugin URL to Discourse link
- Updated tagImagesWithPerfTags plugin URL to Discourse link
- Updated tagScenesWithPerfTags plugin URL to Discourse link
- Updated themeSwitch plugin URL to Discourse link
- Updated timestampTrade plugin URL to Discourse link
- Updated titleFromFilename plugin URL to Discourse link
- Updated untagRedundantTags plugin URL to Discourse link
- Updated videoChapterMarkers plugin URL to Discourse link
- Updated BlackHole theme URL to Discourse link
- Updated ColorPalette theme URL to Discourse link
- Updated Minimal theme URL to Discourse link
- Updated ModernDark theme URL to Discourse link
- Updated NeonDark theme URL to Discourse link
- Updated Night theme URL to Discourse link
- Updated Plex theme URL to Discourse link
- Updated PornHub theme URL to Discourse link
- Updated Pulsar theme URL to Discourse link
- Updated PulsarLight theme URL to Discourse link
- Updated RoundedYellow theme URL to Discourse link
- Updated FansDB Submission Helper userscript URL to Discourse link
- Updated StashDB Submission Helper userscript URL to Discourse link
2025-12-20 03:59:59 +02:00

546 lines
14 KiB
JavaScript

// ==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://discourse.stashapp.cc/t/stashdb-submission-helper/1417
// @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;
}
});