From 0f593dca9fa995d88eb763170a932da61c8f24dc Mon Sep 17 00:00:00 2001 From: Imran Hussain Date: Sun, 20 Oct 2024 18:10:26 +0100 Subject: [PATCH] Add option `--plugin-dirs` (#11277) Closes #3260 Authored by: imranh2, coletdjnz Co-authored-by: coletdjnz --- README.md | 7 +++++++ test/test_plugins.py | 19 +++++++++++++++++++ .../yt_dlp_plugins/extractor/package.py | 5 +++++ yt_dlp/__init__.py | 5 +++++ yt_dlp/options.py | 8 ++++++++ yt_dlp/plugins.py | 7 +++++++ yt_dlp/utils/_utils.py | 4 ++++ 7 files changed, 55 insertions(+) create mode 100644 test/testdata/plugin_packages/testpackage/yt_dlp_plugins/extractor/package.py diff --git a/README.md b/README.md index 1cafe51d5..fc38a529a 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,13 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git containing directory ("-" for stdin). Can be used multiple times and inside other configuration files + --plugin-dirs PATH Path to an additional directory to search + for plugins. This option can be used + multiple times to add multiple directories. + Note that this currently only works for + extractor plugins; postprocessor plugins can + only be loaded from the default plugin + directories --flat-playlist Do not extract the videos of a playlist, only list them --no-flat-playlist Fully extract the videos of a playlist diff --git a/test/test_plugins.py b/test/test_plugins.py index c82158e9f..77545d136 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -10,6 +10,7 @@ TEST_DATA_DIR = Path(os.path.dirname(os.path.abspath(__file__)), 'testdata') sys.path.append(str(TEST_DATA_DIR)) importlib.invalidate_caches() +from yt_dlp.utils import Config from yt_dlp.plugins import PACKAGE_NAME, directories, load_plugins @@ -68,6 +69,24 @@ class TestPlugins(unittest.TestCase): os.remove(zip_path) importlib.invalidate_caches() # reset the import caches + def test_plugin_dirs(self): + # Internal plugin dirs hack for CLI --plugin-dirs + # To be replaced with proper system later + custom_plugin_dir = TEST_DATA_DIR / 'plugin_packages' + Config._plugin_dirs = [str(custom_plugin_dir)] + importlib.invalidate_caches() # reset the import caches + + try: + package = importlib.import_module(f'{PACKAGE_NAME}.extractor') + self.assertIn(custom_plugin_dir / 'testpackage' / PACKAGE_NAME / 'extractor', map(Path, package.__path__)) + + plugins_ie = load_plugins('extractor', 'IE') + self.assertIn('PackagePluginIE', plugins_ie.keys()) + + finally: + Config._plugin_dirs = [] + importlib.invalidate_caches() # reset the import caches + if __name__ == '__main__': unittest.main() diff --git a/test/testdata/plugin_packages/testpackage/yt_dlp_plugins/extractor/package.py b/test/testdata/plugin_packages/testpackage/yt_dlp_plugins/extractor/package.py new file mode 100644 index 000000000..b860300d8 --- /dev/null +++ b/test/testdata/plugin_packages/testpackage/yt_dlp_plugins/extractor/package.py @@ -0,0 +1,5 @@ +from yt_dlp.extractor.common import InfoExtractor + + +class PackagePluginIE(InfoExtractor): + pass diff --git a/yt_dlp/__init__.py b/yt_dlp/__init__.py index f598b6c2f..d976f5bbc 100644 --- a/yt_dlp/__init__.py +++ b/yt_dlp/__init__.py @@ -34,6 +34,7 @@ from .postprocessor import ( ) from .update import Updater from .utils import ( + Config, NO_DEFAULT, POSTPROCESS_WHEN, DateRange, @@ -967,6 +968,10 @@ def _real_main(argv=None): parser, opts, all_urls, ydl_opts = parse_options(argv) + # HACK: Set the plugin dirs early on + # TODO(coletdjnz): remove when plugin globals system is implemented + Config._plugin_dirs = opts.plugin_dirs + # Dump user agent if opts.dump_user_agent: ua = traverse_obj(opts.headers, 'User-Agent', casesense=False, default=std_headers['User-Agent']) diff --git a/yt_dlp/options.py b/yt_dlp/options.py index 9980b7fc3..c3a647da7 100644 --- a/yt_dlp/options.py +++ b/yt_dlp/options.py @@ -408,6 +408,14 @@ def create_parser(): help=( 'Location of the main configuration file; either the path to the config or its containing directory ' '("-" for stdin). Can be used multiple times and inside other configuration files')) + general.add_option( + '--plugin-dirs', + dest='plugin_dirs', metavar='PATH', action='append', + help=( + 'Path to an additional directory to search for plugins. ' + 'This option can be used multiple times to add multiple directories. ' + 'Note that this currently only works for extractor plugins; ' + 'postprocessor plugins can only be loaded from the default plugin directories')) general.add_option( '--flat-playlist', action='store_const', dest='extract_flat', const='in_playlist', default=False, diff --git a/yt_dlp/plugins.py b/yt_dlp/plugins.py index d777d14e7..204558d60 100644 --- a/yt_dlp/plugins.py +++ b/yt_dlp/plugins.py @@ -15,6 +15,7 @@ from zipfile import ZipFile from .compat import functools # isort: split from .utils import ( + Config, get_executable_path, get_system_config_dirs, get_user_config_dirs, @@ -84,6 +85,12 @@ class PluginFinder(importlib.abc.MetaPathFinder): with contextlib.suppress(ValueError): # Added when running __main__.py directly candidate_locations.remove(Path(__file__).parent) + # TODO(coletdjnz): remove when plugin globals system is implemented + if Config._plugin_dirs: + candidate_locations.extend(_get_package_paths( + *Config._plugin_dirs, + containing_folder='')) + parts = Path(*fullname.split('.')) for path in orderedSet(candidate_locations, lazy=True): candidate = path / parts diff --git a/yt_dlp/utils/_utils.py b/yt_dlp/utils/_utils.py index 27ebfefbc..ea748898f 100644 --- a/yt_dlp/utils/_utils.py +++ b/yt_dlp/utils/_utils.py @@ -4897,6 +4897,10 @@ class Config: filename = None __initialized = False + # Internal only, do not use! Hack to enable --plugin-dirs + # TODO(coletdjnz): remove when plugin globals system is implemented + _plugin_dirs = None + def __init__(self, parser, label=None): self.parser, self.label = parser, label self._loaded_paths, self.configs = set(), []