Skip to content

Commit

Permalink
add table
Browse files Browse the repository at this point in the history
  • Loading branch information
coletdjnz committed Dec 1, 2023
1 parent ea3efb1 commit 3ab4524
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 75 deletions.
42 changes: 25 additions & 17 deletions yt_dlp/YoutubeDL.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@
clean_headers,
clean_proxies,
std_headers,
parse_impersonate_target,
compile_impersonate_target
)
from .version import CHANNEL, ORIGIN, RELEASE_GIT_HEAD, VARIANT, __version__

Expand Down Expand Up @@ -398,9 +400,8 @@ class YoutubeDL:
- "detect_or_warn": check whether we can do anything
about it, warn otherwise (default)
source_address: Client-side IP address to bind to.
impersonate: HTTP client to impersonate for requests.
A string in the format CLIENT[:[VERSION][:[OS][:OS_VERSION]]]
list_impersonate_targets: List available HTTP clients to impersonate
impersonate: Client to impersonate for requests.
A tuple in the form (client, version, os, os_version)
sleep_interval_requests: Number of seconds to sleep between requests
during extraction
sleep_interval: Number of seconds to sleep before each download when
Expand Down Expand Up @@ -684,20 +685,6 @@ def process_color_policy(stream):
self.params['http_headers'].pop('Cookie', None)
self._request_director = self.build_request_director(_REQUEST_HANDLERS.values(), _RH_PREFERENCES)

impersonate_target = self.params.get('impersonate')
if impersonate_target:
# This assumes that all handlers that support impersonation subclass ImpersonateRequestHandler
results = self._request_director.collect_from_handlers(
lambda x: [x.is_supported_target(impersonate_target)],
[lambda _, v: isinstance(v, ImpersonateRequestHandler)]
)
if not results:
self.report_warning('Ignoring --impersonate as required dependencies are not installed. ')

elif not any(results):
self.report_warning(f'Impersonate target "{self.params.get("impersonate")}" is not supported. '
f'Supported targets: {join_nonempty(*get_available_impersonate_targets(self._request_director), delim=", ")}')

if auto_init and auto_init != 'no_verbose_header':
self.print_debug_header()

Expand All @@ -720,6 +707,21 @@ def check_deprecated(param, option, suggestion):
for msg in self.params.get('_deprecation_warnings', []):
self.deprecated_feature(msg)

impersonate_target = self.params.get('impersonate')
if impersonate_target:
# This assumes that all handlers that support impersonation subclass ImpersonateRequestHandler
results = self._request_director.collect_from_handlers(
lambda x: [x.is_supported_target(impersonate_target)],
[lambda _, v: isinstance(v, ImpersonateRequestHandler)]
)
if not results:
self.report_warning('Ignoring --impersonate as required dependencies are not installed. ')

elif not any(results):
raise ValueError(
f'Impersonate target "{compile_impersonate_target(*self.params.get("impersonate"))}" is not available. '
f'Use --list-impersonate-targets to see available targets.')

if 'list-formats' in self.params['compat_opts']:
self.params['listformats_table'] = False

Expand Down Expand Up @@ -4049,6 +4051,12 @@ def _opener(self):
handler = self._request_director.handlers['Urllib']
return handler._get_instance(cookiejar=self.cookiejar, proxies=self.proxies)

def get_impersonate_targets(self):
return sorted(self._request_director.collect_from_handlers(
lambda x: x.get_supported_targets(),
[lambda _, v: isinstance(v, ImpersonateRequestHandler)]
), key=lambda x: x[0])

def urlopen(self, req):
""" Start an HTTP download """
if isinstance(req, str):
Expand Down
19 changes: 18 additions & 1 deletion yt_dlp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
variadic,
write_string,
)
from .utils.networking import std_headers
from .utils.networking import std_headers, parse_impersonate_target, compile_impersonate_target
from .YoutubeDL import YoutubeDL

_IN_CLI = False
Expand Down Expand Up @@ -386,6 +386,12 @@ def parse_chapters(name, value, advanced=False):
f'Supported keyrings are: {", ".join(sorted(SUPPORTED_KEYRINGS))}')
opts.cookiesfrombrowser = (browser_name, profile, keyring, container)

if opts.impersonate:
target = parse_impersonate_target(opts.impersonate)
if target is None:
raise ValueError(f'invalid impersonate target "{opts.impersonate}"')
opts.impersonate = target

# MetadataParser
def metadataparser_actions(f):
if isinstance(f, str):
Expand Down Expand Up @@ -979,6 +985,17 @@ def _real_main(argv=None):
traceback.print_exc()
ydl._download_retcode = 100

if opts.list_impersonate_targets:
available_targets = ydl.get_impersonate_targets()
rows = [[*[item or '' for item in target], compile_impersonate_target(*target)] for target in
available_targets]

ydl.to_screen(f'[info] Available impersonate targets')
ydl.to_stdout(
render_table(['Client', 'Version', 'OS', 'OS Version', 'Example'], rows)
)
return

if not actual_use:
if pre_process:
return ydl._download_retcode
Expand Down
78 changes: 23 additions & 55 deletions yt_dlp/networking/impersonate.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,6 @@
ImpersonateTarget = Tuple[str, Optional[str], Optional[str], Optional[str]]


def parse_impersonate_target(target: str) -> ImpersonateTarget | None:
"""
Parse an impersonate target string into a tuple of (client, version, os, os_vers)
If the target is invalid, return None
"""
client, version, os, os_vers = [None if (v or '').strip() == '' else v for v in (
target.split(':') + [None, None, None, None])][:4]

if client is not None:
return client, version, os, os_vers


def compile_impersonate_target(client, version, os, os_vers) -> str | None:
if not client:
return
filtered_parts = [str(part) if part is not None else '' for part in (client, version, os, os_vers)]
return ':'.join(filtered_parts).rstrip(':')


def _target_within(target1: ImpersonateTarget, target2: ImpersonateTarget):
if target1[0] != target2[0]:
return False
Expand All @@ -52,17 +33,14 @@ class ImpersonateRequestHandler(RequestHandler, ABC):
This provides a method for checking the validity of the impersonate extension,
which can be used in _check_extensions.
Impersonate target tuples are defined as a tuple of (client, version, os, os_vers) internally.
To simplify the interface, this is compiled into a string format of "client[:[version][:[os][:os_vers]]]" to be used externally.
- In this handler, "impersonate target tuple" refers to the tuple version,
and "impersonate target" refers to the string version.
- Impersonate target [tuples] are not required to define all fields (except browser).
Impersonate targets are defined as a tuple of (client, version, os, os_vers).
Note: Impersonate targets are not required to define all fields (except client).
The following may be defined:
- `_SUPPORTED_IMPERSONATE_TARGET_TUPLES`: a tuple of supported target tuples to impersonate.
- `_SUPPORTED_IMPERSONATE_TARGET_TUPLES`: a tuple of supported targets to impersonate.
Any Request with an impersonate target not in this list will raise an UnsupportedRequest.
Set to None to disable this check.
- `_SUPPORTED_IMPERSONATE_TARGET_TUPLE_MAP`: a dict mapping supported target tuples to custom targets.
- `_SUPPORTED_IMPERSONATE_TARGET_TUPLE_MAP`: a dict mapping supported targets to custom targets.
This works similar to `_SUPPORTED_IMPERSONATE_TARGET_TUPLES`.
Note: Only one of `_SUPPORTED_IMPERSONATE_TARGET_TUPLE_MAP` and `_SUPPORTED_IMPERSONATE_TARGET_TUPLES` can be defined.
Expand All @@ -75,12 +53,12 @@ class ImpersonateRequestHandler(RequestHandler, ABC):
_SUPPORTED_IMPERSONATE_TARGET_TUPLES: tuple[ImpersonateTarget] = ()
_SUPPORTED_IMPERSONATE_TARGET_TUPLE_MAP: dict[ImpersonateTarget, Any] = {}

def __init__(self, *, impersonate=None, **kwargs):
def __init__(self, *, impersonate: ImpersonateTarget = None, **kwargs):
super().__init__(**kwargs)
self.impersonate = impersonate

def _check_impersonate_target(self, target: str):
assert isinstance(target, (str, NoneType))
def _check_impersonate_target(self, target: ImpersonateTarget):
assert isinstance(target, (tuple, NoneType))
if target is None or not self.get_supported_targets():
return
if not self.is_supported_target(target):
Expand All @@ -95,49 +73,40 @@ def _validate(self, request):
super()._validate(request)
self._check_impersonate_target(self.impersonate)

def _get_supported_target_tuples(self):
return tuple(self._SUPPORTED_IMPERSONATE_TARGET_TUPLE_MAP.keys()) or tuple(self._SUPPORTED_IMPERSONATE_TARGET_TUPLES)

def _resolve_target_tuple(self, target: ImpersonateTarget | None):
def _resolve_target(self, target: ImpersonateTarget | None):
"""Resolve a target to a supported target."""
if not target:
return
for supported_target in self._get_supported_target_tuples():
for supported_target in self.get_supported_targets():
if _target_within(target, supported_target):
if self.verbose:
self._logger.stdout(
f'{self.RH_NAME}: resolved impersonate target "{compile_impersonate_target(*target)}" '
f'to "{compile_impersonate_target(*supported_target)}"')
f'{self.RH_NAME}: resolved impersonate target "{target}" to "{supported_target}"')
return supported_target

def get_supported_targets(self) -> tuple[str]:
return tuple(filter(compile_impersonate_target(*target) for target in self._get_supported_target_tuples()))

def is_supported_target(self, target: str):
return self._is_supported_target_tuple(parse_impersonate_target(target))
def get_supported_targets(self) -> tuple[ImpersonateTarget]:
return tuple(self._SUPPORTED_IMPERSONATE_TARGET_TUPLE_MAP.keys()) or tuple(self._SUPPORTED_IMPERSONATE_TARGET_TUPLES)

def _is_supported_target_tuple(self, target: ImpersonateTarget):
return self._resolve_target_tuple(target) is not None
def is_supported_target(self, target: ImpersonateTarget):
return self._resolve_target(target) is not None

def _get_target_tuple(self, request):
"""Get the requested target tuple for the request"""
target = request.extensions.get('impersonate') or self.impersonate
if target:
return parse_impersonate_target(target)
def _get_request_target(self, request):
"""Get the requested target for the request"""
return request.extensions.get('impersonate') or self.impersonate

def _get_resolved_target_tuple(self, request) -> ImpersonateTarget:
"""Get the resolved target tuple for this request. This gives the matching supported target"""
return self._resolve_target_tuple(self._get_target_tuple(request))
def _get_resolved_request_target(self, request) -> ImpersonateTarget:
"""Get the resolved target for this request. This gives the matching supported target"""
return self._resolve_target(self._get_request_target(request))

def _get_mapped_target(self, request):
def _get_mapped_request_target(self, request):
"""Get the resolved mapped target for the request target"""
resolved_target = self._resolve_target_tuple(self._get_target_tuple(request))
resolved_target = self._resolve_target(self._get_request_target(request))
return self._SUPPORTED_IMPERSONATE_TARGET_TUPLE_MAP.get(
resolved_target, None)

def _get_impersonate_headers(self, request):
headers = self._merge_headers(request.headers)
if self._get_target_tuple(request):
if self._get_request_target(request):
# remove all headers present in std_headers
headers.pop('User-Agent', None)
for header in std_headers:
Expand All @@ -152,7 +121,6 @@ def impersonate_preference(rh, request):
return 1000
return 0


def get_available_impersonate_targets(director):
return director.collect_from_handlers(
lambda x: x.get_supported_targets(),
Expand Down
4 changes: 2 additions & 2 deletions yt_dlp/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,11 +514,11 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
network.add_option(
'--impersonate',
metavar='CLIENT[:[VERSION][:[OS][:OS_VERSION]]]', dest='impersonate', default=None,
help='HTTP client to impersonate for requests',
help='Client to impersonate for requests',
)
network.add_option(
'--list-impersonate-targets',
dest='list_impersonate_targets', default=False,
dest='list_impersonate_targets', default=False, action='store_true',
help='List available HTTP clients to impersonate',
)
network.add_option(
Expand Down
22 changes: 22 additions & 0 deletions yt_dlp/utils/networking.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from __future__ import annotations

import collections
import random
import urllib.parse
import urllib.request
from typing import Optional, Tuple

from ._utils import remove_start

Expand Down Expand Up @@ -162,3 +165,22 @@ def normalize_url(url):
query=escape_rfc3986(url_parsed.query),
fragment=escape_rfc3986(url_parsed.fragment)
).geturl()


def parse_impersonate_target(target: str) -> Tuple[str, Optional[str], Optional[str], Optional[str]] | None:
"""
Parse an impersonate target string into a tuple of (client, version, os, os_vers)
If the target is invalid, return None
"""
client, version, os, os_vers = [None if (v or '').strip() == '' else v for v in (
target.split(':') + [None, None, None, None])][:4]

if client is not None:
return client, version, os, os_vers


def compile_impersonate_target(client, version, os, os_vers) -> str | None:
if not client:
return
filtered_parts = [str(part) if part is not None else '' for part in (client, version, os, os_vers)]
return ':'.join(filtered_parts).rstrip(':')

0 comments on commit 3ab4524

Please sign in to comment.