From a1aeb504c3e6485069a439cfcafc5907cceec414 Mon Sep 17 00:00:00 2001
From: mady20 <165700212+mady20@users.noreply.github.com>
Date: Thu, 19 Mar 2026 22:27:29 +0530
Subject: [PATCH] Split sync/tests.py into smaller test modules
---
tubesync/sync/testdata/README.md | 2 +-
tubesync/sync/tests/__init__.py | 0
tubesync/sync/tests/fixtures.py | 24 +
tubesync/sync/tests/test_filepath.py | 177 +++
.../test_format_matching.py} | 1042 +----------------
tubesync/sync/tests/test_frontend.py | 491 ++++++++
tubesync/sync/tests/test_media.py | 259 ++++
.../sync/tests/test_response_filtering.py | 91 ++
tubesync/sync/tests/test_tasks.py | 38 +
9 files changed, 1087 insertions(+), 1037 deletions(-)
create mode 100644 tubesync/sync/tests/__init__.py
create mode 100644 tubesync/sync/tests/fixtures.py
create mode 100644 tubesync/sync/tests/test_filepath.py
rename tubesync/sync/{tests.py => tests/test_format_matching.py} (64%)
create mode 100644 tubesync/sync/tests/test_frontend.py
create mode 100644 tubesync/sync/tests/test_media.py
create mode 100644 tubesync/sync/tests/test_response_filtering.py
create mode 100644 tubesync/sync/tests/test_tasks.py
diff --git a/tubesync/sync/testdata/README.md b/tubesync/sync/testdata/README.md
index 8cdd3c8f..a32ee124 100644
--- a/tubesync/sync/testdata/README.md
+++ b/tubesync/sync/testdata/README.md
@@ -3,6 +3,6 @@
This directory contains metadata extracted from some test YouTube videos with
youtube-dl.
-They are used to test (with `sync/tests.py`) the format matchers in `sync/matching.py`
+They are used to test (with `sync/tests/test_format_matching.py`) the format matchers in `sync/matching.py`
and are not otherwise used in TubeSync. Removing this directory will not break TubeSync
but will break test running.
diff --git a/tubesync/sync/tests/__init__.py b/tubesync/sync/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tubesync/sync/tests/fixtures.py b/tubesync/sync/tests/fixtures.py
new file mode 100644
index 00000000..cf259656
--- /dev/null
+++ b/tubesync/sync/tests/fixtures.py
@@ -0,0 +1,24 @@
+from django.conf import settings
+
+metadata_filepath = settings.BASE_DIR / 'sync' / 'testdata' / 'metadata.json'
+with open(metadata_filepath, 'rt') as file:
+ metadata = file.read()
+metadata_hdr_filepath = settings.BASE_DIR / 'sync' / 'testdata' / 'metadata_hdr.json'
+with open(metadata_hdr_filepath, 'rt') as file:
+ metadata_hdr = file.read()
+metadata_60fps_filepath = settings.BASE_DIR / 'sync' / 'testdata' / 'metadata_60fps.json'
+with open(metadata_60fps_filepath, 'rt') as file:
+ metadata_60fps = file.read()
+metadata_60fps_hdr_filepath = settings.BASE_DIR / 'sync' / 'testdata' / 'metadata_60fps_hdr.json'
+with open(metadata_60fps_hdr_filepath, 'rt') as file:
+ metadata_60fps_hdr = file.read()
+metadata_20230629_filepath = settings.BASE_DIR / 'sync' / 'testdata' / 'metadata_2023-06-29.json'
+with open(metadata_20230629_filepath, 'rt') as file:
+ metadata_20230629 = file.read()
+all_test_metadata = {
+ 'boring': metadata,
+ 'hdr': metadata_hdr,
+ '60fps': metadata_60fps,
+ '60fps+hdr': metadata_60fps_hdr,
+ '20230629': metadata_20230629,
+}
diff --git a/tubesync/sync/tests/test_filepath.py b/tubesync/sync/tests/test_filepath.py
new file mode 100644
index 00000000..c58cc226
--- /dev/null
+++ b/tubesync/sync/tests/test_filepath.py
@@ -0,0 +1,177 @@
+import logging
+from pathlib import Path
+from django.conf import settings
+from django.test import TestCase
+from django.utils import timezone
+from sync.models import Source, Media
+from sync.choices import (
+ Val, Fallback, SourceResolution,
+ YouTube_AudioCodec, YouTube_VideoCodec,
+ YouTube_SourceType,
+)
+
+from .fixtures import metadata
+
+class FilepathTestCase(TestCase):
+
+ def setUp(self):
+ # Disable general logging for test case
+ logging.disable(logging.CRITICAL)
+ # Add a test source
+ self.source = Source.objects.create(
+ source_type=Val(YouTube_SourceType.CHANNEL),
+ key='testkey',
+ name='testname',
+ directory='testdirectory',
+ media_format=settings.MEDIA_FORMATSTR_DEFAULT,
+ index_schedule=3600,
+ delete_old_media=False,
+ days_to_keep=14,
+ source_resolution=Val(SourceResolution.VIDEO_1080P),
+ source_vcodec=Val(YouTube_VideoCodec.VP9),
+ source_acodec=Val(YouTube_AudioCodec.OPUS),
+ prefer_60fps=False,
+ prefer_hdr=False,
+ fallback=Val(Fallback.FAIL)
+ )
+ # Add some test media
+ self.media = Media.objects.create(
+ key='mediakey',
+ source=self.source,
+ metadata=metadata,
+ )
+
+ def test_source_media_format(self):
+ # Check media format validation is working
+ # Empty
+ self.source.media_format = ''
+ self.assertEqual(self.source.get_example_media_format(), '')
+ # Invalid, bad key
+ self.source.media_format = '{test}'
+ self.assertEqual(self.source.get_example_media_format(), '')
+ # Invalid, extra brackets
+ self.source.media_format = '{key}}'
+ self.assertEqual(self.source.get_example_media_format(), '')
+ # Invalid, not a string
+ self.source.media_format = 1
+ self.assertEqual(self.source.get_example_media_format(), '')
+ # Check all expected keys validate
+ self.source.media_format = 'test-{yyyymmdd}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-' + timezone.now().strftime('%Y%m%d'))
+ self.source.media_format = 'test-{yyyy_mm_dd}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-' + timezone.now().strftime('%Y-%m-%d'))
+ self.source.media_format = 'test-{yyyy_0mm_dd}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-' + timezone.now().strftime('%Y-0%m-%d'))
+ self.source.media_format = 'test-{yyyy}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-' + timezone.now().strftime('%Y'))
+ self.source.media_format = 'test-{mm}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-' + timezone.now().strftime('%m'))
+ self.source.media_format = 'test-{dd}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-' + timezone.now().strftime('%d'))
+ self.source.media_format = 'test-{source}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-' + self.source.slugname)
+ self.source.media_format = 'test-{source_full}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-' + self.source.name)
+ self.source.media_format = 'test-{title}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-some-media-title-name')
+ self.source.media_format = 'test-{title_full}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-Some Media Title Name')
+ self.source.media_format = 'test-{key}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-SoMeUnIqUiD')
+ self.source.media_format = 'test-{format}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-1080p-vp9-opus')
+ self.source.media_format = 'test-{playlist_title}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-Some Playlist Title')
+ self.source.media_format = 'test-{ext}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-' + self.source.extension)
+ self.source.media_format = 'test-{resolution}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-' + self.source.source_resolution)
+ self.source.media_format = 'test-{height}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-720')
+ self.source.media_format = 'test-{width}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-1280')
+ self.source.media_format = 'test-{vcodec}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-' + self.source.source_vcodec.lower())
+ self.source.media_format = 'test-{acodec}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-' + self.source.source_acodec.lower())
+ self.source.media_format = 'test-{fps}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-24')
+ self.source.media_format = 'test-{hdr}'
+ self.assertEqual(self.source.get_example_media_format(),
+ 'test-hdr')
+
+ def test_media_filename(self):
+ # Check child directories work
+ self.source.media_format = '{yyyy}/{key}.{ext}'
+ self.assertEqual(self.media.directory_path,
+ self.source.directory_path / '2017')
+ self.assertEqual(self.media.filename, '2017/mediakey.mkv')
+ self.source.media_format = '{yyyy}/{yyyy_mm_dd}/{key}.{ext}'
+ self.assertEqual(self.media.directory_path,
+ self.source.directory_path / '2017/2017-09-11')
+ self.assertEqual(self.media.filename, '2017/2017-09-11/mediakey.mkv')
+ # Check media specific media format keys work
+ test_media = Media.objects.create(
+ key='test',
+ source=self.source,
+ metadata=metadata,
+ downloaded=True,
+ download_date=timezone.now(),
+ downloaded_format='720p',
+ downloaded_height=720,
+ downloaded_width=1280,
+ downloaded_audio_codec='opus',
+ downloaded_video_codec='vp9',
+ downloaded_container='mkv',
+ downloaded_fps=30,
+ downloaded_hdr=True,
+ downloaded_filesize=12345
+ )
+ # Bypass media-file-exists on-save signal
+ test_media.downloaded = True
+ self.source.media_format = ('{title}_{key}_{resolution}-{height}x{width}-'
+ '{acodec}-{vcodec}-{fps}fps-{hdr}.{ext}')
+ self.assertEqual(test_media.filename,
+ ('no-fancy-stuff-title_test_720p-720x1280-opus'
+ '-vp9-30fps-hdr.mkv'))
+
+ def test_directory_prefix(self):
+ # Confirm the setting exists and is valid
+ self.assertTrue(hasattr(settings, 'SOURCE_DOWNLOAD_DIRECTORY_PREFIX'))
+ self.assertTrue(isinstance(settings.SOURCE_DOWNLOAD_DIRECTORY_PREFIX, bool))
+ # Test the default behavior for "True", forced "audio" or "video" parent directories for sources
+ settings.SOURCE_DOWNLOAD_DIRECTORY_PREFIX = True
+ self.source.source_resolution = Val(SourceResolution.AUDIO)
+ test_audio_prefix_path = Path(self.source.directory_path)
+ self.assertEqual(test_audio_prefix_path.parts[-2], 'audio')
+ self.assertEqual(test_audio_prefix_path.parts[-1], 'testdirectory')
+ self.source.source_resolution = Val(SourceResolution.VIDEO_1080P)
+ test_video_prefix_path = Path(self.source.directory_path)
+ self.assertEqual(test_video_prefix_path.parts[-2], 'video')
+ self.assertEqual(test_video_prefix_path.parts[-1], 'testdirectory')
+ # Test the default behavior for "False", no parent directories for sources
+ settings.SOURCE_DOWNLOAD_DIRECTORY_PREFIX = False
+ test_no_prefix_path = Path(self.source.directory_path)
+ self.assertEqual(test_no_prefix_path.parts[-1], 'testdirectory')
+
+
diff --git a/tubesync/sync/tests.py b/tubesync/sync/tests/test_format_matching.py
similarity index 64%
rename from tubesync/sync/tests.py
rename to tubesync/sync/tests/test_format_matching.py
index 62883068..965e7acd 100644
--- a/tubesync/sync/tests.py
+++ b/tubesync/sync/tests/test_format_matching.py
@@ -1,935 +1,13 @@
-'''
- Tests do not test the scheduled tasks that perform live requests to index media or
- download content. They only check for compliance of web interface and check
- the matching code logic is working as expected.
-'''
-
-
import logging
-from datetime import datetime, timedelta
-from pathlib import Path
-from urllib.parse import urlsplit
-from xml.etree import ElementTree
-from django.conf import settings
-from django.test import TestCase, Client, override_settings
-from django.utils import timezone
-from django_huey import DJANGO_HUEY, get_queue
-from common.models import TaskHistory
-from .models import Source, Media
-from .tasks import (
- cleanup_old_media, check_source_directory_exists,
- get_media_download_task, get_media_thumbnail_task,
-)
-from .filtering import filter_media
-from .utils import filter_response
-from .choices import (
- Val, Fallback, FilterSeconds, IndexSchedule, SourceResolution,
- TaskQueue, YouTube_AudioCodec, YouTube_VideoCodec,
+from django.test import TestCase
+from sync.models import Source, Media
+from sync.choices import (
+ Val, Fallback, SourceResolution,
+ YouTube_AudioCodec, YouTube_VideoCodec,
YouTube_SourceType,
)
-
-class FrontEndTestCase(TestCase):
- maxDiff = None
-
- @classmethod
- def setUpClass(cls):
- super().setUpClass()
- # Use immediate mode to execute tasks in this process
- for qn in DJANGO_HUEY.get('queues', dict()):
- q = get_queue(qn)
- # Set the storage variable before using the property.
- q.immediate_use_memory = True
- q.immediate = False
-
- def setUp(self):
- # Disable general logging for test case
- logging.disable(logging.CRITICAL)
-
- def test_dashboard(self):
- c = Client()
- response = c.get('/')
- self.assertEqual(response.status_code, 200)
-
- def test_validate_source(self):
- test_sources = {
- 'youtube-channel': {
- 'valid': (
- 'https://m.youtube.com/testchannel',
- 'https://m.youtube.com/c/testchannel',
- 'https://m.youtube.com/c/testchannel/videos',
- 'https://www.youtube.com/testchannel',
- 'https://www.youtube.com/c/testchannel',
- 'https://www.youtube.com/c/testchannel/videos',
- ),
- 'invalid_schema': (
- 'http://www.youtube.com/c/playlist',
- 'ftp://www.youtube.com/c/playlist',
- ),
- 'invalid_domain': (
- 'https://www.test.com/c/testchannel',
- 'https://www.example.com/c/testchannel',
- 'https://n.youtube.com/c/testchannel',
- ),
- 'invalid_path': (
- 'https://www.youtube.com/test/invalid',
- 'https://www.youtube.com/c/test/invalid',
- ),
- 'invalid_reserved_paths': (
- 'https://www.youtube.com/watch?v=OkMadb8cpIw',
- 'https://www.youtube.com/watch',
- 'https://www.youtube.com/shorts',
- 'https://www.youtube.com/live',
- 'https://www.youtube.com/feed',
- 'https://www.youtube.com/trending',
- ),
- },
- 'youtube-channel-id': {
- 'valid': (
- 'https://m.youtube.com/channel/channelid',
- 'https://m.youtube.com/channel/channelid/videos',
- 'https://www.youtube.com/channel/channelid',
- 'https://www.youtube.com/channel/channelid/videos',
- ),
- 'invalid_schema': (
- 'http://www.youtube.com/channel/channelid',
- 'ftp://www.youtube.com/channel/channelid',
- ),
- 'invalid_domain': (
- 'https://www.test.com/channel/channelid',
- 'https://www.example.com/channel/channelid',
- 'https://n.youtube.com/channel/channelid',
- ),
- 'invalid_path': (
- 'https://www.youtube.com/test/invalid',
- 'https://www.youtube.com/channel/test/invalid',
- ),
- },
- 'youtube-playlist': {
- 'valid': (
- 'https://m.youtube.com/playlist?list=testplaylist',
- 'https://m.youtube.com/watch?v=testvideo&list=testplaylist',
- 'https://www.youtube.com/playlist?list=testplaylist',
- 'https://www.youtube.com/watch?v=testvideo&list=testplaylist',
- ),
- 'invalid_schema': (
- 'http://www.youtube.com/playlist?list=testplaylist',
- 'ftp://www.youtube.com/playlist?list=testplaylist',
- ),
- 'invalid_domain': (
- 'https://www.test.com/playlist?list=testplaylist',
- 'https://www.example.com/playlist?list=testplaylist',
- 'https://n.youtube.com/playlist?list=testplaylist',
- ),
- 'invalid_path': (
- 'https://www.youtube.com/test/invalid',
- ),
- }
- }
- c = Client()
- response = c.get('/source-validate')
- self.assertEqual(response.status_code, 200)
- for (source_type, tests) in test_sources.items():
- for test, urls in tests.items():
- for url in urls:
- data = {'source_url': url}
- response = c.post('/source-validate', data)
- if test == 'valid':
- # Valid source tests should bounce to /source-add
- self.assertEqual(response.status_code, 302)
- url_parts = urlsplit(response.url)
- self.assertEqual(url_parts.path, '/source-add')
- else:
- # Invalid source tests should reload the page with an error
- self.assertEqual(response.status_code, 200)
- self.assertIn('
",
- '',
- ' no fancy stuff title',
- ' testname',
- ' 2017',
- ' ',
- ' ',
- ' ',
- ' 1.2345',
- ' 579',
- ' ',
- ' ',
- ' no fancy stuff desc',
- ' ', # media.thumbfile is empty without media existing
- ' 50',
- ' 401',
- ' mediakey',
- ' mediakey',
- ' test uploader',
- ' 2017-09-11',
- ' 2020-01-01 01:01:01',
- ' test category 1',
- ' test category 2',
- '',
- ]
- expected_tree = ElementTree.fromstring('\n'.join(expected_nfo))
- nfo_tree = ElementTree.fromstring(self.media.nfoxml)
- # Check each node with attribs in expected_tree is present in test_nfo
- for expected_node in expected_tree:
- # Ignore checking , only tag we may have multiple of
- if expected_node.tag == 'genre':
- continue
- # Find the same node in the NFO XML tree
- nfo_node = nfo_tree.find(expected_node.tag)
- self.assertEqual(expected_node.attrib, nfo_node.attrib)
- self.assertEqual(expected_node.tag, nfo_node.tag)
- self.assertEqual(expected_node.text, nfo_node.text)
-
-
-class MediaFilterTestCase(TestCase):
-
- def setUp(self):
- # Disable general logging for test case
- # logging.disable(logging.CRITICAL)
- # Add a test source
- self.source = Source.objects.create(
- source_type=Val(YouTube_SourceType.CHANNEL),
- key="testkey",
- name="testname",
- directory="testdirectory",
- media_format=settings.MEDIA_FORMATSTR_DEFAULT,
- index_schedule=3600,
- delete_old_media=False,
- days_to_keep=14,
- source_resolution=Val(SourceResolution.VIDEO_1080P),
- source_vcodec=Val(YouTube_VideoCodec.VP9),
- source_acodec=Val(YouTube_AudioCodec.OPUS),
- prefer_60fps=False,
- prefer_hdr=False,
- fallback=Val(Fallback.FAIL),
- )
- # Add some test media
- self.media = Media.objects.create(
- key="mediakey",
- source=self.source,
- metadata=metadata,
- skip=False,
- published=timezone.make_aware(
- datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
- ),
- )
- # Fix a created datetime for predictable testing
- self.media.created = datetime(
- year=2020, month=1, day=1, hour=1, minute=1, second=1
- )
-
- def test_filter_unpublished_skip(self):
- # Check if unpublished that we skip download it
- self.media.skip = False
- self.media.published = False
- changed = filter_media(self.media)
- self.assertTrue(changed)
- self.assertTrue(self.media.skip)
-
- def test_filter_published_unskip(self):
- # Check if we had previously skipped it, but now it's published, we won't skip it
- self.media.skip = True
- self.media.published = timezone.make_aware(
- datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
- )
- changed = filter_media(self.media)
- self.assertTrue(changed)
- self.assertFalse(self.media.skip)
-
- def test_filter_filter_text_nomatch(self):
- # Check that if we don't match the filter text, we skip
- self.media.source.filter_text = "No fancy stuff"
- self.media.skip = False
- self.media.published = timezone.make_aware(
- datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
- )
- changed = filter_media(self.media)
- self.assertTrue(changed)
- self.assertTrue(self.media.skip)
-
- def test_filter_filter_text_match(self):
- # Check that if we match the filter text, we don't skip
- self.media.source.filter_text = "(?i)No fancy stuff"
- self.media.skip = True
- self.media.published = timezone.make_aware(
- datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
- )
- changed = filter_media(self.media)
- self.assertTrue(changed)
- self.assertFalse(self.media.skip)
-
- def test_filter_filter_text_invert_nomatch(self):
- # Check that if we don't match the filter text, we don't skip
- self.media.source.filter_text = "No fancy stuff"
- self.media.source.filter_text_invert = True
- self.media.skip = True
- self.media.published = timezone.make_aware(
- datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
- )
- changed = filter_media(self.media)
- self.assertTrue(changed)
- self.assertFalse(self.media.skip)
-
- def test_filter_filter_text_invert_match(self):
- # Check that if we match the filter text and do skip
- self.media.source.filter_text = "(?i)No fancy stuff"
- self.media.source.filter_text_invert = True
- self.media.skip = False
- self.media.published = timezone.make_aware(
- datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
- )
- changed = filter_media(self.media)
- self.assertTrue(changed)
- self.assertTrue(self.media.skip)
-
- def test_filter_max_cap_skip(self):
- # Check if it's older than the max_cap, we don't download it (1 second so it will always fail)
- self.media.source.download_cap = 1
- self.media.skip = False
- self.media.published = timezone.make_aware(
- datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
- )
- changed = filter_media(self.media)
- self.assertTrue(changed)
- self.assertTrue(self.media.skip)
-
- def test_filter_max_cap_unskip(self):
- # Make sure it's newer than the cap so we download it, ensure that we are published in the last seconds
- self.media.source.download_cap = 3600
- self.media.skip = True
- self.media.published = timezone.now()
- changed = filter_media(self.media)
- self.assertTrue(changed)
- self.assertFalse(self.media.skip)
-
- def test_filter_below_min(self):
- # Filter videos shorter than the minimum limit
- self.media.skip = False
- self.media.source.filter_seconds_min = True
- self.media.source.filter_seconds = 500
- self.media.published = timezone.make_aware(
- datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
- )
- changed = filter_media(self.media)
- self.assertTrue(changed)
- self.assertTrue(self.media.skip)
-
- def test_filter_above_min(self):
- # Video is longer than the minimum, allow it
- self.media.skip = True
- self.media.source.filter_seconds_min = True
- self.media.source.filter_seconds = 300
- self.media.published = timezone.make_aware(
- datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
- )
- changed = filter_media(self.media)
- self.assertTrue(changed)
- self.assertFalse(self.media.skip)
-
- def test_filter_above_max(self):
- # Filter videos longer than the maximum limit
- self.media.skip = False
- self.media.source.filter_seconds_min = False
- self.media.source.filter_seconds = 300
- self.media.published = timezone.make_aware(
- datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
- )
- changed = filter_media(self.media)
- self.assertTrue(changed)
- self.assertTrue(self.media.skip)
-
- def test_filter_below_max(self):
- # Video is below the maximum, allow it
- self.media.skip = True
- self.media.source.filter_seconds_min = False
- self.media.source.filter_seconds = 500
- self.media.published = timezone.make_aware(
- datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
- )
- changed = filter_media(self.media)
- self.assertTrue(changed)
- self.assertFalse(self.media.skip)
-
+from .fixtures import all_test_metadata
class FormatMatchingTestCase(TestCase):
@@ -2035,111 +1113,3 @@ class FormatMatchingTestCase(TestCase):
f'expected {expected_match_result}')
-class ResponseFilteringTestCase(TestCase):
-
- def setUp(self):
- # Disable general logging for test case
- logging.disable(logging.CRITICAL)
- # Add a test source
- self.source = Source.objects.create(
- source_type=Val(YouTube_SourceType.CHANNEL),
- key='testkey',
- name='testname',
- directory='testdirectory',
- index_schedule=3600,
- delete_old_media=False,
- days_to_keep=14,
- source_resolution=Val(SourceResolution.VIDEO_1080P),
- source_vcodec=Val(YouTube_VideoCodec.VP9),
- source_acodec=Val(YouTube_AudioCodec.OPUS),
- prefer_60fps=False,
- prefer_hdr=False,
- fallback=Val(Fallback.FAIL)
- )
- # Add some media
- self.media = Media.objects.create(
- key='mediakey',
- source=self.source,
- metadata='{}'
- )
-
- @override_settings(SHRINK_OLD_MEDIA_METADATA=False, SHRINK_NEW_MEDIA_METADATA=False)
- def test_metadata_20230629(self):
- self.media.metadata = all_test_metadata['20230629']
- self.media.save()
-
- unfiltered = self.media.loaded_metadata
- filtered = filter_response(self.media.loaded_metadata, True)
- self.assertIn('formats', unfiltered.keys())
- self.assertIn('formats', filtered.keys())
- # filtered 'downloader_options'
- self.assertIn('downloader_options', unfiltered['formats'][10].keys())
- self.assertNotIn('downloader_options', filtered['formats'][10].keys())
- # filtered 'http_headers'
- self.assertIn('http_headers', unfiltered['formats'][0].keys())
- self.assertNotIn('http_headers', filtered['formats'][0].keys())
- # did not lose any formats
- self.assertEqual(48, len(unfiltered['formats']))
- self.assertEqual(48, len(filtered['formats']))
- self.assertEqual(len(unfiltered['formats']), len(filtered['formats']))
- # did not remove everything with url
- self.assertIn('original_url', unfiltered.keys())
- self.assertIn('original_url', filtered.keys())
- self.assertEqual(unfiltered['original_url'], filtered['original_url'])
- # did reduce the size of the metadata
- self.assertTrue(len(str(filtered)) < len(str(unfiltered)))
-
- url_keys = []
- for format in unfiltered['formats']:
- for key in format.keys():
- if 'url' in key:
- url_keys.append((format['format_id'], key, format[key],))
- unfiltered_url_keys = url_keys
- self.assertEqual(63, len(unfiltered_url_keys), msg=str(unfiltered_url_keys))
-
- url_keys = []
- for format in filtered['formats']:
- for key in format.keys():
- if 'url' in key:
- url_keys.append((format['format_id'], key, format[key],))
- filtered_url_keys = url_keys
- self.assertEqual(3, len(filtered_url_keys), msg=str(filtered_url_keys))
-
- url_keys = []
- for lang_code, captions in filtered['automatic_captions'].items():
- for caption in captions:
- for key in caption.keys():
- if 'url' in key:
- url_keys.append((lang_code, caption['ext'], caption[key],))
- self.assertEqual(0, len(url_keys), msg=str(url_keys))
-
-
-class TasksTestCase(TestCase):
-
- def setUp(self):
- # Disable general logging for test case
- logging.disable(logging.CRITICAL)
-
- def test_delete_old_media(self):
- src1 = Source.objects.create(key='aaa', name='aaa', directory='/tmp/a', delete_old_media=False, days_to_keep=14)
- src2 = Source.objects.create(key='bbb', name='bbb', directory='/tmp/b', delete_old_media=True, days_to_keep=14)
-
- now = timezone.now()
-
- m11 = Media.objects.create(source=src1, downloaded=True, key='a11', download_date=now - timedelta(days=5)) # noqa
- m12 = Media.objects.create(source=src1, downloaded=True, key='a12', download_date=now - timedelta(days=25)) # noqa
- m13 = Media.objects.create(source=src1, downloaded=False, key='a13') # noqa
-
- m21 = Media.objects.create(source=src2, downloaded=True, key='a21', download_date=now - timedelta(days=5)) # noqa
- m22 = Media.objects.create(source=src2, downloaded=True, key='a22', download_date=now - timedelta(days=25))
- m23 = Media.objects.create(source=src2, downloaded=False, key='a23') # noqa
-
- self.assertEqual(src1.media_source.all().count(), 3)
- self.assertEqual(src2.media_source.all().count(), 3)
-
- cleanup_old_media.call_local(durable=False)
-
- self.assertEqual(src1.media_source.all().count(), 3)
- self.assertEqual(src2.media_source.all().count(), 3)
- self.assertEqual(Media.objects.filter(pk=m22.pk).exists(), False)
- self.assertEqual(Media.objects.filter(source=src2, key=m22.key, skip=True).exists(), True)
diff --git a/tubesync/sync/tests/test_frontend.py b/tubesync/sync/tests/test_frontend.py
new file mode 100644
index 00000000..836780fb
--- /dev/null
+++ b/tubesync/sync/tests/test_frontend.py
@@ -0,0 +1,491 @@
+import logging
+from datetime import datetime
+from urllib.parse import urlsplit
+from django.conf import settings
+from django.test import TestCase, Client
+from django.utils import timezone
+from django_huey import DJANGO_HUEY, get_queue
+from common.models import TaskHistory
+from sync.models import Source, Media
+from sync.tasks import (
+ check_source_directory_exists,
+ get_media_download_task, get_media_thumbnail_task,
+)
+from sync.choices import (
+ Val, Fallback, FilterSeconds, IndexSchedule, SourceResolution,
+ TaskQueue, YouTube_AudioCodec, YouTube_VideoCodec,
+ YouTube_SourceType,
+)
+
+class FrontEndTestCase(TestCase):
+ maxDiff = None
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ # Use immediate mode to execute tasks in this process
+ for qn in DJANGO_HUEY.get('queues', dict()):
+ q = get_queue(qn)
+ # Set the storage variable before using the property.
+ q.immediate_use_memory = True
+ q.immediate = False
+
+ def setUp(self):
+ # Disable general logging for test case
+ logging.disable(logging.CRITICAL)
+
+ def test_dashboard(self):
+ c = Client()
+ response = c.get('/')
+ self.assertEqual(response.status_code, 200)
+
+ def test_validate_source(self):
+ test_sources = {
+ 'youtube-channel': {
+ 'valid': (
+ 'https://m.youtube.com/testchannel',
+ 'https://m.youtube.com/c/testchannel',
+ 'https://m.youtube.com/c/testchannel/videos',
+ 'https://www.youtube.com/testchannel',
+ 'https://www.youtube.com/c/testchannel',
+ 'https://www.youtube.com/c/testchannel/videos',
+ ),
+ 'invalid_schema': (
+ 'http://www.youtube.com/c/playlist',
+ 'ftp://www.youtube.com/c/playlist',
+ ),
+ 'invalid_domain': (
+ 'https://www.test.com/c/testchannel',
+ 'https://www.example.com/c/testchannel',
+ 'https://n.youtube.com/c/testchannel',
+ ),
+ 'invalid_path': (
+ 'https://www.youtube.com/test/invalid',
+ 'https://www.youtube.com/c/test/invalid',
+ ),
+ 'invalid_reserved_paths': (
+ 'https://www.youtube.com/watch?v=OkMadb8cpIw',
+ 'https://www.youtube.com/watch',
+ 'https://www.youtube.com/shorts',
+ 'https://www.youtube.com/live',
+ 'https://www.youtube.com/feed',
+ 'https://www.youtube.com/trending',
+ ),
+ },
+ 'youtube-channel-id': {
+ 'valid': (
+ 'https://m.youtube.com/channel/channelid',
+ 'https://m.youtube.com/channel/channelid/videos',
+ 'https://www.youtube.com/channel/channelid',
+ 'https://www.youtube.com/channel/channelid/videos',
+ ),
+ 'invalid_schema': (
+ 'http://www.youtube.com/channel/channelid',
+ 'ftp://www.youtube.com/channel/channelid',
+ ),
+ 'invalid_domain': (
+ 'https://www.test.com/channel/channelid',
+ 'https://www.example.com/channel/channelid',
+ 'https://n.youtube.com/channel/channelid',
+ ),
+ 'invalid_path': (
+ 'https://www.youtube.com/test/invalid',
+ 'https://www.youtube.com/channel/test/invalid',
+ ),
+ },
+ 'youtube-playlist': {
+ 'valid': (
+ 'https://m.youtube.com/playlist?list=testplaylist',
+ 'https://m.youtube.com/watch?v=testvideo&list=testplaylist',
+ 'https://www.youtube.com/playlist?list=testplaylist',
+ 'https://www.youtube.com/watch?v=testvideo&list=testplaylist',
+ ),
+ 'invalid_schema': (
+ 'http://www.youtube.com/playlist?list=testplaylist',
+ 'ftp://www.youtube.com/playlist?list=testplaylist',
+ ),
+ 'invalid_domain': (
+ 'https://www.test.com/playlist?list=testplaylist',
+ 'https://www.example.com/playlist?list=testplaylist',
+ 'https://n.youtube.com/playlist?list=testplaylist',
+ ),
+ 'invalid_path': (
+ 'https://www.youtube.com/test/invalid',
+ ),
+ }
+ }
+ c = Client()
+ response = c.get('/source-validate')
+ self.assertEqual(response.status_code, 200)
+ for (source_type, tests) in test_sources.items():
+ for test, urls in tests.items():
+ for url in urls:
+ data = {'source_url': url}
+ response = c.post('/source-validate', data)
+ if test == 'valid':
+ # Valid source tests should bounce to /source-add
+ self.assertEqual(response.status_code, 302)
+ url_parts = urlsplit(response.url)
+ self.assertEqual(url_parts.path, '/source-add')
+ else:
+ # Invalid source tests should reload the page with an error
+ self.assertEqual(response.status_code, 200)
+ self.assertIn('",
+ '',
+ ' no fancy stuff title',
+ ' testname',
+ ' 2017',
+ ' ',
+ ' ',
+ ' ',
+ ' 1.2345',
+ ' 579',
+ ' ',
+ ' ',
+ ' no fancy stuff desc',
+ ' ', # media.thumbfile is empty without media existing
+ ' 50',
+ ' 401',
+ ' mediakey',
+ ' mediakey',
+ ' test uploader',
+ ' 2017-09-11',
+ ' 2020-01-01 01:01:01',
+ ' test category 1',
+ ' test category 2',
+ '',
+ ]
+ expected_tree = ElementTree.fromstring('\n'.join(expected_nfo))
+ nfo_tree = ElementTree.fromstring(self.media.nfoxml)
+ # Check each node with attribs in expected_tree is present in test_nfo
+ for expected_node in expected_tree:
+ # Ignore checking , only tag we may have multiple of
+ if expected_node.tag == 'genre':
+ continue
+ # Find the same node in the NFO XML tree
+ nfo_node = nfo_tree.find(expected_node.tag)
+ self.assertEqual(expected_node.attrib, nfo_node.attrib)
+ self.assertEqual(expected_node.tag, nfo_node.tag)
+ self.assertEqual(expected_node.text, nfo_node.text)
+
+
+class MediaFilterTestCase(TestCase):
+
+ def setUp(self):
+ # Disable general logging for test case
+ # logging.disable(logging.CRITICAL)
+ # Add a test source
+ self.source = Source.objects.create(
+ source_type=Val(YouTube_SourceType.CHANNEL),
+ key="testkey",
+ name="testname",
+ directory="testdirectory",
+ media_format=settings.MEDIA_FORMATSTR_DEFAULT,
+ index_schedule=3600,
+ delete_old_media=False,
+ days_to_keep=14,
+ source_resolution=Val(SourceResolution.VIDEO_1080P),
+ source_vcodec=Val(YouTube_VideoCodec.VP9),
+ source_acodec=Val(YouTube_AudioCodec.OPUS),
+ prefer_60fps=False,
+ prefer_hdr=False,
+ fallback=Val(Fallback.FAIL),
+ )
+ # Add some test media
+ self.media = Media.objects.create(
+ key="mediakey",
+ source=self.source,
+ metadata=metadata,
+ skip=False,
+ published=timezone.make_aware(
+ datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
+ ),
+ )
+ # Fix a created datetime for predictable testing
+ self.media.created = datetime(
+ year=2020, month=1, day=1, hour=1, minute=1, second=1
+ )
+
+ def test_filter_unpublished_skip(self):
+ # Check if unpublished that we skip download it
+ self.media.skip = False
+ self.media.published = False
+ changed = filter_media(self.media)
+ self.assertTrue(changed)
+ self.assertTrue(self.media.skip)
+
+ def test_filter_published_unskip(self):
+ # Check if we had previously skipped it, but now it's published, we won't skip it
+ self.media.skip = True
+ self.media.published = timezone.make_aware(
+ datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
+ )
+ changed = filter_media(self.media)
+ self.assertTrue(changed)
+ self.assertFalse(self.media.skip)
+
+ def test_filter_filter_text_nomatch(self):
+ # Check that if we don't match the filter text, we skip
+ self.media.source.filter_text = "No fancy stuff"
+ self.media.skip = False
+ self.media.published = timezone.make_aware(
+ datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
+ )
+ changed = filter_media(self.media)
+ self.assertTrue(changed)
+ self.assertTrue(self.media.skip)
+
+ def test_filter_filter_text_match(self):
+ # Check that if we match the filter text, we don't skip
+ self.media.source.filter_text = "(?i)No fancy stuff"
+ self.media.skip = True
+ self.media.published = timezone.make_aware(
+ datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
+ )
+ changed = filter_media(self.media)
+ self.assertTrue(changed)
+ self.assertFalse(self.media.skip)
+
+ def test_filter_filter_text_invert_nomatch(self):
+ # Check that if we don't match the filter text, we don't skip
+ self.media.source.filter_text = "No fancy stuff"
+ self.media.source.filter_text_invert = True
+ self.media.skip = True
+ self.media.published = timezone.make_aware(
+ datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
+ )
+ changed = filter_media(self.media)
+ self.assertTrue(changed)
+ self.assertFalse(self.media.skip)
+
+ def test_filter_filter_text_invert_match(self):
+ # Check that if we match the filter text and do skip
+ self.media.source.filter_text = "(?i)No fancy stuff"
+ self.media.source.filter_text_invert = True
+ self.media.skip = False
+ self.media.published = timezone.make_aware(
+ datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
+ )
+ changed = filter_media(self.media)
+ self.assertTrue(changed)
+ self.assertTrue(self.media.skip)
+
+ def test_filter_max_cap_skip(self):
+ # Check if it's older than the max_cap, we don't download it (1 second so it will always fail)
+ self.media.source.download_cap = 1
+ self.media.skip = False
+ self.media.published = timezone.make_aware(
+ datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
+ )
+ changed = filter_media(self.media)
+ self.assertTrue(changed)
+ self.assertTrue(self.media.skip)
+
+ def test_filter_max_cap_unskip(self):
+ # Make sure it's newer than the cap so we download it, ensure that we are published in the last seconds
+ self.media.source.download_cap = 3600
+ self.media.skip = True
+ self.media.published = timezone.now()
+ changed = filter_media(self.media)
+ self.assertTrue(changed)
+ self.assertFalse(self.media.skip)
+
+ def test_filter_below_min(self):
+ # Filter videos shorter than the minimum limit
+ self.media.skip = False
+ self.media.source.filter_seconds_min = True
+ self.media.source.filter_seconds = 500
+ self.media.published = timezone.make_aware(
+ datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
+ )
+ changed = filter_media(self.media)
+ self.assertTrue(changed)
+ self.assertTrue(self.media.skip)
+
+ def test_filter_above_min(self):
+ # Video is longer than the minimum, allow it
+ self.media.skip = True
+ self.media.source.filter_seconds_min = True
+ self.media.source.filter_seconds = 300
+ self.media.published = timezone.make_aware(
+ datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
+ )
+ changed = filter_media(self.media)
+ self.assertTrue(changed)
+ self.assertFalse(self.media.skip)
+
+ def test_filter_above_max(self):
+ # Filter videos longer than the maximum limit
+ self.media.skip = False
+ self.media.source.filter_seconds_min = False
+ self.media.source.filter_seconds = 300
+ self.media.published = timezone.make_aware(
+ datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
+ )
+ changed = filter_media(self.media)
+ self.assertTrue(changed)
+ self.assertTrue(self.media.skip)
+
+ def test_filter_below_max(self):
+ # Video is below the maximum, allow it
+ self.media.skip = True
+ self.media.source.filter_seconds_min = False
+ self.media.source.filter_seconds = 500
+ self.media.published = timezone.make_aware(
+ datetime(year=2020, month=1, day=1, hour=1, minute=1, second=1)
+ )
+ changed = filter_media(self.media)
+ self.assertTrue(changed)
+ self.assertFalse(self.media.skip)
+
+
diff --git a/tubesync/sync/tests/test_response_filtering.py b/tubesync/sync/tests/test_response_filtering.py
new file mode 100644
index 00000000..34cb9e9a
--- /dev/null
+++ b/tubesync/sync/tests/test_response_filtering.py
@@ -0,0 +1,91 @@
+import logging
+from django.test import TestCase, override_settings
+from sync.models import Source, Media
+from sync.utils import filter_response
+from sync.choices import (
+ Val, Fallback, SourceResolution,
+ YouTube_AudioCodec, YouTube_VideoCodec,
+ YouTube_SourceType,
+)
+
+from .fixtures import all_test_metadata
+
+class ResponseFilteringTestCase(TestCase):
+
+ def setUp(self):
+ # Disable general logging for test case
+ logging.disable(logging.CRITICAL)
+ # Add a test source
+ self.source = Source.objects.create(
+ source_type=Val(YouTube_SourceType.CHANNEL),
+ key='testkey',
+ name='testname',
+ directory='testdirectory',
+ index_schedule=3600,
+ delete_old_media=False,
+ days_to_keep=14,
+ source_resolution=Val(SourceResolution.VIDEO_1080P),
+ source_vcodec=Val(YouTube_VideoCodec.VP9),
+ source_acodec=Val(YouTube_AudioCodec.OPUS),
+ prefer_60fps=False,
+ prefer_hdr=False,
+ fallback=Val(Fallback.FAIL)
+ )
+ # Add some media
+ self.media = Media.objects.create(
+ key='mediakey',
+ source=self.source,
+ metadata='{}'
+ )
+
+ @override_settings(SHRINK_OLD_MEDIA_METADATA=False, SHRINK_NEW_MEDIA_METADATA=False)
+ def test_metadata_20230629(self):
+ self.media.metadata = all_test_metadata['20230629']
+ self.media.save()
+
+ unfiltered = self.media.loaded_metadata
+ filtered = filter_response(self.media.loaded_metadata, True)
+ self.assertIn('formats', unfiltered.keys())
+ self.assertIn('formats', filtered.keys())
+ # filtered 'downloader_options'
+ self.assertIn('downloader_options', unfiltered['formats'][10].keys())
+ self.assertNotIn('downloader_options', filtered['formats'][10].keys())
+ # filtered 'http_headers'
+ self.assertIn('http_headers', unfiltered['formats'][0].keys())
+ self.assertNotIn('http_headers', filtered['formats'][0].keys())
+ # did not lose any formats
+ self.assertEqual(48, len(unfiltered['formats']))
+ self.assertEqual(48, len(filtered['formats']))
+ self.assertEqual(len(unfiltered['formats']), len(filtered['formats']))
+ # did not remove everything with url
+ self.assertIn('original_url', unfiltered.keys())
+ self.assertIn('original_url', filtered.keys())
+ self.assertEqual(unfiltered['original_url'], filtered['original_url'])
+ # did reduce the size of the metadata
+ self.assertTrue(len(str(filtered)) < len(str(unfiltered)))
+
+ url_keys = []
+ for format in unfiltered['formats']:
+ for key in format.keys():
+ if 'url' in key:
+ url_keys.append((format['format_id'], key, format[key],))
+ unfiltered_url_keys = url_keys
+ self.assertEqual(63, len(unfiltered_url_keys), msg=str(unfiltered_url_keys))
+
+ url_keys = []
+ for format in filtered['formats']:
+ for key in format.keys():
+ if 'url' in key:
+ url_keys.append((format['format_id'], key, format[key],))
+ filtered_url_keys = url_keys
+ self.assertEqual(3, len(filtered_url_keys), msg=str(filtered_url_keys))
+
+ url_keys = []
+ for lang_code, captions in filtered['automatic_captions'].items():
+ for caption in captions:
+ for key in caption.keys():
+ if 'url' in key:
+ url_keys.append((lang_code, caption['ext'], caption[key],))
+ self.assertEqual(0, len(url_keys), msg=str(url_keys))
+
+
diff --git a/tubesync/sync/tests/test_tasks.py b/tubesync/sync/tests/test_tasks.py
new file mode 100644
index 00000000..357647a2
--- /dev/null
+++ b/tubesync/sync/tests/test_tasks.py
@@ -0,0 +1,38 @@
+import logging
+from datetime import timedelta
+from django.test import TestCase
+from django.utils import timezone
+from sync.models import Source, Media
+from sync.tasks import (
+ cleanup_old_media,
+)
+
+class TasksTestCase(TestCase):
+
+ def setUp(self):
+ # Disable general logging for test case
+ logging.disable(logging.CRITICAL)
+
+ def test_delete_old_media(self):
+ src1 = Source.objects.create(key='aaa', name='aaa', directory='/tmp/a', delete_old_media=False, days_to_keep=14)
+ src2 = Source.objects.create(key='bbb', name='bbb', directory='/tmp/b', delete_old_media=True, days_to_keep=14)
+
+ now = timezone.now()
+
+ m11 = Media.objects.create(source=src1, downloaded=True, key='a11', download_date=now - timedelta(days=5)) # noqa
+ m12 = Media.objects.create(source=src1, downloaded=True, key='a12', download_date=now - timedelta(days=25)) # noqa
+ m13 = Media.objects.create(source=src1, downloaded=False, key='a13') # noqa
+
+ m21 = Media.objects.create(source=src2, downloaded=True, key='a21', download_date=now - timedelta(days=5)) # noqa
+ m22 = Media.objects.create(source=src2, downloaded=True, key='a22', download_date=now - timedelta(days=25))
+ m23 = Media.objects.create(source=src2, downloaded=False, key='a23') # noqa
+
+ self.assertEqual(src1.media_source.all().count(), 3)
+ self.assertEqual(src2.media_source.all().count(), 3)
+
+ cleanup_old_media.call_local(durable=False)
+
+ self.assertEqual(src1.media_source.all().count(), 3)
+ self.assertEqual(src2.media_source.all().count(), 3)
+ self.assertEqual(Media.objects.filter(pk=m22.pk).exists(), False)
+ self.assertEqual(Media.objects.filter(source=src2, key=m22.key, skip=True).exists(), True)