├── .gitignore ├── CHANGELOG.md ├── README.md ├── build.py ├── config.py ├── dist └── public │ ├── Stash Batch Query Edit.user.js │ ├── Stash Batch Result Toggle.user.js │ ├── Stash Batch Save.user.js │ ├── Stash Batch Search.user.js │ ├── Stash Markdown.user.js │ ├── Stash Markers Autoscroll.user.js │ ├── Stash New Performer Filter Button.user.js │ ├── Stash Open Media Player.user.js │ ├── Stash Performer Audit Task Button.user.js │ ├── Stash Performer Image Cropper.user.js │ ├── Stash Performer Markers Tab.user.js │ ├── Stash Performer Tagger Additions.user.js │ ├── Stash Performer URL Searchbox.user.js │ ├── Stash Scene Tagger Additions.user.js │ ├── Stash Scene Tagger Colorizer.user.js │ ├── Stash Scene Tagger Draft Submit.user.js │ ├── Stash Scene Tagger Linkify.user.js │ ├── Stash Set Stashbox Favorite Performers.user.js │ ├── Stash StashID Icon.user.js │ ├── Stash StashID Input.user.js │ ├── Stash Stats.user.js │ ├── Stash Studio Image And Parent On Create.user.js │ ├── Stash Tag Image Cropper.user.js │ └── Stash Userscripts Bundle.user.js ├── images ├── Stash Batch Query Edit │ ├── config.png │ └── scenes-tagger.png ├── Stash Batch Result Toggle │ ├── config.png │ └── scenes-tagger.png ├── Stash Batch Save │ └── scenes-tagger.png ├── Stash Batch Search │ └── scenes-tagger.png ├── Stash Markdown │ └── tag-description.png ├── Stash Markers Autoscroll │ └── scroll-settings.png ├── Stash New Performer Filter Button │ └── performers-page.png ├── Stash Open Media Player │ └── system-settings.png ├── Stash Performer Audit Task Button │ ├── performers-page.png │ ├── plugin-tasks.png │ └── system-settings.png ├── Stash Performer Image Cropper │ └── performer-image-cropper.png ├── Stash Performer Markers Tab │ └── performer-page.png ├── Stash Performer Tagger Additions │ └── performer-tagger.png ├── Stash Performer URL Searchbox │ └── performers-page.png ├── Stash Scene Tagger Additions │ ├── config.png │ └── scenes-tagger.png ├── Stash Scene Tagger Colorizer │ ├── config.png │ ├── scenes-tagger.png │ └── tag-colors.png ├── Stash Scene Tagger Draft Submit │ └── scenes-tagger.png ├── Stash Set Stashbox Favorite Performers │ ├── performers-page.png │ ├── plugin-tasks.png │ └── system-settings.png ├── Stash StashID Icon │ ├── performer-page.png │ ├── scene-page.png │ └── studio-page.png ├── Stash StashID Input │ ├── performer-page.png │ └── studio-page.png ├── Stash Stats │ └── stats-page.png ├── Stash Tag Image Cropper │ └── tag-image-cropper.png └── Userscript Functions Plugin │ ├── plugin-tasks.png │ └── system-settings.png ├── plugins └── userscript_functions │ ├── audit_performer_urls.py │ ├── config.ini │ ├── config_manager.py │ ├── favorite_performers_sync.py │ ├── graphql.py │ ├── log.py │ ├── performer_url_regexes.txt │ ├── studiodownloader.py │ ├── userscript_functions.py │ └── userscript_functions.yml └── src ├── StashUserscriptLibrary.js ├── body ├── Stash Batch Query Edit.user.js ├── Stash Batch Result Toggle.user.js ├── Stash Batch Save.user.js ├── Stash Batch Search.user.js ├── Stash Markdown.user.js ├── Stash Markers Autoscroll.user.js ├── Stash New Performer Filter Button.user.js ├── Stash Open Media Player.user.js ├── Stash Performer Audit Task Button.user.js ├── Stash Performer Image Cropper.user.js ├── Stash Performer Markers Tab.user.js ├── Stash Performer Tagger Additions.user.js ├── Stash Performer URL Searchbox.user.js ├── Stash Scene Tagger Additions.user.js ├── Stash Scene Tagger Colorizer.user.js ├── Stash Scene Tagger Draft Submit.user.js ├── Stash Set Stashbox Favorite Performers.user.js ├── Stash StashID Icon.user.js ├── Stash StashID Input.user.js ├── Stash Stats.user.js └── Stash Tag Image Cropper.user.js └── header ├── Stash Batch Query Edit.user.js ├── Stash Batch Result Toggle.user.js ├── Stash Batch Save.user.js ├── Stash Batch Search.user.js ├── Stash Markdown.user.js ├── Stash Markers Autoscroll.user.js ├── Stash New Performer Filter Button.user.js ├── Stash Open Media Player.user.js ├── Stash Performer Audit Task Button.user.js ├── Stash Performer Image Cropper.user.js ├── Stash Performer Markers Tab.user.js ├── Stash Performer Tagger Additions.user.js ├── Stash Performer URL Searchbox.user.js ├── Stash Scene Tagger Additions.user.js ├── Stash Scene Tagger Colorizer.user.js ├── Stash Scene Tagger Draft Submit.user.js ├── Stash Set Stashbox Favorite Performers.user.js ├── Stash StashID Icon.user.js ├── Stash StashID Input.user.js ├── Stash Stats.user.js └── Stash Tag Image Cropper.user.js /.gitignore: -------------------------------------------------------------------------------- 1 | dist/local 2 | __pycache__ 3 | stash-userscripts.code-workspace -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | import os 2 | import config 3 | from pathlib import Path 4 | 5 | def get_active_branch_name(): 6 | head_dir = Path(".") / ".git" / "HEAD" 7 | with head_dir.open("r") as f: content = f.read().splitlines() 8 | 9 | for line in content: 10 | if line[0:4] == "ref:": 11 | return line.partition("refs/heads/")[2] 12 | 13 | def build(): 14 | ROOTDIR = Path(__file__).parent.resolve() 15 | LIBFILE = "StashUserscriptLibrary.js" 16 | GIT_BRANCH = get_active_branch_name() 17 | GITHUB_ROOT_URL = config.GITHUB_ROOT_URL.replace('%%BRANCH%%', GIT_BRANCH) 18 | print('git branch', GIT_BRANCH) 19 | 20 | localbodyfiles = [] 21 | distbodyfiles = [] 22 | distlibfile = os.path.join(GITHUB_ROOT_URL, 'src', LIBFILE) 23 | for file in os.listdir('src/header'): 24 | headerpath = os.path.join('src/header', file) 25 | bodypath = os.path.join('src/body', file) 26 | distpublicpath = os.path.join('dist/public', file) 27 | header = open(headerpath, 'r').read() 28 | body = open(bodypath, 'r').read() 29 | 30 | localbodyfiles.append("file://" + os.path.join(ROOTDIR, 'src/body', file)) 31 | distbodyfiles.append(os.path.join(GITHUB_ROOT_URL, 'src/body', file)) 32 | 33 | header = header.replace("%NAMESPACE%", config.NAMESPACE) \ 34 | .replace("%LIBRARYPATH%", distlibfile) \ 35 | .replace("%MATCHURL%", f"{config.SERVER_URL}/*") \ 36 | .replace("// @require %FILEPATH%\n", "") 37 | distscript = header + "\n\n" + body 38 | with open(distpublicpath, 'w') as f: 39 | f.write(distscript) 40 | print(distpublicpath) 41 | 42 | localpath = 'dist/local/Stash Userscripts Development Bundle.user.js' 43 | locallibfile = "file://" + os.path.join(ROOTDIR, 'src', LIBFILE) 44 | with open(localpath, 'w') as f: 45 | f.write(f"""// ==UserScript== 46 | // @name Stash Userscripts Development Bundle 47 | // @namespace {config.NAMESPACE} 48 | // @description Stash Userscripts Development Bundle 49 | // @version {config.BUNDLE_VERSION} 50 | // @author 7dJx1qP 51 | // @match {config.SERVER_URL}/* 52 | // @resource IMPORTED_CSS https://raw.githubusercontent.com/fengyuanchen/cropperjs/main/dist/cropper.min.css 53 | // @grant unsafeWindow 54 | // @grant GM_setClipboard 55 | // @grant GM_getResourceText 56 | // @grant GM_addStyle 57 | // @grant GM.getValue 58 | // @grant GM.setValue 59 | // @require https://raw.githubusercontent.com/fengyuanchen/cropperjs/main/dist/cropper.min.js 60 | // @require https://raw.githubusercontent.com/nodeca/js-yaml/master/dist/js-yaml.js 61 | // @require https://cdnjs.cloudflare.com/ajax/libs/marked/4.2.2/marked.min.js 62 | // @require {locallibfile} 63 | // 64 | // ************************************************************************************************** 65 | // * YOU MAY REMOVE ANY OF THE @require LINES BELOW FOR SCRIPTS YOU DO NOT WANT * 66 | // ************************************************************************************************** 67 | //\n""") 68 | for localbodyfile in localbodyfiles: 69 | f.write(f"// @require {localbodyfile}\n") 70 | f.write("\n// ==/UserScript==\n") 71 | print(localpath) 72 | 73 | distpath = 'dist/public/Stash Userscripts Bundle.user.js' 74 | with open(distpath, 'w') as f: 75 | f.write(f"""// ==UserScript== 76 | // @name Stash Userscripts Bundle 77 | // @namespace {config.NAMESPACE} 78 | // @description Stash Userscripts Bundle 79 | // @version {config.BUNDLE_VERSION} 80 | // @author 7dJx1qP 81 | // @match {config.SERVER_URL}/* 82 | // @resource IMPORTED_CSS https://raw.githubusercontent.com/fengyuanchen/cropperjs/main/dist/cropper.min.css 83 | // @grant unsafeWindow 84 | // @grant GM_setClipboard 85 | // @grant GM_getResourceText 86 | // @grant GM_addStyle 87 | // @grant GM.getValue 88 | // @grant GM.setValue 89 | // @require https://raw.githubusercontent.com/fengyuanchen/cropperjs/main/dist/cropper.min.js 90 | // @require https://raw.githubusercontent.com/nodeca/js-yaml/master/dist/js-yaml.js 91 | // @require https://cdnjs.cloudflare.com/ajax/libs/marked/4.2.2/marked.min.js 92 | // @require {distlibfile} 93 | // 94 | // ************************************************************************************************** 95 | // * YOU MAY REMOVE ANY OF THE @require LINES BELOW FOR SCRIPTS YOU DO NOT WANT * 96 | // ************************************************************************************************** 97 | //\n""") 98 | for distbodyfile in distbodyfiles: 99 | f.write(f"// @require {distbodyfile}\n") 100 | f.write("\n// ==/UserScript==\n") 101 | print(distpath) 102 | 103 | build() -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | GITHUB_ROOT_URL = r"https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/%%BRANCH%%/" 2 | BUNDLE_VERSION = "0.24.2" 3 | SERVER_URL = "http://localhost:9999" 4 | NAMESPACE = "https://github.com/7dJx1qP/stash-userscripts" -------------------------------------------------------------------------------- /dist/public/Stash Batch Save.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Batch Save 3 | // @namespace https://github.com/7dJx1qP/stash-userscripts 4 | // @description Adds a batch save button to scenes tagger 5 | // @version 0.5.3 6 | // @author 7dJx1qP 7 | // @match http://localhost:9999/* 8 | // @grant unsafeWindow 9 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src\StashUserscriptLibrary.js 10 | // ==/UserScript== 11 | 12 | (function () { 13 | 'use strict'; 14 | 15 | const { 16 | stash, 17 | Stash, 18 | waitForElementId, 19 | waitForElementClass, 20 | waitForElementByXpath, 21 | getElementByXpath, 22 | getElementsByXpath, 23 | getClosestAncestor, 24 | sortElementChildren, 25 | createElementFromHTML, 26 | } = unsafeWindow.stash; 27 | 28 | document.body.appendChild(document.createElement('style')).textContent = ` 29 | .search-item > div.row:first-child > div.col-md-6.my-1 > div:first-child { display: flex; flex-direction: column; } 30 | .tagger-remove { order: 10; } 31 | `; 32 | 33 | let running = false; 34 | const buttons = []; 35 | let maxCount = 0; 36 | let sceneId = null; 37 | 38 | function run() { 39 | if (!running) return; 40 | const button = buttons.pop(); 41 | stash.setProgress((maxCount - buttons.length) / maxCount * 100); 42 | if (button) { 43 | const searchItem = getClosestAncestor(button, '.search-item'); 44 | if (searchItem.classList.contains('d-none')) { 45 | setTimeout(() => { 46 | run(); 47 | }, 0); 48 | return; 49 | } 50 | 51 | const { id } = stash.parseSearchItem(searchItem); 52 | sceneId = id; 53 | if (!button.disabled) { 54 | button.click(); 55 | } 56 | else { 57 | buttons.push(button); 58 | } 59 | } 60 | else { 61 | stop(); 62 | } 63 | } 64 | 65 | function processSceneUpdate(evt) { 66 | if (running && evt.detail.data?.sceneUpdate?.id === sceneId) { 67 | setTimeout(() => { 68 | run(); 69 | }, 0); 70 | } 71 | } 72 | 73 | const btnId = 'batch-save'; 74 | const startLabel = 'Save All'; 75 | const stopLabel = 'Stop Save'; 76 | const btn = document.createElement("button"); 77 | btn.setAttribute("id", btnId); 78 | btn.classList.add('btn', 'btn-primary', 'ml-3'); 79 | btn.innerHTML = startLabel; 80 | btn.onclick = () => { 81 | if (running) { 82 | stop(); 83 | } 84 | else { 85 | start(); 86 | } 87 | }; 88 | 89 | function start() { 90 | if (!confirm("Are you sure you want to batch save?")) return; 91 | btn.innerHTML = stopLabel; 92 | btn.classList.remove('btn-primary'); 93 | btn.classList.add('btn-danger'); 94 | running = true; 95 | stash.setProgress(0); 96 | buttons.length = 0; 97 | for (const button of document.querySelectorAll('.btn.btn-primary')) { 98 | if (button.innerText === 'Save') { 99 | buttons.push(button); 100 | } 101 | } 102 | maxCount = buttons.length; 103 | stash.addEventListener('stash:response', processSceneUpdate); 104 | run(); 105 | } 106 | 107 | function stop() { 108 | btn.innerHTML = startLabel; 109 | btn.classList.remove('btn-danger'); 110 | btn.classList.add('btn-primary'); 111 | running = false; 112 | stash.setProgress(0); 113 | sceneId = null; 114 | stash.removeEventListener('stash:response', processSceneUpdate); 115 | } 116 | 117 | stash.addEventListener('tagger:mutations:header', evt => { 118 | const el = getElementByXpath("//button[text()='Scrape All']"); 119 | if (el && !document.getElementById(btnId)) { 120 | const container = el.parentElement; 121 | container.appendChild(btn); 122 | sortElementChildren(container); 123 | el.classList.add('ml-3'); 124 | } 125 | }); 126 | 127 | function checkSaveButtonDisplay() { 128 | const taggerContainer = document.querySelector('.tagger-container'); 129 | const saveButton = getElementByXpath("//button[text()='Save']", taggerContainer); 130 | btn.style.display = saveButton ? 'inline-block' : 'none'; 131 | } 132 | 133 | stash.addEventListener('tagger:mutations:searchitems', checkSaveButtonDisplay); 134 | 135 | async function initRemoveButtons() { 136 | const nodes = getElementsByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']"); 137 | const buttons = []; 138 | let node = null; 139 | while (node = nodes.iterateNext()) { 140 | buttons.push(node); 141 | } 142 | for (const button of buttons) { 143 | const searchItem = getClosestAncestor(button, '.search-item'); 144 | 145 | const removeButtonExists = searchItem.querySelector('.tagger-remove'); 146 | if (removeButtonExists) { 147 | continue; 148 | } 149 | 150 | const removeEl = createElementFromHTML('
'); 151 | const removeButton = removeEl.querySelector('button'); 152 | button.parentElement.parentElement.appendChild(removeEl); 153 | removeButton.addEventListener('click', async () => { 154 | searchItem.classList.add('d-none'); 155 | }); 156 | } 157 | } 158 | 159 | stash.addEventListener('page:studio:scenes', function () { 160 | waitForElementByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']", initRemoveButtons); 161 | }); 162 | 163 | stash.addEventListener('page:performer:scenes', function () { 164 | waitForElementByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']", initRemoveButtons); 165 | }); 166 | 167 | stash.addEventListener('page:scenes', function () { 168 | waitForElementByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']", initRemoveButtons); 169 | }); 170 | })(); -------------------------------------------------------------------------------- /dist/public/Stash Batch Search.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Batch Search 3 | // @namespace https://github.com/7dJx1qP/stash-userscripts 4 | // @description Adds a batch search button to scenes and performers tagger 5 | // @version 0.4.2 6 | // @author 7dJx1qP 7 | // @match http://localhost:9999/* 8 | // @grant unsafeWindow 9 | // @grant GM.getValue 10 | // @grant GM.setValue 11 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src\StashUserscriptLibrary.js 12 | // ==/UserScript== 13 | 14 | (function() { 15 | 'use strict'; 16 | 17 | const DEFAULT_DELAY = 200; 18 | let delay = DEFAULT_DELAY; 19 | 20 | const { 21 | stash, 22 | Stash, 23 | waitForElementId, 24 | waitForElementClass, 25 | waitForElementByXpath, 26 | getElementByXpath, 27 | sortElementChildren, 28 | createElementFromHTML, 29 | } = unsafeWindow.stash; 30 | 31 | let running = false; 32 | const buttons = []; 33 | let maxCount = 0; 34 | 35 | function run() { 36 | if (!running) return; 37 | const button = buttons.pop(); 38 | stash.setProgress((maxCount - buttons.length) / maxCount * 100); 39 | if (button) { 40 | if (!button.disabled) { 41 | button.click(); 42 | } 43 | else { 44 | buttons.push(button); 45 | } 46 | setTimeout(run, delay); 47 | } 48 | else { 49 | stop(); 50 | } 51 | } 52 | 53 | const btnId = 'batch-search'; 54 | const startLabel = 'Search All'; 55 | const stopLabel = 'Stop Search'; 56 | const btn = document.createElement("button"); 57 | btn.setAttribute("id", btnId); 58 | btn.classList.add('btn', 'btn-primary', 'ml-3'); 59 | btn.innerHTML = startLabel; 60 | btn.onclick = () => { 61 | if (running) { 62 | stop(); 63 | } 64 | else { 65 | start(); 66 | } 67 | }; 68 | 69 | function start() { 70 | btn.innerHTML = stopLabel; 71 | btn.classList.remove('btn-primary'); 72 | btn.classList.add('btn-danger'); 73 | running = true; 74 | stash.setProgress(0); 75 | buttons.length = 0; 76 | for (const button of document.querySelectorAll('.btn.btn-primary')) { 77 | if (button.innerText === 'Search') { 78 | buttons.push(button); 79 | } 80 | } 81 | maxCount = buttons.length; 82 | run(); 83 | } 84 | 85 | function stop() { 86 | btn.innerHTML = startLabel; 87 | btn.classList.remove('btn-danger'); 88 | btn.classList.add('btn-primary'); 89 | running = false; 90 | stash.setProgress(0); 91 | } 92 | 93 | stash.addEventListener('page:performers', function () { 94 | waitForElementByXpath("//button[text()='Batch Update Performers']", function (xpath, el) { 95 | if (!document.getElementById(btnId)) { 96 | const container = el.parentElement; 97 | 98 | container.appendChild(btn); 99 | } 100 | }); 101 | }); 102 | 103 | stash.addEventListener('tagger:mutations:header', evt => { 104 | const el = getElementByXpath("//button[text()='Scrape All']"); 105 | if (el && !document.getElementById(btnId)) { 106 | const container = el.parentElement; 107 | container.appendChild(btn); 108 | sortElementChildren(container); 109 | el.classList.add('ml-3'); 110 | } 111 | }); 112 | 113 | const batchSearchConfigId = 'batch-search-config'; 114 | 115 | stash.addEventListener('tagger:configuration', evt => { 116 | const el = evt.detail; 117 | if (!document.getElementById(batchSearchConfigId)) { 118 | const configContainer = el.parentElement; 119 | const batchSearchConfig = createElementFromHTML(` 120 |
121 |
Batch Search
122 |
123 |
124 |
125 | 126 |
127 | 128 |
129 |
130 | Wait time in milliseconds between scene searches. 131 |
132 |
133 |
134 | `); 135 | configContainer.appendChild(batchSearchConfig); 136 | loadSettings(); 137 | } 138 | }); 139 | 140 | async function loadSettings() { 141 | for (const input of document.querySelectorAll(`#${batchSearchConfigId} input[type="text"]`)) { 142 | input.value = parseInt(await GM.getValue(input.id, input.dataset.default)); 143 | delay = input.value; 144 | input.addEventListener('change', async () => { 145 | let value = parseInt(input.value.trim()) 146 | if (isNaN(value)) { 147 | value = parseInt(input.dataset.default); 148 | } 149 | input.value = value; 150 | delay = value; 151 | await GM.setValue(input.id, value); 152 | }); 153 | } 154 | } 155 | 156 | })(); -------------------------------------------------------------------------------- /dist/public/Stash Markdown.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Markdown 3 | // @namespace https://github.com/7dJx1qP/stash-userscripts 4 | // @description Adds markdown parsing to tag description fields 5 | // @version 0.2.0 6 | // @author 7dJx1qP 7 | // @match http://localhost:9999/* 8 | // @grant unsafeWindow 9 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src\StashUserscriptLibrary.js 10 | // @require https://cdnjs.cloudflare.com/ajax/libs/marked/4.2.2/marked.min.js 11 | // ==/UserScript== 12 | 13 | /* global marked */ 14 | 15 | (function () { 16 | 'use strict'; 17 | 18 | const { 19 | stash, 20 | Stash, 21 | waitForElementId, 22 | waitForElementClass, 23 | waitForElementByXpath, 24 | getElementByXpath, 25 | insertAfter, 26 | reloadImg, 27 | } = unsafeWindow.stash; 28 | 29 | function processMarkdown(el) { 30 | el.innerHTML = marked.parse(el.innerHTML); 31 | } 32 | 33 | stash.addEventListener('page:tag:any', function () { 34 | waitForElementByXpath("//span[contains(@class, 'detail-item-value') and contains(@class, 'description')]", function (xpath, el) { 35 | el.style.display = 'block'; 36 | el.style.whiteSpace = 'initial'; 37 | processMarkdown(el); 38 | }); 39 | }); 40 | 41 | stash.addEventListener('page:tags', function () { 42 | waitForElementByXpath("//div[contains(@class, 'tag-description')]", function (xpath, el) { 43 | for (const node of document.querySelectorAll('.tag-description')) { 44 | processMarkdown(node); 45 | } 46 | }); 47 | }); 48 | })(); -------------------------------------------------------------------------------- /dist/public/Stash New Performer Filter Button.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash New Performer Filter Button 3 | // @namespace https://github.com/7dJx1qP/stash-userscripts 4 | // @description Adds a button to the performers page to switch to a new performers filter 5 | // @version 0.3.0 6 | // @author 7dJx1qP 7 | // @match http://localhost:9999/* 8 | // @grant unsafeWindow 9 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src\StashUserscriptLibrary.js 10 | // ==/UserScript== 11 | 12 | (function () { 13 | 'use strict'; 14 | 15 | const { 16 | stash, 17 | Stash, 18 | waitForElementId, 19 | waitForElementClass, 20 | waitForElementByXpath, 21 | getElementByXpath, 22 | } = unsafeWindow.stash; 23 | 24 | stash.addEventListener('page:performers', function () { 25 | waitForElementClass("btn-toolbar", function () { 26 | if (!document.getElementById('new-performer-filter')) { 27 | const toolbar = document.querySelector(".btn-toolbar"); 28 | 29 | const newGroup = document.createElement('div'); 30 | newGroup.classList.add('mx-2', 'mb-2', 'd-flex'); 31 | toolbar.appendChild(newGroup); 32 | 33 | const newButton = document.createElement("a"); 34 | newButton.setAttribute("id", "new-performer-filter"); 35 | newButton.classList.add('btn', 'btn-secondary'); 36 | newButton.innerHTML = 'New Performers'; 37 | newButton.href = `${stash.serverUrl}/performers?disp=3&sortby=created_at&sortdir=desc`; 38 | newGroup.appendChild(newButton); 39 | } 40 | }); 41 | }); 42 | })(); -------------------------------------------------------------------------------- /dist/public/Stash Open Media Player.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Open Media Player 3 | // @namespace https://github.com/7dJx1qP/stash-userscripts 4 | // @description Open scene filepath links in an external media player. Requires userscript_functions stash plugin 5 | // @version 0.2.1 6 | // @author 7dJx1qP 7 | // @match http://localhost:9999/* 8 | // @grant unsafeWindow 9 | // @grant GM.getValue 10 | // @grant GM.setValue 11 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src\StashUserscriptLibrary.js 12 | // ==/UserScript== 13 | 14 | (function () { 15 | 'use strict'; 16 | 17 | const { 18 | stash, 19 | Stash, 20 | waitForElementId, 21 | waitForElementClass, 22 | waitForElementByXpath, 23 | getElementByXpath, 24 | } = unsafeWindow.stash; 25 | 26 | const MIN_REQUIRED_PLUGIN_VERSION = '0.4.0'; 27 | 28 | function openMediaPlayerTask(path) { 29 | // fixes decodeURI breaking on %'s because they are not encoded 30 | const encodedPctPath = path.replace(/%([^\d].)/, "%25$1"); 31 | // decode encoded path but then encode % and # otherwise VLC breaks 32 | const encodedPath = decodeURI(encodedPctPath).replaceAll('%', '%25').replaceAll('#', '%23'); 33 | 34 | stash.runPluginTask("userscript_functions", "Open in Media Player", {"key":"path", "value":{"str": encodedPath}}); 35 | } 36 | 37 | // scene filepath open with Media Player 38 | stash.addEventListener('page:scene', function () { 39 | waitForElementClass('scene-file-info', function () { 40 | const a = getElementByXpath("//dt[text()='Path']/following-sibling::dd/a"); 41 | if (a) { 42 | a.addEventListener('click', function () { 43 | openMediaPlayerTask(a.href); 44 | }); 45 | } 46 | }); 47 | }); 48 | 49 | const settingsId = 'userscript-settings-mediaplayer'; 50 | 51 | stash.addSystemSetting(async (elementId, el) => { 52 | const inputId = 'userscript-settings-mediaplayer-input'; 53 | if (document.getElementById(inputId)) return; 54 | const settingsHeader = 'Media Player Path'; 55 | const settingsSubheader = 'Path to external media player.'; 56 | const placeholder = 'Media Player Path…'; 57 | const textbox = await stash.createSystemSettingTextbox(el, settingsId, inputId, settingsHeader, settingsSubheader, placeholder, false); 58 | textbox.addEventListener('change', () => { 59 | const value = textbox.value; 60 | if (value) { 61 | stash.updateConfigValueTask('MEDIAPLAYER', 'path', value); 62 | alert(`Media player path set to ${value}`); 63 | } 64 | else { 65 | stash.getConfigValueTask('MEDIAPLAYER', 'path').then(value => { 66 | textbox.value = value; 67 | }); 68 | } 69 | }); 70 | textbox.disabled = true; 71 | stash.getConfigValueTask('MEDIAPLAYER', 'path').then(value => { 72 | textbox.value = value; 73 | textbox.disabled = false; 74 | }); 75 | }); 76 | 77 | stash.addEventListener('stash:pluginVersion', async function () { 78 | waitForElementId(settingsId, async (elementId, el) => { 79 | el.style.display = stash.pluginVersion != null ? 'flex' : 'none'; 80 | }); 81 | if (stash.comparePluginVersion(MIN_REQUIRED_PLUGIN_VERSION) < 0) { 82 | const alertedPluginVersion = await GM.getValue('alerted_plugin_version'); 83 | if (alertedPluginVersion !== stash.pluginVersion) { 84 | await GM.setValue('alerted_plugin_version', stash.pluginVersion); 85 | alert(`User functions plugin version is ${stash.pluginVersion}. Stash Open Media Player userscript requires version ${MIN_REQUIRED_PLUGIN_VERSION} or higher.`); 86 | } 87 | } 88 | }); 89 | })(); -------------------------------------------------------------------------------- /dist/public/Stash Performer Audit Task Button.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Performer Audit Task Button 3 | // @namespace https://github.com/7dJx1qP/stash-userscripts 4 | // @description Adds a button to the performers page to run the audit plugin task 5 | // @version 0.3.0 6 | // @author 7dJx1qP 7 | // @match http://localhost:9999/* 8 | // @grant unsafeWindow 9 | // @grant GM.getValue 10 | // @grant GM.setValue 11 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src\StashUserscriptLibrary.js 12 | // ==/UserScript== 13 | 14 | (function () { 15 | 'use strict'; 16 | 17 | const { 18 | stash, 19 | Stash, 20 | waitForElementId, 21 | waitForElementClass, 22 | waitForElementByXpath, 23 | getElementByXpath, 24 | } = unsafeWindow.stash; 25 | 26 | stash.visiblePluginTasks.push('Audit performer urls'); 27 | 28 | const settingsId = 'userscript-settings-audit-task'; 29 | const inputId = 'userscript-settings-audit-task-button-visible'; 30 | 31 | stash.addEventListener('page:performers', function () { 32 | waitForElementClass("btn-toolbar", async () => { 33 | if (!document.getElementById('audit-task')) { 34 | const toolbar = document.querySelector(".btn-toolbar"); 35 | 36 | const newGroup = document.createElement('div'); 37 | newGroup.classList.add('mx-2', 'mb-2', await GM.getValue(inputId, false) ? 'd-flex' : 'd-none'); 38 | toolbar.appendChild(newGroup); 39 | 40 | const auditButton = document.createElement("button"); 41 | auditButton.setAttribute("id", "audit-task"); 42 | auditButton.classList.add('btn', 'btn-secondary'); 43 | auditButton.innerHTML = 'Audit URLs'; 44 | auditButton.onclick = () => { 45 | stash.runPluginTask("userscript_functions", "Audit performer urls"); 46 | }; 47 | newGroup.appendChild(auditButton); 48 | } 49 | }); 50 | }); 51 | 52 | stash.addSystemSetting(async (elementId, el) => { 53 | if (document.getElementById(inputId)) return; 54 | const settingsHeader = 'Show Audit Performer URLs Button'; 55 | const settingsSubheader = 'Display audit performer urls button on performers page.'; 56 | const checkbox = await stash.createSystemSettingCheckbox(el, settingsId, inputId, settingsHeader, settingsSubheader); 57 | checkbox.checked = await GM.getValue(inputId, false); 58 | checkbox.addEventListener('change', async () => { 59 | const value = checkbox.checked; 60 | await GM.setValue(inputId, value); 61 | }); 62 | }); 63 | 64 | stash.addEventListener('stash:pluginVersion', async function () { 65 | waitForElementId(settingsId, async (elementId, el) => { 66 | el.style.display = stash.pluginVersion != null ? 'flex' : 'none'; 67 | }); 68 | }); 69 | })(); -------------------------------------------------------------------------------- /dist/public/Stash Performer Image Cropper.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Performer Image Cropper 3 | // @namespace https://github.com/7dJx1qP/stash-userscripts 4 | // @description Adds an image cropper to performer page 5 | // @version 0.3.0 6 | // @author 7dJx1qP 7 | // @match http://localhost:9999/* 8 | // @resource IMPORTED_CSS https://raw.githubusercontent.com/fengyuanchen/cropperjs/main/dist/cropper.min.css 9 | // @grant unsafeWindow 10 | // @grant GM_getResourceText 11 | // @grant GM_addStyle 12 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src\StashUserscriptLibrary.js 13 | // @require https://raw.githubusercontent.com/fengyuanchen/cropperjs/main/dist/cropper.min.js 14 | // ==/UserScript== 15 | 16 | /* global Cropper */ 17 | 18 | (function () { 19 | 'use strict'; 20 | 21 | const { 22 | stash, 23 | Stash, 24 | waitForElementId, 25 | waitForElementClass, 26 | waitForElementByXpath, 27 | getElementByXpath, 28 | reloadImg, 29 | } = unsafeWindow.stash; 30 | 31 | const css = GM_getResourceText("IMPORTED_CSS"); 32 | GM_addStyle(css); 33 | GM_addStyle(".cropper-view-box img { transition: none; }"); 34 | GM_addStyle(".detail-header-image { flex-direction: column; }"); 35 | 36 | let cropping = false; 37 | let cropper = null; 38 | 39 | stash.addEventListener('page:performer', function () { 40 | waitForElementClass('detail-container', function () { 41 | const cropBtnContainerId = "crop-btn-container"; 42 | if (!document.getElementById(cropBtnContainerId)) { 43 | const performerId = window.location.pathname.replace('/performers/', '').split('/')[0]; 44 | const image = getElementByXpath("//div[contains(@class, 'detail-header-image')]//img[@class='performer']"); 45 | image.parentElement.addEventListener('click', (evt) => { 46 | if (cropping) { 47 | evt.preventDefault(); 48 | evt.stopPropagation(); 49 | } 50 | }) 51 | const cropBtnContainer = document.createElement('div'); 52 | cropBtnContainer.setAttribute("id", cropBtnContainerId); 53 | image.parentElement.parentElement.appendChild(cropBtnContainer); 54 | 55 | const cropInfo = document.createElement('p'); 56 | 57 | const imageUrl = getElementByXpath("//div[contains(@class, 'detail-header-image')]//img[@class='performer']/@src").nodeValue; 58 | const cropStart = document.createElement('button'); 59 | cropStart.setAttribute("id", "crop-start"); 60 | cropStart.classList.add('btn', 'btn-primary'); 61 | cropStart.innerText = 'Crop Image'; 62 | cropStart.addEventListener('click', evt => { 63 | cropping = true; 64 | cropStart.style.display = 'none'; 65 | cropCancel.style.display = 'inline-block'; 66 | 67 | cropper = new Cropper(image, { 68 | viewMode: 1, 69 | initialAspectRatio: 2 /3, 70 | movable: false, 71 | rotatable: false, 72 | scalable: false, 73 | zoomable: false, 74 | zoomOnTouch: false, 75 | zoomOnWheel: false, 76 | ready() { 77 | cropAccept.style.display = 'inline-block'; 78 | }, 79 | crop(e) { 80 | cropInfo.innerText = `X: ${Math.round(e.detail.x)}, Y: ${Math.round(e.detail.y)}, Width: ${Math.round(e.detail.width)}px, Height: ${Math.round(e.detail.height)}px`; 81 | } 82 | }); 83 | }); 84 | cropBtnContainer.appendChild(cropStart); 85 | 86 | const cropAccept = document.createElement('button'); 87 | cropAccept.setAttribute("id", "crop-accept"); 88 | cropAccept.classList.add('btn', 'btn-success', 'mr-2'); 89 | cropAccept.innerText = 'OK'; 90 | cropAccept.addEventListener('click', async evt => { 91 | cropping = false; 92 | cropStart.style.display = 'inline-block'; 93 | cropAccept.style.display = 'none'; 94 | cropCancel.style.display = 'none'; 95 | cropInfo.innerText = ''; 96 | 97 | const reqData = { 98 | "operationName": "PerformerUpdate", 99 | "variables": { 100 | "input": { 101 | "image": cropper.getCroppedCanvas().toDataURL(), 102 | "id": performerId 103 | } 104 | }, 105 | "query": `mutation PerformerUpdate($input: PerformerUpdateInput!) { 106 | performerUpdate(input: $input) { 107 | id 108 | } 109 | }` 110 | } 111 | await stash.callGQL(reqData); 112 | reloadImg(image.src); 113 | cropper.destroy(); 114 | }); 115 | cropBtnContainer.appendChild(cropAccept); 116 | 117 | const cropCancel = document.createElement('button'); 118 | cropCancel.setAttribute("id", "crop-accept"); 119 | cropCancel.classList.add('btn', 'btn-danger'); 120 | cropCancel.innerText = 'Cancel'; 121 | cropCancel.addEventListener('click', evt => { 122 | cropping = false; 123 | cropStart.style.display = 'inline-block'; 124 | cropAccept.style.display = 'none'; 125 | cropCancel.style.display = 'none'; 126 | cropInfo.innerText = ''; 127 | 128 | cropper.destroy(); 129 | }); 130 | cropBtnContainer.appendChild(cropCancel); 131 | cropAccept.style.display = 'none'; 132 | cropCancel.style.display = 'none'; 133 | 134 | cropBtnContainer.appendChild(cropInfo); 135 | } 136 | }); 137 | }); 138 | })(); -------------------------------------------------------------------------------- /dist/public/Stash Performer Markers Tab.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Performer Markers Tab 3 | // @namespace https://github.com/7dJx1qP/stash-userscripts 4 | // @description Adds a Markers link to performer pages 5 | // @version 0.1.0 6 | // @author 7dJx1qP 7 | // @match http://localhost:9999/* 8 | // @grant unsafeWindow 9 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src\StashUserscriptLibrary.js 10 | // ==/UserScript== 11 | 12 | (function () { 13 | 'use strict'; 14 | 15 | const { 16 | stash, 17 | Stash, 18 | waitForElementId, 19 | waitForElementClass, 20 | waitForElementByXpath, 21 | getElementByXpath, 22 | createElementFromHTML, 23 | } = unsafeWindow.stash; 24 | 25 | async function getPerformerMarkersCount(performerId) { 26 | const reqData = { 27 | "operationName": "FindSceneMarkers", 28 | "variables": { 29 | "scene_marker_filter": { 30 | "performers": { 31 | "value": [ 32 | performerId 33 | ], 34 | "modifier": "INCLUDES_ALL" 35 | } 36 | } 37 | }, 38 | "query": `query FindSceneMarkers($filter: FindFilterType, $scene_marker_filter: SceneMarkerFilterType) { 39 | findSceneMarkers(filter: $filter, scene_marker_filter: $scene_marker_filter) { 40 | count 41 | } 42 | }` 43 | } 44 | return stash.callGQL(reqData); 45 | } 46 | 47 | const markersTabId = 'performer-details-tab-markers'; 48 | 49 | stash.addEventListener('page:performer:details', function () { 50 | waitForElementClass("nav-tabs", async function (className, el) { 51 | const navTabs = el.item(0); 52 | if (!document.getElementById(markersTabId)) { 53 | const performerId = window.location.pathname.replace('/performers/', ''); 54 | const markersCount = (await getPerformerMarkersCount(performerId)).data.findSceneMarkers.count; 55 | const markerTab = createElementFromHTML(`Markers${markersCount}`) 56 | navTabs.appendChild(markerTab); 57 | const performerName = document.querySelector('.performer-head h2').innerText; 58 | const markersUrl = `${window.location.origin}/scenes/markers?c=${JSON.stringify({"type":"performers","value":[{"id":performerId,"label":performerName}],"modifier":"INCLUDES_ALL"})}` 59 | markerTab.href = markersUrl; 60 | } 61 | }); 62 | }); 63 | })(); -------------------------------------------------------------------------------- /dist/public/Stash Performer Tagger Additions.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Performer Tagger Additions 3 | // @namespace https://github.com/7dJx1qP/stash-userscripts 4 | // @description Adds performer birthdate and url to tagger view. Makes clicking performer name open stash profile in new tab instead of current tab. 5 | // @version 0.2.0 6 | // @author 7dJx1qP 7 | // @match http://localhost:9999/* 8 | // @grant unsafeWindow 9 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src\StashUserscriptLibrary.js 10 | // ==/UserScript== 11 | 12 | (function () { 13 | 'use strict'; 14 | 15 | const { 16 | stash, 17 | Stash, 18 | waitForElementId, 19 | waitForElementClass, 20 | waitForElementByXpath, 21 | getElementByXpath, 22 | insertAfter, 23 | createElementFromHTML, 24 | } = unsafeWindow.stash; 25 | 26 | stash.addEventListener('page:performers', function () { 27 | waitForElementClass("tagger-container", function () { 28 | const performerElements = document.querySelectorAll('.PerformerTagger-details'); 29 | for (const performerElement of performerElements) { 30 | let birthdateElement = performerElement.querySelector('.PerformerTagger-birthdate'); 31 | if (!birthdateElement) { 32 | birthdateElement = document.createElement('h5'); 33 | birthdateElement.classList.add('PerformerTagger-birthdate'); 34 | const headerElement = performerElement.querySelector('.PerformerTagger-header'); 35 | headerElement.classList.add('d-inline-block', 'mr-2'); 36 | headerElement.addEventListener("click", (event) => { 37 | event.preventDefault(); 38 | window.open(headerElement.href, '_blank'); 39 | }); 40 | const performerId = headerElement.href.split('/').pop(); 41 | const performer = stash.performers[performerId]; 42 | birthdateElement.innerText = performer.birthdate; 43 | if (performer.url) { 44 | const urlElement = createElementFromHTML(``); 51 | urlElement.classList.add('d-inline-block'); 52 | insertAfter(urlElement, headerElement); 53 | insertAfter(birthdateElement, urlElement); 54 | } 55 | else { 56 | insertAfter(birthdateElement, headerElement); 57 | } 58 | } 59 | } 60 | }); 61 | }); 62 | })(); -------------------------------------------------------------------------------- /dist/public/Stash Performer URL Searchbox.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Performer URL Searchbox 3 | // @namespace https://github.com/7dJx1qP/stash-userscripts 4 | // @description Adds a search by performer url textbox to the performers page 5 | // @version 0.2.0 6 | // @author 7dJx1qP 7 | // @match http://localhost:9999/* 8 | // @grant unsafeWindow 9 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src\StashUserscriptLibrary.js 10 | // ==/UserScript== 11 | 12 | (function () { 13 | 'use strict'; 14 | 15 | const { 16 | stash, 17 | Stash, 18 | waitForElementId, 19 | waitForElementClass, 20 | waitForElementByXpath, 21 | getElementByXpath, 22 | } = unsafeWindow.stash; 23 | 24 | stash.addEventListener('page:performers', function () { 25 | waitForElementClass("btn-toolbar", function () { 26 | if (!document.getElementById('performer-url-search-input')) { 27 | const toolbar = document.querySelector(".btn-toolbar"); 28 | 29 | const newGroup = document.createElement('div'); 30 | newGroup.classList.add('mx-2', 'mb-2', 'd-flex'); 31 | toolbar.appendChild(newGroup); 32 | 33 | const perfUrlGroup = document.createElement('div'); 34 | perfUrlGroup.classList.add('flex-grow-1', 'query-text-field-group'); 35 | newGroup.appendChild(perfUrlGroup); 36 | 37 | const perfUrlTextbox = document.createElement('input'); 38 | perfUrlTextbox.setAttribute('id', 'performer-url-search-input'); 39 | perfUrlTextbox.classList.add('query-text-field', 'bg-secondary', 'text-white', 'border-secondary', 'form-control'); 40 | perfUrlTextbox.setAttribute('placeholder', 'URL…'); 41 | perfUrlTextbox.addEventListener('change', () => { 42 | const url = `${window.location.origin}/performers?c={"type":"url","value":"${perfUrlTextbox.value}","modifier":"EQUALS"}` 43 | window.location = url; 44 | }); 45 | perfUrlGroup.appendChild(perfUrlTextbox); 46 | } 47 | }); 48 | }); 49 | })(); -------------------------------------------------------------------------------- /dist/public/Stash Scene Tagger Additions.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Scene Tagger Additions 3 | // @namespace https://github.com/7dJx1qP/stash-userscripts 4 | // @description Adds scene duration and filepath to tagger view. 5 | // @version 0.3.1 6 | // @author 7dJx1qP 7 | // @match http://localhost:9999/* 8 | // @grant unsafeWindow 9 | // @grant GM.getValue 10 | // @grant GM.setValue 11 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src\StashUserscriptLibrary.js 12 | // ==/UserScript== 13 | 14 | (function () { 15 | 'use strict'; 16 | 17 | const { 18 | stash, 19 | Stash, 20 | waitForElementId, 21 | waitForElementClass, 22 | waitForElementByXpath, 23 | getElementByXpath, 24 | insertAfter, 25 | createElementFromHTML, 26 | } = unsafeWindow.stash; 27 | 28 | function formatDuration(s) { 29 | const sec_num = parseInt(s, 10); 30 | let hours = Math.floor(sec_num / 3600); 31 | let minutes = Math.floor((sec_num - (hours * 3600)) / 60); 32 | let seconds = sec_num - (hours * 3600) - (minutes * 60); 33 | 34 | if (hours < 10) { hours = "0" + hours; } 35 | if (minutes < 10) { minutes = "0" + minutes; } 36 | if (seconds < 10) { seconds = "0" + seconds; } 37 | return hours + ':' + minutes + ':' + seconds; 38 | } 39 | 40 | function openMediaPlayerTask(path) { 41 | stash.runPluginTask("userscript_functions", "Open in Media Player", {"key":"path", "value":{"str": path}}); 42 | } 43 | 44 | stash.addEventListener('tagger:searchitem', async function (evt) { 45 | const searchItem = evt.detail; 46 | const { 47 | urlNode, 48 | url, 49 | id, 50 | data, 51 | nameNode, 52 | name, 53 | queryInput, 54 | performerNodes 55 | } = stash.parseSearchItem(searchItem); 56 | 57 | const includeDuration = await GM.getValue('additions-duration', true); 58 | const includePath = await GM.getValue('additions-path', true); 59 | const includeUrl = await GM.getValue('additions-url', true); 60 | 61 | const originalSceneDetails = searchItem.querySelector('.original-scene-details'); 62 | 63 | if (!originalSceneDetails.firstChild.firstChild.querySelector('.scene-url') && data.url) { 64 | const sceneUrlNode = createElementFromHTML(`${data.url}`); 65 | sceneUrlNode.style.display = includeUrl ? 'block' : 'none'; 66 | sceneUrlNode.style.fontWeight = 500; 67 | sceneUrlNode.style.color = '#fff'; 68 | originalSceneDetails.firstChild.firstChild.appendChild(sceneUrlNode); 69 | } 70 | 71 | const paths = stash.compareVersion("0.17.0") >= 0 ? data.files.map(file => file.path) : [data.path]; 72 | if (!originalSceneDetails.firstChild.firstChild.querySelector('.scene-path')) { 73 | for (const path of paths) { 74 | if (path) { 75 | const pathNode = createElementFromHTML(`${path}`); 76 | pathNode.style.display = includePath ? 'block' : 'none'; 77 | pathNode.style.fontWeight = 500; 78 | pathNode.style.color = '#fff'; 79 | pathNode.addEventListener('click', evt => { 80 | evt.preventDefault(); 81 | if (stash.pluginVersion) { 82 | openMediaPlayerTask(path); 83 | } 84 | }); 85 | originalSceneDetails.firstChild.firstChild.appendChild(pathNode); 86 | } 87 | } 88 | } 89 | 90 | const duration = stash.compareVersion("0.17.0") >= 0 ? data.files[0].duration : data.file.duration; 91 | if (!originalSceneDetails.firstChild.firstChild.querySelector('.scene-duration') && duration) { 92 | const durationNode = createElementFromHTML(`Duration: ${formatDuration(duration)}`); 93 | durationNode.style.display = includeDuration ? 'block' : 'none'; 94 | durationNode.style.fontWeight = 500; 95 | durationNode.style.color = '#fff'; 96 | originalSceneDetails.firstChild.firstChild.appendChild(durationNode); 97 | } 98 | 99 | const expandDetailsButton = originalSceneDetails.querySelector('button'); 100 | if (!expandDetailsButton.classList.contains('.enhanced')) { 101 | expandDetailsButton.classList.add('enhanced'); 102 | expandDetailsButton.addEventListener('click', evt => { 103 | const icon = expandDetailsButton.firstChild.dataset.icon; 104 | if (evt.shiftKey) { 105 | evt.preventDefault(); 106 | evt.stopPropagation(); 107 | for (const button of document.querySelectorAll('.original-scene-details button')) { 108 | if (button.firstChild.dataset.icon === icon) { 109 | button.click(); 110 | } 111 | } 112 | } 113 | }); 114 | } 115 | }); 116 | 117 | const additionsConfigId = 'additionsconfig'; 118 | 119 | stash.addEventListener('tagger:configuration', evt => { 120 | const el = evt.detail; 121 | if (!document.getElementById(additionsConfigId)) { 122 | const configContainer = el.parentElement; 123 | const additionsConfig = createElementFromHTML(` 124 |
125 |
Tagger Additions Configuration
126 |
127 |
128 |
129 | 130 | 131 |
132 |
133 |
134 |
135 | 136 | 137 |
138 |
139 |
140 |
141 | 142 | 143 |
144 |
145 |
146 |
147 | `); 148 | configContainer.appendChild(additionsConfig); 149 | loadSettings(); 150 | document.getElementById('additions-duration').addEventListener('change', function () { 151 | for (const node of document.querySelectorAll('.scene-duration')) { 152 | node.style.display = this.checked ? 'block' : 'none'; 153 | } 154 | }); 155 | document.getElementById('additions-path').addEventListener('change', function () { 156 | for (const node of document.querySelectorAll('.scene-path')) { 157 | node.style.display = this.checked ? 'block' : 'none'; 158 | } 159 | }); 160 | document.getElementById('additions-url').addEventListener('change', function () { 161 | for (const node of document.querySelectorAll('.scene-url')) { 162 | node.style.display = this.checked ? 'block' : 'none'; 163 | } 164 | }); 165 | } 166 | }); 167 | 168 | async function loadSettings() { 169 | for (const input of document.querySelectorAll(`#${additionsConfigId} input`)) { 170 | input.checked = await GM.getValue(input.id, input.dataset.default === 'true'); 171 | input.addEventListener('change', async () => { 172 | await GM.setValue(input.id, input.checked); 173 | }); 174 | } 175 | } 176 | })(); -------------------------------------------------------------------------------- /dist/public/Stash Scene Tagger Draft Submit.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Scene Tagger Draft Submit 3 | // @namespace https://github.com/7dJx1qP/stash-userscripts 4 | // @description Adds button to Scene Tagger to submit draft to stashdb 5 | // @version 0.1.1 6 | // @author 7dJx1qP 7 | // @match http://localhost:9999/* 8 | // @grant unsafeWindow 9 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src\StashUserscriptLibrary.js 10 | // ==/UserScript== 11 | 12 | (function () { 13 | 'use strict'; 14 | 15 | const { 16 | stash, 17 | Stash, 18 | waitForElementId, 19 | waitForElementClass, 20 | waitForElementByXpath, 21 | getElementByXpath, 22 | getElementsByXpath, 23 | getClosestAncestor, 24 | insertAfter, 25 | createElementFromHTML, 26 | } = unsafeWindow.stash; 27 | 28 | document.body.appendChild(document.createElement('style')).textContent = ` 29 | .search-item > div.row:first-child > div.col-md-6.my-1 > div:first-child { display: flex; flex-direction: column; } 30 | .submit-draft { order: 5; } 31 | `; 32 | 33 | async function submitDraft(sceneId, stashBoxIndex) { 34 | const reqData = { 35 | "variables": { 36 | "input": { 37 | "id": sceneId, 38 | "stash_box_index": stashBoxIndex 39 | } 40 | }, 41 | "operationName": "SubmitStashBoxSceneDraft", 42 | "query": `mutation SubmitStashBoxSceneDraft($input: StashBoxDraftSubmissionInput!) { 43 | submitStashBoxSceneDraft(input: $input) 44 | }` 45 | } 46 | const res = await stash.callGQL(reqData); 47 | return res?.data?.submitStashBoxSceneDraft; 48 | } 49 | 50 | async function initDraftButtons() { 51 | const data = await stash.getStashBoxes(); 52 | let i = 0; 53 | const stashBoxes = data.data.configuration.general.stashBoxes; 54 | 55 | const nodes = getElementsByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']"); 56 | const buttons = []; 57 | let node = null; 58 | while (node = nodes.iterateNext()) { 59 | buttons.push(node); 60 | } 61 | for (const button of buttons) { 62 | const searchItem = getClosestAncestor(button, '.search-item'); 63 | const { 64 | urlNode, 65 | url, 66 | id, 67 | data, 68 | nameNode, 69 | name, 70 | queryInput, 71 | performerNodes 72 | } = stash.parseSearchItem(searchItem); 73 | 74 | const draftButtonExists = searchItem.querySelector('.submit-draft'); 75 | if (draftButtonExists) { 76 | continue; 77 | } 78 | 79 | const submit = createElementFromHTML('
'); 80 | const submitButton = submit.querySelector('button'); 81 | button.parentElement.parentElement.appendChild(submit); 82 | submitButton.addEventListener('click', async () => { 83 | const selectedStashbox = document.getElementById('scraper').value; 84 | if (!selectedStashbox.startsWith('stashbox:')) { 85 | alert('No stashbox source selected.'); 86 | return; 87 | } 88 | const selectedStashboxIndex = parseInt(selectedStashbox.replace(/^stashbox:/, '')); 89 | const existingStashId = data.stash_ids.find(o => o.endpoint === stashBoxes[selectedStashboxIndex].endpoint); 90 | if (existingStashId) { 91 | alert(`Scene already has StashID for ${stashBoxes[selectedStashboxIndex].endpoint}.`); 92 | return; 93 | } 94 | const draftId = await submitDraft(id, selectedStashboxIndex); 95 | const draftLink = createElementFromHTML(`Draft: ${draftId}`); 96 | submitButton.parentElement.appendChild(draftLink); 97 | submitButton.remove(); 98 | }); 99 | } 100 | } 101 | 102 | stash.addEventListener('page:studio:scenes', function () { 103 | waitForElementByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']", initDraftButtons); 104 | }); 105 | 106 | stash.addEventListener('page:performer:scenes', function () { 107 | waitForElementByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']", initDraftButtons); 108 | }); 109 | 110 | stash.addEventListener('page:scenes', function () { 111 | waitForElementByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']", initDraftButtons); 112 | }); 113 | })(); -------------------------------------------------------------------------------- /dist/public/Stash Set Stashbox Favorite Performers.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Set Stashbox Favorite Performers 3 | // @namespace https://github.com/7dJx1qP/stash-userscripts 4 | // @description Set Stashbox favorite performers according to stash favorites. Requires userscript_functions stash plugin 5 | // @version 0.3.0 6 | // @author 7dJx1qP 7 | // @match http://localhost:9999/* 8 | // @grant unsafeWindow 9 | // @grant GM.getValue 10 | // @grant GM.setValue 11 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src\StashUserscriptLibrary.js 12 | // ==/UserScript== 13 | 14 | (function() { 15 | 'use strict'; 16 | 17 | const { 18 | stash, 19 | Stash, 20 | waitForElementId, 21 | waitForElementClass, 22 | waitForElementByXpath, 23 | getElementByXpath, 24 | getClosestAncestor, 25 | updateTextInput, 26 | } = unsafeWindow.stash; 27 | 28 | const MIN_REQUIRED_PLUGIN_VERSION = '0.6.0'; 29 | 30 | const TASK_NAME = 'Set Stashbox Favorite Performers'; 31 | stash.visiblePluginTasks.push(TASK_NAME); 32 | 33 | const settingsId = 'userscript-settings-set-stashbox-favorites-task'; 34 | const inputId = 'userscript-settings-set-stashbox-favorites-button-visible'; 35 | 36 | async function runSetStashBoxFavoritePerformersTask() { 37 | const data = await stash.getStashBoxes(); 38 | if (!data.data.configuration.general.stashBoxes.length) { 39 | alert('No Stashbox configured.'); 40 | } 41 | for (const { endpoint } of data.data.configuration.general.stashBoxes) { 42 | if (endpoint !== 'https://stashdb.org/graphql') continue; 43 | await stash.runPluginTask("userscript_functions", "Set Stashbox Favorite Performers", [{"key":"endpoint", "value":{"str": endpoint}}]); 44 | } 45 | } 46 | 47 | async function runSetStashBoxFavoritePerformerTask(endpoint, stashId, favorite) { 48 | if (endpoint !== 'https://stashdb.org/graphql') return; 49 | return stash.runPluginTask("userscript_functions", "Set Stashbox Favorite Performer", [{"key":"endpoint", "value":{"str": endpoint}}, {"key":"stash_id", "value":{"str": stashId}}, {"key":"favorite", "value":{"b": favorite}}]); 50 | } 51 | 52 | stash.addEventListener('page:performers', function () { 53 | waitForElementClass("btn-toolbar", async function () { 54 | if (!document.getElementById('stashbox-favorite-task')) { 55 | const toolbar = document.querySelector(".btn-toolbar"); 56 | 57 | const newGroup = document.createElement('div'); 58 | newGroup.classList.add('mx-2', 'mb-2', await GM.getValue(inputId, false) ? 'd-flex' : 'd-none'); 59 | toolbar.appendChild(newGroup); 60 | 61 | const button = document.createElement("button"); 62 | button.setAttribute("id", "stashbox-favorite-task"); 63 | button.classList.add('btn', 'btn-secondary'); 64 | button.innerHTML = 'Set Stashbox Favorites'; 65 | button.onclick = () => { 66 | runSetStashBoxFavoritePerformersTask(); 67 | }; 68 | newGroup.appendChild(button); 69 | } 70 | }); 71 | }); 72 | 73 | stash.addEventListener('stash:response', function (evt) { 74 | const data = evt.detail; 75 | let performers; 76 | if (data.data?.performerUpdate?.stash_ids?.length) { 77 | performers = [data.data.performerUpdate]; 78 | } 79 | else if (data.data?.bulkPerformerUpdate) { 80 | performers = data.data.bulkPerformerUpdate.filter(performer => performer?.stash_ids?.length); 81 | } 82 | if (performers) { 83 | if (performers.length <= 10) { 84 | for (const performer of performers) { 85 | for (const { endpoint, stash_id } of performer.stash_ids) { 86 | runSetStashBoxFavoritePerformerTask(endpoint, stash_id, performer.favorite); 87 | } 88 | } 89 | } 90 | else { 91 | runSetStashBoxFavoritePerformersTask(); 92 | } 93 | } 94 | }); 95 | 96 | stash.addSystemSetting(async (elementId, el) => { 97 | if (document.getElementById(inputId)) return; 98 | const settingsHeader = 'Show Set Stashbox Favorites Button'; 99 | const settingsSubheader = 'Display set stashbox favorites button on performers page.'; 100 | const checkbox = await stash.createSystemSettingCheckbox(el, settingsId, inputId, settingsHeader, settingsSubheader); 101 | checkbox.checked = await GM.getValue(inputId, false); 102 | checkbox.addEventListener('change', async () => { 103 | const value = checkbox.checked; 104 | await GM.setValue(inputId, value); 105 | }); 106 | }); 107 | 108 | stash.addEventListener('stash:pluginVersion', async function () { 109 | waitForElementId(settingsId, async (elementId, el) => { 110 | el.style.display = stash.pluginVersion != null ? 'flex' : 'none'; 111 | }); 112 | if (stash.comparePluginVersion(MIN_REQUIRED_PLUGIN_VERSION) < 0) { 113 | const alertedPluginVersion = await GM.getValue('alerted_plugin_version'); 114 | if (alertedPluginVersion !== stash.pluginVersion) { 115 | await GM.setValue('alerted_plugin_version', stash.pluginVersion); 116 | alert(`User functions plugin version is ${stash.pluginVersion}. Set Stashbox Favorite Performers userscript requires version ${MIN_REQUIRED_PLUGIN_VERSION} or higher.`); 117 | } 118 | } 119 | }); 120 | 121 | stash.addEventListener('stash:plugin:task', async function (evt) { 122 | const { taskName, task } = evt.detail; 123 | if (taskName === TASK_NAME) { 124 | const taskButton = task.querySelector('button'); 125 | if (!taskButton.classList.contains('hooked')) { 126 | taskButton.classList.add('hooked'); 127 | taskButton.addEventListener('click', evt => { 128 | evt.preventDefault(); 129 | evt.stopPropagation(); 130 | runSetStashBoxFavoritePerformersTask(); 131 | }); 132 | } 133 | } 134 | }); 135 | 136 | })(); -------------------------------------------------------------------------------- /dist/public/Stash StashID Icon.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash StashID Icon 3 | // @namespace https://github.com/7dJx1qP/stash-userscripts 4 | // @description Adds checkmark icon to performer and studio cards that have a stashid 5 | // @version 0.2.0 6 | // @author 7dJx1qP 7 | // @match http://localhost:9999/* 8 | // @grant unsafeWindow 9 | // @grant GM_addStyle 10 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src\StashUserscriptLibrary.js 11 | // ==/UserScript== 12 | 13 | (function () { 14 | 'use strict'; 15 | 16 | const { 17 | stash, 18 | Stash, 19 | waitForElementId, 20 | waitForElementClass, 21 | waitForElementByXpath, 22 | getElementByXpath, 23 | insertAfter, 24 | createElementFromHTML, 25 | } = unsafeWindow.stash; 26 | 27 | GM_addStyle(` 28 | .peformer-stashid-icon { 29 | position: absolute; 30 | bottom: .8rem; 31 | left: .8rem; 32 | } 33 | .studio-stashid-icon { 34 | position: absolute; 35 | top: 10px; 36 | right: 5px; 37 | } 38 | .col-3.d-xl-none .studio-stashid-icon { 39 | position: relative; 40 | top: 0; 41 | right: 0; 42 | } 43 | `); 44 | 45 | function createCheckmarkElement() { 46 | return createElementFromHTML(``); 52 | } 53 | 54 | function addPerformerStashIDIcons(performerDatas) { 55 | for (const performerCard of document.querySelectorAll('.performer-card')) { 56 | const performerLink = performerCard.querySelector('.thumbnail-section > a'); 57 | if (performerLink) { 58 | const performerUrl = performerLink.href; 59 | const performerId = performerUrl.split('/').pop(); 60 | const performerData = performerDatas[performerId]; 61 | if (performerData?.stash_ids.length) { 62 | const el = createElementFromHTML(`
`); 63 | el.appendChild(createCheckmarkElement()); 64 | 65 | performerLink.parentElement.appendChild(el); 66 | } 67 | } 68 | } 69 | } 70 | 71 | function addStudioStashIDIcons(studioDatas) { 72 | for (const studioCard of document.querySelectorAll('.studio-card')) { 73 | const studioLink = studioCard.querySelector('.thumbnail-section > a'); 74 | const studioUrl = studioLink.href; 75 | const studioId = studioUrl.split('/').pop(); 76 | const studioData = studioDatas[studioId]; 77 | if (studioData?.stash_ids.length) { 78 | const el = createElementFromHTML(`
`); 79 | el.appendChild(createCheckmarkElement()); 80 | 81 | studioCard.appendChild(el); 82 | } 83 | } 84 | } 85 | 86 | function addSceneStudioStashIDIcons(studioData) { 87 | for (const studioCard of document.querySelectorAll('.studio-logo')) { 88 | if (studioData?.stash_ids.length) { 89 | const el = createElementFromHTML(`
`); 90 | el.appendChild(createCheckmarkElement()); 91 | 92 | studioCard.parentElement.appendChild(el); 93 | } 94 | } 95 | } 96 | 97 | stash.addEventListener('page:scene', function () { 98 | waitForElementClass("performer-card", function () { 99 | const sceneId = window.location.pathname.split('/').pop(); 100 | const performerDatas = {}; 101 | for (const performerData of stash.scenes[sceneId].performers) { 102 | performerDatas[performerData.id] = performerData; 103 | } 104 | addPerformerStashIDIcons(performerDatas); 105 | if (stash.scenes[sceneId].studio) { 106 | addSceneStudioStashIDIcons(stash.scenes[sceneId].studio); 107 | } 108 | }); 109 | }); 110 | 111 | stash.addEventListener('page:performers', function () { 112 | waitForElementClass("performer-card", function () { 113 | addPerformerStashIDIcons(stash.performers); 114 | }); 115 | }); 116 | 117 | stash.addEventListener('page:studios', function () { 118 | waitForElementClass("studio-card", function () { 119 | addStudioStashIDIcons(stash.studios); 120 | }); 121 | }); 122 | 123 | stash.addEventListener('page:studio:performers', function () { 124 | waitForElementClass("performer-card", function () { 125 | addPerformerStashIDIcons(stash.performers); 126 | }); 127 | }); 128 | })(); -------------------------------------------------------------------------------- /dist/public/Stash Stats.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Stats 3 | // @namespace https://github.com/7dJx1qP/stash-userscripts 4 | // @description Add stats to stats page 5 | // @version 0.3.1 6 | // @author 7dJx1qP 7 | // @match http://localhost:9999/* 8 | // @grant unsafeWindow 9 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src\StashUserscriptLibrary.js 10 | // ==/UserScript== 11 | 12 | (function() { 13 | 'use strict'; 14 | 15 | const { 16 | stash, 17 | Stash, 18 | waitForElementId, 19 | waitForElementClass, 20 | waitForElementByXpath, 21 | getElementByXpath, 22 | getClosestAncestor, 23 | updateTextInput, 24 | } = unsafeWindow.stash; 25 | 26 | function createStatElement(container, title, heading) { 27 | const statEl = document.createElement('div'); 28 | statEl.classList.add('stats-element'); 29 | container.appendChild(statEl); 30 | 31 | const statTitle = document.createElement('p'); 32 | statTitle.classList.add('title'); 33 | statTitle.innerText = title; 34 | statEl.appendChild(statTitle); 35 | 36 | const statHeading = document.createElement('p'); 37 | statHeading.classList.add('heading'); 38 | statHeading.innerText = heading; 39 | statEl.appendChild(statHeading); 40 | } 41 | async function createSceneStashIDPct(row) { 42 | const reqData = { 43 | "variables": { 44 | "scene_filter": { 45 | "stash_id_endpoint": { 46 | "endpoint": "", 47 | "stash_id": "", 48 | "modifier": "NOT_NULL" 49 | } 50 | } 51 | }, 52 | "query": "query FindScenes($filter: FindFilterType, $scene_filter: SceneFilterType, $scene_ids: [Int!]) {\n findScenes(filter: $filter, scene_filter: $scene_filter, scene_ids: $scene_ids) {\n count\n }\n}" 53 | }; 54 | const resp = (await stash.callGQL(reqData)); 55 | console.log('resp', resp); 56 | const stashIdCount = (await stash.callGQL(reqData)).data.findScenes.count; 57 | 58 | const reqData2 = { 59 | "variables": { 60 | "scene_filter": {} 61 | }, 62 | "query": "query FindScenes($filter: FindFilterType, $scene_filter: SceneFilterType, $scene_ids: [Int!]) {\n findScenes(filter: $filter, scene_filter: $scene_filter, scene_ids: $scene_ids) {\n count\n }\n}" 63 | }; 64 | const totalCount = (await stash.callGQL(reqData2)).data.findScenes.count; 65 | 66 | createStatElement(row, (stashIdCount / totalCount * 100).toFixed(2) + '%', 'Scene StashIDs'); 67 | } 68 | 69 | async function createPerformerStashIDPct(row) { 70 | const reqData = { 71 | "variables": { 72 | "performer_filter": { 73 | "stash_id_endpoint": { 74 | "endpoint": "", 75 | "stash_id": "", 76 | "modifier": "NOT_NULL" 77 | } 78 | } 79 | }, 80 | "query": "query FindPerformers($filter: FindFilterType, $performer_filter: PerformerFilterType) {\n findPerformers(filter: $filter, performer_filter: $performer_filter) {\n count\n }\n}" 81 | }; 82 | const stashIdCount = (await stash.callGQL(reqData)).data.findPerformers.count; 83 | 84 | const reqData2 = { 85 | "variables": { 86 | "performer_filter": {} 87 | }, 88 | "query": "query FindPerformers($filter: FindFilterType, $performer_filter: PerformerFilterType) {\n findPerformers(filter: $filter, performer_filter: $performer_filter) {\n count\n }\n}" 89 | }; 90 | const totalCount = (await stash.callGQL(reqData2)).data.findPerformers.count; 91 | 92 | createStatElement(row, (stashIdCount / totalCount * 100).toFixed(2) + '%', 'Performer StashIDs'); 93 | } 94 | 95 | async function createStudioStashIDPct(row) { 96 | const reqData = { 97 | "variables": { 98 | "studio_filter": { 99 | "stash_id_endpoint": { 100 | "endpoint": "", 101 | "stash_id": "", 102 | "modifier": "NOT_NULL" 103 | } 104 | } 105 | }, 106 | "query": "query FindStudios($filter: FindFilterType, $studio_filter: StudioFilterType) {\n findStudios(filter: $filter, studio_filter: $studio_filter) {\n count\n }\n}" 107 | }; 108 | const stashIdCount = (await stash.callGQL(reqData)).data.findStudios.count; 109 | 110 | const reqData2 = { 111 | "variables": { 112 | "scene_filter": {} 113 | }, 114 | "query": "query FindStudios($filter: FindFilterType, $studio_filter: StudioFilterType) {\n findStudios(filter: $filter, studio_filter: $studio_filter) {\n count\n }\n}" 115 | }; 116 | const totalCount = (await stash.callGQL(reqData2)).data.findStudios.count; 117 | 118 | createStatElement(row, (stashIdCount / totalCount * 100).toFixed(2) + '%', 'Studio StashIDs'); 119 | } 120 | 121 | async function createPerformerFavorites(row) { 122 | const reqData = { 123 | "variables": { 124 | "performer_filter": { 125 | "filter_favorites": true 126 | } 127 | }, 128 | "query": "query FindPerformers($filter: FindFilterType, $performer_filter: PerformerFilterType) {\n findPerformers(filter: $filter, performer_filter: $performer_filter) {\n count\n }\n}" 129 | }; 130 | const perfCount = (await stash.callGQL(reqData)).data.findPerformers.count; 131 | 132 | createStatElement(row, perfCount, 'Favorite Performers'); 133 | } 134 | 135 | async function createMarkersStat(row) { 136 | const reqData = { 137 | "variables": { 138 | "scene_marker_filter": {} 139 | }, 140 | "query": "query FindSceneMarkers($filter: FindFilterType, $scene_marker_filter: SceneMarkerFilterType) {\n findSceneMarkers(filter: $filter, scene_marker_filter: $scene_marker_filter) {\n count\n }\n}" 141 | }; 142 | const totalCount = (await stash.callGQL(reqData)).data.findSceneMarkers.count; 143 | 144 | createStatElement(row, totalCount, 'Markers'); 145 | } 146 | 147 | stash.addEventListener('page:stats', function () { 148 | waitForElementByXpath("//div[contains(@class, 'container-fluid')]/div[@class='mt-5']", function (xpath, el) { 149 | if (!document.getElementById('custom-stats-row')) { 150 | const changelog = el.querySelector('div.changelog'); 151 | const row = document.createElement('div'); 152 | row.setAttribute('id', 'custom-stats-row'); 153 | row.classList.add('col', 'col-sm-8', 'm-sm-auto', 'row', 'stats'); 154 | el.insertBefore(row, changelog); 155 | 156 | createSceneStashIDPct(row); 157 | createStudioStashIDPct(row); 158 | createPerformerStashIDPct(row); 159 | createPerformerFavorites(row); 160 | createMarkersStat(row); 161 | } 162 | }); 163 | }); 164 | 165 | })(); -------------------------------------------------------------------------------- /dist/public/Stash Studio Image And Parent On Create.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Studio Image And Parent On Create 3 | // @namespace https://github.com/7dJx1qP/stash-userscripts 4 | // @description Set studio image and parent when creating from StashDB. Requires userscript_functions stash plugin 5 | // @version 0.3.0 6 | // @author 7dJx1qP 7 | // @match http://localhost:9999/* 8 | // @grant unsafeWindow 9 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/develop/src\StashUserscriptLibrary.js 10 | // ==/UserScript== 11 | 12 | (function() { 13 | 'use strict'; 14 | 15 | const { 16 | stash, 17 | Stash, 18 | waitForElementId, 19 | waitForElementClass, 20 | waitForElementByXpath, 21 | getElementByXpath, 22 | getClosestAncestor, 23 | updateTextInput, 24 | } = unsafeWindow.stash; 25 | 26 | stash.userscripts.push('Stash Studio Image And Parent On Create'); 27 | 28 | async function runStudioUpdateTask(studioId, endpoint, remoteSiteId) { 29 | return stash.runPluginTask("userscript_functions", "Update Studio", [{"key":"studio_id", "value":{"str": studioId}}, {"key":"endpoint", "value":{"str": endpoint}}, {"key":"remote_site_id", "value":{"str": remoteSiteId}}]); 30 | } 31 | 32 | stash.addEventListener('stash:response', function (evt) { 33 | const data = evt.detail; 34 | if (data.data?.studioCreate) { 35 | const studioId = data.data?.studioCreate.id; 36 | const endpoint = data.data?.studioCreate.stash_ids[0].endpoint; 37 | const remoteSiteId = data.data?.studioCreate.stash_ids[0].stash_id; 38 | runStudioUpdateTask(studioId, endpoint, remoteSiteId); 39 | } 40 | }); 41 | 42 | stash.addEventListener('userscript_functions:update_studio', async function (evt) { 43 | const { studioId, endpoint, remoteSiteId, callback, errCallback } = evt.detail; 44 | await runStudioUpdateTask(studioId, endpoint, remoteSiteId); 45 | const prefix = `[Plugin / Userscript Functions] update_studio: Done.`; 46 | try { 47 | await this.pollLogsForMessage(prefix); 48 | if (callback) callback(); 49 | } 50 | catch (err) { 51 | if (errCallback) errCallback(err); 52 | } 53 | }); 54 | 55 | })(); -------------------------------------------------------------------------------- /dist/public/Stash Tag Image Cropper.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Tag Image Cropper 3 | // @namespace https://github.com/7dJx1qP/stash-userscripts 4 | // @description Adds an image cropper to tag page 5 | // @version 0.2.0 6 | // @author 7dJx1qP 7 | // @match http://localhost:9999/* 8 | // @resource IMPORTED_CSS https://raw.githubusercontent.com/fengyuanchen/cropperjs/main/dist/cropper.min.css 9 | // @grant unsafeWindow 10 | // @grant GM_getResourceText 11 | // @grant GM_addStyle 12 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src\StashUserscriptLibrary.js 13 | // @require https://raw.githubusercontent.com/fengyuanchen/cropperjs/main/dist/cropper.min.js 14 | // ==/UserScript== 15 | 16 | /* global Cropper */ 17 | 18 | (function () { 19 | 'use strict'; 20 | 21 | const { 22 | stash, 23 | Stash, 24 | waitForElementId, 25 | waitForElementClass, 26 | waitForElementByXpath, 27 | getElementByXpath, 28 | insertAfter, 29 | reloadImg, 30 | } = unsafeWindow.stash; 31 | 32 | const css = GM_getResourceText("IMPORTED_CSS"); 33 | GM_addStyle(css); 34 | 35 | let cropping = false; 36 | let cropper = null; 37 | 38 | stash.addEventListener('page:tag:scenes', function () { 39 | waitForElementClass('detail-container', function () { 40 | const cropBtnContainerId = "crop-btn-container"; 41 | if (!document.getElementById(cropBtnContainerId)) { 42 | const tagId = window.location.pathname.replace('/tags/', '').split('/')[0]; 43 | const image = getElementByXpath("//div[contains(@class, 'detail-header-image')]//img[@class='logo']"); 44 | image.parentElement.addEventListener('click', (evt) => { 45 | if (cropping) { 46 | evt.preventDefault(); 47 | evt.stopPropagation(); 48 | } 49 | }) 50 | const cropBtnContainer = document.createElement('div'); 51 | cropBtnContainer.setAttribute("id", cropBtnContainerId); 52 | cropBtnContainer.classList.add('mb-2', 'text-center'); 53 | image.parentElement.appendChild(cropBtnContainer); 54 | 55 | const cropInfo = document.createElement('p'); 56 | 57 | const imageUrl = getElementByXpath("//div[contains(@class, 'detail-header-image')]//img[@class='logo']/@src").nodeValue; 58 | const cropStart = document.createElement('button'); 59 | cropStart.setAttribute("id", "crop-start"); 60 | cropStart.classList.add('btn', 'btn-primary'); 61 | cropStart.innerText = 'Crop Image'; 62 | cropStart.addEventListener('click', evt => { 63 | cropping = true; 64 | cropStart.style.display = 'none'; 65 | cropCancel.style.display = 'inline-block'; 66 | 67 | cropper = new Cropper(image, { 68 | viewMode: 1, 69 | initialAspectRatio: 1, 70 | movable: false, 71 | rotatable: false, 72 | scalable: false, 73 | zoomable: false, 74 | zoomOnTouch: false, 75 | zoomOnWheel: false, 76 | ready() { 77 | cropAccept.style.display = 'inline-block'; 78 | }, 79 | crop(e) { 80 | cropInfo.innerText = `X: ${Math.round(e.detail.x)}, Y: ${Math.round(e.detail.y)}, Width: ${Math.round(e.detail.width)}px, Height: ${Math.round(e.detail.height)}px`; 81 | } 82 | }); 83 | }); 84 | cropBtnContainer.appendChild(cropStart); 85 | 86 | const cropAccept = document.createElement('button'); 87 | cropAccept.setAttribute("id", "crop-accept"); 88 | cropAccept.classList.add('btn', 'btn-success', 'mr-2'); 89 | cropAccept.innerText = 'OK'; 90 | cropAccept.addEventListener('click', async evt => { 91 | cropping = false; 92 | cropStart.style.display = 'inline-block'; 93 | cropAccept.style.display = 'none'; 94 | cropCancel.style.display = 'none'; 95 | cropInfo.innerText = ''; 96 | 97 | const reqData = { 98 | "operationName": "TagUpdate", 99 | "variables": { 100 | "input": { 101 | "image": cropper.getCroppedCanvas().toDataURL(), 102 | "id": tagId 103 | } 104 | }, 105 | "query": `mutation TagUpdate($input: TagUpdateInput!) { 106 | tagUpdate(input: $input) { 107 | id 108 | } 109 | }` 110 | } 111 | await stash.callGQL(reqData); 112 | reloadImg(image.src); 113 | cropper.destroy(); 114 | }); 115 | cropBtnContainer.appendChild(cropAccept); 116 | 117 | const cropCancel = document.createElement('button'); 118 | cropCancel.setAttribute("id", "crop-accept"); 119 | cropCancel.classList.add('btn', 'btn-danger'); 120 | cropCancel.innerText = 'Cancel'; 121 | cropCancel.addEventListener('click', evt => { 122 | cropping = false; 123 | cropStart.style.display = 'inline-block'; 124 | cropAccept.style.display = 'none'; 125 | cropCancel.style.display = 'none'; 126 | cropInfo.innerText = ''; 127 | 128 | cropper.destroy(); 129 | }); 130 | cropBtnContainer.appendChild(cropCancel); 131 | cropAccept.style.display = 'none'; 132 | cropCancel.style.display = 'none'; 133 | 134 | cropBtnContainer.appendChild(cropInfo); 135 | } 136 | }); 137 | }); 138 | })(); -------------------------------------------------------------------------------- /dist/public/Stash Userscripts Bundle.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Userscripts Bundle 3 | // @namespace https://github.com/7dJx1qP/stash-userscripts 4 | // @description Stash Userscripts Bundle 5 | // @version 0.24.2 6 | // @author 7dJx1qP 7 | // @match http://localhost:9999/* 8 | // @resource IMPORTED_CSS https://raw.githubusercontent.com/fengyuanchen/cropperjs/main/dist/cropper.min.css 9 | // @grant unsafeWindow 10 | // @grant GM_setClipboard 11 | // @grant GM_getResourceText 12 | // @grant GM_addStyle 13 | // @grant GM.getValue 14 | // @grant GM.setValue 15 | // @require https://raw.githubusercontent.com/fengyuanchen/cropperjs/main/dist/cropper.min.js 16 | // @require https://raw.githubusercontent.com/nodeca/js-yaml/master/dist/js-yaml.js 17 | // @require https://cdnjs.cloudflare.com/ajax/libs/marked/4.2.2/marked.min.js 18 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src\StashUserscriptLibrary.js 19 | // 20 | // ************************************************************************************************** 21 | // * YOU MAY REMOVE ANY OF THE @require LINES BELOW FOR SCRIPTS YOU DO NOT WANT * 22 | // ************************************************************************************************** 23 | // 24 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash Batch Query Edit.user.js 25 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash Batch Result Toggle.user.js 26 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash Batch Save.user.js 27 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash Batch Search.user.js 28 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash Markdown.user.js 29 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash Markers Autoscroll.user.js 30 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash New Performer Filter Button.user.js 31 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash Open Media Player.user.js 32 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash Performer Audit Task Button.user.js 33 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash Performer Image Cropper.user.js 34 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash Performer Markers Tab.user.js 35 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash Performer Tagger Additions.user.js 36 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash Performer URL Searchbox.user.js 37 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash Scene Tagger Additions.user.js 38 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash Scene Tagger Colorizer.user.js 39 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash Scene Tagger Draft Submit.user.js 40 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash Set Stashbox Favorite Performers.user.js 41 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash StashID Icon.user.js 42 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash StashID Input.user.js 43 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash Stats.user.js 44 | // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src/body\Stash Tag Image Cropper.user.js 45 | 46 | // ==/UserScript== 47 | -------------------------------------------------------------------------------- /images/Stash Batch Query Edit/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Batch Query Edit/config.png -------------------------------------------------------------------------------- /images/Stash Batch Query Edit/scenes-tagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Batch Query Edit/scenes-tagger.png -------------------------------------------------------------------------------- /images/Stash Batch Result Toggle/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Batch Result Toggle/config.png -------------------------------------------------------------------------------- /images/Stash Batch Result Toggle/scenes-tagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Batch Result Toggle/scenes-tagger.png -------------------------------------------------------------------------------- /images/Stash Batch Save/scenes-tagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Batch Save/scenes-tagger.png -------------------------------------------------------------------------------- /images/Stash Batch Search/scenes-tagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Batch Search/scenes-tagger.png -------------------------------------------------------------------------------- /images/Stash Markdown/tag-description.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Markdown/tag-description.png -------------------------------------------------------------------------------- /images/Stash Markers Autoscroll/scroll-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Markers Autoscroll/scroll-settings.png -------------------------------------------------------------------------------- /images/Stash New Performer Filter Button/performers-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash New Performer Filter Button/performers-page.png -------------------------------------------------------------------------------- /images/Stash Open Media Player/system-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Open Media Player/system-settings.png -------------------------------------------------------------------------------- /images/Stash Performer Audit Task Button/performers-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Performer Audit Task Button/performers-page.png -------------------------------------------------------------------------------- /images/Stash Performer Audit Task Button/plugin-tasks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Performer Audit Task Button/plugin-tasks.png -------------------------------------------------------------------------------- /images/Stash Performer Audit Task Button/system-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Performer Audit Task Button/system-settings.png -------------------------------------------------------------------------------- /images/Stash Performer Image Cropper/performer-image-cropper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Performer Image Cropper/performer-image-cropper.png -------------------------------------------------------------------------------- /images/Stash Performer Markers Tab/performer-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Performer Markers Tab/performer-page.png -------------------------------------------------------------------------------- /images/Stash Performer Tagger Additions/performer-tagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Performer Tagger Additions/performer-tagger.png -------------------------------------------------------------------------------- /images/Stash Performer URL Searchbox/performers-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Performer URL Searchbox/performers-page.png -------------------------------------------------------------------------------- /images/Stash Scene Tagger Additions/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Scene Tagger Additions/config.png -------------------------------------------------------------------------------- /images/Stash Scene Tagger Additions/scenes-tagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Scene Tagger Additions/scenes-tagger.png -------------------------------------------------------------------------------- /images/Stash Scene Tagger Colorizer/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Scene Tagger Colorizer/config.png -------------------------------------------------------------------------------- /images/Stash Scene Tagger Colorizer/scenes-tagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Scene Tagger Colorizer/scenes-tagger.png -------------------------------------------------------------------------------- /images/Stash Scene Tagger Colorizer/tag-colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Scene Tagger Colorizer/tag-colors.png -------------------------------------------------------------------------------- /images/Stash Scene Tagger Draft Submit/scenes-tagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Scene Tagger Draft Submit/scenes-tagger.png -------------------------------------------------------------------------------- /images/Stash Set Stashbox Favorite Performers/performers-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Set Stashbox Favorite Performers/performers-page.png -------------------------------------------------------------------------------- /images/Stash Set Stashbox Favorite Performers/plugin-tasks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Set Stashbox Favorite Performers/plugin-tasks.png -------------------------------------------------------------------------------- /images/Stash Set Stashbox Favorite Performers/system-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Set Stashbox Favorite Performers/system-settings.png -------------------------------------------------------------------------------- /images/Stash StashID Icon/performer-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash StashID Icon/performer-page.png -------------------------------------------------------------------------------- /images/Stash StashID Icon/scene-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash StashID Icon/scene-page.png -------------------------------------------------------------------------------- /images/Stash StashID Icon/studio-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash StashID Icon/studio-page.png -------------------------------------------------------------------------------- /images/Stash StashID Input/performer-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash StashID Input/performer-page.png -------------------------------------------------------------------------------- /images/Stash StashID Input/studio-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash StashID Input/studio-page.png -------------------------------------------------------------------------------- /images/Stash Stats/stats-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Stats/stats-page.png -------------------------------------------------------------------------------- /images/Stash Tag Image Cropper/tag-image-cropper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Stash Tag Image Cropper/tag-image-cropper.png -------------------------------------------------------------------------------- /images/Userscript Functions Plugin/plugin-tasks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Userscript Functions Plugin/plugin-tasks.png -------------------------------------------------------------------------------- /images/Userscript Functions Plugin/system-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/de19a639d29adca89db7e03b423868f3184d4284/images/Userscript Functions Plugin/system-settings.png -------------------------------------------------------------------------------- /plugins/userscript_functions/audit_performer_urls.py: -------------------------------------------------------------------------------- 1 | import log 2 | import os 3 | import pathlib 4 | import re 5 | import sys 6 | from urllib.parse import unquote 7 | try: 8 | from stashlib.stash_database import StashDatabase 9 | from stashlib.stash_models import PerformersRow 10 | except ModuleNotFoundError: 11 | print("If you have pip (normally installed with python), run this command in a terminal (cmd): pip install pystashlib)", file=sys.stderr) 12 | sys.exit() 13 | 14 | def to_iafd_fragment(url): 15 | performer_prefix = 'https://www.iafd.com/person.rme/perfid=' 16 | decoded_url = unquote(url) 17 | fragment = decoded_url.removeprefix(performer_prefix) 18 | return '/'.join(fragment.split('/')[:-1]) 19 | 20 | def audit_performer_urls(db: StashDatabase): 21 | """Check for valid iafd url format and duplicate urls""" 22 | 23 | regexpath = os.path.join(pathlib.Path(__file__).parent.resolve(), 'performer_url_regexes.txt') 24 | patterns = [re.compile(s.strip()) for s in open(regexpath, 'r').readlines()] 25 | 26 | rows = db.fetchall("""SELECT * FROM performers WHERE url IS NOT NULL AND url <> ''""") 27 | performers = [PerformersRow().from_sqliterow(row) for row in rows] 28 | log.info(f'Checking {str(len(rows))} performers with urls...') 29 | site_performer_fragments = {} 30 | for performer in performers: 31 | if 'iafd.com' in performer.url and not performer.url.startswith('https://www.iafd.com/person.rme/perfid='): 32 | log.info(f'malformed url {performer.id} {performer.name} {performer.url}') 33 | site_id = 'OTHER' 34 | url = performer.url.lower().strip() 35 | performer_id = url 36 | for pattern in patterns: 37 | m = pattern.search(url) 38 | if m: 39 | site_id = m.group(1) 40 | performer_id = m.group(2) 41 | break 42 | if site_id not in site_performer_fragments: 43 | site_performer_fragments[site_id] = {} 44 | if performer_id not in site_performer_fragments[site_id]: 45 | site_performer_fragments[site_id][performer_id] = performer 46 | else: 47 | log.info(f'Duplicate performer url: {performer.id} {performer.name}, {site_performer_fragments[site_id][performer_id].id} {site_performer_fragments[site_id][performer_id].name}') 48 | log.info('Done.') -------------------------------------------------------------------------------- /plugins/userscript_functions/config.ini: -------------------------------------------------------------------------------- 1 | [STASH] 2 | url = http://localhost:9999 3 | api_key = 4 | 5 | [MEDIAPLAYER] 6 | path = C:/Program Files/VideoLAN/VLC/vlc.exe 7 | 8 | -------------------------------------------------------------------------------- /plugins/userscript_functions/config_manager.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import os 3 | from configparser import ConfigParser 4 | 5 | def init_config(configpath): 6 | config_object = ConfigParser() 7 | 8 | config_object["STASH"] = { 9 | "url": "http://localhost:9999", 10 | "api_key": "" 11 | } 12 | 13 | config_object["MEDIAPLAYER"] = { 14 | "path": "C:/Program Files/VideoLAN/VLC/vlc.exe" 15 | } 16 | 17 | #Write the above sections to config.ini file 18 | with open(configpath, 'w') as conf: 19 | config_object.write(conf) 20 | 21 | def get_config_value(configpath, section_key, prop_name): 22 | config_object = ConfigParser() 23 | config_object.read(configpath) 24 | 25 | return config_object[section_key][prop_name] 26 | 27 | def update_config_value(configpath, section_key, prop_name, new_value): 28 | config_object = ConfigParser() 29 | config_object.read(configpath) 30 | 31 | config_object[section_key][prop_name] = new_value 32 | 33 | with open(configpath, 'w') as conf: 34 | config_object.write(conf) 35 | 36 | if __name__ == "__main__": 37 | init_config(os.path.join(pathlib.Path(__file__).parent.resolve(), 'config.ini')) -------------------------------------------------------------------------------- /plugins/userscript_functions/favorite_performers_sync.py: -------------------------------------------------------------------------------- 1 | import math 2 | import sys 3 | import graphql 4 | import log 5 | 6 | try: 7 | import requests 8 | except ModuleNotFoundError: 9 | print("If you have pip (normally installed with python), run this command in a terminal (cmd): pip install requests)", file=sys.stderr) 10 | sys.exit() 11 | 12 | try: 13 | from stashlib.stash_database import StashDatabase 14 | from stashlib.stash_models import PerformersRow 15 | except ModuleNotFoundError: 16 | print("If you have pip (normally installed with python), run this command in a terminal (cmd): pip install pystashlib)", file=sys.stderr) 17 | sys.exit() 18 | 19 | def call_graphql(query, variables=None): 20 | return graphql.callGraphQL(query, variables) 21 | 22 | def get_api_key(endpoint): 23 | query = """ 24 | query getstashbox { 25 | configuration { 26 | general { 27 | stashBoxes { 28 | name 29 | endpoint 30 | api_key 31 | } 32 | } 33 | } 34 | } 35 | """ 36 | 37 | result = call_graphql(query) 38 | # log.debug(result) 39 | boxapi_key = None 40 | for x in result["configuration"]["general"]["stashBoxes"]: 41 | # log.debug(x) 42 | if x["endpoint"] == endpoint: 43 | boxapi_key = x["api_key"] 44 | if not boxapi_key: 45 | log.error(f"Stashbox apikey for {endpoint} not found.") 46 | sys.exit(0) 47 | return boxapi_key 48 | 49 | def stashbox_call_graphql(endpoint, query, variables=None): 50 | boxapi_key = get_api_key(endpoint) 51 | # this is basically the same code as call_graphql except it calls out to the stashbox. 52 | 53 | headers = { 54 | "Accept-Encoding": "gzip, deflate, br", 55 | "Content-Type": "application/json", 56 | "Accept": "application/json", 57 | "Connection": "keep-alive", 58 | "DNT": "1", 59 | "ApiKey": boxapi_key 60 | } 61 | json = { 62 | 'query': query 63 | } 64 | if variables is not None: 65 | json['variables'] = variables 66 | try: 67 | response = requests.post(endpoint, json=json, headers=headers) 68 | if response.status_code == 200: 69 | result = response.json() 70 | if result.get("error"): 71 | for error in result["error"]["errors"]: 72 | raise Exception("GraphQL error: {}".format(error)) 73 | if result.get("data"): 74 | return result.get("data") 75 | elif response.status_code == 401: 76 | log.error( 77 | "[ERROR][GraphQL] HTTP Error 401, Unauthorised. You need to add a Stash box instance and API Key in your Stash config") 78 | return None 79 | else: 80 | raise ConnectionError( 81 | "GraphQL query failed:{} - {}".format(response.status_code, response.content)) 82 | except Exception as err: 83 | log.error(err) 84 | return None 85 | 86 | def get_stashbox_performer_favorite(endpoint, stash_id): 87 | query = """ 88 | query FullPerformer($id: ID!) { 89 | findPerformer(id: $id) { 90 | id 91 | is_favorite 92 | } 93 | } 94 | """ 95 | 96 | variables = { 97 | "id": stash_id 98 | } 99 | 100 | return stashbox_call_graphql(endpoint, query, variables) 101 | 102 | def update_stashbox_performer_favorite(endpoint, stash_id, favorite): 103 | query = """ 104 | mutation FavoritePerformer($id: ID!, $favorite: Boolean!) { 105 | favoritePerformer(id: $id, favorite: $favorite) 106 | } 107 | """ 108 | 109 | variables = { 110 | "id": stash_id, 111 | "favorite": favorite 112 | } 113 | 114 | return stashbox_call_graphql(endpoint, query, variables) 115 | 116 | def get_favorite_performers_from_stashbox(endpoint): 117 | query = """ 118 | query Performers($input: PerformerQueryInput!) { 119 | queryPerformers(input: $input) { 120 | count 121 | performers { 122 | id 123 | is_favorite 124 | } 125 | } 126 | } 127 | """ 128 | 129 | per_page = 100 130 | 131 | variables = { 132 | "input": { 133 | "names": "", 134 | "is_favorite": True, 135 | "page": 1, 136 | "per_page": per_page, 137 | "sort": "NAME", 138 | "direction": "ASC" 139 | } 140 | } 141 | 142 | performers = set() 143 | 144 | total_count = None 145 | request_count = 0 146 | max_request_count = 1 147 | 148 | performercounts = {} 149 | 150 | while request_count < max_request_count: 151 | result = stashbox_call_graphql(endpoint, query, variables) 152 | request_count += 1 153 | variables["input"]["page"] += 1 154 | if not result: 155 | break 156 | query_performers = result.get("queryPerformers") 157 | if not query_performers: 158 | break 159 | if total_count is None: 160 | total_count = query_performers.get("count") 161 | max_request_count = math.ceil(total_count / per_page) 162 | 163 | log.info(f'Received page {variables["input"]["page"] - 1} of {max_request_count}') 164 | for performer in query_performers.get("performers"): 165 | performer_id = performer['id'] 166 | if performer_id not in performercounts: 167 | performercounts[performer_id] = 1 168 | else: 169 | performercounts[performer_id] += 1 170 | performers.update([performer["id"] for performer in query_performers.get("performers")]) 171 | return performers, performercounts 172 | 173 | def set_stashbox_favorite_performers(db: StashDatabase, endpoint): 174 | stash_ids = set([row["stash_id"] for row in db.fetchall("""SELECT DISTINCT b.stash_id 175 | FROM performers a 176 | JOIN performer_stash_ids b 177 | ON a.id = b.performer_id 178 | WHERE a.favorite = 1""")]) 179 | log.info(f'Stashbox endpoint {endpoint}') 180 | log.info(f'Stash {len(stash_ids)} favorite performers') 181 | log.info(f'Fetching Stashbox favorite performers...') 182 | stashbox_stash_ids, performercounts = get_favorite_performers_from_stashbox(endpoint) 183 | log.info(f'Stashbox {len(stashbox_stash_ids)} favorite performers') 184 | 185 | favorites_to_add = stash_ids - stashbox_stash_ids 186 | favorites_to_remove = stashbox_stash_ids - stash_ids 187 | log.info(f'{len(favorites_to_add)} favorites to add') 188 | log.info(f'{len(favorites_to_remove)} favorites to remove') 189 | 190 | for stash_id in favorites_to_add: 191 | update_stashbox_performer_favorite(endpoint, stash_id, True) 192 | log.info('Add done.') 193 | 194 | for stash_id in favorites_to_remove: 195 | update_stashbox_performer_favorite(endpoint, stash_id, False) 196 | log.info('Remove done.') 197 | 198 | for performer_id, count in performercounts.items(): 199 | if count > 1: 200 | log.info(f'Fixing duplicate stashbox favorite {performer_id} count={count}') 201 | update_stashbox_performer_favorite(endpoint, performer_id, False) 202 | update_stashbox_performer_favorite(endpoint, performer_id, True) 203 | log.info('Fixed duplicates.') 204 | 205 | def set_stashbox_favorite_performer(endpoint, stash_id, favorite): 206 | result = get_stashbox_performer_favorite(endpoint, stash_id) 207 | if not result: 208 | return 209 | if favorite != result["findPerformer"]["is_favorite"]: 210 | update_stashbox_performer_favorite(endpoint, stash_id, favorite) 211 | log.info(f'Updated Stashbox performer {stash_id} favorite={favorite}') 212 | else: 213 | log.info(f'Stashbox performer {stash_id} already in sync favorite={favorite}') -------------------------------------------------------------------------------- /plugins/userscript_functions/log.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import re 3 | # Log messages sent from a script scraper instance are transmitted via stderr and are 4 | # encoded with a prefix consisting of special character SOH, then the log 5 | # level (one of t, d, i, w or e - corresponding to trace, debug, info, 6 | # warning and error levels respectively), then special character 7 | # STX. 8 | # 9 | # The log.trace, log.debug, log.info, log.warning, and log.error methods, and their equivalent 10 | # formatted methods are intended for use by script scraper instances to transmit log 11 | # messages. 12 | # 13 | 14 | def __log(level_char: bytes, s): 15 | if level_char: 16 | lvl_char = "\x01{}\x02".format(level_char.decode()) 17 | s = re.sub(r"data:image.+?;base64(.+?')","[...]",str(s)) 18 | for x in s.split("\n"): 19 | print(lvl_char, x, file=sys.stderr, flush=True) 20 | 21 | 22 | def trace(s): 23 | __log(b't', s) 24 | 25 | 26 | def debug(s): 27 | __log(b'd', s) 28 | 29 | 30 | def info(s): 31 | __log(b'i', s) 32 | 33 | 34 | def warning(s): 35 | __log(b'w', s) 36 | 37 | 38 | def error(s): 39 | __log(b'e', s) -------------------------------------------------------------------------------- /plugins/userscript_functions/performer_url_regexes.txt: -------------------------------------------------------------------------------- 1 | (babepedia).*?\/babe\/(.*?)$ 2 | (bgafd).*?\/(?:gallery|details)\.php\/id\/(.*?)(?:\/.*?)?$ 3 | (boobpedia).*?\/boobs\/(.*?)$ 4 | (egafd).*?\/(?:gallery|details)\.php\/id\/(.*?)(?:\/.*?)?$ 5 | (eurobabeindex).*?\/sbandoindex\/(.*?)\.html$ 6 | (freeones).*?\/(.*?)(?:\/.*?)?$ 7 | (iafd).*?\/perfid=(.*?\/gender=.)\/ 8 | (imdb).*?\/(?:name|title)\/(.*?)(?:\/.*?)?$ 9 | (indexxx).*?\/m\/(.*?)$ 10 | (instagram).*?\/(.*?)\/?$ 11 | (onlyfans).*?\/(.*?)\/?$ 12 | (pornhub).*?\/(?:model|pornstar)\/(.*?)(?:\/.*?)?$ 13 | (pornteengirl).*?\/model\/(.*?)\.html$ 14 | (thenude).*?\/(?:.*?)_(.*?)\.html?$ 15 | (twitter).*?\/(.*?)\/?$ -------------------------------------------------------------------------------- /plugins/userscript_functions/studiodownloader.py: -------------------------------------------------------------------------------- 1 | """Based on https://github.com/scruffynerf/CommunityScrapers/tree/studiodownloader 2 | """ 3 | 4 | import sys 5 | import graphql 6 | import log 7 | 8 | try: 9 | import requests 10 | except ModuleNotFoundError: 11 | print("If you have pip (normally installed with python), run this command in a terminal (cmd): pip install requests)", file=sys.stderr) 12 | sys.exit() 13 | 14 | def call_graphql(query, variables=None): 15 | return graphql.callGraphQL(query, variables) 16 | 17 | def get_api_key(endpoint): 18 | query = """ 19 | query getstashbox { 20 | configuration { 21 | general { 22 | stashBoxes { 23 | name 24 | endpoint 25 | api_key 26 | } 27 | } 28 | } 29 | } 30 | """ 31 | 32 | result = call_graphql(query) 33 | # log.debug(result) 34 | boxapi_key = None 35 | for x in result["configuration"]["general"]["stashBoxes"]: 36 | # log.debug(x) 37 | if x["endpoint"] == endpoint: 38 | boxapi_key = x["api_key"] 39 | if not boxapi_key: 40 | log.error(f"Stashbox apikey for {endpoint} not found.") 41 | sys.exit(0) 42 | return boxapi_key 43 | 44 | def stashbox_call_graphql(endpoint, query, variables=None): 45 | boxapi_key = get_api_key(endpoint) 46 | # this is basically the same code as call_graphql except it calls out to the stashbox. 47 | 48 | headers = { 49 | "Accept-Encoding": "gzip, deflate, br", 50 | "Content-Type": "application/json", 51 | "Accept": "application/json", 52 | "Connection": "keep-alive", 53 | "DNT": "1", 54 | "ApiKey": boxapi_key 55 | } 56 | json = { 57 | 'query': query 58 | } 59 | if variables is not None: 60 | json['variables'] = variables 61 | try: 62 | response = requests.post(endpoint, json=json, headers=headers) 63 | if response.status_code == 200: 64 | result = response.json() 65 | if result.get("error"): 66 | for error in result["error"]["errors"]: 67 | raise Exception("GraphQL error: {}".format(error)) 68 | if result.get("data"): 69 | return result.get("data") 70 | elif response.status_code == 401: 71 | log.error( 72 | "[ERROR][GraphQL] HTTP Error 401, Unauthorised. You need to add a Stash box instance and API Key in your Stash config") 73 | return None 74 | else: 75 | raise ConnectionError( 76 | "GraphQL query failed:{} - {}".format(response.status_code, response.content)) 77 | except Exception as err: 78 | log.error(err) 79 | return None 80 | 81 | def get_id(obj): 82 | ids = [] 83 | for item in obj: 84 | ids.append(item['id']) 85 | return ids 86 | 87 | def get_studio_from_stashbox(endpoint, studio_stashid): 88 | query = """ 89 | query getStudio($id : ID!) { 90 | findStudio(id: $id) { 91 | name 92 | id 93 | images { 94 | url 95 | } 96 | parent { 97 | name 98 | id 99 | } 100 | } 101 | } 102 | """ 103 | 104 | variables = { 105 | "id": studio_stashid 106 | } 107 | result = stashbox_call_graphql(endpoint, query, variables) 108 | #log.debug(result["findStudio"]) 109 | if result: 110 | return result.get("findStudio") 111 | return 112 | 113 | def update_studio(studio, studio_data): 114 | query = """ 115 | mutation studioimageadd($input: StudioUpdateInput!) { 116 | studioUpdate(input: $input) { 117 | image_path 118 | parent_studio { 119 | id 120 | } 121 | } 122 | } 123 | """ 124 | 125 | parent_id = None 126 | if studio_data["parent"]: 127 | parent_stash_id = studio_data["parent"]["id"] 128 | log.debug(f'parent_stash_id: {parent_stash_id}') 129 | parent_studio = get_studio_by_stash_id(parent_stash_id) 130 | if parent_studio: 131 | parent_id = parent_studio["id"] 132 | log.debug(f'parent_id: {parent_id}') 133 | 134 | variables = { 135 | "input": { 136 | "id": studio["id"], 137 | "image": None, 138 | "parent_id": parent_id 139 | } 140 | } 141 | if studio_data["images"]: 142 | variables["input"]["image"] = studio_data["images"][0]["url"] 143 | call_graphql(query, variables) 144 | 145 | def get_studio(studio_id): 146 | query = """ 147 | query FindStudio($id: ID!) { 148 | findStudio(id: $id) { 149 | ...StudioData 150 | } 151 | } 152 | 153 | fragment StudioData on Studio { 154 | id 155 | name 156 | updated_at 157 | created_at 158 | stash_ids { 159 | endpoint 160 | stash_id 161 | } 162 | } 163 | """ 164 | 165 | variables = { "id": studio_id } 166 | result = call_graphql(query, variables) 167 | if result: 168 | # log.debug(result) 169 | return result["findStudio"] 170 | 171 | def get_studio_by_stash_id(stash_id): 172 | query = """ 173 | query FindStudios($studio_filter: StudioFilterType) { 174 | findStudios(studio_filter: $studio_filter) { 175 | count 176 | studios { 177 | id 178 | } 179 | } 180 | } 181 | """ 182 | 183 | variables = { 184 | "studio_filter": { 185 | "stash_id": { 186 | "value": stash_id, 187 | "modifier": "EQUALS" 188 | } 189 | } 190 | } 191 | result = call_graphql(query, variables) 192 | if not result['findStudios']['studios']: 193 | return None 194 | return result['findStudios']['studios'][0] 195 | 196 | def update_studio_from_stashbox(studio_id, endpoint, remote_site_id): 197 | studio = get_studio(studio_id) 198 | log.debug(studio) 199 | if not studio: 200 | return 201 | studioboxdata = get_studio_from_stashbox(endpoint, remote_site_id) 202 | log.debug(studioboxdata) 203 | if studioboxdata: 204 | result = update_studio(studio, studioboxdata) -------------------------------------------------------------------------------- /plugins/userscript_functions/userscript_functions.py: -------------------------------------------------------------------------------- 1 | import config_manager 2 | import json 3 | import log 4 | import os 5 | import pathlib 6 | import sys 7 | import subprocess 8 | from favorite_performers_sync import set_stashbox_favorite_performers, set_stashbox_favorite_performer 9 | from studiodownloader import update_studio_from_stashbox 10 | from audit_performer_urls import audit_performer_urls 11 | try: 12 | from stashlib.stash_database import StashDatabase 13 | from stashlib.stash_interface import StashInterface 14 | except ModuleNotFoundError: 15 | print("If you have pip (normally installed with python), run this command in a terminal (cmd): pip install pystashlib)", file=sys.stderr) 16 | sys.exit() 17 | 18 | json_input = json.loads(sys.stdin.read()) 19 | name = json_input['args']['name'] 20 | 21 | configpath = os.path.join(pathlib.Path(__file__).parent.resolve(), 'config.ini') 22 | 23 | def get_database_config(): 24 | client = StashInterface(json_input["server_connection"]) 25 | result = client.callGraphQL("""query Configuration { configuration { general { databasePath, blobsPath, blobsStorage } } }""") 26 | database_path = result["configuration"]["general"]["databasePath"] 27 | blobs_path = result["configuration"]["general"]["blobsPath"] 28 | blobs_storage = result["configuration"]["general"]["blobsStorage"] 29 | log.debug(f"databasePath: {database_path}") 30 | return database_path, blobs_path, blobs_storage 31 | 32 | if name == 'explorer': 33 | path = json_input['args']['path'] 34 | log.debug(f"{name}: {path}") 35 | subprocess.Popen(f'explorer "{path}"') 36 | elif name == 'mediaplayer': 37 | mediaplayer_path = config_manager.get_config_value(configpath, 'MEDIAPLAYER', 'path') 38 | path = json_input['args']['path'] 39 | log.debug(f"mediaplayer_path: {mediaplayer_path}") 40 | log.debug(f"{name}: {path}") 41 | subprocess.Popen([mediaplayer_path, path]) 42 | elif name == 'update_studio': 43 | studio_id = json_input['args']['studio_id'] 44 | endpoint = json_input['args']['endpoint'] 45 | remote_site_id = json_input['args']['remote_site_id'] 46 | log.debug(f"{name}: {studio_id} {endpoint} {remote_site_id}") 47 | update_studio_from_stashbox(studio_id, endpoint, remote_site_id) 48 | log.debug(f"{name}: Done.") 49 | elif name == 'audit_performer_urls': 50 | try: 51 | db = StashDatabase(*get_database_config()) 52 | except Exception as e: 53 | log.error(str(e)) 54 | sys.exit(0) 55 | audit_performer_urls(db) 56 | db.close() 57 | elif name == 'update_config_value': 58 | log.debug(f"configpath: {configpath}") 59 | section_key = json_input['args']['section_key'] 60 | prop_name = json_input['args']['prop_name'] 61 | value = json_input['args']['value'] 62 | if not section_key or not prop_name: 63 | log.error(f"{name}: Missing args") 64 | sys.exit(0) 65 | log.debug(f"{name}: [{section_key}][{prop_name}] = {value}") 66 | config_manager.update_config_value(configpath, section_key, prop_name, value) 67 | elif name == 'get_config_value': 68 | log.debug(f"configpath: {configpath}") 69 | section_key = json_input['args']['section_key'] 70 | prop_name = json_input['args']['prop_name'] 71 | if not section_key or not prop_name: 72 | log.error(f"{name}: Missing args") 73 | sys.exit(0) 74 | value = config_manager.get_config_value(configpath, section_key, prop_name) 75 | log.debug(f"{name}: [{section_key}][{prop_name}] = {value}") 76 | elif name == 'favorite_performers_sync': 77 | endpoint = json_input['args']['endpoint'] 78 | try: 79 | db = StashDatabase(*get_database_config()) 80 | except Exception as e: 81 | log.error(str(e)) 82 | sys.exit(0) 83 | set_stashbox_favorite_performers(db, endpoint) 84 | db.close() 85 | elif name == 'favorite_performer_sync': 86 | endpoint = json_input['args']['endpoint'] 87 | stash_id = json_input['args']['stash_id'] 88 | favorite = json_input['args']['favorite'] 89 | log.debug(f"Favorite performer sync: endpoint={endpoint}, stash_id={stash_id}, favorite={favorite}") 90 | set_stashbox_favorite_performer(endpoint, stash_id, favorite) -------------------------------------------------------------------------------- /plugins/userscript_functions/userscript_functions.yml: -------------------------------------------------------------------------------- 1 | name: Userscript Functions 2 | description: Tasks for userscripts 3 | url: https://github.com/7dJx1qP/stash-userscripts 4 | version: 0.6.0 5 | exec: 6 | - python 7 | - "{pluginDir}/userscript_functions.py" 8 | interface: raw 9 | tasks: 10 | - name: Open in File Explorer 11 | description: Open folder 12 | defaultArgs: 13 | name: explorer 14 | path: null 15 | - name: Open in Media Player 16 | description: Open video 17 | defaultArgs: 18 | name: mediaplayer 19 | path: null 20 | - name: Update Studio 21 | description: Update studio 22 | defaultArgs: 23 | name: update_studio 24 | studio_id: null 25 | endpoint: null 26 | remote_site_id: null 27 | - name: Audit performer urls 28 | description: Audit performer IAFD urls for dupes 29 | defaultArgs: 30 | name: audit_performer_urls 31 | - name: Update Config Value 32 | description: Update value in config.ini 33 | defaultArgs: 34 | name: update_config_value 35 | section_key: null 36 | prop_name: null 37 | value: null 38 | - name: Get Config Value 39 | description: Get value in config.ini 40 | defaultArgs: 41 | name: get_config_value 42 | section_key: null 43 | prop_name: null 44 | - name: Set Stashbox Favorite Performers 45 | description: Set Stashbox favorite performers according to stash favorites 46 | defaultArgs: 47 | name: favorite_performers_sync 48 | endpoint: null 49 | - name: Set Stashbox Favorite Performer 50 | description: Update Stashbox performer favorite status 51 | defaultArgs: 52 | name: favorite_performer_sync 53 | endpoint: null 54 | stash_id: null 55 | favorite: null -------------------------------------------------------------------------------- /src/body/Stash Batch Query Edit.user.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | const { 5 | stash, 6 | Stash, 7 | waitForElementId, 8 | waitForElementClass, 9 | waitForElementByXpath, 10 | getElementByXpath, 11 | getElementsByXpath, 12 | getClosestAncestor, 13 | createElementFromHTML, 14 | updateTextInput, 15 | sortElementChildren, 16 | } = unsafeWindow.stash; 17 | 18 | let running = false; 19 | const buttons = []; 20 | let maxCount = 0; 21 | 22 | function run(videoExtensions) { 23 | if (!running) return; 24 | const button = buttons.pop(); 25 | stash.setProgress((maxCount - buttons.length) / maxCount * 100); 26 | if (button) { 27 | const searchItem = getClosestAncestor(button, '.search-item'); 28 | const { 29 | data, 30 | queryInput, 31 | } = stash.parseSearchItem(searchItem); 32 | 33 | const includeStudio = document.getElementById('query-edit-include-studio').checked; 34 | const includeDate = document.getElementById('query-edit-include-date').checked; 35 | const includePerformers = document.querySelector('input[name="query-edit-include-performers"]:checked').value; 36 | const includeTitle = document.getElementById('query-edit-include-title').checked; 37 | const applyBlacklist = document.getElementById('query-edit-apply-blacklist').checked; 38 | const useStashID = document.getElementById('query-edit-use-stashid').checked; 39 | 40 | const videoExtensionRegexes = videoExtensions.map(s => [new RegExp(`.${s}$`, "gi"), '']); 41 | const blacklist = []; 42 | if (applyBlacklist) { 43 | const blacklistTags = getElementsByXpath("//div[@class='tagger-container-header']//h5[text()='Blacklist']/following-sibling::span/text()") 44 | let node = null; 45 | while (node = blacklistTags.iterateNext()) { 46 | blacklist.push([new RegExp(node.nodeValue, "gi"), '']); 47 | } 48 | } 49 | blacklist.push([/[_-]/gi, ' ']); 50 | blacklist.push([/[^a-z0-9\s]/gi, '']); 51 | if (data.date) { 52 | blacklist.push([new RegExp(data.date.replaceAll('-', ''), "gi"), '']); 53 | } 54 | 55 | const filterBlacklist = (s, regexes) => regexes.reduce((acc, [regex, repl]) => { 56 | return acc.replace(regex, repl); 57 | }, s) 58 | 59 | const queryData = []; 60 | const stashId = data.stash_ids[0]?.stash_id; 61 | if (useStashID && stashId) { 62 | queryData.push(stashId); 63 | } 64 | else { 65 | if (data.date && includeDate) queryData.push(data.date); 66 | if (data.studio && includeStudio) queryData.push(filterBlacklist(data.studio.name, blacklist)); 67 | if (data.performers && includePerformers !== 'none') { 68 | for (const performer of data.performers) { 69 | if (includePerformers === 'all' || (includePerformers === 'female-only' && performer.gender.toUpperCase() === 'FEMALE')) { 70 | queryData.push(filterBlacklist(performer.name, blacklist)); 71 | } 72 | } 73 | } 74 | if (data.title && includeTitle) queryData.push(filterBlacklist(data.title, videoExtensionRegexes.concat(blacklist))); 75 | } 76 | 77 | const queryValue = queryData.join(' '); 78 | updateTextInput(queryInput, queryValue); 79 | 80 | setTimeout(() => run(videoExtensions), 50); 81 | } 82 | else { 83 | stop(); 84 | } 85 | } 86 | 87 | const queryEditConfigId = 'query-edit-config'; 88 | const btnId = 'batch-query-edit'; 89 | const startLabel = 'Query Edit All'; 90 | const stopLabel = 'Stop Query Edit'; 91 | const btn = document.createElement("button"); 92 | btn.setAttribute("id", btnId); 93 | btn.classList.add('btn', 'btn-primary', 'ml-3'); 94 | btn.innerHTML = startLabel; 95 | btn.onclick = () => { 96 | if (running) { 97 | stop(); 98 | } 99 | else { 100 | start(); 101 | } 102 | }; 103 | 104 | function start() { 105 | btn.innerHTML = stopLabel; 106 | btn.classList.remove('btn-primary'); 107 | btn.classList.add('btn-danger'); 108 | running = true; 109 | stash.setProgress(0); 110 | buttons.length = 0; 111 | for (const button of document.querySelectorAll('.btn.btn-primary')) { 112 | if (button.innerText === 'Search') { 113 | buttons.push(button); 114 | } 115 | } 116 | maxCount = buttons.length; 117 | const reqData = { 118 | "variables": {}, 119 | "query": `query Configuration { 120 | configuration { 121 | general { 122 | videoExtensions 123 | } 124 | } 125 | }` 126 | } 127 | stash.callGQL(reqData).then(data => { 128 | run(data.data.configuration.general.videoExtensions); 129 | }); 130 | } 131 | 132 | function stop() { 133 | btn.innerHTML = startLabel; 134 | btn.classList.remove('btn-danger'); 135 | btn.classList.add('btn-primary'); 136 | running = false; 137 | stash.setProgress(0); 138 | } 139 | 140 | stash.addEventListener('tagger:mutations:header', evt => { 141 | const el = getElementByXpath("//button[text()='Scrape All']"); 142 | if (el && !document.getElementById(btnId)) { 143 | const container = el.parentElement; 144 | container.appendChild(btn); 145 | sortElementChildren(container); 146 | el.classList.add('ml-3'); 147 | } 148 | }); 149 | 150 | stash.addEventListener('tagger:configuration', evt => { 151 | const el = evt.detail; 152 | if (!document.getElementById(queryEditConfigId)) { 153 | const configContainer = el.parentElement; 154 | const queryEditConfig = createElementFromHTML(` 155 |
156 |
Query Edit Configuration
157 |
158 |
159 |
160 | 161 | 162 |
163 | Toggle whether date is included in query. 164 |
165 |
166 |
167 | 168 | 169 |
170 | Toggle whether studio is included in query. 171 |
172 |
173 |
174 | 175 | 176 | 177 | 178 | 179 | 180 |
181 | Toggle whether performers are included in query. 182 |
183 |
184 |
185 | 186 | 187 |
188 | Toggle whether title is included in query. 189 |
190 |
191 |
192 | 193 | 194 |
195 | Toggle whether blacklist is applied to query. 196 |
197 |
198 |
199 | 200 | 201 |
202 | Toggle whether query is set to StashID if scene has one. 203 |
204 |
205 |
206 | `); 207 | configContainer.appendChild(queryEditConfig); 208 | loadSettings(); 209 | } 210 | }); 211 | 212 | async function loadSettings() { 213 | for (const input of document.querySelectorAll(`#${queryEditConfigId} input`)) { 214 | input.checked = await GM.getValue(input.id, input.dataset.default === 'true'); 215 | input.addEventListener('change', async () => { 216 | await GM.setValue(input.id, input.checked); 217 | }); 218 | } 219 | } 220 | 221 | })(); -------------------------------------------------------------------------------- /src/body/Stash Batch Save.user.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const { 5 | stash, 6 | Stash, 7 | waitForElementId, 8 | waitForElementClass, 9 | waitForElementByXpath, 10 | getElementByXpath, 11 | getElementsByXpath, 12 | getClosestAncestor, 13 | sortElementChildren, 14 | createElementFromHTML, 15 | } = unsafeWindow.stash; 16 | 17 | document.body.appendChild(document.createElement('style')).textContent = ` 18 | .search-item > div.row:first-child > div.col-md-6.my-1 > div:first-child { display: flex; flex-direction: column; } 19 | .tagger-remove { order: 10; } 20 | `; 21 | 22 | let running = false; 23 | const buttons = []; 24 | let maxCount = 0; 25 | let sceneId = null; 26 | 27 | function run() { 28 | if (!running) return; 29 | const button = buttons.pop(); 30 | stash.setProgress((maxCount - buttons.length) / maxCount * 100); 31 | if (button) { 32 | const searchItem = getClosestAncestor(button, '.search-item'); 33 | if (searchItem.classList.contains('d-none')) { 34 | setTimeout(() => { 35 | run(); 36 | }, 0); 37 | return; 38 | } 39 | 40 | const { id } = stash.parseSearchItem(searchItem); 41 | sceneId = id; 42 | if (!button.disabled) { 43 | button.click(); 44 | } 45 | else { 46 | buttons.push(button); 47 | } 48 | } 49 | else { 50 | stop(); 51 | } 52 | } 53 | 54 | function processSceneUpdate(evt) { 55 | if (running && evt.detail.data?.sceneUpdate?.id === sceneId) { 56 | setTimeout(() => { 57 | run(); 58 | }, 0); 59 | } 60 | } 61 | 62 | const btnId = 'batch-save'; 63 | const startLabel = 'Save All'; 64 | const stopLabel = 'Stop Save'; 65 | const btn = document.createElement("button"); 66 | btn.setAttribute("id", btnId); 67 | btn.classList.add('btn', 'btn-primary', 'ml-3'); 68 | btn.innerHTML = startLabel; 69 | btn.onclick = () => { 70 | if (running) { 71 | stop(); 72 | } 73 | else { 74 | start(); 75 | } 76 | }; 77 | 78 | function start() { 79 | if (!confirm("Are you sure you want to batch save?")) return; 80 | btn.innerHTML = stopLabel; 81 | btn.classList.remove('btn-primary'); 82 | btn.classList.add('btn-danger'); 83 | running = true; 84 | stash.setProgress(0); 85 | buttons.length = 0; 86 | for (const button of document.querySelectorAll('.btn.btn-primary')) { 87 | if (button.innerText === 'Save') { 88 | buttons.push(button); 89 | } 90 | } 91 | maxCount = buttons.length; 92 | stash.addEventListener('stash:response', processSceneUpdate); 93 | run(); 94 | } 95 | 96 | function stop() { 97 | btn.innerHTML = startLabel; 98 | btn.classList.remove('btn-danger'); 99 | btn.classList.add('btn-primary'); 100 | running = false; 101 | stash.setProgress(0); 102 | sceneId = null; 103 | stash.removeEventListener('stash:response', processSceneUpdate); 104 | } 105 | 106 | stash.addEventListener('tagger:mutations:header', evt => { 107 | const el = getElementByXpath("//button[text()='Scrape All']"); 108 | if (el && !document.getElementById(btnId)) { 109 | const container = el.parentElement; 110 | container.appendChild(btn); 111 | sortElementChildren(container); 112 | el.classList.add('ml-3'); 113 | } 114 | }); 115 | 116 | function checkSaveButtonDisplay() { 117 | const taggerContainer = document.querySelector('.tagger-container'); 118 | const saveButton = getElementByXpath("//button[text()='Save']", taggerContainer); 119 | btn.style.display = saveButton ? 'inline-block' : 'none'; 120 | } 121 | 122 | stash.addEventListener('tagger:mutations:searchitems', checkSaveButtonDisplay); 123 | 124 | async function initRemoveButtons() { 125 | const nodes = getElementsByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']"); 126 | const buttons = []; 127 | let node = null; 128 | while (node = nodes.iterateNext()) { 129 | buttons.push(node); 130 | } 131 | for (const button of buttons) { 132 | const searchItem = getClosestAncestor(button, '.search-item'); 133 | 134 | const removeButtonExists = searchItem.querySelector('.tagger-remove'); 135 | if (removeButtonExists) { 136 | continue; 137 | } 138 | 139 | const removeEl = createElementFromHTML('
'); 140 | const removeButton = removeEl.querySelector('button'); 141 | button.parentElement.parentElement.appendChild(removeEl); 142 | removeButton.addEventListener('click', async () => { 143 | searchItem.classList.add('d-none'); 144 | }); 145 | } 146 | } 147 | 148 | stash.addEventListener('page:studio:scenes', function () { 149 | waitForElementByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']", initRemoveButtons); 150 | }); 151 | 152 | stash.addEventListener('page:performer:scenes', function () { 153 | waitForElementByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']", initRemoveButtons); 154 | }); 155 | 156 | stash.addEventListener('page:scenes', function () { 157 | waitForElementByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']", initRemoveButtons); 158 | }); 159 | })(); -------------------------------------------------------------------------------- /src/body/Stash Batch Search.user.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | const DEFAULT_DELAY = 200; 5 | let delay = DEFAULT_DELAY; 6 | 7 | const { 8 | stash, 9 | Stash, 10 | waitForElementId, 11 | waitForElementClass, 12 | waitForElementByXpath, 13 | getElementByXpath, 14 | sortElementChildren, 15 | createElementFromHTML, 16 | } = unsafeWindow.stash; 17 | 18 | let running = false; 19 | const buttons = []; 20 | let maxCount = 0; 21 | 22 | function run() { 23 | if (!running) return; 24 | const button = buttons.pop(); 25 | stash.setProgress((maxCount - buttons.length) / maxCount * 100); 26 | if (button) { 27 | if (!button.disabled) { 28 | button.click(); 29 | } 30 | else { 31 | buttons.push(button); 32 | } 33 | setTimeout(run, delay); 34 | } 35 | else { 36 | stop(); 37 | } 38 | } 39 | 40 | const btnId = 'batch-search'; 41 | const startLabel = 'Search All'; 42 | const stopLabel = 'Stop Search'; 43 | const btn = document.createElement("button"); 44 | btn.setAttribute("id", btnId); 45 | btn.classList.add('btn', 'btn-primary', 'ml-3'); 46 | btn.innerHTML = startLabel; 47 | btn.onclick = () => { 48 | if (running) { 49 | stop(); 50 | } 51 | else { 52 | start(); 53 | } 54 | }; 55 | 56 | function start() { 57 | btn.innerHTML = stopLabel; 58 | btn.classList.remove('btn-primary'); 59 | btn.classList.add('btn-danger'); 60 | running = true; 61 | stash.setProgress(0); 62 | buttons.length = 0; 63 | for (const button of document.querySelectorAll('.btn.btn-primary')) { 64 | if (button.innerText === 'Search') { 65 | buttons.push(button); 66 | } 67 | } 68 | maxCount = buttons.length; 69 | run(); 70 | } 71 | 72 | function stop() { 73 | btn.innerHTML = startLabel; 74 | btn.classList.remove('btn-danger'); 75 | btn.classList.add('btn-primary'); 76 | running = false; 77 | stash.setProgress(0); 78 | } 79 | 80 | stash.addEventListener('page:performers', function () { 81 | waitForElementByXpath("//button[text()='Batch Update Performers']", function (xpath, el) { 82 | if (!document.getElementById(btnId)) { 83 | const container = el.parentElement; 84 | 85 | container.appendChild(btn); 86 | } 87 | }); 88 | }); 89 | 90 | stash.addEventListener('tagger:mutations:header', evt => { 91 | const el = getElementByXpath("//button[text()='Scrape All']"); 92 | if (el && !document.getElementById(btnId)) { 93 | const container = el.parentElement; 94 | container.appendChild(btn); 95 | sortElementChildren(container); 96 | el.classList.add('ml-3'); 97 | } 98 | }); 99 | 100 | const batchSearchConfigId = 'batch-search-config'; 101 | 102 | stash.addEventListener('tagger:configuration', evt => { 103 | const el = evt.detail; 104 | if (!document.getElementById(batchSearchConfigId)) { 105 | const configContainer = el.parentElement; 106 | const batchSearchConfig = createElementFromHTML(` 107 |
108 |
Batch Search
109 |
110 |
111 |
112 | 113 |
114 | 115 |
116 |
117 | Wait time in milliseconds between scene searches. 118 |
119 |
120 |
121 | `); 122 | configContainer.appendChild(batchSearchConfig); 123 | loadSettings(); 124 | } 125 | }); 126 | 127 | async function loadSettings() { 128 | for (const input of document.querySelectorAll(`#${batchSearchConfigId} input[type="text"]`)) { 129 | input.value = parseInt(await GM.getValue(input.id, input.dataset.default)); 130 | delay = input.value; 131 | input.addEventListener('change', async () => { 132 | let value = parseInt(input.value.trim()) 133 | if (isNaN(value)) { 134 | value = parseInt(input.dataset.default); 135 | } 136 | input.value = value; 137 | delay = value; 138 | await GM.setValue(input.id, value); 139 | }); 140 | } 141 | } 142 | 143 | })(); -------------------------------------------------------------------------------- /src/body/Stash Markdown.user.js: -------------------------------------------------------------------------------- 1 | /* global marked */ 2 | 3 | (function () { 4 | 'use strict'; 5 | 6 | const { 7 | stash, 8 | Stash, 9 | waitForElementId, 10 | waitForElementClass, 11 | waitForElementByXpath, 12 | getElementByXpath, 13 | insertAfter, 14 | reloadImg, 15 | } = unsafeWindow.stash; 16 | 17 | function processMarkdown(el) { 18 | el.innerHTML = marked.parse(el.innerHTML); 19 | } 20 | 21 | stash.addEventListener('page:tag:any', function () { 22 | waitForElementByXpath("//span[contains(@class, 'detail-item-value') and contains(@class, 'description')]", function (xpath, el) { 23 | el.style.display = 'block'; 24 | el.style.whiteSpace = 'initial'; 25 | processMarkdown(el); 26 | }); 27 | }); 28 | 29 | stash.addEventListener('page:tags', function () { 30 | waitForElementByXpath("//div[contains(@class, 'tag-description')]", function (xpath, el) { 31 | for (const node of document.querySelectorAll('.tag-description')) { 32 | processMarkdown(node); 33 | } 34 | }); 35 | }); 36 | })(); -------------------------------------------------------------------------------- /src/body/Stash Markers Autoscroll.user.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const { 5 | stash, 6 | Stash, 7 | waitForElementId, 8 | waitForElementClass, 9 | waitForElementByXpath, 10 | getElementByXpath, 11 | insertAfter, 12 | reloadImg, 13 | } = unsafeWindow.stash; 14 | 15 | let markers = []; 16 | let markersFilter; 17 | let sceneMarkerFilter; 18 | let markerResponseCache = {}; 19 | 20 | let markerFetchInterval; 21 | 22 | const cloneJSON = obj => JSON.parse(JSON.stringify(obj)); 23 | 24 | // intercept the page's initial markers request so the same query filter can be used for our requests 25 | // we will get more markers by making the same request but increment the page count each time 26 | stash.addEventListener('stash:request', evt => { 27 | if (evt.detail?.body) { 28 | const body = JSON.parse(evt.detail.body); 29 | if (body.operationName === "FindSceneMarkers") { 30 | markersFilter = body.variables.filter; 31 | sceneMarkerFilter = body.variables.scene_marker_filter; 32 | clearInterval(markerFetchInterval); 33 | } 34 | } 35 | }); 36 | 37 | stash.addEventListener('stash:response', async evt => { 38 | if (evt?.detail?.data?.findSceneMarkers?.__typename === 'FindSceneMarkersResultType') { 39 | const data = evt.detail.data.findSceneMarkers; 40 | maxMarkers = data.count; 41 | maxPage = Math.ceil(maxMarkers / markersFilter.per_page); 42 | markers = data.scene_markers; 43 | for (let i = 0; i < markers.length; i++) { 44 | markerIndex[i] = i; 45 | } 46 | markerResponseCache[window.location.search] = { 47 | markersFilter: cloneJSON(markersFilter), 48 | sceneMarkerFilter: cloneJSON(sceneMarkerFilter), 49 | data 50 | }; 51 | await fetchMarkers(); // buffer next page of markers 52 | markerFetchInterval = setInterval(fetchMarkers, 10000); // get next page of markers every 20 seconds 53 | } 54 | }); 55 | 56 | var _wr = function(type) { 57 | var orig = history[type]; 58 | return function() { 59 | var rv = orig.apply(this, arguments); 60 | var e = new Event(type); 61 | e.arguments = arguments; 62 | window.dispatchEvent(e); 63 | return rv; 64 | }; 65 | }; 66 | history.pushState = _wr('replaceState'); 67 | history.replaceState = _wr('replaceState'); 68 | 69 | window.addEventListener('replaceState', async function () { 70 | if (markerResponseCache.hasOwnProperty(window.location.search)) { 71 | markersFilter = cloneJSON(markerResponseCache[window.location.search].markersFilter); 72 | sceneMarkerFilter = cloneJSON(markerResponseCache[window.location.search].sceneMarkerFilter); 73 | clearInterval(markerFetchInterval); 74 | 75 | const data = markerResponseCache[window.location.search].data; 76 | maxMarkers = data.count; 77 | maxPage = Math.ceil(maxMarkers / markersFilter.per_page); 78 | markers = data.scene_markers; 79 | for (let i = 0; i < markers.length; i++) { 80 | markerIndex[i] = i; 81 | } 82 | await fetchMarkers(); // buffer next page of markers 83 | markerFetchInterval = setInterval(fetchMarkers, 10000); // get next page of markers every 20 seconds 84 | } 85 | }); 86 | 87 | function fmtMSS(s) { 88 | return(s - (s %= 60)) / 60 + (9 < s ? ':': ':0') + s 89 | } 90 | 91 | let maxPage = 1; 92 | let maxMarkers = 1; 93 | let scrollSize = 1; 94 | let markerIndex = []; 95 | let playbackRate = 1; 96 | let videoEls = []; 97 | 98 | async function getMarkers() { 99 | const reqData = { 100 | "variables": { 101 | "filter": markersFilter, 102 | "scene_marker_filter": sceneMarkerFilter 103 | }, 104 | "query": `query FindSceneMarkers($filter: FindFilterType, $scene_marker_filter: SceneMarkerFilterType) { 105 | findSceneMarkers(filter: $filter, scene_marker_filter: $scene_marker_filter) { 106 | count 107 | scene_markers { 108 | id 109 | seconds 110 | stream 111 | screenshot 112 | scene { 113 | id 114 | } 115 | primary_tag { 116 | name 117 | } 118 | title 119 | tags { 120 | name 121 | } 122 | } 123 | } 124 | }` 125 | }; 126 | const data = (await stash.callGQL(reqData)).data.findSceneMarkers; 127 | maxMarkers = data.count; 128 | maxPage = Math.ceil(maxMarkers / markersFilter.per_page); 129 | return data.scene_markers.filter(marker => marker.stream); 130 | } 131 | 132 | async function fetchMarkers() { 133 | markersFilter.page++; 134 | if (markersFilter.page > maxPage) { 135 | markersFilter.page = 1; 136 | } 137 | markers = markers.concat(await getMarkers()); 138 | } 139 | 140 | stash.addEventListener('page:markers', function () { 141 | waitForElementClass("btn-toolbar", function () { 142 | if (!document.getElementById('scroll-size-input')) { 143 | const toolbar = document.querySelector(".btn-toolbar"); 144 | 145 | const newGroup = document.createElement('div'); 146 | newGroup.classList.add('ml-2', 'mb-2', 'd-none', 'd-sm-inline-flex'); 147 | toolbar.appendChild(newGroup); 148 | 149 | const scrollSizeInput = document.createElement('input'); 150 | scrollSizeInput.type = 'number'; 151 | scrollSizeInput.setAttribute('id', 'scroll-size-input'); 152 | scrollSizeInput.classList.add('ml-1', 'btn-secondary', 'form-control'); 153 | scrollSizeInput.setAttribute('min', '0'); 154 | scrollSizeInput.setAttribute('max', markersFilter.per_page); 155 | scrollSizeInput.value = scrollSize; 156 | scrollSizeInput.addEventListener('change', () => { 157 | scrollSize = parseInt(scrollSizeInput.value); 158 | }); 159 | newGroup.appendChild(scrollSizeInput); 160 | } 161 | if (!document.getElementById('playback-rate-input')) { 162 | const toolbar = document.querySelector(".btn-toolbar"); 163 | 164 | const newGroup = document.createElement('div'); 165 | newGroup.classList.add('ml-2', 'mb-2', 'd-none', 'd-sm-inline-flex'); 166 | toolbar.appendChild(newGroup); 167 | 168 | const playbackRateInput = document.createElement('input'); 169 | playbackRateInput.type = 'range'; 170 | playbackRateInput.setAttribute('id', 'playback-rate-input'); 171 | playbackRateInput.classList.add('zoom-slider', 'ml-1', 'form-control-range'); 172 | playbackRateInput.setAttribute('min', '0.25'); 173 | playbackRateInput.setAttribute('max', '2'); 174 | playbackRateInput.setAttribute('step', '0.25'); 175 | playbackRateInput.value = playbackRate; 176 | playbackRateInput.addEventListener('change', () => { 177 | playbackRate = parseFloat(playbackRateInput.value); 178 | for (const videoEl of videoEls) { 179 | videoEl.playbackRate = playbackRate; 180 | } 181 | }); 182 | newGroup.appendChild(playbackRateInput); 183 | } 184 | }); 185 | 186 | waitForElementClass('wall-item-anchor', async function (className, els) { 187 | //await fetchMarkers(); // load initial markers page 188 | //await fetchMarkers(); // buffer next page of markers 189 | for (let i = 0; i < els.length; i++) { 190 | const el = els[i]; 191 | const video = el.querySelector('video'); 192 | video.removeAttribute('loop'); 193 | video.playbackRate = playbackRate; 194 | videoEls.push(video); 195 | markerIndex[i] = i; 196 | video.parentElement.addEventListener('click', evt => { 197 | // suppress click, so clicking marker goes to scene specified by anchor link 198 | // otherwise it goes to scene specified by original marker 199 | evt.stopPropagation(); 200 | }); 201 | video.addEventListener('ended', async evt => { 202 | markerIndex[i] += scrollSize; 203 | markerIndex[i] %= maxMarkers; // loops back to beginning if past end 204 | const marker = markers[markerIndex[i]]; 205 | evt.target.src = marker.stream; 206 | evt.target.playbackRate = playbackRate; 207 | evt.target.setAttribute('poster', marker.screenshot); 208 | evt.target.play(); 209 | evt.target.parentElement.setAttribute('href', `/scenes/${marker.scene.id}?t=${marker.seconds}`); 210 | 211 | // update marker title and tags 212 | evt.target.nextSibling.innerHTML = ''; 213 | 214 | const markerTitle = document.createElement('div'); 215 | markerTitle.innerText = `${marker.title} - ${fmtMSS(marker.seconds)}`; 216 | evt.target.nextSibling.appendChild(markerTitle); 217 | 218 | const markerPrimaryTag = document.createElement('span'); 219 | markerPrimaryTag.classList.add('wall-tag'); 220 | markerPrimaryTag.innerText = marker.primary_tag.name; 221 | evt.target.nextSibling.appendChild(markerPrimaryTag); 222 | 223 | for (const tag of marker.tags) { 224 | const markerTag = document.createElement('span'); 225 | markerTag.classList.add('wall-tag'); 226 | markerTag.innerText = tag.name; 227 | evt.target.nextSibling.appendChild(markerTag); 228 | } 229 | }); 230 | } 231 | }); 232 | }); 233 | })(); -------------------------------------------------------------------------------- /src/body/Stash New Performer Filter Button.user.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const { 5 | stash, 6 | Stash, 7 | waitForElementId, 8 | waitForElementClass, 9 | waitForElementByXpath, 10 | getElementByXpath, 11 | } = unsafeWindow.stash; 12 | 13 | stash.addEventListener('page:performers', function () { 14 | waitForElementClass("btn-toolbar", function () { 15 | if (!document.getElementById('new-performer-filter')) { 16 | const toolbar = document.querySelector(".btn-toolbar"); 17 | 18 | const newGroup = document.createElement('div'); 19 | newGroup.classList.add('mx-2', 'mb-2', 'd-flex'); 20 | toolbar.appendChild(newGroup); 21 | 22 | const newButton = document.createElement("a"); 23 | newButton.setAttribute("id", "new-performer-filter"); 24 | newButton.classList.add('btn', 'btn-secondary'); 25 | newButton.innerHTML = 'New Performers'; 26 | newButton.href = `${stash.serverUrl}/performers?disp=3&sortby=created_at&sortdir=desc`; 27 | newGroup.appendChild(newButton); 28 | } 29 | }); 30 | }); 31 | })(); -------------------------------------------------------------------------------- /src/body/Stash Open Media Player.user.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const { 5 | stash, 6 | Stash, 7 | waitForElementId, 8 | waitForElementClass, 9 | waitForElementByXpath, 10 | getElementByXpath, 11 | } = unsafeWindow.stash; 12 | 13 | const MIN_REQUIRED_PLUGIN_VERSION = '0.4.0'; 14 | 15 | function openMediaPlayerTask(path) { 16 | // fixes decodeURI breaking on %'s because they are not encoded 17 | const encodedPctPath = path.replace(/%([^\d].)/, "%25$1"); 18 | // decode encoded path but then encode % and # otherwise VLC breaks 19 | const encodedPath = decodeURI(encodedPctPath).replaceAll('%', '%25').replaceAll('#', '%23'); 20 | 21 | stash.runPluginTask("userscript_functions", "Open in Media Player", {"key":"path", "value":{"str": encodedPath}}); 22 | } 23 | 24 | // scene filepath open with Media Player 25 | stash.addEventListener('page:scene', function () { 26 | waitForElementClass('scene-file-info', function () { 27 | const a = getElementByXpath("//dt[text()='Path']/following-sibling::dd/a"); 28 | if (a) { 29 | a.addEventListener('click', function () { 30 | openMediaPlayerTask(a.href); 31 | }); 32 | } 33 | }); 34 | }); 35 | 36 | const settingsId = 'userscript-settings-mediaplayer'; 37 | 38 | stash.addSystemSetting(async (elementId, el) => { 39 | const inputId = 'userscript-settings-mediaplayer-input'; 40 | if (document.getElementById(inputId)) return; 41 | const settingsHeader = 'Media Player Path'; 42 | const settingsSubheader = 'Path to external media player.'; 43 | const placeholder = 'Media Player Path…'; 44 | const textbox = await stash.createSystemSettingTextbox(el, settingsId, inputId, settingsHeader, settingsSubheader, placeholder, false); 45 | textbox.addEventListener('change', () => { 46 | const value = textbox.value; 47 | if (value) { 48 | stash.updateConfigValueTask('MEDIAPLAYER', 'path', value); 49 | alert(`Media player path set to ${value}`); 50 | } 51 | else { 52 | stash.getConfigValueTask('MEDIAPLAYER', 'path').then(value => { 53 | textbox.value = value; 54 | }); 55 | } 56 | }); 57 | textbox.disabled = true; 58 | stash.getConfigValueTask('MEDIAPLAYER', 'path').then(value => { 59 | textbox.value = value; 60 | textbox.disabled = false; 61 | }); 62 | }); 63 | 64 | stash.addEventListener('stash:pluginVersion', async function () { 65 | waitForElementId(settingsId, async (elementId, el) => { 66 | el.style.display = stash.pluginVersion != null ? 'flex' : 'none'; 67 | }); 68 | if (stash.comparePluginVersion(MIN_REQUIRED_PLUGIN_VERSION) < 0) { 69 | const alertedPluginVersion = await GM.getValue('alerted_plugin_version'); 70 | if (alertedPluginVersion !== stash.pluginVersion) { 71 | await GM.setValue('alerted_plugin_version', stash.pluginVersion); 72 | alert(`User functions plugin version is ${stash.pluginVersion}. Stash Open Media Player userscript requires version ${MIN_REQUIRED_PLUGIN_VERSION} or higher.`); 73 | } 74 | } 75 | }); 76 | })(); -------------------------------------------------------------------------------- /src/body/Stash Performer Audit Task Button.user.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const { 5 | stash, 6 | Stash, 7 | waitForElementId, 8 | waitForElementClass, 9 | waitForElementByXpath, 10 | getElementByXpath, 11 | } = unsafeWindow.stash; 12 | 13 | stash.visiblePluginTasks.push('Audit performer urls'); 14 | 15 | const settingsId = 'userscript-settings-audit-task'; 16 | const inputId = 'userscript-settings-audit-task-button-visible'; 17 | 18 | stash.addEventListener('page:performers', function () { 19 | waitForElementClass("btn-toolbar", async () => { 20 | if (!document.getElementById('audit-task')) { 21 | const toolbar = document.querySelector(".btn-toolbar"); 22 | 23 | const newGroup = document.createElement('div'); 24 | newGroup.classList.add('mx-2', 'mb-2', await GM.getValue(inputId, false) ? 'd-flex' : 'd-none'); 25 | toolbar.appendChild(newGroup); 26 | 27 | const auditButton = document.createElement("button"); 28 | auditButton.setAttribute("id", "audit-task"); 29 | auditButton.classList.add('btn', 'btn-secondary'); 30 | auditButton.innerHTML = 'Audit URLs'; 31 | auditButton.onclick = () => { 32 | stash.runPluginTask("userscript_functions", "Audit performer urls"); 33 | }; 34 | newGroup.appendChild(auditButton); 35 | } 36 | }); 37 | }); 38 | 39 | stash.addSystemSetting(async (elementId, el) => { 40 | if (document.getElementById(inputId)) return; 41 | const settingsHeader = 'Show Audit Performer URLs Button'; 42 | const settingsSubheader = 'Display audit performer urls button on performers page.'; 43 | const checkbox = await stash.createSystemSettingCheckbox(el, settingsId, inputId, settingsHeader, settingsSubheader); 44 | checkbox.checked = await GM.getValue(inputId, false); 45 | checkbox.addEventListener('change', async () => { 46 | const value = checkbox.checked; 47 | await GM.setValue(inputId, value); 48 | }); 49 | }); 50 | 51 | stash.addEventListener('stash:pluginVersion', async function () { 52 | waitForElementId(settingsId, async (elementId, el) => { 53 | el.style.display = stash.pluginVersion != null ? 'flex' : 'none'; 54 | }); 55 | }); 56 | })(); -------------------------------------------------------------------------------- /src/body/Stash Performer Image Cropper.user.js: -------------------------------------------------------------------------------- 1 | /* global Cropper */ 2 | 3 | (function () { 4 | 'use strict'; 5 | 6 | const { 7 | stash, 8 | Stash, 9 | waitForElementId, 10 | waitForElementClass, 11 | waitForElementByXpath, 12 | getElementByXpath, 13 | reloadImg, 14 | } = unsafeWindow.stash; 15 | 16 | const css = GM_getResourceText("IMPORTED_CSS"); 17 | GM_addStyle(css); 18 | GM_addStyle(".cropper-view-box img { transition: none; }"); 19 | GM_addStyle(".detail-header-image { flex-direction: column; }"); 20 | 21 | let cropping = false; 22 | let cropper = null; 23 | 24 | stash.addEventListener('page:performer', function () { 25 | waitForElementClass('detail-container', function () { 26 | const cropBtnContainerId = "crop-btn-container"; 27 | if (!document.getElementById(cropBtnContainerId)) { 28 | const performerId = window.location.pathname.replace('/performers/', '').split('/')[0]; 29 | const image = getElementByXpath("//div[contains(@class, 'detail-header-image')]//img[@class='performer']"); 30 | image.parentElement.addEventListener('click', (evt) => { 31 | if (cropping) { 32 | evt.preventDefault(); 33 | evt.stopPropagation(); 34 | } 35 | }) 36 | const cropBtnContainer = document.createElement('div'); 37 | cropBtnContainer.setAttribute("id", cropBtnContainerId); 38 | image.parentElement.parentElement.appendChild(cropBtnContainer); 39 | 40 | const cropInfo = document.createElement('p'); 41 | 42 | const imageUrl = getElementByXpath("//div[contains(@class, 'detail-header-image')]//img[@class='performer']/@src").nodeValue; 43 | const cropStart = document.createElement('button'); 44 | cropStart.setAttribute("id", "crop-start"); 45 | cropStart.classList.add('btn', 'btn-primary'); 46 | cropStart.innerText = 'Crop Image'; 47 | cropStart.addEventListener('click', evt => { 48 | cropping = true; 49 | cropStart.style.display = 'none'; 50 | cropCancel.style.display = 'inline-block'; 51 | 52 | cropper = new Cropper(image, { 53 | viewMode: 1, 54 | initialAspectRatio: 2 /3, 55 | movable: false, 56 | rotatable: false, 57 | scalable: false, 58 | zoomable: false, 59 | zoomOnTouch: false, 60 | zoomOnWheel: false, 61 | ready() { 62 | cropAccept.style.display = 'inline-block'; 63 | }, 64 | crop(e) { 65 | cropInfo.innerText = `X: ${Math.round(e.detail.x)}, Y: ${Math.round(e.detail.y)}, Width: ${Math.round(e.detail.width)}px, Height: ${Math.round(e.detail.height)}px`; 66 | } 67 | }); 68 | }); 69 | cropBtnContainer.appendChild(cropStart); 70 | 71 | const cropAccept = document.createElement('button'); 72 | cropAccept.setAttribute("id", "crop-accept"); 73 | cropAccept.classList.add('btn', 'btn-success', 'mr-2'); 74 | cropAccept.innerText = 'OK'; 75 | cropAccept.addEventListener('click', async evt => { 76 | cropping = false; 77 | cropStart.style.display = 'inline-block'; 78 | cropAccept.style.display = 'none'; 79 | cropCancel.style.display = 'none'; 80 | cropInfo.innerText = ''; 81 | 82 | const reqData = { 83 | "operationName": "PerformerUpdate", 84 | "variables": { 85 | "input": { 86 | "image": cropper.getCroppedCanvas().toDataURL(), 87 | "id": performerId 88 | } 89 | }, 90 | "query": `mutation PerformerUpdate($input: PerformerUpdateInput!) { 91 | performerUpdate(input: $input) { 92 | id 93 | } 94 | }` 95 | } 96 | await stash.callGQL(reqData); 97 | reloadImg(image.src); 98 | cropper.destroy(); 99 | }); 100 | cropBtnContainer.appendChild(cropAccept); 101 | 102 | const cropCancel = document.createElement('button'); 103 | cropCancel.setAttribute("id", "crop-accept"); 104 | cropCancel.classList.add('btn', 'btn-danger'); 105 | cropCancel.innerText = 'Cancel'; 106 | cropCancel.addEventListener('click', evt => { 107 | cropping = false; 108 | cropStart.style.display = 'inline-block'; 109 | cropAccept.style.display = 'none'; 110 | cropCancel.style.display = 'none'; 111 | cropInfo.innerText = ''; 112 | 113 | cropper.destroy(); 114 | }); 115 | cropBtnContainer.appendChild(cropCancel); 116 | cropAccept.style.display = 'none'; 117 | cropCancel.style.display = 'none'; 118 | 119 | cropBtnContainer.appendChild(cropInfo); 120 | } 121 | }); 122 | }); 123 | })(); -------------------------------------------------------------------------------- /src/body/Stash Performer Markers Tab.user.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const { 5 | stash, 6 | Stash, 7 | waitForElementId, 8 | waitForElementClass, 9 | waitForElementByXpath, 10 | getElementByXpath, 11 | createElementFromHTML, 12 | } = unsafeWindow.stash; 13 | 14 | async function getPerformerMarkersCount(performerId) { 15 | const reqData = { 16 | "operationName": "FindSceneMarkers", 17 | "variables": { 18 | "scene_marker_filter": { 19 | "performers": { 20 | "value": [ 21 | performerId 22 | ], 23 | "modifier": "INCLUDES_ALL" 24 | } 25 | } 26 | }, 27 | "query": `query FindSceneMarkers($filter: FindFilterType, $scene_marker_filter: SceneMarkerFilterType) { 28 | findSceneMarkers(filter: $filter, scene_marker_filter: $scene_marker_filter) { 29 | count 30 | } 31 | }` 32 | } 33 | return stash.callGQL(reqData); 34 | } 35 | 36 | const markersTabId = 'performer-details-tab-markers'; 37 | 38 | stash.addEventListener('page:performer:details', function () { 39 | waitForElementClass("nav-tabs", async function (className, el) { 40 | const navTabs = el.item(0); 41 | if (!document.getElementById(markersTabId)) { 42 | const performerId = window.location.pathname.replace('/performers/', ''); 43 | const markersCount = (await getPerformerMarkersCount(performerId)).data.findSceneMarkers.count; 44 | const markerTab = createElementFromHTML(`Markers${markersCount}`) 45 | navTabs.appendChild(markerTab); 46 | const performerName = document.querySelector('.performer-head h2').innerText; 47 | const markersUrl = `${window.location.origin}/scenes/markers?c=${JSON.stringify({"type":"performers","value":[{"id":performerId,"label":performerName}],"modifier":"INCLUDES_ALL"})}` 48 | markerTab.href = markersUrl; 49 | } 50 | }); 51 | }); 52 | })(); -------------------------------------------------------------------------------- /src/body/Stash Performer Tagger Additions.user.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const { 5 | stash, 6 | Stash, 7 | waitForElementId, 8 | waitForElementClass, 9 | waitForElementByXpath, 10 | getElementByXpath, 11 | insertAfter, 12 | createElementFromHTML, 13 | } = unsafeWindow.stash; 14 | 15 | stash.addEventListener('page:performers', function () { 16 | waitForElementClass("tagger-container", function () { 17 | const performerElements = document.querySelectorAll('.PerformerTagger-details'); 18 | for (const performerElement of performerElements) { 19 | let birthdateElement = performerElement.querySelector('.PerformerTagger-birthdate'); 20 | if (!birthdateElement) { 21 | birthdateElement = document.createElement('h5'); 22 | birthdateElement.classList.add('PerformerTagger-birthdate'); 23 | const headerElement = performerElement.querySelector('.PerformerTagger-header'); 24 | headerElement.classList.add('d-inline-block', 'mr-2'); 25 | headerElement.addEventListener("click", (event) => { 26 | event.preventDefault(); 27 | window.open(headerElement.href, '_blank'); 28 | }); 29 | const performerId = headerElement.href.split('/').pop(); 30 | const performer = stash.performers[performerId]; 31 | birthdateElement.innerText = performer.birthdate; 32 | if (performer.url) { 33 | const urlElement = createElementFromHTML(``); 40 | urlElement.classList.add('d-inline-block'); 41 | insertAfter(urlElement, headerElement); 42 | insertAfter(birthdateElement, urlElement); 43 | } 44 | else { 45 | insertAfter(birthdateElement, headerElement); 46 | } 47 | } 48 | } 49 | }); 50 | }); 51 | })(); -------------------------------------------------------------------------------- /src/body/Stash Performer URL Searchbox.user.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const { 5 | stash, 6 | Stash, 7 | waitForElementId, 8 | waitForElementClass, 9 | waitForElementByXpath, 10 | getElementByXpath, 11 | } = unsafeWindow.stash; 12 | 13 | stash.addEventListener('page:performers', function () { 14 | waitForElementClass("btn-toolbar", function () { 15 | if (!document.getElementById('performer-url-search-input')) { 16 | const toolbar = document.querySelector(".btn-toolbar"); 17 | 18 | const newGroup = document.createElement('div'); 19 | newGroup.classList.add('mx-2', 'mb-2', 'd-flex'); 20 | toolbar.appendChild(newGroup); 21 | 22 | const perfUrlGroup = document.createElement('div'); 23 | perfUrlGroup.classList.add('flex-grow-1', 'query-text-field-group'); 24 | newGroup.appendChild(perfUrlGroup); 25 | 26 | const perfUrlTextbox = document.createElement('input'); 27 | perfUrlTextbox.setAttribute('id', 'performer-url-search-input'); 28 | perfUrlTextbox.classList.add('query-text-field', 'bg-secondary', 'text-white', 'border-secondary', 'form-control'); 29 | perfUrlTextbox.setAttribute('placeholder', 'URL…'); 30 | perfUrlTextbox.addEventListener('change', () => { 31 | const url = `${window.location.origin}/performers?c={"type":"url","value":"${perfUrlTextbox.value}","modifier":"EQUALS"}` 32 | window.location = url; 33 | }); 34 | perfUrlGroup.appendChild(perfUrlTextbox); 35 | } 36 | }); 37 | }); 38 | })(); -------------------------------------------------------------------------------- /src/body/Stash Scene Tagger Additions.user.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const { 5 | stash, 6 | Stash, 7 | waitForElementId, 8 | waitForElementClass, 9 | waitForElementByXpath, 10 | getElementByXpath, 11 | insertAfter, 12 | createElementFromHTML, 13 | } = unsafeWindow.stash; 14 | 15 | function formatDuration(s) { 16 | const sec_num = parseInt(s, 10); 17 | let hours = Math.floor(sec_num / 3600); 18 | let minutes = Math.floor((sec_num - (hours * 3600)) / 60); 19 | let seconds = sec_num - (hours * 3600) - (minutes * 60); 20 | 21 | if (hours < 10) { hours = "0" + hours; } 22 | if (minutes < 10) { minutes = "0" + minutes; } 23 | if (seconds < 10) { seconds = "0" + seconds; } 24 | return hours + ':' + minutes + ':' + seconds; 25 | } 26 | 27 | function openMediaPlayerTask(path) { 28 | stash.runPluginTask("userscript_functions", "Open in Media Player", {"key":"path", "value":{"str": path}}); 29 | } 30 | 31 | stash.addEventListener('tagger:searchitem', async function (evt) { 32 | const searchItem = evt.detail; 33 | const { 34 | urlNode, 35 | url, 36 | id, 37 | data, 38 | nameNode, 39 | name, 40 | queryInput, 41 | performerNodes 42 | } = stash.parseSearchItem(searchItem); 43 | 44 | const includeDuration = await GM.getValue('additions-duration', true); 45 | const includePath = await GM.getValue('additions-path', true); 46 | const includeUrl = await GM.getValue('additions-url', true); 47 | 48 | const originalSceneDetails = searchItem.querySelector('.original-scene-details'); 49 | 50 | if (!originalSceneDetails.firstChild.firstChild.querySelector('.scene-url') && data.url) { 51 | const sceneUrlNode = createElementFromHTML(`${data.url}`); 52 | sceneUrlNode.style.display = includeUrl ? 'block' : 'none'; 53 | sceneUrlNode.style.fontWeight = 500; 54 | sceneUrlNode.style.color = '#fff'; 55 | originalSceneDetails.firstChild.firstChild.appendChild(sceneUrlNode); 56 | } 57 | 58 | const paths = stash.compareVersion("0.17.0") >= 0 ? data.files.map(file => file.path) : [data.path]; 59 | if (!originalSceneDetails.firstChild.firstChild.querySelector('.scene-path')) { 60 | for (const path of paths) { 61 | if (path) { 62 | const pathNode = createElementFromHTML(`${path}`); 63 | pathNode.style.display = includePath ? 'block' : 'none'; 64 | pathNode.style.fontWeight = 500; 65 | pathNode.style.color = '#fff'; 66 | pathNode.addEventListener('click', evt => { 67 | evt.preventDefault(); 68 | if (stash.pluginVersion) { 69 | openMediaPlayerTask(path); 70 | } 71 | }); 72 | originalSceneDetails.firstChild.firstChild.appendChild(pathNode); 73 | } 74 | } 75 | } 76 | 77 | const duration = stash.compareVersion("0.17.0") >= 0 ? data.files[0].duration : data.file.duration; 78 | if (!originalSceneDetails.firstChild.firstChild.querySelector('.scene-duration') && duration) { 79 | const durationNode = createElementFromHTML(`Duration: ${formatDuration(duration)}`); 80 | durationNode.style.display = includeDuration ? 'block' : 'none'; 81 | durationNode.style.fontWeight = 500; 82 | durationNode.style.color = '#fff'; 83 | originalSceneDetails.firstChild.firstChild.appendChild(durationNode); 84 | } 85 | 86 | const expandDetailsButton = originalSceneDetails.querySelector('button'); 87 | if (!expandDetailsButton.classList.contains('.enhanced')) { 88 | expandDetailsButton.classList.add('enhanced'); 89 | expandDetailsButton.addEventListener('click', evt => { 90 | const icon = expandDetailsButton.firstChild.dataset.icon; 91 | if (evt.shiftKey) { 92 | evt.preventDefault(); 93 | evt.stopPropagation(); 94 | for (const button of document.querySelectorAll('.original-scene-details button')) { 95 | if (button.firstChild.dataset.icon === icon) { 96 | button.click(); 97 | } 98 | } 99 | } 100 | }); 101 | } 102 | }); 103 | 104 | const additionsConfigId = 'additionsconfig'; 105 | 106 | stash.addEventListener('tagger:configuration', evt => { 107 | const el = evt.detail; 108 | if (!document.getElementById(additionsConfigId)) { 109 | const configContainer = el.parentElement; 110 | const additionsConfig = createElementFromHTML(` 111 |
112 |
Tagger Additions Configuration
113 |
114 |
115 |
116 | 117 | 118 |
119 |
120 |
121 |
122 | 123 | 124 |
125 |
126 |
127 |
128 | 129 | 130 |
131 |
132 |
133 |
134 | `); 135 | configContainer.appendChild(additionsConfig); 136 | loadSettings(); 137 | document.getElementById('additions-duration').addEventListener('change', function () { 138 | for (const node of document.querySelectorAll('.scene-duration')) { 139 | node.style.display = this.checked ? 'block' : 'none'; 140 | } 141 | }); 142 | document.getElementById('additions-path').addEventListener('change', function () { 143 | for (const node of document.querySelectorAll('.scene-path')) { 144 | node.style.display = this.checked ? 'block' : 'none'; 145 | } 146 | }); 147 | document.getElementById('additions-url').addEventListener('change', function () { 148 | for (const node of document.querySelectorAll('.scene-url')) { 149 | node.style.display = this.checked ? 'block' : 'none'; 150 | } 151 | }); 152 | } 153 | }); 154 | 155 | async function loadSettings() { 156 | for (const input of document.querySelectorAll(`#${additionsConfigId} input`)) { 157 | input.checked = await GM.getValue(input.id, input.dataset.default === 'true'); 158 | input.addEventListener('change', async () => { 159 | await GM.setValue(input.id, input.checked); 160 | }); 161 | } 162 | } 163 | })(); -------------------------------------------------------------------------------- /src/body/Stash Scene Tagger Draft Submit.user.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const { 5 | stash, 6 | Stash, 7 | waitForElementId, 8 | waitForElementClass, 9 | waitForElementByXpath, 10 | getElementByXpath, 11 | getElementsByXpath, 12 | getClosestAncestor, 13 | insertAfter, 14 | createElementFromHTML, 15 | } = unsafeWindow.stash; 16 | 17 | document.body.appendChild(document.createElement('style')).textContent = ` 18 | .search-item > div.row:first-child > div.col-md-6.my-1 > div:first-child { display: flex; flex-direction: column; } 19 | .submit-draft { order: 5; } 20 | `; 21 | 22 | async function submitDraft(sceneId, stashBoxIndex) { 23 | const reqData = { 24 | "variables": { 25 | "input": { 26 | "id": sceneId, 27 | "stash_box_index": stashBoxIndex 28 | } 29 | }, 30 | "operationName": "SubmitStashBoxSceneDraft", 31 | "query": `mutation SubmitStashBoxSceneDraft($input: StashBoxDraftSubmissionInput!) { 32 | submitStashBoxSceneDraft(input: $input) 33 | }` 34 | } 35 | const res = await stash.callGQL(reqData); 36 | return res?.data?.submitStashBoxSceneDraft; 37 | } 38 | 39 | async function initDraftButtons() { 40 | const data = await stash.getStashBoxes(); 41 | let i = 0; 42 | const stashBoxes = data.data.configuration.general.stashBoxes; 43 | 44 | const nodes = getElementsByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']"); 45 | const buttons = []; 46 | let node = null; 47 | while (node = nodes.iterateNext()) { 48 | buttons.push(node); 49 | } 50 | for (const button of buttons) { 51 | const searchItem = getClosestAncestor(button, '.search-item'); 52 | const { 53 | urlNode, 54 | url, 55 | id, 56 | data, 57 | nameNode, 58 | name, 59 | queryInput, 60 | performerNodes 61 | } = stash.parseSearchItem(searchItem); 62 | 63 | const draftButtonExists = searchItem.querySelector('.submit-draft'); 64 | if (draftButtonExists) { 65 | continue; 66 | } 67 | 68 | const submit = createElementFromHTML('
'); 69 | const submitButton = submit.querySelector('button'); 70 | button.parentElement.parentElement.appendChild(submit); 71 | submitButton.addEventListener('click', async () => { 72 | const selectedStashbox = document.getElementById('scraper').value; 73 | if (!selectedStashbox.startsWith('stashbox:')) { 74 | alert('No stashbox source selected.'); 75 | return; 76 | } 77 | const selectedStashboxIndex = parseInt(selectedStashbox.replace(/^stashbox:/, '')); 78 | const existingStashId = data.stash_ids.find(o => o.endpoint === stashBoxes[selectedStashboxIndex].endpoint); 79 | if (existingStashId) { 80 | alert(`Scene already has StashID for ${stashBoxes[selectedStashboxIndex].endpoint}.`); 81 | return; 82 | } 83 | const draftId = await submitDraft(id, selectedStashboxIndex); 84 | const draftLink = createElementFromHTML(`Draft: ${draftId}`); 85 | submitButton.parentElement.appendChild(draftLink); 86 | submitButton.remove(); 87 | }); 88 | } 89 | } 90 | 91 | stash.addEventListener('page:studio:scenes', function () { 92 | waitForElementByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']", initDraftButtons); 93 | }); 94 | 95 | stash.addEventListener('page:performer:scenes', function () { 96 | waitForElementByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']", initDraftButtons); 97 | }); 98 | 99 | stash.addEventListener('page:scenes', function () { 100 | waitForElementByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']", initDraftButtons); 101 | }); 102 | })(); -------------------------------------------------------------------------------- /src/body/Stash Set Stashbox Favorite Performers.user.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | const { 5 | stash, 6 | Stash, 7 | waitForElementId, 8 | waitForElementClass, 9 | waitForElementByXpath, 10 | getElementByXpath, 11 | getClosestAncestor, 12 | updateTextInput, 13 | } = unsafeWindow.stash; 14 | 15 | const MIN_REQUIRED_PLUGIN_VERSION = '0.6.0'; 16 | 17 | const TASK_NAME = 'Set Stashbox Favorite Performers'; 18 | stash.visiblePluginTasks.push(TASK_NAME); 19 | 20 | const settingsId = 'userscript-settings-set-stashbox-favorites-task'; 21 | const inputId = 'userscript-settings-set-stashbox-favorites-button-visible'; 22 | 23 | async function runSetStashBoxFavoritePerformersTask() { 24 | const data = await stash.getStashBoxes(); 25 | if (!data.data.configuration.general.stashBoxes.length) { 26 | alert('No Stashbox configured.'); 27 | } 28 | for (const { endpoint } of data.data.configuration.general.stashBoxes) { 29 | if (endpoint !== 'https://stashdb.org/graphql') continue; 30 | await stash.runPluginTask("userscript_functions", "Set Stashbox Favorite Performers", [{"key":"endpoint", "value":{"str": endpoint}}]); 31 | } 32 | } 33 | 34 | async function runSetStashBoxFavoritePerformerTask(endpoint, stashId, favorite) { 35 | if (endpoint !== 'https://stashdb.org/graphql') return; 36 | return stash.runPluginTask("userscript_functions", "Set Stashbox Favorite Performer", [{"key":"endpoint", "value":{"str": endpoint}}, {"key":"stash_id", "value":{"str": stashId}}, {"key":"favorite", "value":{"b": favorite}}]); 37 | } 38 | 39 | stash.addEventListener('page:performers', function () { 40 | waitForElementClass("btn-toolbar", async function () { 41 | if (!document.getElementById('stashbox-favorite-task')) { 42 | const toolbar = document.querySelector(".btn-toolbar"); 43 | 44 | const newGroup = document.createElement('div'); 45 | newGroup.classList.add('mx-2', 'mb-2', await GM.getValue(inputId, false) ? 'd-flex' : 'd-none'); 46 | toolbar.appendChild(newGroup); 47 | 48 | const button = document.createElement("button"); 49 | button.setAttribute("id", "stashbox-favorite-task"); 50 | button.classList.add('btn', 'btn-secondary'); 51 | button.innerHTML = 'Set Stashbox Favorites'; 52 | button.onclick = () => { 53 | runSetStashBoxFavoritePerformersTask(); 54 | }; 55 | newGroup.appendChild(button); 56 | } 57 | }); 58 | }); 59 | 60 | stash.addEventListener('stash:response', function (evt) { 61 | const data = evt.detail; 62 | let performers; 63 | if (data.data?.performerUpdate?.stash_ids?.length) { 64 | performers = [data.data.performerUpdate]; 65 | } 66 | else if (data.data?.bulkPerformerUpdate) { 67 | performers = data.data.bulkPerformerUpdate.filter(performer => performer?.stash_ids?.length); 68 | } 69 | if (performers) { 70 | if (performers.length <= 10) { 71 | for (const performer of performers) { 72 | for (const { endpoint, stash_id } of performer.stash_ids) { 73 | runSetStashBoxFavoritePerformerTask(endpoint, stash_id, performer.favorite); 74 | } 75 | } 76 | } 77 | else { 78 | runSetStashBoxFavoritePerformersTask(); 79 | } 80 | } 81 | }); 82 | 83 | stash.addSystemSetting(async (elementId, el) => { 84 | if (document.getElementById(inputId)) return; 85 | const settingsHeader = 'Show Set Stashbox Favorites Button'; 86 | const settingsSubheader = 'Display set stashbox favorites button on performers page.'; 87 | const checkbox = await stash.createSystemSettingCheckbox(el, settingsId, inputId, settingsHeader, settingsSubheader); 88 | checkbox.checked = await GM.getValue(inputId, false); 89 | checkbox.addEventListener('change', async () => { 90 | const value = checkbox.checked; 91 | await GM.setValue(inputId, value); 92 | }); 93 | }); 94 | 95 | stash.addEventListener('stash:pluginVersion', async function () { 96 | waitForElementId(settingsId, async (elementId, el) => { 97 | el.style.display = stash.pluginVersion != null ? 'flex' : 'none'; 98 | }); 99 | if (stash.comparePluginVersion(MIN_REQUIRED_PLUGIN_VERSION) < 0) { 100 | const alertedPluginVersion = await GM.getValue('alerted_plugin_version'); 101 | if (alertedPluginVersion !== stash.pluginVersion) { 102 | await GM.setValue('alerted_plugin_version', stash.pluginVersion); 103 | alert(`User functions plugin version is ${stash.pluginVersion}. Set Stashbox Favorite Performers userscript requires version ${MIN_REQUIRED_PLUGIN_VERSION} or higher.`); 104 | } 105 | } 106 | }); 107 | 108 | stash.addEventListener('stash:plugin:task', async function (evt) { 109 | const { taskName, task } = evt.detail; 110 | if (taskName === TASK_NAME) { 111 | const taskButton = task.querySelector('button'); 112 | if (!taskButton.classList.contains('hooked')) { 113 | taskButton.classList.add('hooked'); 114 | taskButton.addEventListener('click', evt => { 115 | evt.preventDefault(); 116 | evt.stopPropagation(); 117 | runSetStashBoxFavoritePerformersTask(); 118 | }); 119 | } 120 | } 121 | }); 122 | 123 | })(); -------------------------------------------------------------------------------- /src/body/Stash StashID Icon.user.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const { 5 | stash, 6 | Stash, 7 | waitForElementId, 8 | waitForElementClass, 9 | waitForElementByXpath, 10 | getElementByXpath, 11 | insertAfter, 12 | createElementFromHTML, 13 | } = unsafeWindow.stash; 14 | 15 | GM_addStyle(` 16 | .peformer-stashid-icon { 17 | position: absolute; 18 | bottom: .8rem; 19 | left: .8rem; 20 | } 21 | .studio-stashid-icon { 22 | position: absolute; 23 | top: 10px; 24 | right: 5px; 25 | } 26 | .col-3.d-xl-none .studio-stashid-icon { 27 | position: relative; 28 | top: 0; 29 | right: 0; 30 | } 31 | `); 32 | 33 | function createCheckmarkElement() { 34 | return createElementFromHTML(``); 40 | } 41 | 42 | function addPerformerStashIDIcons(performerDatas) { 43 | for (const performerCard of document.querySelectorAll('.performer-card')) { 44 | const performerLink = performerCard.querySelector('.thumbnail-section > a'); 45 | if (performerLink) { 46 | const performerUrl = performerLink.href; 47 | const performerId = performerUrl.split('/').pop(); 48 | const performerData = performerDatas[performerId]; 49 | if (performerData?.stash_ids.length) { 50 | const el = createElementFromHTML(`
`); 51 | el.appendChild(createCheckmarkElement()); 52 | 53 | performerLink.parentElement.appendChild(el); 54 | } 55 | } 56 | } 57 | } 58 | 59 | function addStudioStashIDIcons(studioDatas) { 60 | for (const studioCard of document.querySelectorAll('.studio-card')) { 61 | const studioLink = studioCard.querySelector('.thumbnail-section > a'); 62 | const studioUrl = studioLink.href; 63 | const studioId = studioUrl.split('/').pop(); 64 | const studioData = studioDatas[studioId]; 65 | if (studioData?.stash_ids.length) { 66 | const el = createElementFromHTML(`
`); 67 | el.appendChild(createCheckmarkElement()); 68 | 69 | studioCard.appendChild(el); 70 | } 71 | } 72 | } 73 | 74 | function addSceneStudioStashIDIcons(studioData) { 75 | for (const studioCard of document.querySelectorAll('.studio-logo')) { 76 | if (studioData?.stash_ids.length) { 77 | const el = createElementFromHTML(`
`); 78 | el.appendChild(createCheckmarkElement()); 79 | 80 | studioCard.parentElement.appendChild(el); 81 | } 82 | } 83 | } 84 | 85 | stash.addEventListener('page:scene', function () { 86 | waitForElementClass("performer-card", function () { 87 | const sceneId = window.location.pathname.split('/').pop(); 88 | const performerDatas = {}; 89 | for (const performerData of stash.scenes[sceneId].performers) { 90 | performerDatas[performerData.id] = performerData; 91 | } 92 | addPerformerStashIDIcons(performerDatas); 93 | if (stash.scenes[sceneId].studio) { 94 | addSceneStudioStashIDIcons(stash.scenes[sceneId].studio); 95 | } 96 | }); 97 | }); 98 | 99 | stash.addEventListener('page:performers', function () { 100 | waitForElementClass("performer-card", function () { 101 | addPerformerStashIDIcons(stash.performers); 102 | }); 103 | }); 104 | 105 | stash.addEventListener('page:studios', function () { 106 | waitForElementClass("studio-card", function () { 107 | addStudioStashIDIcons(stash.studios); 108 | }); 109 | }); 110 | 111 | stash.addEventListener('page:studio:performers', function () { 112 | waitForElementClass("performer-card", function () { 113 | addPerformerStashIDIcons(stash.performers); 114 | }); 115 | }); 116 | })(); -------------------------------------------------------------------------------- /src/body/Stash Stats.user.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | const { 5 | stash, 6 | Stash, 7 | waitForElementId, 8 | waitForElementClass, 9 | waitForElementByXpath, 10 | getElementByXpath, 11 | getClosestAncestor, 12 | updateTextInput, 13 | } = unsafeWindow.stash; 14 | 15 | function createStatElement(container, title, heading) { 16 | const statEl = document.createElement('div'); 17 | statEl.classList.add('stats-element'); 18 | container.appendChild(statEl); 19 | 20 | const statTitle = document.createElement('p'); 21 | statTitle.classList.add('title'); 22 | statTitle.innerText = title; 23 | statEl.appendChild(statTitle); 24 | 25 | const statHeading = document.createElement('p'); 26 | statHeading.classList.add('heading'); 27 | statHeading.innerText = heading; 28 | statEl.appendChild(statHeading); 29 | } 30 | async function createSceneStashIDPct(row) { 31 | const reqData = { 32 | "variables": { 33 | "scene_filter": { 34 | "stash_id_endpoint": { 35 | "endpoint": "", 36 | "stash_id": "", 37 | "modifier": "NOT_NULL" 38 | } 39 | } 40 | }, 41 | "query": "query FindScenes($filter: FindFilterType, $scene_filter: SceneFilterType, $scene_ids: [Int!]) {\n findScenes(filter: $filter, scene_filter: $scene_filter, scene_ids: $scene_ids) {\n count\n }\n}" 42 | }; 43 | const resp = (await stash.callGQL(reqData)); 44 | console.log('resp', resp); 45 | const stashIdCount = (await stash.callGQL(reqData)).data.findScenes.count; 46 | 47 | const reqData2 = { 48 | "variables": { 49 | "scene_filter": {} 50 | }, 51 | "query": "query FindScenes($filter: FindFilterType, $scene_filter: SceneFilterType, $scene_ids: [Int!]) {\n findScenes(filter: $filter, scene_filter: $scene_filter, scene_ids: $scene_ids) {\n count\n }\n}" 52 | }; 53 | const totalCount = (await stash.callGQL(reqData2)).data.findScenes.count; 54 | 55 | createStatElement(row, (stashIdCount / totalCount * 100).toFixed(2) + '%', 'Scene StashIDs'); 56 | } 57 | 58 | async function createPerformerStashIDPct(row) { 59 | const reqData = { 60 | "variables": { 61 | "performer_filter": { 62 | "stash_id_endpoint": { 63 | "endpoint": "", 64 | "stash_id": "", 65 | "modifier": "NOT_NULL" 66 | } 67 | } 68 | }, 69 | "query": "query FindPerformers($filter: FindFilterType, $performer_filter: PerformerFilterType) {\n findPerformers(filter: $filter, performer_filter: $performer_filter) {\n count\n }\n}" 70 | }; 71 | const stashIdCount = (await stash.callGQL(reqData)).data.findPerformers.count; 72 | 73 | const reqData2 = { 74 | "variables": { 75 | "performer_filter": {} 76 | }, 77 | "query": "query FindPerformers($filter: FindFilterType, $performer_filter: PerformerFilterType) {\n findPerformers(filter: $filter, performer_filter: $performer_filter) {\n count\n }\n}" 78 | }; 79 | const totalCount = (await stash.callGQL(reqData2)).data.findPerformers.count; 80 | 81 | createStatElement(row, (stashIdCount / totalCount * 100).toFixed(2) + '%', 'Performer StashIDs'); 82 | } 83 | 84 | async function createStudioStashIDPct(row) { 85 | const reqData = { 86 | "variables": { 87 | "studio_filter": { 88 | "stash_id_endpoint": { 89 | "endpoint": "", 90 | "stash_id": "", 91 | "modifier": "NOT_NULL" 92 | } 93 | } 94 | }, 95 | "query": "query FindStudios($filter: FindFilterType, $studio_filter: StudioFilterType) {\n findStudios(filter: $filter, studio_filter: $studio_filter) {\n count\n }\n}" 96 | }; 97 | const stashIdCount = (await stash.callGQL(reqData)).data.findStudios.count; 98 | 99 | const reqData2 = { 100 | "variables": { 101 | "scene_filter": {} 102 | }, 103 | "query": "query FindStudios($filter: FindFilterType, $studio_filter: StudioFilterType) {\n findStudios(filter: $filter, studio_filter: $studio_filter) {\n count\n }\n}" 104 | }; 105 | const totalCount = (await stash.callGQL(reqData2)).data.findStudios.count; 106 | 107 | createStatElement(row, (stashIdCount / totalCount * 100).toFixed(2) + '%', 'Studio StashIDs'); 108 | } 109 | 110 | async function createPerformerFavorites(row) { 111 | const reqData = { 112 | "variables": { 113 | "performer_filter": { 114 | "filter_favorites": true 115 | } 116 | }, 117 | "query": "query FindPerformers($filter: FindFilterType, $performer_filter: PerformerFilterType) {\n findPerformers(filter: $filter, performer_filter: $performer_filter) {\n count\n }\n}" 118 | }; 119 | const perfCount = (await stash.callGQL(reqData)).data.findPerformers.count; 120 | 121 | createStatElement(row, perfCount, 'Favorite Performers'); 122 | } 123 | 124 | async function createMarkersStat(row) { 125 | const reqData = { 126 | "variables": { 127 | "scene_marker_filter": {} 128 | }, 129 | "query": "query FindSceneMarkers($filter: FindFilterType, $scene_marker_filter: SceneMarkerFilterType) {\n findSceneMarkers(filter: $filter, scene_marker_filter: $scene_marker_filter) {\n count\n }\n}" 130 | }; 131 | const totalCount = (await stash.callGQL(reqData)).data.findSceneMarkers.count; 132 | 133 | createStatElement(row, totalCount, 'Markers'); 134 | } 135 | 136 | stash.addEventListener('page:stats', function () { 137 | waitForElementByXpath("//div[contains(@class, 'container-fluid')]/div[@class='mt-5']", function (xpath, el) { 138 | if (!document.getElementById('custom-stats-row')) { 139 | const changelog = el.querySelector('div.changelog'); 140 | const row = document.createElement('div'); 141 | row.setAttribute('id', 'custom-stats-row'); 142 | row.classList.add('col', 'col-sm-8', 'm-sm-auto', 'row', 'stats'); 143 | el.insertBefore(row, changelog); 144 | 145 | createSceneStashIDPct(row); 146 | createStudioStashIDPct(row); 147 | createPerformerStashIDPct(row); 148 | createPerformerFavorites(row); 149 | createMarkersStat(row); 150 | } 151 | }); 152 | }); 153 | 154 | })(); -------------------------------------------------------------------------------- /src/body/Stash Tag Image Cropper.user.js: -------------------------------------------------------------------------------- 1 | /* global Cropper */ 2 | 3 | (function () { 4 | 'use strict'; 5 | 6 | const { 7 | stash, 8 | Stash, 9 | waitForElementId, 10 | waitForElementClass, 11 | waitForElementByXpath, 12 | getElementByXpath, 13 | insertAfter, 14 | reloadImg, 15 | } = unsafeWindow.stash; 16 | 17 | const css = GM_getResourceText("IMPORTED_CSS"); 18 | GM_addStyle(css); 19 | 20 | let cropping = false; 21 | let cropper = null; 22 | 23 | stash.addEventListener('page:tag:scenes', function () { 24 | waitForElementClass('detail-container', function () { 25 | const cropBtnContainerId = "crop-btn-container"; 26 | if (!document.getElementById(cropBtnContainerId)) { 27 | const tagId = window.location.pathname.replace('/tags/', '').split('/')[0]; 28 | const image = getElementByXpath("//div[contains(@class, 'detail-header-image')]//img[@class='logo']"); 29 | image.parentElement.addEventListener('click', (evt) => { 30 | if (cropping) { 31 | evt.preventDefault(); 32 | evt.stopPropagation(); 33 | } 34 | }) 35 | const cropBtnContainer = document.createElement('div'); 36 | cropBtnContainer.setAttribute("id", cropBtnContainerId); 37 | cropBtnContainer.classList.add('mb-2', 'text-center'); 38 | image.parentElement.appendChild(cropBtnContainer); 39 | 40 | const cropInfo = document.createElement('p'); 41 | 42 | const imageUrl = getElementByXpath("//div[contains(@class, 'detail-header-image')]//img[@class='logo']/@src").nodeValue; 43 | const cropStart = document.createElement('button'); 44 | cropStart.setAttribute("id", "crop-start"); 45 | cropStart.classList.add('btn', 'btn-primary'); 46 | cropStart.innerText = 'Crop Image'; 47 | cropStart.addEventListener('click', evt => { 48 | cropping = true; 49 | cropStart.style.display = 'none'; 50 | cropCancel.style.display = 'inline-block'; 51 | 52 | cropper = new Cropper(image, { 53 | viewMode: 1, 54 | initialAspectRatio: 1, 55 | movable: false, 56 | rotatable: false, 57 | scalable: false, 58 | zoomable: false, 59 | zoomOnTouch: false, 60 | zoomOnWheel: false, 61 | ready() { 62 | cropAccept.style.display = 'inline-block'; 63 | }, 64 | crop(e) { 65 | cropInfo.innerText = `X: ${Math.round(e.detail.x)}, Y: ${Math.round(e.detail.y)}, Width: ${Math.round(e.detail.width)}px, Height: ${Math.round(e.detail.height)}px`; 66 | } 67 | }); 68 | }); 69 | cropBtnContainer.appendChild(cropStart); 70 | 71 | const cropAccept = document.createElement('button'); 72 | cropAccept.setAttribute("id", "crop-accept"); 73 | cropAccept.classList.add('btn', 'btn-success', 'mr-2'); 74 | cropAccept.innerText = 'OK'; 75 | cropAccept.addEventListener('click', async evt => { 76 | cropping = false; 77 | cropStart.style.display = 'inline-block'; 78 | cropAccept.style.display = 'none'; 79 | cropCancel.style.display = 'none'; 80 | cropInfo.innerText = ''; 81 | 82 | const reqData = { 83 | "operationName": "TagUpdate", 84 | "variables": { 85 | "input": { 86 | "image": cropper.getCroppedCanvas().toDataURL(), 87 | "id": tagId 88 | } 89 | }, 90 | "query": `mutation TagUpdate($input: TagUpdateInput!) { 91 | tagUpdate(input: $input) { 92 | id 93 | } 94 | }` 95 | } 96 | await stash.callGQL(reqData); 97 | reloadImg(image.src); 98 | cropper.destroy(); 99 | }); 100 | cropBtnContainer.appendChild(cropAccept); 101 | 102 | const cropCancel = document.createElement('button'); 103 | cropCancel.setAttribute("id", "crop-accept"); 104 | cropCancel.classList.add('btn', 'btn-danger'); 105 | cropCancel.innerText = 'Cancel'; 106 | cropCancel.addEventListener('click', evt => { 107 | cropping = false; 108 | cropStart.style.display = 'inline-block'; 109 | cropAccept.style.display = 'none'; 110 | cropCancel.style.display = 'none'; 111 | cropInfo.innerText = ''; 112 | 113 | cropper.destroy(); 114 | }); 115 | cropBtnContainer.appendChild(cropCancel); 116 | cropAccept.style.display = 'none'; 117 | cropCancel.style.display = 'none'; 118 | 119 | cropBtnContainer.appendChild(cropInfo); 120 | } 121 | }); 122 | }); 123 | })(); -------------------------------------------------------------------------------- /src/header/Stash Batch Query Edit.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Batch Query Edit 3 | // @namespace %NAMESPACE% 4 | // @description Batch modify scene tagger search query 5 | // @version 0.6.0 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @grant unsafeWindow 9 | // @grant GM.getValue 10 | // @grant GM.setValue 11 | // @require %LIBRARYPATH% 12 | // @require %FILEPATH% 13 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash Batch Result Toggle.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Batch Result Toggle 3 | // @namespace %NAMESPACE% 4 | // @description Batch toggle scene tagger search result fields 5 | // @version 0.6.0 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @grant unsafeWindow 9 | // @grant GM.getValue 10 | // @grant GM.setValue 11 | // @require %LIBRARYPATH% 12 | // @require %FILEPATH% 13 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash Batch Save.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Batch Save 3 | // @namespace %NAMESPACE% 4 | // @description Adds a batch save button to scenes tagger 5 | // @version 0.5.3 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @grant unsafeWindow 9 | // @require %LIBRARYPATH% 10 | // @require %FILEPATH% 11 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash Batch Search.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Batch Search 3 | // @namespace %NAMESPACE% 4 | // @description Adds a batch search button to scenes and performers tagger 5 | // @version 0.4.2 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @grant unsafeWindow 9 | // @grant GM.getValue 10 | // @grant GM.setValue 11 | // @require %LIBRARYPATH% 12 | // @require %FILEPATH% 13 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash Markdown.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Markdown 3 | // @namespace %NAMESPACE% 4 | // @description Adds markdown parsing to tag description fields 5 | // @version 0.2.0 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @grant unsafeWindow 9 | // @require %LIBRARYPATH% 10 | // @require https://cdnjs.cloudflare.com/ajax/libs/marked/4.2.2/marked.min.js 11 | // @require %FILEPATH% 12 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash Markers Autoscroll.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Markers Autoscroll 3 | // @namespace %NAMESPACE% 4 | // @description Automatically scrolls markers page 5 | // @version 0.1.0 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @grant unsafeWindow 9 | // @require %LIBRARYPATH% 10 | // @require %FILEPATH% 11 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash New Performer Filter Button.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash New Performer Filter Button 3 | // @namespace %NAMESPACE% 4 | // @description Adds a button to the performers page to switch to a new performers filter 5 | // @version 0.3.0 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @grant unsafeWindow 9 | // @require %LIBRARYPATH% 10 | // @require %FILEPATH% 11 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash Open Media Player.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Open Media Player 3 | // @namespace %NAMESPACE% 4 | // @description Open scene filepath links in an external media player. Requires userscript_functions stash plugin 5 | // @version 0.2.1 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @grant unsafeWindow 9 | // @grant GM.getValue 10 | // @grant GM.setValue 11 | // @require %LIBRARYPATH% 12 | // @require %FILEPATH% 13 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash Performer Audit Task Button.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Performer Audit Task Button 3 | // @namespace %NAMESPACE% 4 | // @description Adds a button to the performers page to run the audit plugin task 5 | // @version 0.3.0 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @grant unsafeWindow 9 | // @grant GM.getValue 10 | // @grant GM.setValue 11 | // @require %LIBRARYPATH% 12 | // @require %FILEPATH% 13 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash Performer Image Cropper.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Performer Image Cropper 3 | // @namespace %NAMESPACE% 4 | // @description Adds an image cropper to performer page 5 | // @version 0.3.0 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @resource IMPORTED_CSS https://raw.githubusercontent.com/fengyuanchen/cropperjs/main/dist/cropper.min.css 9 | // @grant unsafeWindow 10 | // @grant GM_getResourceText 11 | // @grant GM_addStyle 12 | // @require %LIBRARYPATH% 13 | // @require https://raw.githubusercontent.com/fengyuanchen/cropperjs/main/dist/cropper.min.js 14 | // @require %FILEPATH% 15 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash Performer Markers Tab.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Performer Markers Tab 3 | // @namespace %NAMESPACE% 4 | // @description Adds a Markers link to performer pages 5 | // @version 0.1.0 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @grant unsafeWindow 9 | // @require %LIBRARYPATH% 10 | // @require %FILEPATH% 11 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash Performer Tagger Additions.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Performer Tagger Additions 3 | // @namespace %NAMESPACE% 4 | // @description Adds performer birthdate and url to tagger view. Makes clicking performer name open stash profile in new tab instead of current tab. 5 | // @version 0.2.0 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @grant unsafeWindow 9 | // @require %LIBRARYPATH% 10 | // @require %FILEPATH% 11 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash Performer URL Searchbox.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Performer URL Searchbox 3 | // @namespace %NAMESPACE% 4 | // @description Adds a search by performer url textbox to the performers page 5 | // @version 0.2.0 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @grant unsafeWindow 9 | // @require %LIBRARYPATH% 10 | // @require %FILEPATH% 11 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash Scene Tagger Additions.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Scene Tagger Additions 3 | // @namespace %NAMESPACE% 4 | // @description Adds scene duration and filepath to tagger view. 5 | // @version 0.3.1 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @grant unsafeWindow 9 | // @grant GM.getValue 10 | // @grant GM.setValue 11 | // @require %LIBRARYPATH% 12 | // @require %FILEPATH% 13 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash Scene Tagger Colorizer.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Scene Tagger Colorizer 3 | // @namespace %NAMESPACE% 4 | // @description Colorize scene tagger match results to show matching and mismatching scene data. 5 | // @version 0.7.0 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @grant unsafeWindow 9 | // @grant GM.getValue 10 | // @grant GM.setValue 11 | // @require %LIBRARYPATH% 12 | // @require %FILEPATH% 13 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash Scene Tagger Draft Submit.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Scene Tagger Draft Submit 3 | // @namespace %NAMESPACE% 4 | // @description Adds button to Scene Tagger to submit draft to stashdb 5 | // @version 0.1.1 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @grant unsafeWindow 9 | // @require %LIBRARYPATH% 10 | // @require %FILEPATH% 11 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash Set Stashbox Favorite Performers.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Set Stashbox Favorite Performers 3 | // @namespace %NAMESPACE% 4 | // @description Set Stashbox favorite performers according to stash favorites. Requires userscript_functions stash plugin 5 | // @version 0.3.0 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @grant unsafeWindow 9 | // @grant GM.getValue 10 | // @grant GM.setValue 11 | // @require %LIBRARYPATH% 12 | // @require %FILEPATH% 13 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash StashID Icon.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash StashID Icon 3 | // @namespace %NAMESPACE% 4 | // @description Adds checkmark icon to performer and studio cards that have a stashid 5 | // @version 0.2.0 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @grant unsafeWindow 9 | // @grant GM_addStyle 10 | // @require %LIBRARYPATH% 11 | // @require %FILEPATH% 12 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash StashID Input.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash StashID Input 3 | // @namespace %NAMESPACE% 4 | // @description Adds input for entering new stash id to performer details page and studio page 5 | // @version 0.5.0 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @grant unsafeWindow 9 | // @grant GM_setClipboard 10 | // @require %LIBRARYPATH% 11 | // @require %FILEPATH% 12 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash Stats.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Stats 3 | // @namespace %NAMESPACE% 4 | // @description Add stats to stats page 5 | // @version 0.3.1 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @grant unsafeWindow 9 | // @require %LIBRARYPATH% 10 | // @require %FILEPATH% 11 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/Stash Tag Image Cropper.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Stash Tag Image Cropper 3 | // @namespace %NAMESPACE% 4 | // @description Adds an image cropper to tag page 5 | // @version 0.2.0 6 | // @author 7dJx1qP 7 | // @match %MATCHURL% 8 | // @resource IMPORTED_CSS https://raw.githubusercontent.com/fengyuanchen/cropperjs/main/dist/cropper.min.css 9 | // @grant unsafeWindow 10 | // @grant GM_getResourceText 11 | // @grant GM_addStyle 12 | // @require %LIBRARYPATH% 13 | // @require https://raw.githubusercontent.com/fengyuanchen/cropperjs/main/dist/cropper.min.js 14 | // @require %FILEPATH% 15 | // ==/UserScript== --------------------------------------------------------------------------------