diff --git a/plugins/additionalFilesDeleter/README.md b/plugins/additionalFilesDeleter/README.md new file mode 100644 index 00000000..3621cab8 --- /dev/null +++ b/plugins/additionalFilesDeleter/README.md @@ -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. diff --git a/plugins/additionalFilesDeleter/deleter.py b/plugins/additionalFilesDeleter/deleter.py new file mode 100644 index 00000000..50165b87 --- /dev/null +++ b/plugins/additionalFilesDeleter/deleter.py @@ -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() diff --git a/plugins/additionalFilesDeleter/deleter.yml b/plugins/additionalFilesDeleter/deleter.yml new file mode 100644 index 00000000..456e7ceb --- /dev/null +++ b/plugins/additionalFilesDeleter/deleter.yml @@ -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 diff --git a/plugins/additionalFilesDeleter/requirements.txt b/plugins/additionalFilesDeleter/requirements.txt new file mode 100644 index 00000000..eeba91d4 --- /dev/null +++ b/plugins/additionalFilesDeleter/requirements.txt @@ -0,0 +1 @@ +stashapp-tools==0.2.40 \ No newline at end of file