Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions plugins/setPerformersFromTags/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# **Set Performers From Tags**

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.

## **Features**
✅ **Auto-matching performers** – Identifies performers in scenes and images by comparing tags with performer names and aliases.
✅ **Handles special characters** – Matches tags like `joe-mama`, `joe_mama`, `joe.mama`, `#Joe+mama` to the performer "Joe Mama".
✅ **Runs manually or via hooks** – Can be executed on demand or triggered automatically when scenes or images are created or updated.
✅ **Prevents unnecessary updates** – Only updates scenes/images when performers actually change.
✅ **Logging support** – Outputs logs to help track plugin activity.

## **Installation**
Refer to Stash-Docs: https://docs.stashapp.cc/plugins/

## **Usage**

### **Manual Execution**
1. Navigate to **Settings → Tasks → Plugin Tasks**
2. Run **Auto Set Performers From Tags** to process all scenes and images.

### **Automatic Execution via Hooks**
The plugin automatically updates performers when:
- A scene is **created or updated**
- An image is **created or updated**

Stash will trigger the plugin to update performer assignments based on the tags present.

## **How It Works**

1. **Fetch Performers**
- Retrieves all performers and their aliases.

2. **Process Scenes & Images**
- Fetches all scenes and images.
- Matches performer names/aliases against scene/image tags.
- Updates scenes and images with matched performers if necessary.

3. **Handle Hooks**
- If triggered by a hook, processes only the relevant scene or image.

### **Example Matching**

| Performer Name | Alias List | Matching Tags |
|--------------|-----------|--------------|
| `Joe Mama` | `["Big Mama", "Mother Joe"]` | `joe-mama`, `joe.mama`, `#Joe_Mama`, `big-mama` |
| `John Doe` | `["JD", "Johnny"]` | `john-doe`, `#JD`, `johnny` |
| `Jane Smith` | `["J. Smith", "J-S"]` | `jane-smith`, `j_smith`, `#J-S` |

### **Logging**
The plugin uses `log.Info()`, `log.Debug()`, and `log.Error()` for debugging. Check logs in Stash for details.
234 changes: 234 additions & 0 deletions plugins/setPerformersFromTags/setPerformersFromTags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
(function () {
if (input.Args.hookContext) {
log.Debug("Hook triggered: " + input.Args.hookContext.type);

const hookData = input.Args.hookContext;
const performers = getAllPerformers();

if (hookData.type.startsWith("Scene")) {
processSingleScene(hookData.id, performers);
} else if (hookData.type.startsWith("Image")) {
processSingleImage(hookData.id, performers);
}

return { Output: "Hook processed: " + hookData.id };
}

log.Info("Fetching all performers...");
const performers = getAllPerformers();

log.Info("Processing scenes...");
processScenes(performers);

log.Info("Processing images...");
processImages(performers);

log.Info("Done!");
return { Output: "Success" };
})();

function getAllPerformers() {
const query = `
query {
findPerformers(filter: { per_page: -1 }) {
performers {
id
name
alias_list
}
}
}
`;

const result = gql.Do(query, {});
return result.findPerformers.performers || [];
}

function getAllScenes() {
const query = `
query {
findScenes(filter: { per_page: -1 }) {
scenes {
id
tags { name }
performers { id }
}
}
}
`;

const result = gql.Do(query, {});
return result.findScenes.scenes || [];
}

function getAllImages() {
const query = `
query {
findImages(filter: { per_page: -1 }) {
images {
id
tags { name }
performers { id }
}
}
}
`;

const result = gql.Do(query, {});
return result.findImages.images || [];
}

function getSceneById(sceneId) {
const query = `
query SceneById($id: ID!) {
findScene(id: $id) {
id
tags { name }
performers { id }
}
}
`;

const result = gql.Do(query, { id: sceneId });
return result.findScene || null;
}

function getImageById(imageId) {
const query = `
query ImageById($id: ID!) {
findImage(id: $id) {
id
tags { name }
performers { id }
}
}
`;

const result = gql.Do(query, { id: imageId });
return result.findImage || null;
}

function updateScenePerformers(sceneId, performerIds) {
const mutation = `
mutation UpdateScene($id: ID!, $performerIds: [ID!]) {
sceneUpdate(input: { id: $id, performer_ids: $performerIds }) {
id
}
}
`;

gql.Do(mutation, { id: sceneId, performerIds: performerIds });
log.Debug(
"Updated Scene " +
sceneId +
" with Performers " +
JSON.stringify(performerIds)
);
}

function updateImagePerformers(imageId, performerIds) {
const mutation = `
mutation UpdateImage($id: ID!, $performerIds: [ID!]) {
imageUpdate(input: { id: $id, performer_ids: $performerIds }) {
id
}
}
`;

gql.Do(mutation, { id: imageId, performerIds: performerIds });
log.Debug(
"Updated Image " +
imageId +
" with Performers " +
JSON.stringify(performerIds)
);
}

function normalizeName(name) {
return name
.toLowerCase()
.replace(/[#@._+\-]/g, " ") // Convert special characters to spaces
.replace(/\s+/g, " ") // Collapse multiple spaces
.trim();
}

function matchPerformers(tags, performers) {
const matchedPerformers = [];
const tagSet = new Set(tags.map((tag) => normalizeName(tag.name)));

for (let performer of performers) {
const performerNames = new Set(
[performer.name].concat(performer.alias_list).map(normalizeName)
);

if ([...performerNames].some((name) => tagSet.has(name))) {
matchedPerformers.push(performer.id);
}
}

return matchedPerformers;
}

function processScenes(performers) {
const scenes = getAllScenes();

for (let scene of scenes) {
const existingPerformerIds = scene.performers.map((p) => p.id); // Extract IDs from performer objects
const matchedPerformerIds = matchPerformers(scene.tags, performers);

if (
matchedPerformerIds.length > 0 &&
JSON.stringify(matchedPerformerIds) !==
JSON.stringify(existingPerformerIds)
) {
updateScenePerformers(scene.id, matchedPerformerIds);
}
}
}

function processImages(performers) {
const images = getAllImages();

for (let image of images) {
const existingPerformerIds = image.performers.map((p) => p.id); // Extract IDs from performer objects
const matchedPerformerIds = matchPerformers(image.tags, performers);

if (
matchedPerformerIds.length > 0 &&
JSON.stringify(matchedPerformerIds) !==
JSON.stringify(existingPerformerIds)
) {
updateImagePerformers(image.id, matchedPerformerIds);
}
}
}

function processSingleScene(sceneId, performers) {
const scene = getSceneById(sceneId);
if (!scene) return;

const existingPerformerIds = scene.performers.map((p) => p.id);
const matchedPerformers = matchPerformers(scene.tags, performers);

if (
matchedPerformers.length > 0 &&
JSON.stringify(matchedPerformers) !== JSON.stringify(existingPerformerIds)
) {
updateScenePerformers(scene.id, matchedPerformers);
}
}

function processSingleImage(imageId, performers) {
const image = getImageById(imageId);
if (!image) return;

const existingPerformerIds = image.performers.map((p) => p.id);
const matchedPerformers = matchPerformers(image.tags, performers);

if (
matchedPerformers.length > 0 &&
JSON.stringify(matchedPerformers) !== JSON.stringify(existingPerformerIds)
) {
updateImagePerformers(image.id, matchedPerformers);
}
}
20 changes: 20 additions & 0 deletions plugins/setPerformersFromTags/setPerformersFromTags.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Set Performers From Tags
description: Automatically sets performers in scenes and images based on tags.
version: 1.0.0
url: https://github.com/Torrafox/stash-community-scripts/tree/main/plugins/setPerformersFromTags
exec:
- setPerformersFromTags.js
interface: js
errLog: info
tasks:
- name: Auto Set Performers From Tags
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.

hooks:
- name: Auto Set Performers From Tags Hook
description: Automatically sets performers when a scene or image is created or updated.
triggeredBy:
- Scene.Create.Post
- Scene.Update.Post
- Image.Create.Post
- Image.Update.Post
Loading