some more features and fixes (title and show info)
This commit is contained in:
@@ -134,10 +134,12 @@ def impersonate_get(url, **kwargs):
|
||||
_discard_session(sess)
|
||||
raise
|
||||
|
||||
# Stream params that have dedicated meaning and must never be treated as headers.
|
||||
# Request params that have dedicated meaning and must never be treated as headers.
|
||||
# `referer` is mapped to a real Referer header by collect_passthrough_headers, but
|
||||
# `live` is purely a playback hint and must not leak upstream as a header.
|
||||
STREAM_RESERVED_PARAMS = {'url', 'live'}
|
||||
# `live` is purely a playback hint and must not leak upstream as a header. `full`
|
||||
# is /api/resolve's "give me everything" switch and is likewise ours, not the
|
||||
# origin's.
|
||||
STREAM_RESERVED_PARAMS = {'url', 'live', 'full'}
|
||||
# Headers that affect the transport layer rather than the resource itself; allowing
|
||||
# these to be forwarded could enable request smuggling or vhost-routing abuse.
|
||||
STREAM_DISALLOWED_HEADER_NAMES = {'host', 'content-length', 'transfer-encoding', 'connection', 'expect'}
|
||||
@@ -297,6 +299,24 @@ _RESOLVE_FORMAT_FIELDS = ('url', 'http_headers', 'height', 'width', 'tbr',
|
||||
'fps', 'vcodec', 'acodec', 'ext', 'video_ext', 'quality',
|
||||
'protocol', 'format_note')
|
||||
|
||||
|
||||
def _trim_resolve_info(info):
|
||||
"""The lean payload playback needs: the media URLs, the headers that make
|
||||
them work, and just enough per-format detail to rank them. This is what
|
||||
every hovered card asks for, so it stays small."""
|
||||
formats = []
|
||||
for fmt in ((info.get('formats') if info else None) or []):
|
||||
if not fmt.get('url'):
|
||||
continue
|
||||
formats.append({k: fmt.get(k) for k in _RESOLVE_FORMAT_FIELDS if fmt.get(k) is not None})
|
||||
|
||||
return {
|
||||
'url': info.get('url') if info else None,
|
||||
'http_headers': (info.get('http_headers') if info else None) or {},
|
||||
'isLive': bool(info.get('is_live')) if info else False,
|
||||
'formats': formats,
|
||||
}
|
||||
|
||||
# Some channels surface pages that yt-dlp can't extract because the video is
|
||||
# embedded in a third-party JS player iframe (e.g. the xtremestream family used
|
||||
# by tube.perverzija.com). The player page declares its HLS playlist URL as
|
||||
@@ -354,7 +374,13 @@ def resolve_video():
|
||||
"""Resolve a page URL to its playable formats via yt-dlp and return them as
|
||||
JSON. The frontend calls this on demand (when a card is hovered or scrolled
|
||||
into view) to learn the real media URLs so it can background-probe them for
|
||||
direct, proxy-free playability."""
|
||||
direct, proxy-free playability.
|
||||
|
||||
`full=1` returns the extractor's whole info dict instead of the trimmed
|
||||
playback payload -- everything it knows about the video (description, dates,
|
||||
counts, tags, thumbnails, every format field), which is what the Show info
|
||||
panel exists to display. Both views come from one extraction and one cache
|
||||
entry, so asking for the full one costs no extra work upstream."""
|
||||
if request.method == 'POST':
|
||||
source = request.json or {}
|
||||
video_url = source.get('url')
|
||||
@@ -365,11 +391,19 @@ def resolve_video():
|
||||
if not video_url:
|
||||
return jsonify({"error": "No URL provided"}), 400
|
||||
|
||||
want_full = str(source.get('full', '')).strip().lower() in ('1', 'true', 'yes', 'on')
|
||||
|
||||
def view_of(info):
|
||||
if not want_full:
|
||||
return _trim_resolve_info(info)
|
||||
# Nothing to show, but answer in the same shape rather than `null`.
|
||||
return info if info else {}
|
||||
|
||||
now = time.time()
|
||||
with _resolve_cache_lock:
|
||||
cached = _resolve_cache.get(video_url)
|
||||
if cached and cached[0] > now:
|
||||
return jsonify(cached[1])
|
||||
return jsonify(view_of(cached[1]))
|
||||
|
||||
ydl_opts = {
|
||||
'quiet': True,
|
||||
@@ -386,6 +420,10 @@ def resolve_video():
|
||||
try:
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
info = ydl.extract_info(video_url, download=False)
|
||||
# The raw info dict holds objects that don't survive JSON (and
|
||||
# internal `__`-prefixed bookkeeping). This is the same pass yt-dlp
|
||||
# itself runs behind --dump-json.
|
||||
info = ydl.sanitize_info(info, remove_private_keys=True)
|
||||
except Exception as e:
|
||||
# Many channels point at sites yt-dlp can't extract ("Unsupported URL").
|
||||
# That's not fatal here -- the embed fallback below may still find a
|
||||
@@ -400,26 +438,17 @@ def resolve_video():
|
||||
if embed:
|
||||
info = embed
|
||||
|
||||
formats = []
|
||||
for fmt in ((info.get('formats') if info else None) or []):
|
||||
if not fmt.get('url'):
|
||||
continue
|
||||
formats.append({k: fmt.get(k) for k in _RESOLVE_FORMAT_FIELDS if fmt.get(k) is not None})
|
||||
|
||||
result = {
|
||||
'url': info.get('url') if info else None,
|
||||
'http_headers': (info.get('http_headers') if info else None) or {},
|
||||
'isLive': bool(info.get('is_live')) if info else False,
|
||||
'formats': formats,
|
||||
}
|
||||
|
||||
# The extraction is cached whole, and each caller is served the view it
|
||||
# asked for. A failed extraction (info is None) is cached the same way, so a
|
||||
# video that can't be resolved is attempted once per TTL rather than on
|
||||
# every hover.
|
||||
with _resolve_cache_lock:
|
||||
# Drop expired entries so the cache doesn't grow without bound.
|
||||
for key in [k for k, v in _resolve_cache.items() if v[0] <= now]:
|
||||
_resolve_cache.pop(key, None)
|
||||
_resolve_cache[video_url] = (now + RESOLVE_CACHE_TTL, result)
|
||||
_resolve_cache[video_url] = (now + RESOLVE_CACHE_TTL, info)
|
||||
|
||||
return jsonify(result)
|
||||
return jsonify(view_of(info))
|
||||
|
||||
@app.route('/api/image', methods=['GET', 'HEAD'])
|
||||
def image_proxy():
|
||||
|
||||
Reference in New Issue
Block a user