From d1de11d647feb135d4d8f5c7e5887cc43eeb987e Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 12 Jul 2024 11:43:31 +0800 Subject: [PATCH] Allow filtering on min/max length of media --- tubesync/sync/filtering.py | 37 +++++++++++++++++ .../migrations/0023_media_duration_filter.py | 41 +++++++++++++++++++ tubesync/sync/models.py | 29 ++++++++++++- tubesync/sync/tasks.py | 4 ++ tubesync/sync/views.py | 4 +- 5 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 tubesync/sync/migrations/0023_media_duration_filter.py diff --git a/tubesync/sync/filtering.py b/tubesync/sync/filtering.py index 3635dced..87abee42 100644 --- a/tubesync/sync/filtering.py +++ b/tubesync/sync/filtering.py @@ -28,6 +28,10 @@ def filter_media(instance: Media): if filter_filter_text(instance): skip = True + # Check if the video is longer than the max, or shorter than the min + if filter_duration(instance): + skip = True + # Check if skipping if instance.skip != skip: instance.skip = skip @@ -95,4 +99,37 @@ def filter_source_cutoff(instance: Media): f'{instance.source.days_to_keep} days, skipping') return True + return False + + +# Check if we skip based on duration (min/max) +def filter_duration(instance: Media): + if not instance.source.filter_seconds: + return False + + duration = instance.duration + if not duration: + # Attempt fallback to slower metadata field, this adds significant time, new media won't need this + # Tests show fetching instance.duration can take as long as the rest of the filtering + if instance.metadata_duration: + duration = instance.metadata_duration + instance.duration = duration + instance.save() + else: + log.info(f'Media: {instance.source} / {instance} has no duration stored, not skipping') + return False + + duration_limit = instance.source.filter_seconds + if instance.source.filter_seconds_min and duration < duration_limit: + # Filter out videos that are shorter than the minimum + log.info(f'Media: {instance.source} / {instance} is shorter ({duration}) than ' + f'the minimum duration ({duration_limit}), skipping') + return True + + if not instance.source.filter_seconds_min and duration > duration_limit: + # Filter out videos that are greater than the maximum + log.info(f'Media: {instance.source} / {instance} is longer ({duration}) than ' + f'the maximum duration ({duration_limit}), skipping') + return True + return False \ No newline at end of file diff --git a/tubesync/sync/migrations/0023_media_duration_filter.py b/tubesync/sync/migrations/0023_media_duration_filter.py new file mode 100644 index 00000000..fde130ad --- /dev/null +++ b/tubesync/sync/migrations/0023_media_duration_filter.py @@ -0,0 +1,41 @@ +# Generated by pac + +from django.db import migrations, models + +class Migration(migrations.Migration): + + dependencies = [ + ('sync', '0022_add_delete_files_on_disk'), + ] + + operations = [ + migrations.AddField( + model_name='media', + name='duration', + field=models.PositiveIntegerField( + verbose_name='duration', + blank=True, + null=True, + help_text='Duration of media in seconds'), + ), + migrations.AddField( + model_name='source', + name='filter_seconds', + field=models.PositiveIntegerField( + verbose_name='filter seconds', + blank=True, + null=True, + help_text='Filter Media based on Min/Max duration. Leave blank or 0 to disable filtering'), + ), + migrations.AddField( + model_name='source', + name='filter_seconds_min', + field=models.BooleanField( + verbose_name='filter seconds min', + choices=[(True, 'Minimum Length'),(False, 'Maximum Length')], + default=True, + help_text='When Filter Seconds is > 0, do we skip on minimum (video shorter than limit) or maximum (' + 'video greater than maximum) video duration' + ), + ), + ] diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index 47346c83..455779fb 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -104,6 +104,11 @@ class Source(models.Model): (FALLBACK_NEXT_BEST_HD, _('Get next best resolution but at least HD')) ) + FILTER_SECONDS_CHOICES = ( + (True, _('Minimum Length')), + (False, _('Maximum Length')), + ) + EXTENSION_M4A = 'm4a' EXTENSION_OGG = 'ogg' EXTENSION_MKV = 'mkv' @@ -293,6 +298,19 @@ class Source(models.Model): blank=True, help_text=_('Regex compatible filter string for video titles') ) + filter_seconds = models.PositiveIntegerField( + _('filter seconds'), + blank=True, + null=True, + help_text=_('Filter Media based on Min/Max duration. Leave blank or 0 to disable filtering') + ) + filter_seconds_min = models.BooleanField( + _('filter seconds min/max'), + choices=FILTER_SECONDS_CHOICES, + default=True, + help_text=_('When Filter Seconds is > 0, do we skip on minimum (video shorter than limit) or maximum (video ' + 'greater than maximum) video duration') + ) delete_removed_media = models.BooleanField( _('delete removed media'), default=False, @@ -785,7 +803,7 @@ class Media(models.Model): _('manual_skip'), db_index=True, default=False, - help_text=_('Media marked as "skipped", won\' be downloaded') + help_text=_('Media marked as "skipped", won\'t be downloaded') ) downloaded = models.BooleanField( _('downloaded'), @@ -859,6 +877,13 @@ class Media(models.Model): help_text=_('Size of the downloaded media in bytes') ) + duration = models.PositiveIntegerField( + _('duration'), + blank=True, + null=True, + help_text=_('Duration of media in seconds') + ) + def __str__(self): return self.key @@ -1115,7 +1140,7 @@ class Media(models.Model): return None @property - def duration(self): + def metadata_duration(self): field = self.get_metadata_field('duration') duration = self.loaded_metadata.get(field, 0) try: diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index b0dc3bf2..8ba04d43 100644 --- a/tubesync/sync/tasks.py +++ b/tubesync/sync/tasks.py @@ -294,6 +294,10 @@ def download_media_metadata(media_id): if upload_date: media.published = timezone.make_aware(upload_date) + # Store duration in DB so it's fast to access + if media.metadata_duration: + media.duration = media.metadata_duration + filter_media(media) # Check we can download the media item if not media.skip: diff --git a/tubesync/sync/views.py b/tubesync/sync/views.py index ad02c018..ef482ec3 100644 --- a/tubesync/sync/views.py +++ b/tubesync/sync/views.py @@ -296,8 +296,8 @@ class ValidateSourceView(FormView): class EditSourceMixin: model = Source - fields = ('source_type', 'key', 'name', 'directory', 'filter_text', 'media_format', - 'index_schedule', 'download_media', 'download_cap', 'delete_old_media', + fields = ('source_type', 'key', 'name', 'directory', 'filter_text', 'filter_seconds', 'filter_seconds_min', + 'media_format', 'index_schedule', 'download_media', 'download_cap', 'delete_old_media', 'delete_removed_media', 'days_to_keep', 'source_resolution', 'source_vcodec', 'source_acodec', 'prefer_60fps', 'prefer_hdr', 'fallback', 'copy_channel_images', 'delete_removed_media', 'delete_files_on_disk', 'days_to_keep', 'source_resolution',