diff --git a/test/test_plugins.py b/test/test_plugins.py index e35adc8b0..8299bfaff 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -124,6 +124,11 @@ def test_jsi_runtime_classes(self): self.assertIn(f'{PACKAGE_NAME}.jsinterp.normal', sys.modules.keys()) self.assertIn('NormalPluginJSI', plugin_jsis.value) + self.assertNotIn('OverrideDenoJSI', plugins_jsi.keys()) + self.assertNotIn('OverrideDenoJSI', plugin_jsis.value) + self.assertNotIn('_UnderscoreOverrideDenoJSI', plugins_jsi.keys()) + self.assertNotIn('_UnderscoreOverrideDenoJSI', plugin_jsis.value) + 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]) @@ -209,6 +214,24 @@ def test_extractor_override_plugin(self): from yt_dlp.extractor.generic import GenericIE self.assertEqual(GenericIE.IE_NAME, 'generic+override+underscore-override') + def test_jsi_override_plugin(self): + load_plugins(JSI_PLUGIN_SPEC) + + from yt_dlp.jsinterp._deno import DenoJSI + + # test that jsi_runtimes is updated with override jsi + self.assertTrue(DenoJSI is jsi_runtimes.value['Deno']) + self.assertEqual(jsi_runtimes.value['Deno'].TEST_FIELD, 'override') + self.assertEqual(jsi_runtimes.value['Deno'].SECONDARY_TEST_FIELD, 'underscore-override') + + self.assertEqual(jsi_runtimes.value['Deno'].JSI_NAME, 'Deno+override+underscore-override') + importlib.invalidate_caches() + # test that loading a second time doesn't wrap a second time + load_plugins(EXTRACTOR_PLUGIN_SPEC) + from yt_dlp.jsinterp._deno import DenoJSI + self.assertTrue(DenoJSI is jsi_runtimes.value['Deno']) + self.assertEqual(jsi_runtimes.value['Deno'].JSI_NAME, 'Deno+override+underscore-override') + def test_load_all_plugin_types(self): # no plugin specs registered diff --git a/test/testdata/yt_dlp_plugins/jsinterp/override.py b/test/testdata/yt_dlp_plugins/jsinterp/override.py new file mode 100644 index 000000000..a55836427 --- /dev/null +++ b/test/testdata/yt_dlp_plugins/jsinterp/override.py @@ -0,0 +1,5 @@ +from yt_dlp.jsinterp._deno import DenoJSI + + +class OverrideDenoJSI(DenoJSI, plugin_name='override'): + TEST_FIELD = 'override' diff --git a/test/testdata/yt_dlp_plugins/jsinterp/overridetwo.py b/test/testdata/yt_dlp_plugins/jsinterp/overridetwo.py new file mode 100644 index 000000000..63e6a721d --- /dev/null +++ b/test/testdata/yt_dlp_plugins/jsinterp/overridetwo.py @@ -0,0 +1,5 @@ +from yt_dlp.jsinterp._deno import DenoJSI + + +class _UnderscoreOverrideDenoJSI(DenoJSI, plugin_name='underscore-override'): + SECONDARY_TEST_FIELD = 'underscore-override' diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index 6d943ca0f..0ace126cf 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -39,6 +39,8 @@ plugin_ies, plugin_ies_overrides, plugin_pps, + plugin_jsis, + plugin_jsis_overrides, all_plugins_loaded, plugin_dirs, ) @@ -4090,13 +4092,17 @@ def get_encoding(stream): write_debug(f'Proxy map: {self.proxies}') write_debug(f'Request Handlers: {", ".join(rh.RH_NAME for rh in self._request_director.handlers.values())}') - for plugin_type, plugins in (('Extractor', plugin_ies), ('Post-Processor', plugin_pps)): + for plugin_type, plugins in (('Extractor', plugin_ies), ('Post-Processor', plugin_pps), + ('JSI-Runtime', plugin_jsis)): display_list = [ klass.__name__ if klass.__name__ == name else f'{klass.__name__} as {name}' for name, klass in plugins.value.items()] if plugin_type == 'Extractor': display_list.extend(f'{plugins[-1].IE_NAME.partition("+")[2]} ({parent.__name__})' for parent, plugins in plugin_ies_overrides.value.items()) + elif plugin_type == 'JSI-Runtime': + display_list.extend(f'{plugins[-1].JSI_NAME.partition("+")[2]} ({parent.__name__})' + for parent, plugins in plugin_jsis_overrides.value.items()) if not display_list: continue write_debug(f'{plugin_type} Plugins: {", ".join(sorted(display_list))}') diff --git a/yt_dlp/globals.py b/yt_dlp/globals.py index a5a3b228d..917b1fa44 100644 --- a/yt_dlp/globals.py +++ b/yt_dlp/globals.py @@ -26,6 +26,7 @@ def __repr__(self, /): plugin_pps = Indirect({}) plugin_jsis = Indirect({}) plugin_ies_overrides = Indirect(defaultdict(list)) +plugin_jsis_overrides = Indirect(defaultdict(list)) # Misc IN_CLI = Indirect(False) diff --git a/yt_dlp/jsinterp/common.py b/yt_dlp/jsinterp/common.py index 2fbd6d119..6cea09618 100644 --- a/yt_dlp/jsinterp/common.py +++ b/yt_dlp/jsinterp/common.py @@ -2,9 +2,10 @@ import abc import inspect +import sys import typing -from ..globals import jsi_runtimes +from ..globals import jsi_runtimes, plugin_jsis_overrides from ..extractor.common import InfoExtractor from ..utils import ( classproperty, @@ -214,24 +215,44 @@ def __init__(self, downloader: YoutubeDL, url: str, timeout: float | int, user_a self.timeout = timeout self.user_agent: str = user_agent or self._downloader.params['http_headers']['User-Agent'] + @classmethod + def __init_subclass__(cls, *, plugin_name=None, **kwargs): + if plugin_name: + mro = inspect.getmro(cls) + next_mro_class = super_class = mro[mro.index(cls) + 1] + + while getattr(super_class, '__wrapped__', None): + super_class = super_class.__wrapped__ + + if not any(override.PLUGIN_NAME == plugin_name for override in plugin_jsis_overrides.value[super_class]): + cls.__wrapped__ = next_mro_class + cls.PLUGIN_NAME, cls.JSI_KEY = plugin_name, next_mro_class.JSI_KEY + cls.JSI_NAME = f'{next_mro_class.JSI_NAME}+{plugin_name}' + + setattr(sys.modules[super_class.__module__], super_class.__name__, cls) + # additional update jsi_runtime because jsis are not further loaded like extractors + jsi_runtimes.value[super_class.JSI_KEY] = cls + plugin_jsis_overrides.value[super_class].append(cls) + return super().__init_subclass__(**kwargs) + @abc.abstractmethod def is_available(self) -> bool: raise NotImplementedError - def write_debug(self, message, *args, **kwargs): - self._downloader.write_debug(f'[{self.JSI_KEY}] {message}', *args, **kwargs) + def write_debug(self, msg, *args, **kwargs): + self._downloader.write_debug(f'[{self.JSI_NAME}] {msg}', *args, **kwargs) - def report_warning(self, message, *args, **kwargs): - self._downloader.report_warning(f'[{self.JSI_KEY}] {message}', *args, **kwargs) + def report_warning(self, msg, *args, **kwargs): + self._downloader.report_warning(f'[{self.JSI_NAME}] {msg}', *args, **kwargs) def to_screen(self, msg, *args, **kwargs): - self._downloader.to_screen(f'[{self.JSI_KEY}] {msg}', *args, **kwargs) + self._downloader.to_screen(f'[{self.JSI_NAME}] {msg}', *args, **kwargs) def report_note(self, video_id, note): self.to_screen(f'{format_field(video_id, None, "%s: ")}{note}') def report_version(self): - raise NotImplementedError + pass @classmethod def supports_extractor(cls, ie_key: str):