# 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-<PROVIDER_KEY>`.
        # 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.
