diff --git a/.gitignore b/.gitignore index cb14cfd6..cf63cfba 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,6 @@ dmypy.json Pipfile.lock .vscode/launch.json + +# Ignore Jetbrains IDE files +.idea/ \ No newline at end of file diff --git a/README.md b/README.md index b85b2b4a..f1fdb6da 100644 --- a/README.md +++ b/README.md @@ -376,7 +376,21 @@ useful if you are manually installing TubeSync in some other environment. These | HTTP_PASS | Sets the password for HTTP basic authentication | some-secure-password | | DATABASE_CONNECTION | Optional external database connection details | mysql://user:pass@host:port/database | | VIDEO_HEIGHT_CUTOFF | Smallest video height in pixels permitted to download | 240 | +| TUBESYNC_DIRECTORY_MODE | Controls how downloaded files are organized. | default | +# TubeSync Directory Mode + +Controls how downloaded files are organized. + +Values: +- default: Audio files go to `audio`, video files to `video`. +- flat: All files are placed in the root of DOWNLOAD_DIR. +- custom:,: Allows custom prefixes for audio and video directories under DOWNLOAD_DIR. + +Example: +``` +TUBESYNC_DIRECTORY_MODE=custom:music,shows +``` # Manual, non-containerised, installation diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index 2f116356..62cd8e9a 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -514,10 +514,35 @@ class Source(models.Model): @property def type_directory_path(self): - if self.source_resolution == self.SOURCE_RESOLUTION_AUDIO: - return Path(settings.DOWNLOAD_AUDIO_DIR) / self.directory + # Get the directory mode from the environment + directory_mode = os.getenv("TUBESYNC_DIRECTORY_MODE", "default") + + # Default behavior + if directory_mode == "default": + if self.source_resolution == self.SOURCE_RESOLUTION_AUDIO: + return Path(settings.DOWNLOAD_AUDIO_DIR) / self.directory + else: + return Path(settings.DOWNLOAD_VIDEO_DIR) / self.directory + + # Flat mode - Place all files in the root + elif directory_mode == "flat": + return Path(settings.DOWNLOAD_DIR) / self.directory + + # Custom mode - Parse prefixes for audio and video + elif directory_mode.startswith("custom:"): + try: + audio_prefix, video_prefix = directory_mode.split(":")[1].split(",") + except ValueError: + raise ValueError("Invalid format for TUBESYNC_DIRECTORY_MODE=custom. Expected 'custom:audio_prefix,video_prefix'.") + + if self.source_resolution == self.SOURCE_RESOLUTION_AUDIO: + return Path(settings.DOWNLOAD_DIR) / audio_prefix / self.directory + else: + return Path(settings.DOWNLOAD_DIR) / video_prefix / self.directory + + # Fallback for invalid modes else: - return Path(settings.DOWNLOAD_VIDEO_DIR) / self.directory + raise ValueError(f"Unsupported TUBESYNC_DIRECTORY_MODE: {directory_mode}") def make_directory(self): return os.makedirs(self.directory_path, exist_ok=True) diff --git a/tubesync/sync/tests.py b/tubesync/sync/tests.py index 72411894..0029a0e9 100644 --- a/tubesync/sync/tests.py +++ b/tubesync/sync/tests.py @@ -6,7 +6,9 @@ import logging +import os from datetime import datetime, timedelta +from pathlib import Path from urllib.parse import urlsplit from xml.etree import ElementTree from django.conf import settings @@ -1714,3 +1716,77 @@ class TasksTestCase(TestCase): self.assertEqual(src1.media_source.all().count(), 3) self.assertEqual(src2.media_source.all().count(), 2) self.assertEqual(Media.objects.filter(pk=m22.pk).exists(), False) + +class TypeDirectoryPathTestCase(TestCase): + def setUp(self): + # Mock settings for testing + self.audio_dir = Path("/mock/audio/dir") + self.video_dir = Path("/mock/video/dir") + self.download_dir = Path("/mock/download/dir") + settings.DOWNLOAD_AUDIO_DIR = self.audio_dir + settings.DOWNLOAD_VIDEO_DIR = self.video_dir + settings.DOWNLOAD_DIR = self.download_dir + + # Create a source object for testing + self.source = Source( + directory="test_directory", + source_resolution=Source.SOURCE_RESOLUTION_AUDIO, + ) + + def test_default_mode_audio(self): + """ + Test default mode for audio resolution. + """ + os.environ["TUBESYNC_DIRECTORY_MODE"] = "default" + expected_path = self.audio_dir / self.source.directory + self.assertEqual(self.source.type_directory_path, expected_path) + + def test_default_mode_video(self): + """ + Test default mode for video resolution. + """ + os.environ["TUBESYNC_DIRECTORY_MODE"] = "default" + self.source.source_resolution = Source.SOURCE_RESOLUTION_1080P + expected_path = self.video_dir / self.source.directory + self.assertEqual(self.source.type_directory_path, expected_path) + + def test_flat_mode(self): + """ + Test flat mode places files in the root download directory. + """ + os.environ["TUBESYNC_DIRECTORY_MODE"] = "flat" + expected_path = self.download_dir / self.source.directory + self.assertEqual(self.source.type_directory_path, expected_path) + + def test_custom_mode_audio(self): + """ + Test custom mode with prefixes for audio. + """ + os.environ["TUBESYNC_DIRECTORY_MODE"] = "custom:audio_prefix,video_prefix" + expected_path = self.download_dir / "audio_prefix" / self.source.directory + self.assertEqual(self.source.type_directory_path, expected_path) + + def test_custom_mode_video(self): + """ + Test custom mode with prefixes for video. + """ + os.environ["TUBESYNC_DIRECTORY_MODE"] = "custom:audio_prefix,video_prefix" + self.source.source_resolution = Source.SOURCE_RESOLUTION_1080P + expected_path = self.download_dir / "video_prefix" / self.source.directory + self.assertEqual(self.source.type_directory_path, expected_path) + + def test_custom_mode_invalid_format(self): + """ + Test custom mode with an invalid format. + """ + os.environ["TUBESYNC_DIRECTORY_MODE"] = "custom:only_audio_prefix" + with self.assertRaises(ValueError): + _ = self.source.type_directory_path + + def test_invalid_mode(self): + """ + Test unsupported directory mode raises an error. + """ + os.environ["TUBESYNC_DIRECTORY_MODE"] = "unsupported_mode" + with self.assertRaises(ValueError): + _ = self.source.type_directory_path \ No newline at end of file