Skip to content
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

Additional Files Deleter #256

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions plugins/additionalFilesDeleter/README.md
Original file line number Diff line number Diff line change
@@ -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.
215 changes: 215 additions & 0 deletions plugins/additionalFilesDeleter/deleter.py
Original file line number Diff line number Diff line change
@@ -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()
32 changes: 32 additions & 0 deletions plugins/additionalFilesDeleter/deleter.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions plugins/additionalFilesDeleter/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
stashapp-tools==0.2.40