aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md7
-rw-r--r--test/test_plugins.py19
-rw-r--r--test/testdata/plugin_packages/testpackage/yt_dlp_plugins/extractor/package.py5
-rw-r--r--yt_dlp/__init__.py5
-rw-r--r--yt_dlp/options.py8
-rw-r--r--yt_dlp/plugins.py7
-rw-r--r--yt_dlp/utils/_utils.py4
7 files changed, 55 insertions, 0 deletions
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
@@ -409,6 +409,14 @@ def create_parser():
'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,
help='Do not extract the videos of a playlist, only list them')
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(), []