mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2025-11-24 18:35:13 +00: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:
161
yt_dlp/extractor/youtube/jsc/provider.py
Normal file
161
yt_dlp/extractor/youtube/jsc/provider.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""PUBLIC API"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import dataclasses
|
||||
import enum
|
||||
import typing
|
||||
|
||||
from yt_dlp.extractor.youtube.jsc._registry import _jsc_preferences, _jsc_providers
|
||||
from yt_dlp.extractor.youtube.pot._provider import (
|
||||
IEContentProvider,
|
||||
IEContentProviderError,
|
||||
register_preference_generic,
|
||||
register_provider_generic,
|
||||
)
|
||||
from yt_dlp.utils import ExtractorError
|
||||
|
||||
__all__ = [
|
||||
'JsChallengeProvider',
|
||||
'JsChallengeProviderError',
|
||||
'JsChallengeProviderRejectedRequest',
|
||||
'JsChallengeProviderResponse',
|
||||
'JsChallengeRequest',
|
||||
'JsChallengeResponse',
|
||||
'JsChallengeType',
|
||||
'NChallengeInput',
|
||||
'NChallengeOutput',
|
||||
'SigChallengeInput',
|
||||
'SigChallengeOutput',
|
||||
'register_preference',
|
||||
'register_provider',
|
||||
]
|
||||
|
||||
|
||||
class JsChallengeType(enum.Enum):
|
||||
N = 'n'
|
||||
SIG = 'sig'
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class JsChallengeRequest:
|
||||
type: JsChallengeType
|
||||
input: NChallengeInput | SigChallengeInput
|
||||
video_id: str | None = None
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class NChallengeInput:
|
||||
player_url: str
|
||||
challenges: list[str] = dataclasses.field(default_factory=list)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class SigChallengeInput:
|
||||
player_url: str
|
||||
challenges: list[str] = dataclasses.field(default_factory=list)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class NChallengeOutput:
|
||||
results: dict[str, str] = dataclasses.field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class SigChallengeOutput:
|
||||
results: dict[str, str] = dataclasses.field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class JsChallengeProviderResponse:
|
||||
request: JsChallengeRequest
|
||||
response: JsChallengeResponse | None = None
|
||||
error: Exception | None = None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class JsChallengeResponse:
|
||||
type: JsChallengeType
|
||||
output: NChallengeOutput | SigChallengeOutput
|
||||
|
||||
|
||||
class JsChallengeProviderRejectedRequest(IEContentProviderError):
|
||||
"""Reject the JsChallengeRequest (cannot handle the request)"""
|
||||
|
||||
def __init__(self, msg=None, expected: bool = False, *, _skipped_components=None):
|
||||
super().__init__(msg, expected)
|
||||
self._skipped_components = _skipped_components
|
||||
|
||||
|
||||
class JsChallengeProviderError(IEContentProviderError):
|
||||
"""An error occurred while solving the challenge"""
|
||||
|
||||
|
||||
class JsChallengeProvider(IEContentProvider, abc.ABC, suffix='JCP'):
|
||||
|
||||
# Set to None to disable the check
|
||||
_SUPPORTED_TYPES: tuple[JsChallengeType] | None = ()
|
||||
|
||||
def __validate_request(self, request: JsChallengeRequest):
|
||||
if not self.is_available():
|
||||
raise JsChallengeProviderRejectedRequest(f'{self.PROVIDER_NAME} is not available')
|
||||
|
||||
# Validate request using built-in settings
|
||||
if (
|
||||
self._SUPPORTED_TYPES is not None
|
||||
and request.type not in self._SUPPORTED_TYPES
|
||||
):
|
||||
raise JsChallengeProviderRejectedRequest(
|
||||
f'JS Challenge type "{request.type}" is not supported by {self.PROVIDER_NAME}')
|
||||
|
||||
def bulk_solve(self, requests: list[JsChallengeRequest]) -> typing.Generator[JsChallengeProviderResponse, None, None]:
|
||||
"""Solve multiple JS challenges and return the results"""
|
||||
validated_requests = []
|
||||
for request in requests:
|
||||
try:
|
||||
self.__validate_request(request)
|
||||
validated_requests.append(request)
|
||||
except JsChallengeProviderRejectedRequest as e:
|
||||
yield JsChallengeProviderResponse(request=request, error=e)
|
||||
continue
|
||||
yield from self._real_bulk_solve(validated_requests)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _real_bulk_solve(self, requests: list[JsChallengeRequest]) -> typing.Generator[JsChallengeProviderResponse, None, None]:
|
||||
"""Subclasses can override this method to handle bulk solving"""
|
||||
raise NotImplementedError(f'{self.PROVIDER_NAME} does not implement bulk solving')
|
||||
|
||||
def _get_player(self, video_id, player_url):
|
||||
try:
|
||||
return self.ie._load_player(
|
||||
video_id=video_id,
|
||||
player_url=player_url,
|
||||
fatal=True,
|
||||
)
|
||||
except ExtractorError as e:
|
||||
raise JsChallengeProviderError(
|
||||
f'Failed to load player for JS challenge: {e}') from e
|
||||
|
||||
|
||||
def register_provider(provider: type[JsChallengeProvider]):
|
||||
"""Register a JsChallengeProvider class"""
|
||||
return register_provider_generic(
|
||||
provider=provider,
|
||||
base_class=JsChallengeProvider,
|
||||
registry=_jsc_providers.value,
|
||||
)
|
||||
|
||||
|
||||
def register_preference(*providers: type[JsChallengeProvider]) -> typing.Callable[[Preference], Preference]:
|
||||
"""Register a preference for a JsChallengeProvider class."""
|
||||
return register_preference_generic(
|
||||
JsChallengeProvider,
|
||||
_jsc_preferences.value,
|
||||
*providers,
|
||||
)
|
||||
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
Preference = typing.Callable[[JsChallengeProvider, list[JsChallengeRequest]], int]
|
||||
__all__.append('Preference')
|
||||
Reference in New Issue
Block a user