default impersonation on yt-dlp
This commit is contained in:
@@ -7,9 +7,30 @@ import urllib.parse
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util import Retry
|
||||
import yt_dlp
|
||||
from yt_dlp.networking.impersonate import ImpersonateTarget
|
||||
from curl_cffi import requests as impersonate_requests
|
||||
import threading
|
||||
import io
|
||||
from urllib.parse import urljoin
|
||||
|
||||
# Browser to impersonate at the TLS/HTTP layer. Some origins (e.g. the
|
||||
# "animeidhentai" hottub channel) fingerprint clients and reset/403 anything
|
||||
# that isn't a real browser, so impersonation must be on by default.
|
||||
IMPERSONATE_TARGET = os.getenv('STREAM_IMPERSONATE', 'chrome').strip() or 'chrome'
|
||||
|
||||
# curl_cffi sessions wrap a single libcurl handle and are not safe to share
|
||||
# across threads; keep one per worker thread so the Flask `threaded=True`
|
||||
# server can proxy concurrent segments without corrupting state.
|
||||
_thread_local = threading.local()
|
||||
|
||||
|
||||
def get_impersonate_session():
|
||||
sess = getattr(_thread_local, 'session', None)
|
||||
if sess is None:
|
||||
sess = impersonate_requests.Session(impersonate=IMPERSONATE_TARGET)
|
||||
_thread_local.session = sess
|
||||
return sess
|
||||
|
||||
# Stream params that have dedicated meaning and must never be treated as headers.
|
||||
STREAM_RESERVED_PARAMS = {'url'}
|
||||
# Headers that affect the transport layer rather than the resource itself; allowing
|
||||
@@ -264,23 +285,17 @@ def stream_video():
|
||||
return chunk[4:8] == b'ftyp'
|
||||
|
||||
def build_upstream_headers(referer):
|
||||
# We fetch upstream through curl_cffi with browser impersonation, which
|
||||
# supplies a coherent User-Agent / Accept / Sec-CH-UA / Accept-Encoding
|
||||
# set matching the impersonated browser. Forwarding the client's own
|
||||
# values (the player may be Firefox while we impersonate Chrome) would
|
||||
# contradict the TLS fingerprint and defeat impersonation, so we only
|
||||
# pass headers the origin genuinely needs for authorization.
|
||||
headers = {
|
||||
'User-Agent': request.headers.get('User-Agent'),
|
||||
'Accept': request.headers.get('Accept'),
|
||||
'Accept-Language': request.headers.get('Accept-Language'),
|
||||
'Accept-Encoding': request.headers.get('Accept-Encoding'),
|
||||
'Referer': referer,
|
||||
'Origin': referer,
|
||||
}
|
||||
|
||||
# Pass through fetch metadata and client hints when present
|
||||
for key in (
|
||||
'Sec-Fetch-Mode', 'Sec-Fetch-Site', 'Sec-Fetch-Dest', 'Sec-Fetch-User',
|
||||
'Sec-CH-UA', 'Sec-CH-UA-Mobile', 'Sec-CH-UA-Platform', 'DNT'
|
||||
):
|
||||
if key in request.headers:
|
||||
headers[key] = request.headers[key]
|
||||
|
||||
if forward_cookies and 'Cookie' in request.headers:
|
||||
headers['Cookie'] = request.headers['Cookie']
|
||||
dbg("forwarding cookies")
|
||||
@@ -340,7 +355,16 @@ def stream_video():
|
||||
if 'Range' in request.headers:
|
||||
safe_request_headers['Range'] = request.headers['Range']
|
||||
|
||||
resp = session.get(target_url, headers=safe_request_headers, stream=True, timeout=30, allow_redirects=True)
|
||||
resp = get_impersonate_session().get(target_url, headers=safe_request_headers, stream=True, timeout=30, allow_redirects=True)
|
||||
# Some channel proxies (e.g. the "animeidhentai" hottub proxy) use
|
||||
# inverted hotlink protection: they 403 any request that carries a
|
||||
# Referer/Origin and only serve referer-less ones. Other CDNs require
|
||||
# the spoofed referer. Satisfy both by retrying once without it.
|
||||
if resp.status_code == 403 and ('Referer' in safe_request_headers or 'Origin' in safe_request_headers):
|
||||
dbg("upstream 403 with referer; retrying without referer/origin")
|
||||
resp.close()
|
||||
referer_less = {k: v for k, v in safe_request_headers.items() if k not in ('Referer', 'Origin')}
|
||||
resp = get_impersonate_session().get(target_url, headers=referer_less, stream=True, timeout=30, allow_redirects=True)
|
||||
if debug_enabled:
|
||||
dbg(f"upstream status={resp.status_code} content_type={resp.headers.get('Content-Type')} content_length={resp.headers.get('Content-Length')}")
|
||||
|
||||
@@ -436,11 +460,14 @@ def stream_video():
|
||||
for key, value in upstream_headers.items():
|
||||
if value:
|
||||
headers[key] = value
|
||||
if 'User-Agent' not in headers:
|
||||
headers['User-Agent'] = 'Mozilla/5.0'
|
||||
if 'Accept' not in headers:
|
||||
headers['Accept'] = '*/*'
|
||||
resp = session.get(playlist_url, headers=headers, stream=True, timeout=30)
|
||||
resp = get_impersonate_session().get(playlist_url, headers=headers, stream=True, timeout=30)
|
||||
# See proxy_response: retry without referer for inverted hotlink
|
||||
# protection that 403s any refered request.
|
||||
if resp.status_code == 403 and ('Referer' in headers or 'Origin' in headers):
|
||||
dbg("playlist upstream 403 with referer; retrying without referer/origin")
|
||||
resp.close()
|
||||
referer_less = {k: v for k, v in headers.items() if k not in ('Referer', 'Origin')}
|
||||
resp = get_impersonate_session().get(playlist_url, headers=referer_less, stream=True, timeout=30)
|
||||
base_url = resp.url
|
||||
|
||||
if resp.status_code >= 400:
|
||||
@@ -624,15 +651,16 @@ def stream_video():
|
||||
'format_sort': ['res', 'fps', 'vcodec:avc1', 'acodec:aac'],
|
||||
'quiet': False,
|
||||
'no_warnings': False,
|
||||
'http_headers': {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
},
|
||||
# Impersonate a real browser by default so origins that fingerprint
|
||||
# clients (e.g. the "animeidhentai" hottub channel) don't 403.
|
||||
'impersonate': ImpersonateTarget.from_str(IMPERSONATE_TARGET),
|
||||
}
|
||||
|
||||
passthrough_source = request.json if request.method == 'POST' else request.args
|
||||
passthrough_headers = collect_passthrough_headers(passthrough_source)
|
||||
dbg(f"passthrough_headers={list(passthrough_headers.keys())}")
|
||||
ydl_opts['http_headers'].update(passthrough_headers)
|
||||
if passthrough_headers:
|
||||
ydl_opts.setdefault('http_headers', {}).update(passthrough_headers)
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
# Extract the info
|
||||
|
||||
Reference in New Issue
Block a user