Additional Files Deleter (#256)

Co-authored-by: DogmaDragon <103123951+DogmaDragon@users.noreply.github.com>
This commit is contained in:
elkorol
2025-05-03 07:48:47 +01:00
committed by GitHub
parent f82757d1aa
commit eeb2d3f9cb
4 changed files with 266 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
# Addtional Files Deleter
This is a plugin that will scan your Stash for either scenes or images where the file count is above 1. It will then skip over the primary file for each scene or image object and delete these extra files. Usually scene that contain multiple files are identical Phash matches (Unless you have manually merged unidentical Phashed files). Image objects that contain multiple files are grouped together under identical checksums, not Phashes. (You can't manually merge images as of yet.)
## Usage
Copy repository into Stash plugins folder or add via the new plugins system and refresh your plugins from the Settings.
If on first run you may want to run the Create Tag task, which creates an ignore tag that you can apply to Scenes or Images, so that they are bypassed when any of the other tasks are run.
Other than Create Tag task you can run the following tasks.
Images - Delete
Images - Delete & Record
Scenes - Delete
Scenes - Delete & Record
Tasks that just specify delete will just delete addtional files from their respective objects and Delete & Record will take the file paths of the files to be deleted, prefix them with "File: " (For latter easy searching) and it will append them to the current list of urls the object has and update the object. This is just a precaution to record perhaps usefull metadata an additional file path may hold for later use.

View File

@@ -0,0 +1,215 @@
import sys, json
import stashapi.log as log
from stashapi.stashapp import StashInterface
SVG_IMAGE = (
"data:image/svg+xml;base64,PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIi"
"AiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4KDTwhLS0gVXBsb2FkZW"
"QgdG86IFNWRyBSZXBvLCB3d3cuc3ZncmVwby5jb20sIFRyYW5zZm9ybWVkIGJ5OiBTVkcgUmVwbyBNaXhlciBUb2"
"9scyAtLT4KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD"
"0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KDTxnIGlkPSJTVkdSZXBvX2JnQ2Fycm"
"llciIgc3Ryb2tlLXdpZHRoPSIwIi8+Cg08ZyBpZD0iU1ZHUmVwb190cmFjZXJDYXJyaWVyIiBzdHJva2UtbGluZW"
"NhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KDTxnIGlkPSJTVkdSZXBvX2ljb25DYXJyaWVyIj"
"4gPHBhdGggZD0iTTUuNjM2MDUgNS42MzYwNUwxOC4zNjQgMTguMzY0TTUuNjM2MDUgMTguMzY0TDE4LjM2NCA1Lj"
"YzNjA1TTIxIDEyQzIxIDE2Ljk3MDYgMTYuOTcwNiAyMSAxMiAyMUM3LjAyOTQ0IDIxIDMgMTYuOTcwNiAzIDEyQz"
"MgNy4wMjk0NCA3LjAyOTQ0IDMgMTIgM0MxNi45NzA2IDMgMjEgNy4wMjk0NCAyMSAxMloiIHN0cm9rZT0iI2ZmZm"
"ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPiA8L2c+Cg08L3N2Zz4="
)
tag_exclude = {
"name": "Addtional Files Deleter: Scenes/Images: Ignore",
"description": "Addtional Files Deleter: Scene/Image Objects that contain addtional files "
"will not be deleted",
"image": SVG_IMAGE
}
def main():
global stash
json_input = json.loads(sys.stdin.read())
mode_arg = json_input["args"]["mode"]
stash = StashInterface(json_input["server_connection"])
if mode_arg == "create_tag":
create_tag(tag_exclude)
if mode_arg == "remove_tag":
remove_tag()
if mode_arg == "images_delete":
images_delete()
if mode_arg == "images_delete_record_paths":
images_delete_record_paths()
if mode_arg == "scenes_delete":
scenes_delete()
if mode_arg == "scenes_delete_record_paths":
scenes_delete_record_paths()
def update_image(image_id, paths):
update = stash.update_image(
{'id': image_id, 'urls': paths})
return update
def update_scene(scene_id, paths):
update = stash.update_scene(
{'id': scene_id, 'urls': paths})
return update
def find_images(find_images_tag):
image_count, images = stash.find_images(
f={
"file_count": {"modifier": "GREATER_THAN", "value": 1},
"tags": {"modifier": "EXCLUDES", "value": find_images_tag},
},
filter={
"per_page": "-1"
},
get_count=True,
)
return image_count, images
def find_scenes(find_scenes_tag):
scene_count, scenes = stash.find_scenes(
f={
"file_count": {"modifier": "GREATER_THAN", "value": 1},
"tags": {"modifier": "EXCLUDES", "value": find_scenes_tag},
},
filter={
"per_page": "-1"
},
get_count=True,
)
return scene_count, scenes
def find_tag(name, create=False):
find_tag_tag = stash.find_tag(name, create)
if find_tag_tag is None:
log.error(f"Tag does not exist: {tag_exclude['name']}")
else:
log.info(f"Found Tag: ID:{find_tag_tag['id']} Name: {find_tag_tag['name']}")
return find_tag_tag
def create_tag(obj):
create_tag_tag = stash.create_tag(obj)
if create_tag_tag is None:
log.error(f'Tag already exists: {tag_exclude["name"]}')
else:
log.info(f"Created Tag: ID:{create_tag_tag['id']} Name: {create_tag_tag['name']}")
return create_tag_tag
def remove_tag():
remove_tag_tag = find_tag(tag_exclude["name"])
if remove_tag_tag is not None:
stash.destroy_tag(remove_tag_tag['id'])
log.info(f"Deleted Tag - ID:{remove_tag_tag['id']}: Name: {remove_tag_tag['name']}")
def images_delete():
images_delete_tag = find_tag(tag_exclude)
if images_delete_tag is None:
images_delete_tag = create_tag(tag_exclude)
image_count, images = find_images(images_delete_tag["id"])
log.info(f"Deleting Addtional files of {image_count} image objects")
for j, image in enumerate(images):
log.progress(j / image_count)
for i, file in enumerate(image["visual_files"]):
if i == 0: # skip first ID
continue
delete = stash.destroy_files(file["id"])
if delete is True:
log.info(f"Image ID:{image['id']} - File ID:{file['id']} - Deleted: {file['path']}")
else:
log.error(f"Image ID:{image['id']} - File ID:{file['id']} - Could not be Deleted: {file['path']}")
def images_delete_record_paths():
images_delete_record_tag = find_tag(tag_exclude)
if images_delete_record_tag is None:
images_delete_record_tag = create_tag(tag_exclude)
image_count, images = find_images(images_delete_record_tag["id"])
log.info(f"Deleting Addtional Images of {image_count} image objects and recording paths in URLs Field")
for j, image in enumerate(images):
image_id = image["id"]
paths = image["urls"]
log.progress(j / image_count)
for i, file in enumerate(image["visual_files"]):
if i == 0: # skip first ID
continue
path = file["path"]
delete = stash.destroy_files(file["id"])
if delete is True:
log.info(f"Image ID:{image['id']} - File ID:{file['id']} - Deleted: {path}")
paths.append("File: " + path)
else:
log.error(f"Image ID:{image['id']} - File ID:{file['id']} - Could not be Deleted: {path}")
update = update_image(image_id, paths)
if update is not None:
log.info(f"Image ID:{image_id}: Updated with path(s) as URLs: {path}")
else:
log.error(f"Image ID:{image_id}: Could not be updated with path(s) as URLs: {path}")
def scenes_delete():
scenes_delete_tag = find_tag(tag_exclude)
if scenes_delete_tag is None:
scenes_delete_tag = create_tag(tag_exclude)
scene_count, scenes = find_scenes(scenes_delete_tag["id"])
log.info(f"Deleting Addtional files of {scene_count} scene objects and recording paths in URLs Field")
for j, scene in enumerate(scenes):
log.progress(j / scene_count)
for i, file in enumerate(scene["files"]):
if i == 0: # skip first ID
continue
delete = stash.destroy_files(file["id"])
if delete is True:
log.info(f"Scene ID:{scene['id']} - File ID:{file['id']} - Deleted: {file['path']}")
else:
log.error(f"Scene ID:{scene['id']} - File ID:{file['id']} - Could not be Deleted: {file['path']}")
def scenes_delete_record_paths():
scenes_delete_record_tag = find_tag(tag_exclude)
if scenes_delete_record_tag is None:
scenes_delete_record_tag = create_tag(tag_exclude)
scene_count, scenes = find_scenes(scenes_delete_record_tag["id"])
log.info(f"Deleting Addtional files of {scene_count} scene objects and recording paths in URLs Field")
for j, scene in enumerate(scenes):
log.progress(j / scene_count)
scene_id = scene["id"]
paths = scene["urls"]
for i, file in enumerate(scene["files"]):
if i == 0: # skip first ID
continue
path = file["path"]
delete = stash.destroy_files(file["id"])
if delete is True:
log.info(f"Scene ID:{scene['id']} - File ID:{file['id']} - Deleted: {path}")
paths.append("File: " + path)
else:
log.error(f"Scene ID:{scene['id']} - File ID:{file['id']} - Could not be Deleted: {path}")
update = update_scene(scene_id, paths)
if update is not None:
log.info(f"Scene ID:{scene_id}: Updated with path(s) as URLs: {path}")
else:
log.error(f"Scene ID:{scene_id}: Could not be updated with path(s) as URLs: "
"{path}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,32 @@
name: Addtional Files Deleter
description: Deletes addtional files assosiated with an image or scene object. Which will usually have identical PHashes for scenes or Checksum for images. Unless is a scene manually merged. Apply ignore tag to scene/image object for plugin to bypass.
version: 0.1
exec:
- python
- "{pluginDir}/deleter.py"
interface: raw
tasks:
- name: Create Tag
description: Create the plugin Ignore Tag
defaultArgs:
mode: create_tag
- name: Remove Tag
description: Remove the plugin Ignore Tag
defaultArgs:
mode: remove_tag
- name: Images - Delete
description: Image objects that contain addtional files will be deleted
defaultArgs:
mode: images_delete
- name: Images - Delete & Record
description: Addtional files will be deleted & old paths will be stored in Image object URLs field (Incase they contain future needed metadata)
defaultArgs:
mode: images_delete_record_paths
- name: Scenes - Delete
description: Scene objects that contain addtional files will be deleted
defaultArgs:
mode: scenes_delete
- name: Scenes - Delete & Record
description: Addtional files will be deleted & old paths will be stored in Scene object URLs field (Incase they contain future needed metadata)
defaultArgs:
mode: scenes_delete_record_paths

View File

@@ -0,0 +1 @@
stashapp-tools==0.2.40