`);
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 |
165 |
172 |
183 |
190 |
197 |
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 |
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 |
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==
--------------------------------------------------------------------------------