diff --git a/app/main.py b/app/main.py index bebcd2d..43a52e0 100644 --- a/app/main.py +++ b/app/main.py @@ -116,6 +116,17 @@ class Config: if not self.URL_PREFIX.endswith('/'): self.URL_PREFIX += '/' + # Strip trailing slashes from the download directories. get_custom_dirs() + # builds the folder dropdown by removing the base path as a prefix from + # each subdirectory, and the base directory's own path does not carry the + # trailing slash — so 'DOWNLOAD_DIR=/downloads/' failed to match itself + # and leaked 'downloads' into the dropdown as a bogus folder option. + # Runs after the '%%' indirection above so AUDIO_DOWNLOAD_DIR is resolved. + for attr in ('DOWNLOAD_DIR', 'AUDIO_DOWNLOAD_DIR', 'TEMP_DIR', 'STATE_DIR'): + val = getattr(self, attr) + if isinstance(val, str) and len(val) > 1 and val.endswith('/'): + setattr(self, attr, val.rstrip('/') or '/') + # A blank PUBLIC_HOST_AUDIO_URL (e.g. set empty in a compose file) bypasses the # default via os.environ.get, which would leave audio links root-relative and 404. # Fall back to the 'audio_download/' route that serves AUDIO_DOWNLOAD_DIR. When diff --git a/app/tests/test_config.py b/app/tests/test_config.py index a877ef6..e34d2b2 100644 --- a/app/tests/test_config.py +++ b/app/tests/test_config.py @@ -77,6 +77,34 @@ class ConfigTests(unittest.TestCase): self.assertEqual(c.PUBLIC_HOST_URL, "https://ytdl.example.com/") self.assertEqual(c.PUBLIC_HOST_AUDIO_URL, "https://audio.example.com/") + def test_download_dirs_lose_trailing_slash(self): + # get_custom_dirs strips the base path as a prefix from each subdirectory, + # and the base directory's own path has no trailing slash -- so a trailing + # slash here leaked the absolute path into the folder dropdown. + with patch.dict(os.environ, _base_env( + DOWNLOAD_DIR="/downloads/", + AUDIO_DOWNLOAD_DIR="/audio/", + TEMP_DIR="/tmp/", + STATE_DIR="/state/", + ), clear=False): + c = Config() + self.assertEqual(c.DOWNLOAD_DIR, "/downloads") + self.assertEqual(c.AUDIO_DOWNLOAD_DIR, "/audio") + self.assertEqual(c.TEMP_DIR, "/tmp") + self.assertEqual(c.STATE_DIR, "/state") + + def test_root_download_dir_survives_normalisation(self): + with patch.dict(os.environ, _base_env(DOWNLOAD_DIR="/", AUDIO_DOWNLOAD_DIR="///"), clear=False): + c = Config() + self.assertEqual(c.DOWNLOAD_DIR, "/") + self.assertEqual(c.AUDIO_DOWNLOAD_DIR, "/") + + def test_download_dirs_without_trailing_slash_unchanged(self): + with patch.dict(os.environ, _base_env(DOWNLOAD_DIR="/downloads", AUDIO_DOWNLOAD_DIR="."), clear=False): + c = Config() + self.assertEqual(c.DOWNLOAD_DIR, "/downloads") + self.assertEqual(c.AUDIO_DOWNLOAD_DIR, ".") + def test_ytdl_options_json_loaded(self): opts = {"quiet": True, "no_warnings": True} with patch.dict(