Skip to content

Commit 04a4a32

Browse files
authored
Add plugin "Set Performers From Tags" (#503)
1 parent 295030d commit 04a4a32

File tree

3 files changed

+304
-0
lines changed

3 files changed

+304
-0
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# **Set Performers From Tags**
2+
3+
This Stash plugin automatically assigns performers to scenes and images based on their tags. It matches performer names (including aliases) with scene/image tags, even if tags contain special characters like dashes, underscores, dots, or hashtags. The plugin can be run manually or triggered automatically when scenes or images are created or updated.
4+
5+
## **Features**
6+
**Auto-matching performers** – Identifies performers in scenes and images by comparing tags with performer names and aliases.
7+
**Handles special characters** – Matches tags like `joe-mama`, `joe_mama`, `joe.mama`, `#Joe+mama` to the performer "Joe Mama".
8+
**Runs manually or via hooks** – Can be executed on demand or triggered automatically when scenes or images are created or updated.
9+
**Prevents unnecessary updates** – Only updates scenes/images when performers actually change.
10+
**Logging support** – Outputs logs to help track plugin activity.
11+
12+
## **Installation**
13+
Refer to Stash-Docs: https://docs.stashapp.cc/plugins/
14+
15+
## **Usage**
16+
17+
### **Manual Execution**
18+
1. Navigate to **Settings → Tasks → Plugin Tasks**
19+
2. Run **Auto Set Performers From Tags** to process all scenes and images.
20+
21+
### **Automatic Execution via Hooks**
22+
The plugin automatically updates performers when:
23+
- A scene is **created or updated**
24+
- An image is **created or updated**
25+
26+
Stash will trigger the plugin to update performer assignments based on the tags present.
27+
28+
## **How It Works**
29+
30+
1. **Fetch Performers**
31+
- Retrieves all performers and their aliases.
32+
33+
2. **Process Scenes & Images**
34+
- Fetches all scenes and images.
35+
- Matches performer names/aliases against scene/image tags.
36+
- Updates scenes and images with matched performers if necessary.
37+
38+
3. **Handle Hooks**
39+
- If triggered by a hook, processes only the relevant scene or image.
40+
41+
### **Example Matching**
42+
43+
| Performer Name | Alias List | Matching Tags |
44+
|--------------|-----------|--------------|
45+
| `Joe Mama` | `["Big Mama", "Mother Joe"]` | `joe-mama`, `joe.mama`, `#Joe_Mama`, `big-mama` |
46+
| `John Doe` | `["JD", "Johnny"]` | `john-doe`, `#JD`, `johnny` |
47+
| `Jane Smith` | `["J. Smith", "J-S"]` | `jane-smith`, `j_smith`, `#J-S` |
48+
49+
### **Logging**
50+
The plugin uses `log.Info()`, `log.Debug()`, and `log.Error()` for debugging. Check logs in Stash for details.
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
(function () {
2+
if (input.Args.hookContext) {
3+
log.Debug("Hook triggered: " + input.Args.hookContext.type);
4+
5+
const hookData = input.Args.hookContext;
6+
const performers = getAllPerformers();
7+
8+
if (hookData.type.startsWith("Scene")) {
9+
processSingleScene(hookData.id, performers);
10+
} else if (hookData.type.startsWith("Image")) {
11+
processSingleImage(hookData.id, performers);
12+
}
13+
14+
return { Output: "Hook processed: " + hookData.id };
15+
}
16+
17+
log.Info("Fetching all performers...");
18+
const performers = getAllPerformers();
19+
20+
log.Info("Processing scenes...");
21+
processScenes(performers);
22+
23+
log.Info("Processing images...");
24+
processImages(performers);
25+
26+
log.Info("Done!");
27+
return { Output: "Success" };
28+
})();
29+
30+
function getAllPerformers() {
31+
const query = `
32+
query {
33+
findPerformers(filter: { per_page: -1 }) {
34+
performers {
35+
id
36+
name
37+
alias_list
38+
}
39+
}
40+
}
41+
`;
42+
43+
const result = gql.Do(query, {});
44+
return result.findPerformers.performers || [];
45+
}
46+
47+
function getAllScenes() {
48+
const query = `
49+
query {
50+
findScenes(filter: { per_page: -1 }) {
51+
scenes {
52+
id
53+
tags { name }
54+
performers { id }
55+
}
56+
}
57+
}
58+
`;
59+
60+
const result = gql.Do(query, {});
61+
return result.findScenes.scenes || [];
62+
}
63+
64+
function getAllImages() {
65+
const query = `
66+
query {
67+
findImages(filter: { per_page: -1 }) {
68+
images {
69+
id
70+
tags { name }
71+
performers { id }
72+
}
73+
}
74+
}
75+
`;
76+
77+
const result = gql.Do(query, {});
78+
return result.findImages.images || [];
79+
}
80+
81+
function getSceneById(sceneId) {
82+
const query = `
83+
query SceneById($id: ID!) {
84+
findScene(id: $id) {
85+
id
86+
tags { name }
87+
performers { id }
88+
}
89+
}
90+
`;
91+
92+
const result = gql.Do(query, { id: sceneId });
93+
return result.findScene || null;
94+
}
95+
96+
function getImageById(imageId) {
97+
const query = `
98+
query ImageById($id: ID!) {
99+
findImage(id: $id) {
100+
id
101+
tags { name }
102+
performers { id }
103+
}
104+
}
105+
`;
106+
107+
const result = gql.Do(query, { id: imageId });
108+
return result.findImage || null;
109+
}
110+
111+
function updateScenePerformers(sceneId, performerIds) {
112+
const mutation = `
113+
mutation UpdateScene($id: ID!, $performerIds: [ID!]) {
114+
sceneUpdate(input: { id: $id, performer_ids: $performerIds }) {
115+
id
116+
}
117+
}
118+
`;
119+
120+
gql.Do(mutation, { id: sceneId, performerIds: performerIds });
121+
log.Debug(
122+
"Updated Scene " +
123+
sceneId +
124+
" with Performers " +
125+
JSON.stringify(performerIds)
126+
);
127+
}
128+
129+
function updateImagePerformers(imageId, performerIds) {
130+
const mutation = `
131+
mutation UpdateImage($id: ID!, $performerIds: [ID!]) {
132+
imageUpdate(input: { id: $id, performer_ids: $performerIds }) {
133+
id
134+
}
135+
}
136+
`;
137+
138+
gql.Do(mutation, { id: imageId, performerIds: performerIds });
139+
log.Debug(
140+
"Updated Image " +
141+
imageId +
142+
" with Performers " +
143+
JSON.stringify(performerIds)
144+
);
145+
}
146+
147+
function normalizeName(name) {
148+
return name
149+
.toLowerCase()
150+
.replace(/[#@._+\-]/g, " ") // Convert special characters to spaces
151+
.replace(/\s+/g, " ") // Collapse multiple spaces
152+
.trim();
153+
}
154+
155+
function matchPerformers(tags, performers) {
156+
const matchedPerformers = [];
157+
const tagSet = new Set(tags.map((tag) => normalizeName(tag.name)));
158+
159+
for (let performer of performers) {
160+
const performerNames = new Set(
161+
[performer.name].concat(performer.alias_list).map(normalizeName)
162+
);
163+
164+
if ([...performerNames].some((name) => tagSet.has(name))) {
165+
matchedPerformers.push(performer.id);
166+
}
167+
}
168+
169+
return matchedPerformers;
170+
}
171+
172+
function processScenes(performers) {
173+
const scenes = getAllScenes();
174+
175+
for (let scene of scenes) {
176+
const existingPerformerIds = scene.performers.map((p) => p.id); // Extract IDs from performer objects
177+
const matchedPerformerIds = matchPerformers(scene.tags, performers);
178+
179+
if (
180+
matchedPerformerIds.length > 0 &&
181+
JSON.stringify(matchedPerformerIds) !==
182+
JSON.stringify(existingPerformerIds)
183+
) {
184+
updateScenePerformers(scene.id, matchedPerformerIds);
185+
}
186+
}
187+
}
188+
189+
function processImages(performers) {
190+
const images = getAllImages();
191+
192+
for (let image of images) {
193+
const existingPerformerIds = image.performers.map((p) => p.id); // Extract IDs from performer objects
194+
const matchedPerformerIds = matchPerformers(image.tags, performers);
195+
196+
if (
197+
matchedPerformerIds.length > 0 &&
198+
JSON.stringify(matchedPerformerIds) !==
199+
JSON.stringify(existingPerformerIds)
200+
) {
201+
updateImagePerformers(image.id, matchedPerformerIds);
202+
}
203+
}
204+
}
205+
206+
function processSingleScene(sceneId, performers) {
207+
const scene = getSceneById(sceneId);
208+
if (!scene) return;
209+
210+
const existingPerformerIds = scene.performers.map((p) => p.id);
211+
const matchedPerformers = matchPerformers(scene.tags, performers);
212+
213+
if (
214+
matchedPerformers.length > 0 &&
215+
JSON.stringify(matchedPerformers) !== JSON.stringify(existingPerformerIds)
216+
) {
217+
updateScenePerformers(scene.id, matchedPerformers);
218+
}
219+
}
220+
221+
function processSingleImage(imageId, performers) {
222+
const image = getImageById(imageId);
223+
if (!image) return;
224+
225+
const existingPerformerIds = image.performers.map((p) => p.id);
226+
const matchedPerformers = matchPerformers(image.tags, performers);
227+
228+
if (
229+
matchedPerformers.length > 0 &&
230+
JSON.stringify(matchedPerformers) !== JSON.stringify(existingPerformerIds)
231+
) {
232+
updateImagePerformers(image.id, matchedPerformers);
233+
}
234+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Set Performers From Tags
2+
description: Automatically sets performers in scenes and images based on tags.
3+
version: 1.0.0
4+
url: https://github.com/Torrafox/stash-community-scripts/tree/main/plugins/setPerformersFromTags
5+
exec:
6+
- setPerformersFromTags.js
7+
interface: js
8+
errLog: info
9+
tasks:
10+
- name: Auto Set Performers From Tags
11+
description: Scans all scenes and images, matches performer names and aliases against scene/image tags, and updates them with the correct performers if necessary. May take a long time on large libraries.
12+
13+
hooks:
14+
- name: Auto Set Performers From Tags Hook
15+
description: Automatically sets performers when a scene or image is created or updated.
16+
triggeredBy:
17+
- Scene.Create.Post
18+
- Scene.Update.Post
19+
- Image.Create.Post
20+
- Image.Update.Post

0 commit comments

Comments
 (0)