mirror of
				https://github.com/yt-dlp/yt-dlp.git
				synced 2025-10-30 22:25:19 +00:00 
			
		
		
		
	Improve plugin architecture (#5553)
to make plugins easier to develop and use: * Plugins are now loaded as namespace packages. * Plugins can be loaded in any distribution of yt-dlp (binary, pip, source, etc.). * Plugin packages can be installed and managed via pip, or dropped into any of the documented locations. * Users do not need to edit any code files to install plugins. * Backwards-compatible with previous plugin architecture. As a side-effect, yt-dlp will now search in a few more locations for config files. Closes https://github.com/yt-dlp/yt-dlp/issues/1389 Authored by: flashdagger, coletdjnz, pukkandan, Grub4K Co-authored-by: Marcel <flashdagger@googlemail.com> Co-authored-by: pukkandan <pukkandan.ytdlp@gmail.com> Co-authored-by: Simon Sawicki <accounts@grub4k.xyz>
This commit is contained in:
		
							
								
								
									
										8
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -120,9 +120,5 @@ yt-dlp.zip | |||||||
| */extractor/lazy_extractors.py | */extractor/lazy_extractors.py | ||||||
|  |  | ||||||
| # Plugins | # Plugins | ||||||
| ytdlp_plugins/extractor/* | ytdlp_plugins/* | ||||||
| !ytdlp_plugins/extractor/__init__.py | yt-dlp-plugins/* | ||||||
| !ytdlp_plugins/extractor/sample.py |  | ||||||
| ytdlp_plugins/postprocessor/* |  | ||||||
| !ytdlp_plugins/postprocessor/__init__.py |  | ||||||
| !ytdlp_plugins/postprocessor/sample.py |  | ||||||
|   | |||||||
							
								
								
									
										66
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										66
									
								
								README.md
									
									
									
									
									
								
							| @@ -61,6 +61,8 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t | |||||||
|     * [Modifying metadata examples](#modifying-metadata-examples) |     * [Modifying metadata examples](#modifying-metadata-examples) | ||||||
| * [EXTRACTOR ARGUMENTS](#extractor-arguments) | * [EXTRACTOR ARGUMENTS](#extractor-arguments) | ||||||
| * [PLUGINS](#plugins) | * [PLUGINS](#plugins) | ||||||
|  |     * [Installing Plugins](#installing-plugins) | ||||||
|  |     * [Developing Plugins](#developing-plugins) | ||||||
| * [EMBEDDING YT-DLP](#embedding-yt-dlp) | * [EMBEDDING YT-DLP](#embedding-yt-dlp) | ||||||
|     * [Embedding examples](#embedding-examples) |     * [Embedding examples](#embedding-examples) | ||||||
| * [DEPRECATED OPTIONS](#deprecated-options) | * [DEPRECATED OPTIONS](#deprecated-options) | ||||||
| @@ -1110,15 +1112,20 @@ You can configure yt-dlp by placing any supported command line option to a confi | |||||||
|     * If `-P` is not given, the current directory is searched |     * If `-P` is not given, the current directory is searched | ||||||
| 1. **User Configuration**: | 1. **User Configuration**: | ||||||
|     * `${XDG_CONFIG_HOME}/yt-dlp/config` (recommended on Linux/macOS) |     * `${XDG_CONFIG_HOME}/yt-dlp/config` (recommended on Linux/macOS) | ||||||
|  |     * `${XDG_CONFIG_HOME}/yt-dlp/config.txt` | ||||||
|     * `${XDG_CONFIG_HOME}/yt-dlp.conf` |     * `${XDG_CONFIG_HOME}/yt-dlp.conf` | ||||||
|     * `${APPDATA}/yt-dlp/config` (recommended on Windows) |     * `${APPDATA}/yt-dlp/config` (recommended on Windows) | ||||||
|     * `${APPDATA}/yt-dlp/config.txt` |     * `${APPDATA}/yt-dlp/config.txt` | ||||||
|     * `~/yt-dlp.conf` |     * `~/yt-dlp.conf` | ||||||
|     * `~/yt-dlp.conf.txt` |     * `~/yt-dlp.conf.txt` | ||||||
|  |     * `~/.yt-dlp/config` | ||||||
|  |     * `~/.yt-dlp/config.txt` | ||||||
| 
 | 
 | ||||||
|     See also: [Notes about environment variables](#notes-about-environment-variables) |     See also: [Notes about environment variables](#notes-about-environment-variables) | ||||||
| 1. **System Configuration**: | 1. **System Configuration**: | ||||||
|     * `/etc/yt-dlp.conf` |     * `/etc/yt-dlp.conf` | ||||||
|  |     * `/etc/yt-dlp/config` | ||||||
|  |     * `/etc/yt-dlp/config.txt` | ||||||
| 
 | 
 | ||||||
| E.g. with the following configuration file yt-dlp will always extract the audio, not copy the mtime, use a proxy and save all videos under `YouTube` directory in your home directory: | E.g. with the following configuration file yt-dlp will always extract the audio, not copy the mtime, use a proxy and save all videos under `YouTube` directory in your home directory: | ||||||
| ``` | ``` | ||||||
| @@ -1789,19 +1796,68 @@ NOTE: These options may be changed/removed in the future without concern for bac | |||||||
| 
 | 
 | ||||||
| # PLUGINS | # PLUGINS | ||||||
| 
 | 
 | ||||||
| Plugins are loaded from `<root-dir>/ytdlp_plugins/<type>/__init__.py`; where `<root-dir>` is the directory of the binary (`<root-dir>/yt-dlp`), or the root directory of the module if you are running directly from source-code (`<root dir>/yt_dlp/__main__.py`). Plugins are currently not supported for the `pip` version | Note that **all** plugins are imported even if not invoked, and that **there are no checks** performed on plugin code. **Use plugins at your own risk and only if you trust the code!** | ||||||
| 
 | 
 | ||||||
| Plugins can be of `<type>`s `extractor` or `postprocessor`. Extractor plugins do not need to be enabled from the CLI and are automatically invoked when the input URL is suitable for it. Postprocessor plugins can be invoked using `--use-postprocessor NAME`. | Plugins can be of `<type>`s `extractor` or `postprocessor`.  | ||||||
|  | - Extractor plugins do not need to be enabled from the CLI and are automatically invoked when the input URL is suitable for it.  | ||||||
|  | - Extractor plugins take priority over builtin extractors. | ||||||
|  | - Postprocessor plugins can be invoked using `--use-postprocessor NAME`. | ||||||
| 
 | 
 | ||||||
| See [ytdlp_plugins](ytdlp_plugins) for example plugins. |  | ||||||
| 
 | 
 | ||||||
| Note that **all** plugins are imported even if not invoked, and that **there are no checks** performed on plugin code. Use plugins at your own risk and only if you trust the code | Plugins are loaded from the namespace packages `yt_dlp_plugins.extractor` and `yt_dlp_plugins.postprocessor`. | ||||||
| 
 | 
 | ||||||
| If you are a plugin author, add [ytdlp-plugins](https://github.com/topics/ytdlp-plugins) as a topic to your repository for discoverability | In other words, the file structure on the disk looks something like: | ||||||
|  |      | ||||||
|  |         yt_dlp_plugins/ | ||||||
|  |             extractor/ | ||||||
|  |                 myplugin.py | ||||||
|  |             postprocessor/ | ||||||
|  |                 myplugin.py | ||||||
|  | 
 | ||||||
|  | yt-dlp looks for these `yt_dlp_plugins` namespace folders in many locations (see below) and loads in plugins from **all** of them. | ||||||
| 
 | 
 | ||||||
| See the [wiki for some known plugins](https://github.com/yt-dlp/yt-dlp/wiki/Plugins) | See the [wiki for some known plugins](https://github.com/yt-dlp/yt-dlp/wiki/Plugins) | ||||||
| 
 | 
 | ||||||
|  | ## Installing Plugins | ||||||
| 
 | 
 | ||||||
|  | Plugins can be installed using various methods and locations. | ||||||
|  | 
 | ||||||
|  | 1. **Configuration directories**: | ||||||
|  |    Plugin packages (containing a `yt_dlp_plugins` namespace folder) can be dropped into the following standard [configuration locations](#configuration): | ||||||
|  |     * **User Plugins** | ||||||
|  |       * `${XDG_CONFIG_HOME}/yt-dlp/plugins/<package name>/yt_dlp_plugins/` (recommended on Linux/macOS) | ||||||
|  |       * `${XDG_CONFIG_HOME}/yt-dlp-plugins/<package name>/yt_dlp_plugins/` | ||||||
|  |       * `${APPDATA}/yt-dlp/plugins/<package name>/yt_dlp_plugins/` (recommended on Windows) | ||||||
|  |       * `~/.yt-dlp/plugins/<package name>/yt_dlp_plugins/` | ||||||
|  |       * `~/yt-dlp-plugins/<package name>/yt_dlp_plugins/` | ||||||
|  |     * **System Plugins** | ||||||
|  |       * `/etc/yt-dlp/plugins/<package name>/yt_dlp_plugins/` | ||||||
|  |       * `/etc/yt-dlp-plugins/<package name>/yt_dlp_plugins/` | ||||||
|  | 2. **Executable location**: Plugin packages can similarly be installed in a `yt-dlp-plugins` directory under the executable location: | ||||||
|  |     * Binary: where `<root-dir>/yt-dlp.exe`, `<root-dir>/yt-dlp-plugins/<package name>/yt_dlp_plugins/` | ||||||
|  |     * Source: where `<root-dir>/yt_dlp/__main__.py`, `<root-dir>/yt-dlp-plugins/<package name>/yt_dlp_plugins/` | ||||||
|  | 
 | ||||||
|  | 3. **pip and other locations in `PYTHONPATH`** | ||||||
|  |     * Plugin packages can be installed and managed using `pip`. See [ytdlp-sample-plugins](https://github.com/yt-dlp/yt-dlp-sample-plugins) for an example. | ||||||
|  |       * Note: plugin files between plugin packages installed with pip must have unique filenames | ||||||
|  |     * Any path in `PYTHONPATH` is searched in for the `yt_dlp_plugins` namespace folder. | ||||||
|  |       * Note: This does not apply for Pyinstaller/py2exe builds. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | .zip, .egg and .whl archives containing a `yt_dlp_plugins` namespace folder in their root are also supported. These can be placed in the same locations `yt_dlp_plugins` namespace folders can be found. | ||||||
|  | - e.g. `${XDG_CONFIG_HOME}/yt-dlp/plugins/mypluginpkg.zip` where `mypluginpkg.zip` contains `yt_dlp_plugins/<type>/myplugin.py` | ||||||
|  | 
 | ||||||
|  | Run yt-dlp with `--verbose`/`-v` to check if the plugin has been loaded. | ||||||
|  | 
 | ||||||
|  | ## Developing Plugins | ||||||
|  | 
 | ||||||
|  | See [ytdlp-sample-plugins](https://github.com/yt-dlp/yt-dlp-sample-plugins) for a sample plugin package with instructions on how to set up an environment for plugin development.  | ||||||
|  | 
 | ||||||
|  | All public classes with a name ending in `IE` are imported from each file. This respects underscore prefix (e.g. `_MyBasePluginIE` is private) and `__all__`. Modules can similarly be excluded by prefixing the module name with an underscore (e.g. `_myplugin.py`) | ||||||
|  | 
 | ||||||
|  | If you are a plugin author, add [yt-dlp-plugins](https://github.com/topics/yt-dlp-plugins) as a topic to your repository for discoverability | ||||||
|  | 
 | ||||||
|  | See the [Developer Instructions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#developer-instructions) on how to write and test an extractor. | ||||||
| 
 | 
 | ||||||
| # EMBEDDING YT-DLP | # EMBEDDING YT-DLP | ||||||
| 
 | 
 | ||||||
|   | |||||||
| @@ -40,8 +40,12 @@ def main(): | |||||||
| 
 | 
 | ||||||
|     _ALL_CLASSES = get_all_ies()  # Must be before import |     _ALL_CLASSES = get_all_ies()  # Must be before import | ||||||
| 
 | 
 | ||||||
|  |     import yt_dlp.plugins | ||||||
|     from yt_dlp.extractor.common import InfoExtractor, SearchInfoExtractor |     from yt_dlp.extractor.common import InfoExtractor, SearchInfoExtractor | ||||||
| 
 | 
 | ||||||
|  |     # Filter out plugins | ||||||
|  |     _ALL_CLASSES = [cls for cls in _ALL_CLASSES if not cls.__module__.startswith(f'{yt_dlp.plugins.PACKAGE_NAME}.')] | ||||||
|  | 
 | ||||||
|     DummyInfoExtractor = type('InfoExtractor', (InfoExtractor,), {'IE_NAME': NO_ATTR}) |     DummyInfoExtractor = type('InfoExtractor', (InfoExtractor,), {'IE_NAME': NO_ATTR}) | ||||||
|     module_src = '\n'.join(( |     module_src = '\n'.join(( | ||||||
|         MODULE_TEMPLATE, |         MODULE_TEMPLATE, | ||||||
|   | |||||||
							
								
								
									
										73
									
								
								test/test_plugins.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								test/test_plugins.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | |||||||
|  | import importlib | ||||||
|  | import os | ||||||
|  | import shutil | ||||||
|  | import sys | ||||||
|  | import unittest | ||||||
|  | from pathlib import Path | ||||||
|  | 
 | ||||||
|  | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||||||
|  | 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.plugins import PACKAGE_NAME, directories, load_plugins | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TestPlugins(unittest.TestCase): | ||||||
|  | 
 | ||||||
|  |     TEST_PLUGIN_DIR = TEST_DATA_DIR / PACKAGE_NAME | ||||||
|  | 
 | ||||||
|  |     def test_directories_containing_plugins(self): | ||||||
|  |         self.assertIn(self.TEST_PLUGIN_DIR, map(Path, directories())) | ||||||
|  | 
 | ||||||
|  |     def test_extractor_classes(self): | ||||||
|  |         for module_name in tuple(sys.modules): | ||||||
|  |             if module_name.startswith(f'{PACKAGE_NAME}.extractor'): | ||||||
|  |                 del sys.modules[module_name] | ||||||
|  |         plugins_ie = load_plugins('extractor', 'IE') | ||||||
|  | 
 | ||||||
|  |         self.assertIn(f'{PACKAGE_NAME}.extractor.normal', sys.modules.keys()) | ||||||
|  |         self.assertIn('NormalPluginIE', plugins_ie.keys()) | ||||||
|  | 
 | ||||||
|  |         # don't load modules with underscore prefix | ||||||
|  |         self.assertFalse( | ||||||
|  |             f'{PACKAGE_NAME}.extractor._ignore' in sys.modules.keys(), | ||||||
|  |             'loaded module beginning with underscore') | ||||||
|  |         self.assertNotIn('IgnorePluginIE', plugins_ie.keys()) | ||||||
|  | 
 | ||||||
|  |         # Don't load extractors with underscore prefix | ||||||
|  |         self.assertNotIn('_IgnoreUnderscorePluginIE', plugins_ie.keys()) | ||||||
|  | 
 | ||||||
|  |         # Don't load extractors not specified in __all__ (if supplied) | ||||||
|  |         self.assertNotIn('IgnoreNotInAllPluginIE', plugins_ie.keys()) | ||||||
|  |         self.assertIn('InAllPluginIE', plugins_ie.keys()) | ||||||
|  | 
 | ||||||
|  |     def test_postprocessor_classes(self): | ||||||
|  |         plugins_pp = load_plugins('postprocessor', 'PP') | ||||||
|  |         self.assertIn('NormalPluginPP', plugins_pp.keys()) | ||||||
|  | 
 | ||||||
|  |     def test_importing_zipped_module(self): | ||||||
|  |         zip_path = TEST_DATA_DIR / 'zipped_plugins.zip' | ||||||
|  |         shutil.make_archive(str(zip_path)[:-4], 'zip', str(zip_path)[:-4]) | ||||||
|  |         sys.path.append(str(zip_path))  # add zip to search paths | ||||||
|  |         importlib.invalidate_caches()  # reset the import caches | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             for plugin_type in ('extractor', 'postprocessor'): | ||||||
|  |                 package = importlib.import_module(f'{PACKAGE_NAME}.{plugin_type}') | ||||||
|  |                 self.assertIn(zip_path / PACKAGE_NAME / plugin_type, map(Path, package.__path__)) | ||||||
|  | 
 | ||||||
|  |             plugins_ie = load_plugins('extractor', 'IE') | ||||||
|  |             self.assertIn('ZippedPluginIE', plugins_ie.keys()) | ||||||
|  | 
 | ||||||
|  |             plugins_pp = load_plugins('postprocessor', 'PP') | ||||||
|  |             self.assertIn('ZippedPluginPP', plugins_pp.keys()) | ||||||
|  | 
 | ||||||
|  |         finally: | ||||||
|  |             sys.path.remove(str(zip_path)) | ||||||
|  |             os.remove(zip_path) | ||||||
|  |             importlib.invalidate_caches()  # reset the import caches | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     unittest.main() | ||||||
							
								
								
									
										5
									
								
								test/testdata/yt_dlp_plugins/extractor/_ignore.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								test/testdata/yt_dlp_plugins/extractor/_ignore.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | from yt_dlp.extractor.common import InfoExtractor | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class IgnorePluginIE(InfoExtractor): | ||||||
|  |     pass | ||||||
							
								
								
									
										12
									
								
								test/testdata/yt_dlp_plugins/extractor/ignore.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								test/testdata/yt_dlp_plugins/extractor/ignore.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | from yt_dlp.extractor.common import InfoExtractor | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class IgnoreNotInAllPluginIE(InfoExtractor): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class InAllPluginIE(InfoExtractor): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | __all__ = ['InAllPluginIE'] | ||||||
							
								
								
									
										9
									
								
								test/testdata/yt_dlp_plugins/extractor/normal.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								test/testdata/yt_dlp_plugins/extractor/normal.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | from yt_dlp.extractor.common import InfoExtractor | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class NormalPluginIE(InfoExtractor): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class _IgnoreUnderscorePluginIE(InfoExtractor): | ||||||
|  |     pass | ||||||
							
								
								
									
										5
									
								
								test/testdata/yt_dlp_plugins/postprocessor/normal.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								test/testdata/yt_dlp_plugins/postprocessor/normal.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | from yt_dlp.postprocessor.common import PostProcessor | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class NormalPluginPP(PostProcessor): | ||||||
|  |     pass | ||||||
							
								
								
									
										5
									
								
								test/testdata/zipped_plugins/yt_dlp_plugins/extractor/zipped.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								test/testdata/zipped_plugins/yt_dlp_plugins/extractor/zipped.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | from yt_dlp.extractor.common import InfoExtractor | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ZippedPluginIE(InfoExtractor): | ||||||
|  |     pass | ||||||
							
								
								
									
										5
									
								
								test/testdata/zipped_plugins/yt_dlp_plugins/postprocessor/zipped.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								test/testdata/zipped_plugins/yt_dlp_plugins/postprocessor/zipped.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | from yt_dlp.postprocessor.common import PostProcessor | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ZippedPluginPP(PostProcessor): | ||||||
|  |     pass | ||||||
| @@ -32,6 +32,7 @@ from .extractor import gen_extractor_classes, get_info_extractor | |||||||
| from .extractor.common import UnsupportedURLIE | from .extractor.common import UnsupportedURLIE | ||||||
| from .extractor.openload import PhantomJSwrapper | from .extractor.openload import PhantomJSwrapper | ||||||
| from .minicurses import format_text | from .minicurses import format_text | ||||||
|  | from .plugins import directories as plugin_directories | ||||||
| from .postprocessor import _PLUGIN_CLASSES as plugin_postprocessors | from .postprocessor import _PLUGIN_CLASSES as plugin_postprocessors | ||||||
| from .postprocessor import ( | from .postprocessor import ( | ||||||
|     EmbedThumbnailPP, |     EmbedThumbnailPP, | ||||||
| @@ -3773,10 +3774,6 @@ class YoutubeDL: | |||||||
|                 write_debug('Lazy loading extractors is forcibly disabled') |                 write_debug('Lazy loading extractors is forcibly disabled') | ||||||
|             else: |             else: | ||||||
|                 write_debug('Lazy loading extractors is disabled') |                 write_debug('Lazy loading extractors is disabled') | ||||||
|         if plugin_extractors or plugin_postprocessors: |  | ||||||
|             write_debug('Plugins: %s' % [ |  | ||||||
|                 '%s%s' % (klass.__name__, '' if klass.__name__ == name else f' as {name}') |  | ||||||
|                 for name, klass in itertools.chain(plugin_extractors.items(), plugin_postprocessors.items())]) |  | ||||||
|         if self.params['compat_opts']: |         if self.params['compat_opts']: | ||||||
|             write_debug('Compatibility options: %s' % ', '.join(self.params['compat_opts'])) |             write_debug('Compatibility options: %s' % ', '.join(self.params['compat_opts'])) | ||||||
| 
 | 
 | ||||||
| @@ -3810,6 +3807,16 @@ class YoutubeDL: | |||||||
|                 proxy_map.update(handler.proxies) |                 proxy_map.update(handler.proxies) | ||||||
|         write_debug(f'Proxy map: {proxy_map}') |         write_debug(f'Proxy map: {proxy_map}') | ||||||
| 
 | 
 | ||||||
|  |         for plugin_type, plugins in {'Extractor': plugin_extractors, 'Post-Processor': plugin_postprocessors}.items(): | ||||||
|  |             if not plugins: | ||||||
|  |                 continue | ||||||
|  |             write_debug(f'{plugin_type} Plugins: %s' % (', '.join(sorted(('%s%s' % ( | ||||||
|  |                 klass.__name__, '' if klass.__name__ == name else f' as {name}') | ||||||
|  |                 for name, klass in plugins.items()))))) | ||||||
|  |         plugin_dirs = plugin_directories() | ||||||
|  |         if plugin_dirs: | ||||||
|  |             write_debug(f'Plugin directories: {plugin_dirs}') | ||||||
|  | 
 | ||||||
|         # Not implemented |         # Not implemented | ||||||
|         if False and self.params.get('call_home'): |         if False and self.params.get('call_home'): | ||||||
|             ipaddr = self.urlopen('https://yt-dl.org/ip').read().decode() |             ipaddr = self.urlopen('https://yt-dl.org/ip').read().decode() | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| import contextlib | import contextlib | ||||||
| import os | import os | ||||||
| 
 | 
 | ||||||
| from ..utils import load_plugins | from ..plugins import load_plugins | ||||||
| 
 | 
 | ||||||
| # NB: Must be before other imports so that plugins can be correctly injected | # NB: Must be before other imports so that plugins can be correctly injected | ||||||
| _PLUGIN_CLASSES = load_plugins('extractor', 'IE', {}) | _PLUGIN_CLASSES = load_plugins('extractor', 'IE') | ||||||
| 
 | 
 | ||||||
| _LAZY_LOADER = False | _LAZY_LOADER = False | ||||||
| if not os.environ.get('YTDLP_NO_LAZY_EXTRACTORS'): | if not os.environ.get('YTDLP_NO_LAZY_EXTRACTORS'): | ||||||
|   | |||||||
| @@ -29,6 +29,8 @@ from .utils import ( | |||||||
|     expand_path, |     expand_path, | ||||||
|     format_field, |     format_field, | ||||||
|     get_executable_path, |     get_executable_path, | ||||||
|  |     get_system_config_dirs, | ||||||
|  |     get_user_config_dirs, | ||||||
|     join_nonempty, |     join_nonempty, | ||||||
|     orderedSet_from_options, |     orderedSet_from_options, | ||||||
|     remove_end, |     remove_end, | ||||||
| @@ -42,62 +44,67 @@ def parseOpts(overrideArguments=None, ignore_config_files='if_override'): | |||||||
|     if ignore_config_files == 'if_override': |     if ignore_config_files == 'if_override': | ||||||
|         ignore_config_files = overrideArguments is not None |         ignore_config_files = overrideArguments is not None | ||||||
| 
 | 
 | ||||||
|     def _readUserConf(package_name, default=[]): |     def _load_from_config_dirs(config_dirs): | ||||||
|         # .config |         for config_dir in config_dirs: | ||||||
|  |             conf_file_path = os.path.join(config_dir, 'config') | ||||||
|  |             conf = Config.read_file(conf_file_path, default=None) | ||||||
|  |             if conf is None: | ||||||
|  |                 conf_file_path += '.txt' | ||||||
|  |                 conf = Config.read_file(conf_file_path, default=None) | ||||||
|  |             if conf is not None: | ||||||
|  |                 return conf, conf_file_path | ||||||
|  |         return None, None | ||||||
|  | 
 | ||||||
|  |     def _read_user_conf(package_name, default=None): | ||||||
|  |         # .config/package_name.conf | ||||||
|         xdg_config_home = os.getenv('XDG_CONFIG_HOME') or compat_expanduser('~/.config') |         xdg_config_home = os.getenv('XDG_CONFIG_HOME') or compat_expanduser('~/.config') | ||||||
|         userConfFile = os.path.join(xdg_config_home, package_name, 'config') |         user_conf_file = os.path.join(xdg_config_home, '%s.conf' % package_name) | ||||||
|         if not os.path.isfile(userConfFile): |         user_conf = Config.read_file(user_conf_file, default=None) | ||||||
|             userConfFile = os.path.join(xdg_config_home, '%s.conf' % package_name) |         if user_conf is not None: | ||||||
|         userConf = Config.read_file(userConfFile, default=None) |             return user_conf, user_conf_file | ||||||
|         if userConf is not None: |  | ||||||
|             return userConf, userConfFile |  | ||||||
| 
 | 
 | ||||||
|         # appdata |         # home (~/package_name.conf or ~/package_name.conf.txt) | ||||||
|         appdata_dir = os.getenv('appdata') |         user_conf_file = os.path.join(compat_expanduser('~'), '%s.conf' % package_name) | ||||||
|         if appdata_dir: |         user_conf = Config.read_file(user_conf_file, default=None) | ||||||
|             userConfFile = os.path.join(appdata_dir, package_name, 'config') |         if user_conf is None: | ||||||
|             userConf = Config.read_file(userConfFile, default=None) |             user_conf_file += '.txt' | ||||||
|             if userConf is None: |             user_conf = Config.read_file(user_conf_file, default=None) | ||||||
|                 userConfFile += '.txt' |         if user_conf is not None: | ||||||
|                 userConf = Config.read_file(userConfFile, default=None) |             return user_conf, user_conf_file | ||||||
|         if userConf is not None: |  | ||||||
|             return userConf, userConfFile |  | ||||||
| 
 | 
 | ||||||
|         # home |         # Package config directories (e.g. ~/.config/package_name/package_name.txt) | ||||||
|         userConfFile = os.path.join(compat_expanduser('~'), '%s.conf' % package_name) |         user_conf, user_conf_file = _load_from_config_dirs(get_user_config_dirs(package_name)) | ||||||
|         userConf = Config.read_file(userConfFile, default=None) |         if user_conf is not None: | ||||||
|         if userConf is None: |             return user_conf, user_conf_file | ||||||
|             userConfFile += '.txt' |         return default if default is not None else [], None | ||||||
|             userConf = Config.read_file(userConfFile, default=None) |  | ||||||
|         if userConf is not None: |  | ||||||
|             return userConf, userConfFile |  | ||||||
| 
 | 
 | ||||||
|         return default, None |     def _read_system_conf(package_name, default=None): | ||||||
|  |         system_conf, system_conf_file = _load_from_config_dirs(get_system_config_dirs(package_name)) | ||||||
|  |         if system_conf is not None: | ||||||
|  |             return system_conf, system_conf_file | ||||||
|  |         return default if default is not None else [], None | ||||||
| 
 | 
 | ||||||
|     def add_config(label, path, user=False): |     def add_config(label, path=None, func=None): | ||||||
|         """ Adds config and returns whether to continue """ |         """ Adds config and returns whether to continue """ | ||||||
|         if root.parse_known_args()[0].ignoreconfig: |         if root.parse_known_args()[0].ignoreconfig: | ||||||
|             return False |             return False | ||||||
|         # Multiple package names can be given here |         elif func: | ||||||
|         # E.g. ('yt-dlp', 'youtube-dlc', 'youtube-dl') will look for |             assert path is None | ||||||
|         # the configuration file of any of these three packages |             args, current_path = func('yt-dlp') | ||||||
|         for package in ('yt-dlp',): |         else: | ||||||
|             if user: |             current_path = os.path.join(path, 'yt-dlp.conf') | ||||||
|                 args, current_path = _readUserConf(package, default=None) |             args = Config.read_file(current_path, default=None) | ||||||
|             else: |         if args is not None: | ||||||
|                 current_path = os.path.join(path, '%s.conf' % package) |             root.append_config(args, current_path, label=label) | ||||||
|                 args = Config.read_file(current_path, default=None) |             return True | ||||||
|             if args is not None: |  | ||||||
|                 root.append_config(args, current_path, label=label) |  | ||||||
|                 return True |  | ||||||
|         return True |         return True | ||||||
| 
 | 
 | ||||||
|     def load_configs(): |     def load_configs(): | ||||||
|         yield not ignore_config_files |         yield not ignore_config_files | ||||||
|         yield add_config('Portable', get_executable_path()) |         yield add_config('Portable', get_executable_path()) | ||||||
|         yield add_config('Home', expand_path(root.parse_known_args()[0].paths.get('home', '')).strip()) |         yield add_config('Home', expand_path(root.parse_known_args()[0].paths.get('home', '')).strip()) | ||||||
|         yield add_config('User', None, user=True) |         yield add_config('User', func=_read_user_conf) | ||||||
|         yield add_config('System', '/etc') |         yield add_config('System', func=_read_system_conf) | ||||||
| 
 | 
 | ||||||
|     opts = optparse.Values({'verbose': True, 'print_help': False}) |     opts = optparse.Values({'verbose': True, 'print_help': False}) | ||||||
|     try: |     try: | ||||||
|   | |||||||
							
								
								
									
										171
									
								
								yt_dlp/plugins.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								yt_dlp/plugins.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,171 @@ | |||||||
|  | import contextlib | ||||||
|  | import importlib | ||||||
|  | import importlib.abc | ||||||
|  | import importlib.machinery | ||||||
|  | import importlib.util | ||||||
|  | import inspect | ||||||
|  | import itertools | ||||||
|  | import os | ||||||
|  | import pkgutil | ||||||
|  | import sys | ||||||
|  | import traceback | ||||||
|  | import zipimport | ||||||
|  | from pathlib import Path | ||||||
|  | from zipfile import ZipFile | ||||||
|  | 
 | ||||||
|  | from .compat import functools  # isort: split | ||||||
|  | from .compat import compat_expanduser | ||||||
|  | from .utils import ( | ||||||
|  |     get_executable_path, | ||||||
|  |     get_system_config_dirs, | ||||||
|  |     get_user_config_dirs, | ||||||
|  |     write_string, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | PACKAGE_NAME = 'yt_dlp_plugins' | ||||||
|  | COMPAT_PACKAGE_NAME = 'ytdlp_plugins' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PluginLoader(importlib.abc.Loader): | ||||||
|  |     """Dummy loader for virtual namespace packages""" | ||||||
|  | 
 | ||||||
|  |     def exec_module(self, module): | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @functools.cache | ||||||
|  | def dirs_in_zip(archive): | ||||||
|  |     with ZipFile(archive) as zip: | ||||||
|  |         return set(itertools.chain.from_iterable( | ||||||
|  |             Path(file).parents for file in zip.namelist())) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PluginFinder(importlib.abc.MetaPathFinder): | ||||||
|  |     """ | ||||||
|  |     This class provides one or multiple namespace packages. | ||||||
|  |     It searches in sys.path and yt-dlp config folders for | ||||||
|  |     the existing subdirectories from which the modules can be imported | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def __init__(self, *packages): | ||||||
|  |         self._zip_content_cache = {} | ||||||
|  |         self.packages = set(itertools.chain.from_iterable( | ||||||
|  |             itertools.accumulate(name.split('.'), lambda a, b: '.'.join((a, b))) | ||||||
|  |             for name in packages)) | ||||||
|  | 
 | ||||||
|  |     def search_locations(self, fullname): | ||||||
|  |         candidate_locations = [] | ||||||
|  | 
 | ||||||
|  |         def _get_package_paths(*root_paths, containing_folder='plugins'): | ||||||
|  |             for config_dir in map(Path, root_paths): | ||||||
|  |                 plugin_dir = config_dir / containing_folder | ||||||
|  |                 if not plugin_dir.is_dir(): | ||||||
|  |                     continue | ||||||
|  |                 yield from plugin_dir.iterdir() | ||||||
|  | 
 | ||||||
|  |         # Load from yt-dlp config folders | ||||||
|  |         candidate_locations.extend(_get_package_paths( | ||||||
|  |             *get_user_config_dirs('yt-dlp'), *get_system_config_dirs('yt-dlp'), | ||||||
|  |             containing_folder='plugins')) | ||||||
|  | 
 | ||||||
|  |         # Load from yt-dlp-plugins folders | ||||||
|  |         candidate_locations.extend(_get_package_paths( | ||||||
|  |             get_executable_path(), | ||||||
|  |             compat_expanduser('~'), | ||||||
|  |             '/etc', | ||||||
|  |             os.getenv('XDG_CONFIG_HOME') or compat_expanduser('~/.config'), | ||||||
|  |             containing_folder='yt-dlp-plugins')) | ||||||
|  | 
 | ||||||
|  |         candidate_locations.extend(map(Path, sys.path))  # PYTHONPATH | ||||||
|  | 
 | ||||||
|  |         parts = Path(*fullname.split('.')) | ||||||
|  |         locations = set() | ||||||
|  |         for path in dict.fromkeys(candidate_locations): | ||||||
|  |             candidate = path / parts | ||||||
|  |             if candidate.is_dir(): | ||||||
|  |                 locations.add(str(candidate)) | ||||||
|  |             elif path.name and any(path.with_suffix(suffix).is_file() for suffix in {'.zip', '.egg', '.whl'}): | ||||||
|  |                 with contextlib.suppress(FileNotFoundError): | ||||||
|  |                     if parts in dirs_in_zip(path): | ||||||
|  |                         locations.add(str(candidate)) | ||||||
|  |         return locations | ||||||
|  | 
 | ||||||
|  |     def find_spec(self, fullname, path=None, target=None): | ||||||
|  |         if fullname not in self.packages: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  |         search_locations = self.search_locations(fullname) | ||||||
|  |         if not search_locations: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|  |         spec = importlib.machinery.ModuleSpec(fullname, PluginLoader(), is_package=True) | ||||||
|  |         spec.submodule_search_locations = search_locations | ||||||
|  |         return spec | ||||||
|  | 
 | ||||||
|  |     def invalidate_caches(self): | ||||||
|  |         dirs_in_zip.cache_clear() | ||||||
|  |         for package in self.packages: | ||||||
|  |             if package in sys.modules: | ||||||
|  |                 del sys.modules[package] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def directories(): | ||||||
|  |     spec = importlib.util.find_spec(PACKAGE_NAME) | ||||||
|  |     return spec.submodule_search_locations if spec else [] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def iter_modules(subpackage): | ||||||
|  |     fullname = f'{PACKAGE_NAME}.{subpackage}' | ||||||
|  |     with contextlib.suppress(ModuleNotFoundError): | ||||||
|  |         pkg = importlib.import_module(fullname) | ||||||
|  |         yield from pkgutil.iter_modules(path=pkg.__path__, prefix=f'{fullname}.') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def load_module(module, module_name, suffix): | ||||||
|  |     return inspect.getmembers(module, lambda obj: ( | ||||||
|  |         inspect.isclass(obj) | ||||||
|  |         and obj.__name__.endswith(suffix) | ||||||
|  |         and obj.__module__.startswith(module_name) | ||||||
|  |         and not obj.__name__.startswith('_') | ||||||
|  |         and obj.__name__ in getattr(module, '__all__', [obj.__name__]))) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def load_plugins(name, suffix): | ||||||
|  |     classes = {} | ||||||
|  | 
 | ||||||
|  |     for finder, module_name, _ in iter_modules(name): | ||||||
|  |         if any(x.startswith('_') for x in module_name.split('.')): | ||||||
|  |             continue | ||||||
|  |         try: | ||||||
|  |             if sys.version_info < (3, 10) and isinstance(finder, zipimport.zipimporter): | ||||||
|  |                 # zipimporter.load_module() is deprecated in 3.10 and removed in 3.12 | ||||||
|  |                 # The exec_module branch below is the replacement for >= 3.10 | ||||||
|  |                 # See: https://docs.python.org/3/library/zipimport.html#zipimport.zipimporter.exec_module | ||||||
|  |                 module = finder.load_module(module_name) | ||||||
|  |             else: | ||||||
|  |                 spec = finder.find_spec(module_name) | ||||||
|  |                 module = importlib.util.module_from_spec(spec) | ||||||
|  |                 sys.modules[module_name] = module | ||||||
|  |                 spec.loader.exec_module(module) | ||||||
|  |         except Exception: | ||||||
|  |             write_string(f'Error while importing module {module_name!r}\n{traceback.format_exc(limit=-1)}') | ||||||
|  |             continue | ||||||
|  |         classes.update(load_module(module, module_name, suffix)) | ||||||
|  | 
 | ||||||
|  |     # Compat: old plugin system using __init__.py | ||||||
|  |     # Note: plugins imported this way do not show up in directories() | ||||||
|  |     # nor are considered part of the yt_dlp_plugins namespace package | ||||||
|  |     with contextlib.suppress(FileNotFoundError): | ||||||
|  |         spec = importlib.util.spec_from_file_location( | ||||||
|  |             name, Path(get_executable_path(), COMPAT_PACKAGE_NAME, name, '__init__.py')) | ||||||
|  |         plugins = importlib.util.module_from_spec(spec) | ||||||
|  |         sys.modules[spec.name] = plugins | ||||||
|  |         spec.loader.exec_module(plugins) | ||||||
|  |         classes.update(load_module(plugins, spec.name, suffix)) | ||||||
|  | 
 | ||||||
|  |     return classes | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | sys.meta_path.insert(0, PluginFinder(f'{PACKAGE_NAME}.extractor', f'{PACKAGE_NAME}.postprocessor')) | ||||||
|  | 
 | ||||||
|  | __all__ = ['directories', 'load_plugins', 'PACKAGE_NAME', 'COMPAT_PACKAGE_NAME'] | ||||||
| @@ -33,14 +33,15 @@ from .movefilesafterdownload import MoveFilesAfterDownloadPP | |||||||
| from .sponskrub import SponSkrubPP | from .sponskrub import SponSkrubPP | ||||||
| from .sponsorblock import SponsorBlockPP | from .sponsorblock import SponsorBlockPP | ||||||
| from .xattrpp import XAttrMetadataPP | from .xattrpp import XAttrMetadataPP | ||||||
| from ..utils import load_plugins | from ..plugins import load_plugins | ||||||
| 
 | 
 | ||||||
| _PLUGIN_CLASSES = load_plugins('postprocessor', 'PP', globals()) | _PLUGIN_CLASSES = load_plugins('postprocessor', 'PP') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def get_postprocessor(key): | def get_postprocessor(key): | ||||||
|     return globals()[key + 'PP'] |     return globals()[key + 'PP'] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | globals().update(_PLUGIN_CLASSES) | ||||||
| __all__ = [name for name in globals().keys() if name.endswith('PP')] | __all__ = [name for name in globals().keys() if name.endswith('PP')] | ||||||
| __all__.extend(('PostProcessor', 'FFmpegPostProcessor')) | __all__.extend(('PostProcessor', 'FFmpegPostProcessor')) | ||||||
|   | |||||||
| @@ -18,7 +18,6 @@ import html.entities | |||||||
| import html.parser | import html.parser | ||||||
| import http.client | import http.client | ||||||
| import http.cookiejar | import http.cookiejar | ||||||
| import importlib.util |  | ||||||
| import inspect | import inspect | ||||||
| import io | import io | ||||||
| import itertools | import itertools | ||||||
| @@ -5372,22 +5371,37 @@ def get_executable_path(): | |||||||
|     return os.path.dirname(os.path.abspath(_get_variant_and_executable_path()[1])) |     return os.path.dirname(os.path.abspath(_get_variant_and_executable_path()[1])) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def load_plugins(name, suffix, namespace): | def get_user_config_dirs(package_name): | ||||||
|     classes = {} |     locations = set() | ||||||
|     with contextlib.suppress(FileNotFoundError): | 
 | ||||||
|         plugins_spec = importlib.util.spec_from_file_location( |     # .config (e.g. ~/.config/package_name) | ||||||
|             name, os.path.join(get_executable_path(), 'ytdlp_plugins', name, '__init__.py')) |     xdg_config_home = os.getenv('XDG_CONFIG_HOME') or compat_expanduser('~/.config') | ||||||
|         plugins = importlib.util.module_from_spec(plugins_spec) |     config_dir = os.path.join(xdg_config_home, package_name) | ||||||
|         sys.modules[plugins_spec.name] = plugins |     if os.path.isdir(config_dir): | ||||||
|         plugins_spec.loader.exec_module(plugins) |         locations.add(config_dir) | ||||||
|         for name in dir(plugins): | 
 | ||||||
|             if name in namespace: |     # appdata (%APPDATA%/package_name) | ||||||
|                 continue |     appdata_dir = os.getenv('appdata') | ||||||
|             if not name.endswith(suffix): |     if appdata_dir: | ||||||
|                 continue |         config_dir = os.path.join(appdata_dir, package_name) | ||||||
|             klass = getattr(plugins, name) |         if os.path.isdir(config_dir): | ||||||
|             classes[name] = namespace[name] = klass |             locations.add(config_dir) | ||||||
|     return classes | 
 | ||||||
|  |     # home (~/.package_name) | ||||||
|  |     user_config_directory = os.path.join(compat_expanduser('~'), '.%s' % package_name) | ||||||
|  |     if os.path.isdir(user_config_directory): | ||||||
|  |         locations.add(user_config_directory) | ||||||
|  | 
 | ||||||
|  |     return locations | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_system_config_dirs(package_name): | ||||||
|  |     locations = set() | ||||||
|  |     # /etc/package_name | ||||||
|  |     system_config_directory = os.path.join('/etc', package_name) | ||||||
|  |     if os.path.isdir(system_config_directory): | ||||||
|  |         locations.add(system_config_directory) | ||||||
|  |     return locations | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def traverse_obj( | def traverse_obj( | ||||||
| @@ -6367,3 +6381,10 @@ class FormatSorter: | |||||||
| # Deprecated | # Deprecated | ||||||
| has_certifi = bool(certifi) | has_certifi = bool(certifi) | ||||||
| has_websockets = bool(websockets) | has_websockets = bool(websockets) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def load_plugins(name, suffix, namespace): | ||||||
|  |     from .plugins import load_plugins | ||||||
|  |     ret = load_plugins(name, suffix) | ||||||
|  |     namespace.update(ret) | ||||||
|  |     return ret | ||||||
|   | |||||||
| @@ -1,4 +0,0 @@ | |||||||
| # flake8: noqa: F401 |  | ||||||
| 
 |  | ||||||
| # ℹ️ The imported name must end in "IE" |  | ||||||
| from .sample import SamplePluginIE |  | ||||||
| @@ -1,14 +0,0 @@ | |||||||
| # ⚠ Don't use relative imports |  | ||||||
| from yt_dlp.extractor.common import InfoExtractor |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| # ℹ️ Instructions on making extractors can be found at: |  | ||||||
| # 🔗 https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#adding-support-for-a-new-site |  | ||||||
| 
 |  | ||||||
| class SamplePluginIE(InfoExtractor): |  | ||||||
|     _WORKING = False |  | ||||||
|     IE_DESC = False |  | ||||||
|     _VALID_URL = r'^sampleplugin:' |  | ||||||
| 
 |  | ||||||
|     def _real_extract(self, url): |  | ||||||
|         self.to_screen('URL "%s" successfully captured' % url) |  | ||||||
| @@ -1,4 +0,0 @@ | |||||||
| # flake8: noqa: F401 |  | ||||||
| 
 |  | ||||||
| # ℹ️ The imported name must end in "PP" and is the name to be used in --use-postprocessor |  | ||||||
| from .sample import SamplePluginPP |  | ||||||
| @@ -1,26 +0,0 @@ | |||||||
| # ⚠ Don't use relative imports |  | ||||||
| from yt_dlp.postprocessor.common import PostProcessor |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| # ℹ️ See the docstring of yt_dlp.postprocessor.common.PostProcessor |  | ||||||
| class SamplePluginPP(PostProcessor): |  | ||||||
|     def __init__(self, downloader=None, **kwargs): |  | ||||||
|         # ⚠ Only kwargs can be passed from the CLI, and all argument values will be string |  | ||||||
|         # Also, "downloader", "when" and "key" are reserved names |  | ||||||
|         super().__init__(downloader) |  | ||||||
|         self._kwargs = kwargs |  | ||||||
| 
 |  | ||||||
|     # ℹ️ See docstring of yt_dlp.postprocessor.common.PostProcessor.run |  | ||||||
|     def run(self, info): |  | ||||||
|         if info.get('_type', 'video') != 'video':  # PP was called for playlist |  | ||||||
|             self.to_screen(f'Post-processing playlist {info.get("id")!r} with {self._kwargs}') |  | ||||||
|         elif info.get('filepath'):  # PP was called after download (default) |  | ||||||
|             filepath = info.get('filepath') |  | ||||||
|             self.to_screen(f'Post-processed {filepath!r} with {self._kwargs}') |  | ||||||
|         elif info.get('requested_downloads'):  # PP was called after_video |  | ||||||
|             filepaths = [f.get('filepath') for f in info.get('requested_downloads')] |  | ||||||
|             self.to_screen(f'Post-processed {filepaths!r} with {self._kwargs}') |  | ||||||
|         else:  # PP was called before actual download |  | ||||||
|             filepath = info.get('_filename') |  | ||||||
|             self.to_screen(f'Pre-processed {filepath!r} with {self._kwargs}') |  | ||||||
|         return [], info  # return list_of_files_to_delete, info_dict |  | ||||||
		Reference in New Issue
	
	Block a user
	 Matthew
					Matthew