# YoutubeIE JS Challenge Provider Framework As part of the YouTube extractor, we have a framework for solving n/sig JS Challenges programmatically. This can be used by plugins. > [!TIP] > If publishing a JS Challenge Provider plugin to GitHub, add the [yt-dlp-jsc-provider](https://github.com/topics/yt-dlp-jsc-provider) topic to your repository to help users find it. ## Public APIs - `yt_dlp.extractor.youtube.jsc.provider` Everything else is **internal-only** and no guarantees are made about the API stability. > [!WARNING] > We will try our best to maintain stability with the public APIs. > However, due to the nature of extractors and YouTube, we may need to remove or change APIs in the future. > If you are using these APIs outside yt-dlp plugins, please account for this by importing them safely. ## JS Challenge Provider `yt_dlp.extractor.youtube.jsc.provider` ```python from yt_dlp.extractor.youtube.jsc.provider import ( register_provider, register_preference, JsChallengeProvider, JsChallengeRequest, JsChallengeResponse, JsChallengeProviderError, JsChallengeProviderRejectedRequest, JsChallengeType, JsChallengeProviderResponse, NChallengeOutput, ) from yt_dlp.utils import traverse_obj, Popen import json import subprocess import typing @register_provider class MyJsChallengeProviderJCP(JsChallengeProvider): # Provider class name must end with "JCP" PROVIDER_VERSION = '0.2.1' # Define a unique display name for the provider PROVIDER_NAME = 'my-provider' BUG_REPORT_LOCATION = 'https://issues.example.com/report' # Set supported challenge types. # If None, the provider will handle all types. _SUPPORTED_TYPES = [JsChallengeType.N] def is_available(self) -> bool: """ Check if the provider is available (e.g. all required dependencies are available) This is used to determine if the provider should be used and to provide debug information. IMPORTANT: This method SHOULD NOT make any network requests or perform any expensive operations. Since this is called multiple times, we recommend caching the result. """ return True def close(self): # Optional close hook, called when YoutubeDL is closed. pass def _real_bulk_solve(self, requests: list[JsChallengeRequest]) -> typing.Generator[JsChallengeProviderResponse, None, None]: # ℹ️ If you need to do additional validation on the requests. # Raise yt_dlp.extractor.youtube.jsc.provider.JsChallengeProviderRejectedRequest if the request is not supported. if len("something") > 255: raise JsChallengeProviderRejectedRequest('Challenges longer than 255 are not supported', expected=True) # ℹ️ Settings are pulled from extractor args passed to yt-dlp with the key `youtubejsc-`. # For this example, the extractor arg would be: # `--extractor-args "youtubejsc-myjschallengeprovider:bin_path=/path/to/bin"` bin_path = self._configuration_arg( 'bin_path', default=['/path/to/bin'])[0] # See below for logging guidelines self.logger.trace(f'Using bin path: {bin_path}') for request in requests: # You can use the _get_player method to get the player JS code if needed. # This shares the same caching as the YouTube extractor, so it will not make unnecessary requests. player_js = self._get_player(request.video_id, request.input.player_url) cmd = f'{bin_path} {request.input.challenges} {player_js}' self.logger.info(f'Executing command: {cmd}') stdout, _, ret = Popen.run(cmd, text=True, shell=True, stdout=subprocess.PIPE) if ret != 0: # ℹ️ If there is an error, raise JsChallengeProviderError. # The request will be sent to the next provider if there is one. # You can specify whether it is expected or not. If it is unexpected, # the log will include a link to the bug report location (BUG_REPORT_LOCATION). # raise JsChallengeProviderError(f'Command returned error code {ret}', expected=False) # You can also only fail this specific request by returning a JsChallengeProviderResponse with the error. # This will allow other requests to be processed by this provider. yield JsChallengeProviderResponse( request=request, error=JsChallengeProviderError(f'Command returned error code {ret}', expected=False) ) yield JsChallengeProviderResponse( request=request, response=JsChallengeResponse( type=JsChallengeType.N, output=NChallengeOutput(results=traverse_obj(json.loads(stdout))), )) # If there are multiple JS Challenge Providers that can handle the same JsChallengeRequest(s), # you can define a preference function to increase/decrease the priority of providers. @register_preference(MyJsChallengeProviderJCP) def my_provider_preference(provider: JsChallengeProvider, requests: list[JsChallengeRequest]) -> int: return 50 ``` ## Logging Guidelines - Use the `self.logger` object to log messages. - When making HTTP requests or any other time-expensive operation, use `self.logger.info` to log a message to standard non-verbose output. - This lets users know what is happening when a time-expensive operation is taking place. - Technical information such as a command being executed should be logged to `self.logger.debug` - Use `self.logger.trace` for very detailed information that is only useful for debugging to avoid cluttering the debug log. ## Debugging - Use `-v --extractor-args "youtube:jsc_trace=true"` to enable JS Challenge debug output.