mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-03-14 06:02:47 +03:00
[ie/youtube] Implement external n/sig solver (#14157)
Closes #14404, Closes #14431, Closes #14680, Closes #14707 Authored by: bashonly, coletdjnz, seproDev, Grub4K Co-authored-by: coletdjnz <coletdjnz@protonmail.com> Co-authored-by: bashonly <bashonly@protonmail.com> Co-authored-by: sepro <sepro@sepr0.com>
This commit is contained in:
132
yt_dlp/extractor/youtube/jsc/README.md
Normal file
132
yt_dlp/extractor/youtube/jsc/README.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user