-
-
Notifications
You must be signed in to change notification settings - Fork 225
Significant performance improvements across tagImagesWithPerfTags #720
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
DogmaDragon
wants to merge
3
commits into
stashapp:main
Choose a base branch
from
DogmaDragon:tagImagesWithPerfTags-update
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+181
−111
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,123 +3,193 @@ | |
| import sys | ||
| import json | ||
|
|
||
| GALLERY_PAGE_SIZE = 50 | ||
| IMAGE_UPDATE_BATCH = 1000 | ||
|
|
||
|
|
||
| def processAll(): | ||
| exclusion_marker_tag_id = None | ||
| if settings["excludeImageWithTag"] != "": | ||
| exclussion_marker_tag = stash.find_tag(settings["excludeImageWithTag"]) | ||
| if exclussion_marker_tag is not None: | ||
| exclusion_marker_tag_id = exclussion_marker_tag['id'] | ||
| if settings["excludeWithTag"]: | ||
| exclusion_marker_tag = stash.find_tag(settings["excludeWithTag"]) | ||
| if exclusion_marker_tag: | ||
| exclusion_marker_tag_id = exclusion_marker_tag["id"] | ||
|
|
||
| query = {"image_count": {"modifier": "NOT_EQUALS", "value": 0}} | ||
| if settings["excludeOrganized"]: | ||
| query["organized"] = False | ||
| if exclusion_marker_tag_id: | ||
| query["tags"] = {"value": [exclusion_marker_tag_id], "modifier": "EXCLUDES"} | ||
|
|
||
| try: | ||
| total_count = stash.find_galleries(f=query, filter={"page": 1, "per_page": 1}, get_count=True)[0] | ||
| except Exception: | ||
| total_count = 0 | ||
|
|
||
| log.info(f"Starting Process All: Inspecting {total_count} total galleries.") | ||
| processed = 0 | ||
| page = 1 | ||
|
|
||
| stats = {"skipped_empty": 0, "skipped_synced": 0, "skipped_excluded": 0} | ||
|
|
||
| while True: | ||
| if total_count > 0: | ||
| log.progress(min(processed / total_count, 1.0)) | ||
|
|
||
| galleries = stash.find_galleries( | ||
| f=query, | ||
| filter={"page": page, "per_page": GALLERY_PAGE_SIZE}, | ||
| fragment="id title code organized tags { id name } performers { id name } studio { id }" | ||
| ) | ||
|
|
||
| if not galleries: | ||
| log.info(f"Finished processing all galleries. Summary: " | ||
| f"Skipped: {stats['skipped_synced']} already synced, " | ||
| f"{stats['skipped_empty']} empty performer/tag metadata, {stats['skipped_excluded']} excluded filter.") | ||
| break | ||
|
|
||
| for gallery in galleries: | ||
| processGallery(gallery, stats) | ||
| processed += 1 | ||
|
|
||
| page += 1 | ||
|
|
||
|
|
||
| def processGallery(gallery: dict, stats: dict = None): | ||
| if stats is None: | ||
| stats = {"skipped_empty": 0, "skipped_synced": 0, "skipped_excluded": 0} | ||
|
|
||
| gallery_name = gallery.get("title") or gallery.get("code") or f"ID {gallery['id']}" | ||
|
|
||
| # Excluded via Settings Check | ||
| if settings["excludeWithTag"]: | ||
| for tag in gallery.get("tags", []): | ||
| if tag["name"] == settings["excludeWithTag"]: | ||
| stats["skipped_excluded"] += 1 | ||
| log.debug(f"Skipping Gallery '{gallery_name}': Has exclusion tag '{settings['excludeWithTag']}'.") | ||
| return | ||
|
|
||
| if settings["excludeOrganized"] and gallery.get("organized"): | ||
| stats["skipped_excluded"] += 1 | ||
| log.debug(f"Skipping Gallery '{gallery_name}': Marked as organized.") | ||
| return | ||
|
|
||
| gallery_tag_ids = [t["id"] for t in gallery.get("tags", [])] | ||
| gallery_performer_ids = [p["id"] for p in gallery.get("performers", [])] | ||
|
|
||
| query = { | ||
| "tags": { | ||
| "modifier": "NOT_NULL", | ||
| }, | ||
| "image_count": { | ||
| "modifier": "NOT_EQUALS", | ||
| "value": 0, | ||
| }, | ||
| } | ||
| performersTotal = stash.find_performers(f=query, filter={"page": 0, "per_page": 0}, get_count=True)[0] | ||
| i = 0 | ||
| while i < performersTotal: | ||
| log.progress((i / performersTotal)) | ||
|
|
||
| perf = stash.find_performers(f=query, filter={"page": i, "per_page": 1}) | ||
| # CRITICAL FIX: If this specific script has no performers AND no tags, skip it immediately. | ||
| # We do not allow a studio-only gallery to pass through this plugin. | ||
| if not gallery_tag_ids and not gallery_performer_ids: | ||
| stats["skipped_empty"] += 1 | ||
| return | ||
|
|
||
| performer_tags_ids = [] | ||
| performer_tags_names = [] | ||
| for performer_tag in perf[0]["tags"]: | ||
| performer_tags_ids.append(performer_tag["id"]) | ||
| performer_tags_names.append(performer_tag["name"]) | ||
|
|
||
| image_query = { | ||
| "performers": { | ||
| "value": [perf[0]["id"]], | ||
| "modifier": "INCLUDES_ALL" | ||
| } | ||
| } | ||
| if settings['excludeImageOrganized']: | ||
| image_query["organized"] = False | ||
| if exclusion_marker_tag_id is not None: | ||
| image_query["tags"] = { | ||
| "value": [exclusion_marker_tag_id], | ||
| "modifier": "EXCLUDES" | ||
| } | ||
|
|
||
| performer_image_count = stash.find_images(f=image_query, filter={"page": 0, "per_page": 0}, get_count=True)[0] | ||
| gallery_studio = gallery.get("studio") | ||
| gallery_studio_id = gallery_studio["id"] if gallery_studio else None | ||
|
|
||
| images = stash.find_gallery_images( | ||
| gallery["id"], | ||
| fragment="id tags { id } performers { id } studio { id }" | ||
| ) | ||
|
|
||
| if not images: | ||
| return | ||
|
|
||
| image_ids_to_update = [] | ||
| gallery_tags_set = set(gallery_tag_ids) | ||
| gallery_perfs_set = set(gallery_performer_ids) | ||
|
|
||
| for img in images: | ||
| existing_img_tags = {t['id'] for t in img.get('tags', [])} | ||
| existing_img_perfs = {p['id'] for p in img.get('performers', [])} | ||
|
|
||
| if performer_image_count > 0: | ||
| log.info(f"updating {performer_image_count} images of performer \"{ perf[0]['name']}\" with tags {performer_tags_names}") | ||
|
|
||
| performer_image_page_size = 100 | ||
| performer_image_page = 0 | ||
| while performer_image_page * performer_image_page_size < performer_image_count: | ||
| performer_images = stash.find_images(f=image_query, filter={"page": performer_image_page, "per_page": performer_image_page_size}, fragment='id') | ||
| performer_image_ids = [performer_image['id'] for performer_image in performer_images] | ||
|
|
||
| stash.update_images( | ||
| { | ||
| "ids": performer_image_ids, | ||
| "tag_ids": {"mode": "ADD", "ids": performer_tags_ids}, | ||
| } | ||
| ) | ||
| performer_image_page += 1 | ||
|
|
||
| i = i + 1 | ||
|
|
||
|
|
||
| def processImage(image): | ||
| tags = [] | ||
| performersIds = [] | ||
| should_tag = True | ||
| if settings["excludeImageWithTag"] != "": | ||
| for tag in image["tags"]: | ||
| if tag["name"] == settings["excludeImageWithTag"]: | ||
| should_tag = False | ||
| break | ||
| img_studio = img.get("studio") | ||
| img_studio_id = img_studio["id"] if img_studio else None | ||
|
|
||
| missing_tags = gallery_tags_set - existing_img_tags | ||
| missing_perfs = gallery_perfs_set - existing_img_perfs | ||
| studio_mismatch = (gallery_studio_id is not None and img_studio_id != gallery_studio_id) | ||
|
|
||
| if missing_tags or missing_perfs or studio_mismatch: | ||
| image_ids_to_update.append(img["id"]) | ||
|
|
||
| if not image_ids_to_update: | ||
| stats["skipped_synced"] += 1 | ||
| log.debug(f"Skipping Gallery '{gallery_name}': All child assets match parent metadata.") | ||
| return | ||
|
|
||
| # Reconstruct log metadata safely | ||
| perf_names = [p["name"] for p in gallery.get("performers", [])] | ||
| tag_names = [t["name"] for t in gallery.get("tags", [])] | ||
|
|
||
| if settings['excludeImageOrganized']: | ||
| if image['organized']: | ||
| should_tag = False | ||
|
|
||
| if should_tag: | ||
| for perf in image["performers"]: | ||
| performersIds.append(perf["id"]) | ||
| performers = [] | ||
| for perfId in performersIds: | ||
| performers.append(stash.find_performer(perfId)) | ||
| for perf in performers: | ||
| for tag in perf["tags"]: | ||
| tags.append(tag["id"]) | ||
| stash.update_images({"ids": image["id"], "tag_ids": {"mode": "ADD", "ids": tags}}) | ||
| tags = [] | ||
| performersIds = [] | ||
| performers = [] | ||
| perfs_string = ", ".join(perf_names) if perf_names else "None" | ||
| tags_string = ", ".join(tag_names) if tag_names else "None" | ||
| context_msg = f"Gallery: '{gallery_name}' | Performers: [{perfs_string}] | Tags: [{tags_string}]" | ||
|
|
||
| for i in range(0, len(image_ids_to_update), IMAGE_UPDATE_BATCH): | ||
| batch = image_ids_to_update[i:i + IMAGE_UPDATE_BATCH] | ||
| sendImageBatch(batch, gallery_tag_ids, gallery_performer_ids, gallery_studio_id, context_msg) | ||
|
|
||
|
|
||
| def sendImageBatch(image_ids, tag_ids, performer_ids, studio_id, context_msg): | ||
| update_data = {"ids": image_ids} | ||
| if tag_ids: | ||
| update_data["tag_ids"] = {"mode": "ADD", "ids": tag_ids} | ||
| if performer_ids: | ||
| update_data["performer_ids"] = {"mode": "ADD", "ids": performer_ids} | ||
| if studio_id: | ||
| update_data["studio_id"] = studio_id | ||
|
|
||
| log.info(f"Bulk updating {len(image_ids)} images ({context_msg})") | ||
| stash.update_images(update_data) | ||
|
|
||
|
|
||
| def processImageHook(image: dict): | ||
| target_tag_ids = set() | ||
| for perf in image.get("performers", []): | ||
| for tag in perf.get("tags", []): | ||
| target_tag_ids.add(tag["id"]) | ||
|
|
||
| if not target_tag_ids: | ||
| return | ||
|
|
||
| existing_tag_ids = {t["id"] for t in image.get("tags", [])} | ||
| missing_tags = target_tag_ids - existing_tag_ids | ||
|
|
||
| if not missing_tags: | ||
| return | ||
|
|
||
| log.info(f"Hook Update: Appending missing performer tags to Image ID {image['id']}") | ||
| stash.update_images({ | ||
| "ids": [image["id"]], | ||
| "tag_ids": {"mode": "ADD", "ids": list(missing_tags)} | ||
| }) | ||
|
|
||
|
|
||
| json_input = json.loads(sys.stdin.read()) | ||
| FRAGMENT_SERVER = json_input["server_connection"] | ||
| stash = StashInterface(FRAGMENT_SERVER) | ||
|
|
||
| config = stash.get_configuration() | ||
| settings = { | ||
| "excludeImageWithTag": "", | ||
| "excludeImageOrganized": False | ||
| } | ||
| if "tagImagesWithPerfTags" in config["plugins"]: | ||
| settings.update(config["plugins"]["tagImagesWithPerfTags"]) | ||
| settings = {"excludeWithTag": "", "excludeOrganized": False} | ||
|
|
||
| if "tagImagesFromGalleries" in config["plugins"]: | ||
| settings.update(config["plugins"]["tagImagesFromGalleries"]) | ||
|
|
||
| if "mode" in json_input["args"]: | ||
| PLUGIN_ARGS = json_input["args"]["mode"] | ||
| if "processAll" in PLUGIN_ARGS: | ||
| if "processAll" in json_input["args"]["mode"]: | ||
| processAll() | ||
| elif "hookContext" in json_input["args"]: | ||
| id = json_input["args"]["hookContext"]["id"] | ||
| if ( | ||
| ( | ||
| json_input["args"]["hookContext"]["type"] == "Image.Update.Post" | ||
| or "Image.Create.Post" | ||
| ) and "inputFields" in json_input["args"]["hookContext"] | ||
| and len(json_input["args"]["hookContext"]["inputFields"]) > 2 | ||
| ): | ||
| image = stash.find_image(id) | ||
| processImage(image) | ||
| hook = json_input["args"]["hookContext"] | ||
| hook_id = hook["id"] | ||
| hook_type = hook.get("type", "") | ||
|
|
||
| if hook_type in ["Gallery.Update.Post", "Gallery.Create.Post"]: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gallery hooks not in manifest, this will never be called. |
||
| if hook.get("inputFields") is not None: | ||
| gallery = stash.find_gallery(hook_id, fragment="id title code organized tags { id name } performers { id name } studio { id }") | ||
| if gallery: | ||
| processGallery(gallery) | ||
|
|
||
| elif hook_type in ["Image.Update.Post", "Image.Create.Post"]: | ||
| if hook.get("inputFields") is not None: | ||
| image = stash.find_image(hook_id, fragment="id tags { id } performers { id tags { id } }") | ||
| if image: | ||
| processImageHook(image) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,31 +1,31 @@ | ||
| name: Tag Images From Performer Tags | ||
| description: tags images with performer tags. | ||
| version: 0.1 | ||
| description: Tags images with performer tags. | ||
| version: 1.0 | ||
| url: https://discourse.stashapp.cc/t/tag-images-from-performer-tags/2059 | ||
| exec: | ||
| - python | ||
| - "{pluginDir}/tagImagesWithPerfTags.py" | ||
| interface: raw | ||
|
|
||
| hooks: | ||
| - name: update image | ||
| description: Will tag image with selected performers tags | ||
| - name: Update image | ||
| description: Will tag image with selected performers tags. | ||
| triggeredBy: | ||
| - Image.Update.Post | ||
| - Image.Create.Post | ||
|
|
||
| settings: | ||
| excludeImageOrganized: | ||
| displayName: Exclude Images marked as organized | ||
| description: Do not automatically tag images with performer tags if the image is marked as organized | ||
| description: Do not automatically tag images with performer tags if the image is marked as organized. | ||
| type: BOOLEAN | ||
| excludeImageWithTag: | ||
| displayName: Exclude Images with Tag from Hook | ||
| description: Do not automatically tag images with performer tags if the image has this tag | ||
| description: Do not automatically tag images with performer tags if the image has this tag. | ||
| type: STRING | ||
|
|
||
| tasks: | ||
| - name: "Tag All Images" | ||
| description: Loops through all performers, finds all of their images, then applies the performers tags to each of the images they appear in. Can take a long time on large db's. | ||
| description: Loops through all performers, finds all of their images, then applies the performers tags to each of the images they appear in. Can take a long time on large databases. | ||
| defaultArgs: | ||
| mode: processAll |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This whole
processAll()no longer inherits tags from performers to images, it is now a gallery to image inheritance logic, which completely changes the purpose of the plugin.