Add plugin "Set Performers From Tags" (#503)

This commit is contained in:
Torrafox
2025-02-10 19:57:23 +01:00
committed by GitHub
parent 295030d69c
commit 04a4a32b7c
3 changed files with 304 additions and 0 deletions

View File

@@ -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.

View File

@@ -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);
}
}

View File

@@ -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