-
-
Notifications
You must be signed in to change notification settings - Fork 225
Significant performance improvement across tagGalleriesFromImages #721
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,13 +2,22 @@ | |
| from stashapi.stashapp import StashInterface | ||
| import sys | ||
| import json | ||
| import time | ||
|
|
||
| GALLERY_PAGE_SIZE = 150 | ||
| tag_cache = {} | ||
|
|
||
|
|
||
| def processAll(): | ||
| exclusion_marker_tag_id = None | ||
| if settings["excludeWithTag"] != "": | ||
| exclussion_marker_tag = stash.find_tag(settings["excludeWithTag"]) | ||
| if exclussion_marker_tag is not None: | ||
| exclusion_marker_tag_id = exclussion_marker_tag['id'] | ||
| if settings["excludeWithTag"]: | ||
| if settings["excludeWithTag"] in tag_cache: | ||
| exclusion_marker_tag_id = tag_cache[settings["excludeWithTag"]]['id'] | ||
| else: | ||
| exclusion_marker_tag = stash.find_tag(settings["excludeWithTag"]) | ||
| if exclusion_marker_tag: | ||
| exclusion_marker_tag_id = exclusion_marker_tag['id'] | ||
| tag_cache[settings["excludeWithTag"]] = exclusion_marker_tag | ||
|
|
||
| query = { | ||
| "image_count": { | ||
|
|
@@ -18,95 +27,137 @@ def processAll(): | |
| } | ||
| if settings['excludeOrganized']: | ||
| query["organized"] = False | ||
| if exclusion_marker_tag_id is not None: | ||
| if exclusion_marker_tag_id: | ||
| query["tags"] = { | ||
| "value": [exclusion_marker_tag_id], | ||
| "modifier": "EXCLUDES" | ||
| } | ||
|
|
||
| total_count = stash.find_galleries(f=query, filter={"page": 0, "per_page": 0}, get_count=True)[0] | ||
| i = 0 | ||
| while i < total_count: | ||
| log.progress((i / total_count)) | ||
| try: | ||
| total_count = stash.find_galleries(f=query, filter={"page": 1, "per_page": 1}, get_count=True)[0] | ||
| except Exception: | ||
| total_count = 0 | ||
|
|
||
| processed = 0 | ||
| page = 1 | ||
|
|
||
| while True: | ||
|
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. Not a huge fan of while loops without an explicit abort condition if there's an option to have one. Any reason |
||
| 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 } studio { id }" | ||
| ) | ||
|
|
||
| galleries = stash.find_galleries(f=query, filter={"page": i, "per_page": 1}) | ||
| if len(galleries) == 0: | ||
| if not galleries: | ||
| log.info("Finished processing all galleries.") | ||
| break | ||
| gallery = galleries[0] | ||
|
|
||
| processGallery(gallery) | ||
| for gallery in galleries: | ||
| processGallery(gallery) | ||
| processed += 1 | ||
|
|
||
| page += 1 | ||
|
|
||
| tag_cache.clear() | ||
|
|
||
|
|
||
| def should_exclude_gallery(gallery, exclusion_tag_name): | ||
| if exclusion_tag_name: | ||
| for tag in gallery.get("tags", []): | ||
| if tag["name"] == exclusion_tag_name: | ||
| return True | ||
|
|
||
| if settings['excludeOrganized'] and gallery.get('organized'): | ||
| return True | ||
|
|
||
| return False | ||
|
|
||
| i = i + 1 | ||
|
|
||
| def processGallery(gallery: dict): | ||
| if should_exclude_gallery(gallery, settings["excludeWithTag"]): | ||
| return | ||
|
|
||
| def processGallery(gallery : dict): | ||
| tags = [] | ||
| performersIds = [] | ||
| should_tag = True | ||
| if settings["excludeWithTag"] != "": | ||
| for tag in gallery["tags"]: | ||
| if tag["name"] == settings["excludeWithTag"]: | ||
| should_tag = False | ||
| break | ||
| gallery_id = gallery['id'] | ||
|
|
||
| images = stash.find_gallery_images( | ||
| gallery_id, | ||
| fragment='tags { id } performers { id } studio { id }' | ||
| ) | ||
|
|
||
| if settings['excludeOrganized']: | ||
| if gallery['organized']: | ||
| should_tag = False | ||
|
|
||
| if should_tag: | ||
| existing_tag_ids = {t['id'] for t in gallery['tags']} | ||
| existing_performer_ids = {p['id'] for p in gallery['performers']} | ||
|
|
||
| images = stash.find_gallery_images(gallery['id'], fragment='tags { id name } performers { id name }') | ||
| if len(images) > 0: | ||
| tag_ids = set() | ||
| tag_names = set() | ||
|
|
||
| performer_ids = set() | ||
| performer_names = set() | ||
|
|
||
| for image in images: | ||
| image_tag_ids = [tag['id'] for tag in image['tags']] | ||
| image_tag_names = [tag['name'] for tag in image['tags']] | ||
| tag_ids.update(image_tag_ids) | ||
| tag_names.update(image_tag_names) | ||
|
|
||
| image_performer_ids = [performer['id'] for performer in image['performers']] | ||
| image_performer_names = [performer['name'] for performer in image['performers']] | ||
| performer_ids.update(image_performer_ids) | ||
| performer_names.update(image_performer_names) | ||
|
|
||
| new_tags_ids = tag_ids - existing_tag_ids | ||
| new_performer_ids = performer_ids - existing_performer_ids | ||
|
|
||
| if len(new_tags_ids) > 0 or len(new_performer_ids) > 0: | ||
| log.info(f"updating gallery {gallery['id']} from {len(images)} images with tags {tag_names} ({len(new_tags_ids)} new) and performers {performer_names} ({len(new_performer_ids)} new)") | ||
| stash.update_galleries({"ids": gallery['id'], "tag_ids": {"mode": "ADD", "ids": list(new_tags_ids)}, "performer_ids": {"mode": "ADD", "ids": list(new_performer_ids)}}) | ||
| if not images: | ||
| return | ||
|
|
||
| all_found_tag_ids = {t['id'] for img in images for t in img.get('tags', []) if 'id' in t} | ||
| all_found_performer_ids = {p['id'] for img in images for p in img.get('performers', []) if 'id' in p} | ||
| all_found_studio_ids = {img['studio']['id'] for img in images if img.get('studio') and img['studio'].get('id')} | ||
|
|
||
| if not all_found_tag_ids and not all_found_performer_ids and not all_found_studio_ids: | ||
| return | ||
|
|
||
| existing_tag_ids = {t['id'] for t in gallery.get('tags', [])} | ||
| existing_perf_ids = {p['id'] for p in gallery.get('performers', [])} | ||
| existing_studio_id = gallery.get('studio', {}).get('id') if gallery.get('studio') else None | ||
|
|
||
| missing_tags = all_found_tag_ids - existing_tag_ids | ||
| missing_perfs = all_found_performer_ids - existing_perf_ids | ||
|
|
||
| target_studio_id = list(all_found_studio_ids)[0] if all_found_studio_ids else None | ||
| studio_missing = bool(target_studio_id and (target_studio_id != existing_studio_id)) | ||
|
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. Performers and tags are additive changes (so manually added tags and performers remain unchanged), but the studio gets overwritten if already set (manually or otherwise). This seems worth talking about, and maybe should be a setting to be enabled if wanted behavior by user. |
||
|
|
||
| if not missing_tags and not missing_perfs and not studio_missing: | ||
| return | ||
|
|
||
| update_data = {"ids": [gallery_id]} | ||
| if missing_tags and all_found_tag_ids: | ||
| update_data["tag_ids"] = {"mode": "ADD", "ids": list(all_found_tag_ids)} | ||
|
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. Why are all tags and performers added again, if the missing IDs are already in |
||
| if missing_perfs and all_found_performer_ids: | ||
| update_data["performer_ids"] = {"mode": "ADD", "ids": list(all_found_performer_ids)} | ||
| if studio_missing: | ||
| update_data["studio_id"] = target_studio_id | ||
|
|
||
| gallery_name = gallery.get("title") or gallery.get("code") or f"ID {gallery_id}" | ||
| log.info(f"Syncing gallery '{gallery_name}' upward with structural image metadata updates.") | ||
|
|
||
| try: | ||
| stash.update_galleries(update_data) | ||
| except Exception as e: | ||
| log.error(f"Error updating gallery {gallery_id}: {str(e)}") | ||
|
|
||
|
|
||
| json_input = json.loads(sys.stdin.read()) | ||
| FRAGMENT_SERVER = json_input["server_connection"] | ||
| stash = StashInterface(FRAGMENT_SERVER) | ||
|
|
||
| config = stash.get_configuration() | ||
| settings = { | ||
| "excludeWithTag": "", | ||
| "excludeOrganized": False | ||
| } | ||
| if "tagGalleriesFromImages" in config["plugins"]: | ||
| settings = {"excludeWithTag": "", "excludeOrganized": False} | ||
|
|
||
| if config and "plugins" in config and "tagGalleriesFromImages" in config["plugins"]: | ||
| settings.update(config["plugins"]["tagGalleriesFromImages"]) | ||
|
|
||
| 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"] == "Gallery.Update.Post" | ||
| or json_input["args"]["hookContext"]["type"] == "Gallery.Create.Post" | ||
| ) and "inputFields" in json_input["args"]["hookContext"] | ||
| and len(json_input["args"]["hookContext"]["inputFields"]) > 2 | ||
| ): | ||
| gallery = stash.find_gallery(id) | ||
| processGallery(gallery) | ||
| time.sleep(0.05) | ||
| hook = json_input["args"]["hookContext"] | ||
| hook_type = hook.get("type", "") | ||
|
|
||
| # --- GALLERY HOOK HANDLER --- | ||
| if hook_type in ["Gallery.Update.Post", "Gallery.Create.Post"]: | ||
| gallery = stash.find_gallery(hook['id'], fragment="id title code organized tags { id name } performers { id } studio { id }") | ||
| if gallery: | ||
| processGallery(gallery) | ||
|
|
||
| # --- IMAGE HOOK HANDLER --- | ||
| elif hook_type in ["Image.Update.Post", "Image.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. These image hooks are not declared in the manifest, and will never be called. Also, should these be active by default? Every hook invocation already makes bulk update operations slower, and the images are not the ones getting changed, so I'd probably add a setting that has to be enabled to be more than a NO-OP |
||
| image_id = hook['id'] | ||
| image_data = stash.find_image(image_id, fragment="gallery { id }") | ||
|
|
||
| if image_data and image_data.get("gallery"): | ||
| gallery_id = image_data["gallery"]["id"] | ||
| gallery = stash.find_gallery(gallery_id, fragment="id title code organized tags { id name } performers { id } studio { id }") | ||
| if gallery: | ||
| processGallery(gallery) | ||
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.
I really don't get the point of the
tag_cache.processAll()gets invoked once, so there are no repeat lookups of the same thing to cache. And even then, it's a single tag lookup by name at worst. Should be removed if pointless, just added complexity.