From 22a46315a135a1c8b269eb6a0d8555b51f082f14 Mon Sep 17 00:00:00 2001 From: tcely Date: Sun, 22 Dec 2024 09:10:03 -0500 Subject: [PATCH 1/2] Remove files in post_delete when Media is deleted --- tubesync/sync/signals.py | 64 ++++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/tubesync/sync/signals.py b/tubesync/sync/signals.py index 9c541e0a..0ca0e897 100644 --- a/tubesync/sync/signals.py +++ b/tubesync/sync/signals.py @@ -1,5 +1,6 @@ import os import glob +from pathlib import Path from django.conf import settings from django.db.models.signals import pre_save, post_save, pre_delete, post_delete from django.dispatch import receiver @@ -13,7 +14,7 @@ map_task_to_instance, check_source_directory_exists, download_media, rescan_media_server, download_source_images, save_all_media_for_source) -from .utils import delete_file +from .utils import delete_file, glob_quote from .filtering import filter_media @@ -185,19 +186,63 @@ def media_pre_delete(sender, instance, **kwargs): if thumbnail_url: delete_task_by_media('sync.tasks.download_media_thumbnail', (str(instance.pk), thumbnail_url)) - if instance.source.delete_files_on_disk and (instance.media_file or instance.thumb): - # Delete all media files if it contains filename - filepath = instance.media_file.path if instance.media_file else instance.thumb.path - barefilepath, fileext = os.path.splitext(filepath) + + +@receiver(post_delete, sender=Media) +def media_post_delete(sender, instance, **kwargs): + # Remove thumbnail file for deleted media + if instance.thumb: + instance.thumb.delete(save=False) + # Remove the video file, when configured to do so + if instance.source.delete_files_on_disk and instance.media_file: + video_path = Path(str(instance.media_file.path)).resolve() + instance.media_file.delete(save=False) + # the other files we created have these known suffixes + for suffix in frozenset(('nfo', 'jpg', 'webp', 'info.json',)): + other_path = video_path.with_suffix(f'.{suffix}').resolve() + log.info(f'Deleting file for: {instance} path: {other_path!s}') + delete_file(other_path) + # Jellyfin creates .trickplay directories and posters + for suffix in frozenset(('.trickplay', '-poster.jpg', '-poster.webp',)): + # with_suffix insists on suffix beginning with '.' for no good reason + other_path = Path(str(video_path.with_suffix('')) + suffix).resolve() + if other_path.is_file(): + log.info(f'Deleting file for: {instance} path: {other_path!s}') + delete_file(other_path) + elif other_path.is_dir(): + # Delete the contents of the directory + paths = list(other_path.rglob('*')) + attempts = len(paths) + while paths and attempts > 0: + attempts -= 1 + # delete files first + for p in list(filter(lambda x: x.is_file(), paths)): + log.info(f'Deleting file for: {instance} path: {p!s}') + delete_file(p) + # refresh the list + paths = list(other_path.rglob('*')) + # delete directories + # a directory with a subdirectory will fail + # we loop to try removing each of them + # a/b/c: c then b then a, 3 times around the loop + for p in list(filter(lambda x: x.is_dir(), paths)): + try: + p.rmdir() + log.info(f'Deleted directory for: {instance} path: {p!s}') + except OSError as e: + pass + # Delete the directory itself + try: + other_path.rmdir() + log.info(f'Deleted directory for: {instance} path: {other_path!s}') + except OSError as e: + pass # Get all files that start with the bare file path - all_related_files = glob.glob(f'{barefilepath}.*') + all_related_files = video_path.parent.glob(f'{glob_quote(video_path.with_suffix("").name)}*') for file in all_related_files: log.info(f'Deleting file for: {instance} path: {file}') delete_file(file) - -@receiver(post_delete, sender=Media) -def media_post_delete(sender, instance, **kwargs): # Schedule a task to update media servers for mediaserver in MediaServer.objects.all(): log.info(f'Scheduling media server updates') @@ -208,3 +253,4 @@ def media_post_delete(sender, instance, **kwargs): verbose_name=verbose_name.format(mediaserver), remove_existing_tasks=True ) + From 3ac661cd1fdee619ec25befd20005c7a9cb4bb1e Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 25 Dec 2024 00:43:44 -0500 Subject: [PATCH 2/2] Pulled `glob_quote` from another branch --- tubesync/sync/utils.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tubesync/sync/utils.py b/tubesync/sync/utils.py index 3e29fe3f..e1ab6142 100644 --- a/tubesync/sync/utils.py +++ b/tubesync/sync/utils.py @@ -93,6 +93,20 @@ def resize_image_to_height(image, width, height): return image +def glob_quote(filestr): + _glob_specials = { + '?': '[?]', + '*': '[*]', + '[': '[[]', + ']': '[]]', # probably not needed, but it won't hurt + } + + if not isinstance(filestr, str): + raise TypeError(f'filestr must be a str, got "{type(filestr)}"') + + return filestr.translate(str.maketrans(_glob_specials)) + + def file_is_editable(filepath): ''' Checks that a file exists and the file is in an allowed predefined tuple of