diff --git a/tubesync/sync/migrations/0023_media_duration_filter.py b/tubesync/sync/migrations/0023_media_duration_filter.py index 6be5148..115c1b1 100644 --- a/tubesync/sync/migrations/0023_media_duration_filter.py +++ b/tubesync/sync/migrations/0023_media_duration_filter.py @@ -1,14 +1,24 @@ -# Generated by pac - from django.db import migrations, models -class Migration(migrations.Migration): +class Migration(migrations.Migration): dependencies = [ ('sync', '0022_add_delete_files_on_disk'), ] operations = [ + migrations.AddField( + model_name='media', + name='title', + field=models.CharField( + verbose_name='title', + max_length=100, + blank=True, + null=False, + default='', + help_text='Video title' + ), + ), migrations.AddField( model_name='media', name='duration', @@ -16,7 +26,8 @@ class Migration(migrations.Migration): verbose_name='duration', blank=True, null=True, - help_text='Duration of media in seconds'), + help_text='Duration of media in seconds' + ), ), migrations.AddField( model_name='source', @@ -25,18 +36,18 @@ class Migration(migrations.Migration): verbose_name='filter seconds', blank=True, null=True, - help_text='Filter Media based on Min/Max duration. Leave blank or 0 to disable filtering'), + 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/max', - choices=[(True, 'Minimum Length'),(False, 'Maximum Length')], + 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 455779f..b71d527 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -876,13 +876,20 @@ class Media(models.Model): null=True, help_text=_('Size of the downloaded media in bytes') ) - duration = models.PositiveIntegerField( _('duration'), blank=True, null=True, help_text=_('Duration of media in seconds') ) + title = models.CharField( + _('title'), + max_length=100, + blank=True, + null=False, + default='', + help_text=_('Video title') + ) def __str__(self): return self.key @@ -894,6 +901,21 @@ class Media(models.Model): ('source', 'key'), ) + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + # Trigger an update of derived fields from metadata + if self.metadata: + self.title = self.metadata_title + self.duration = self.metadata_duration + if update_fields is not None and "metadata" in update_fields: + # If only some fields are being updated, make sure we update title and duration if metadata changes + update_fields = {"title", "duration"}.union(update_fields) + + super().save( + force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields,) + def get_metadata_field(self, field): fields = self.METADATA_FIELDS.get(field, {}) return fields.get(self.source.source_type, '') @@ -1108,7 +1130,7 @@ class Media(models.Model): return self.loaded_metadata.get(field, '').strip() @property - def title(self): + def metadata_title(self): field = self.get_metadata_field('title') return self.loaded_metadata.get(field, '').strip() @@ -1152,7 +1174,7 @@ class Media(models.Model): @property def duration_formatted(self): duration = self.duration - if duration > 0: + if duration and duration > 0: return seconds_to_timestr(duration) return '??:??:??' diff --git a/tubesync/sync/tasks.py b/tubesync/sync/tasks.py index 8ba04d4..8031467 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 title in DB so it's fast to access + if media.metadata_title: + media.title = media.metadata_title + # Store duration in DB so it's fast to access if media.metadata_duration: media.duration = media.metadata_duration diff --git a/tubesync/sync/tests.py b/tubesync/sync/tests.py index e9d8d98..901ecac 100644 --- a/tubesync/sync/tests.py +++ b/tubesync/sync/tests.py @@ -176,7 +176,8 @@ class FrontEndTestCase(TestCase): 'directory': 'testdirectory', 'media_format': settings.MEDIA_FORMATSTR_DEFAULT, 'download_cap': 0, - 'filter_text':'.*', + 'filter_text': '.*', + 'filter_seconds_min': True, 'index_schedule': 3600, 'delete_old_media': False, 'days_to_keep': 14, @@ -219,7 +220,8 @@ class FrontEndTestCase(TestCase): 'directory': 'testdirectory', 'media_format': settings.MEDIA_FORMATSTR_DEFAULT, 'download_cap': 0, - 'filter_text':'.*', + 'filter_text': '.*', + 'filter_seconds_min': True, 'index_schedule': Source.IndexSchedule.EVERY_HOUR, 'delete_old_media': False, 'days_to_keep': 14, @@ -250,7 +252,8 @@ class FrontEndTestCase(TestCase): 'directory': 'testdirectory', 'media_format': settings.MEDIA_FORMATSTR_DEFAULT, 'download_cap': 0, - 'filter_text':'.*', + 'filter_text': '.*', + 'filter_seconds_min': True, 'index_schedule': Source.IndexSchedule.EVERY_2_HOURS, # changed 'delete_old_media': False, 'days_to_keep': 14, @@ -819,6 +822,7 @@ class FormatMatchingTestCase(TestCase): def test_combined_exact_format_matching(self): self.source.fallback = Source.FALLBACK_FAIL self.media.metadata = all_test_metadata['boring'] + self.media.save() expected_matches = { # (format, vcodec, acodec, prefer_60fps, prefer_hdr): (match_type, code), ('360p', 'AVC1', 'MP4A', True, False): (False, False), @@ -948,6 +952,7 @@ class FormatMatchingTestCase(TestCase): def test_audio_exact_format_matching(self): self.source.fallback = Source.FALLBACK_FAIL self.media.metadata = all_test_metadata['boring'] + self.media.save() expected_matches = { # (format, vcodec, acodec, prefer_60fps, prefer_hdr): (match_type, code), ('360p', 'AVC1', 'MP4A', True, False): (True, '140'), @@ -1094,6 +1099,7 @@ class FormatMatchingTestCase(TestCase): self.source.fallback = Source.FALLBACK_FAIL # Test no 60fps, no HDR metadata self.media.metadata = all_test_metadata['boring'] + self.media.save() expected_matches = { # (format, vcodec, prefer_60fps, prefer_hdr): (match_type, code), ('360p', 'AVC1', False, True): (False, False), @@ -1141,6 +1147,7 @@ class FormatMatchingTestCase(TestCase): self.assertEqual(match_type, expeceted_match_type) # Test 60fps metadata self.media.metadata = all_test_metadata['60fps'] + self.media.save() expected_matches = { # (format, vcodec, prefer_60fps, prefer_hdr): (match_type, code), ('360p', 'AVC1', False, True): (False, False), @@ -1180,6 +1187,7 @@ class FormatMatchingTestCase(TestCase): self.assertEqual(match_type, expeceted_match_type) # Test hdr metadata self.media.metadata = all_test_metadata['hdr'] + self.media.save() expected_matches = { # (format, vcodec, prefer_60fps, prefer_hdr): (match_type, code), ('360p', 'AVC1', False, True): (False, False), @@ -1235,6 +1243,7 @@ class FormatMatchingTestCase(TestCase): self.assertEqual(match_type, expeceted_match_type) # Test 60fps+hdr metadata self.media.metadata = all_test_metadata['60fps+hdr'] + self.media.save() expected_matches = { # (format, vcodec, prefer_60fps, prefer_hdr): (match_type, code), ('360p', 'AVC1', False, True): (False, False), @@ -1300,6 +1309,7 @@ class FormatMatchingTestCase(TestCase): self.source.fallback = Source.FALLBACK_NEXT_BEST # Test no 60fps, no HDR metadata self.media.metadata = all_test_metadata['boring'] + self.media.save() expected_matches = { # (format, vcodec, prefer_60fps, prefer_hdr): (match_type, code), ('360p', 'AVC1', False, True): (False, '134'), # Fallback match, no hdr @@ -1347,6 +1357,7 @@ class FormatMatchingTestCase(TestCase): self.assertEqual(match_type, expeceted_match_type) # Test 60fps metadata self.media.metadata = all_test_metadata['60fps'] + self.media.save() expected_matches = { # (format, vcodec, prefer_60fps, prefer_hdr): (match_type, code), ('360p', 'AVC1', False, True): (False, '134'), # Fallback match, no hdr @@ -1386,6 +1397,7 @@ class FormatMatchingTestCase(TestCase): self.assertEqual(match_type, expeceted_match_type) # Test hdr metadata self.media.metadata = all_test_metadata['hdr'] + self.media.save() expected_matches = { # (format, vcodec, prefer_60fps, prefer_hdr): (match_type, code), ('360p', 'AVC1', False, True): (False, '332'), # Fallback match, hdr, switched to VP9 @@ -1441,6 +1453,7 @@ class FormatMatchingTestCase(TestCase): self.assertEqual(match_type, expeceted_match_type) # Test 60fps+hdr metadata self.media.metadata = all_test_metadata['60fps+hdr'] + self.media.save() expected_matches = { # (format, vcodec, prefer_60fps, prefer_hdr): (match_type, code), ('360p', 'AVC1', False, True): (False, '134'), # Fallback match, no hdr @@ -1504,6 +1517,7 @@ class FormatMatchingTestCase(TestCase): def test_metadata_20230629(self): self.media.metadata = all_test_metadata['20230629'] + self.media.save() expected_matches = { # (format, vcodec, prefer_60fps, prefer_hdr): (match_type, code), ('360p', 'AVC1', False, True): (False, '134'), # Fallback match, no hdr @@ -1568,6 +1582,7 @@ class FormatMatchingTestCase(TestCase): def test_is_regex_match(self): self.media.metadata = all_test_metadata['boring'] + self.media.save() expected_matches = { ('.*'): (True), ('no fancy stuff'): (True), @@ -1587,7 +1602,11 @@ class FormatMatchingTestCase(TestCase): for params, expected in expected_matches.items(): self.source.filter_text = params expected_match_result = expected - self.assertEqual(self.source.is_regex_match(self.media.title), expected_match_result) + self.assertEqual( + self.source.is_regex_match(self.media.title), + expected_match_result, + msg=f'Media title "{self.media.title}" checked against regex "{self.source.filter_text}" failed ' + f'expected {expected_match_result}') class TasksTestCase(TestCase): def setUp(self):