Spaces:
Sleeping
Sleeping
Download video via RapidAPI (own-CDN) -> faster-whisper transcript + local screenshots -> delete; retire proxies
Browse files- pipeline/downloader.py +83 -0
pipeline/downloader.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Download a YouTube video via the ``youtube-video-fast-downloader-24-7`` RapidAPI.
|
| 2 |
+
|
| 3 |
+
The API returns a link on the provider's OWN server (e.g. ``s5-audio.12388101.xyz``), not
|
| 4 |
+
a ``googlevideo.com`` URL — so the Space downloads the file directly, bypassing both the
|
| 5 |
+
egress DPI and the datacenter-IP block, with no IP-lock. The link is prepared
|
| 6 |
+
asynchronously: it 404s for ~20-300s while the provider fetches it, then is live for about
|
| 7 |
+
10 minutes. We poll until it's ready, then stream it to disk.
|
| 8 |
+
|
| 9 |
+
Requires the ``RAPIDAPI_KEY`` Space secret. Quality ``18`` is 360p muxed (video+audio) —
|
| 10 |
+
small, and its audio track is enough for faster-whisper.
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
import time
|
| 16 |
+
|
| 17 |
+
import requests
|
| 18 |
+
|
| 19 |
+
RAPIDAPI_HOST = "youtube-video-fast-downloader-24-7.p.rapidapi.com"
|
| 20 |
+
DEFAULT_QUALITY = "18" # itag 18 = 360p muxed (has audio, ~10-30 MB for typical videos)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class DownloadError(RuntimeError):
|
| 24 |
+
"""Raised when the video can't be obtained from the download API."""
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _headers() -> dict:
|
| 28 |
+
key = os.environ.get("RAPIDAPI_KEY", "").strip()
|
| 29 |
+
if not key:
|
| 30 |
+
raise DownloadError("RAPIDAPI_KEY is not set (required for the video download API).")
|
| 31 |
+
return {"X-RapidAPI-Key": key, "X-RapidAPI-Host": RAPIDAPI_HOST}
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _request_urls(video_id: str, quality: str, timeout: int = 60) -> tuple[list[str], dict]:
|
| 35 |
+
"""Ask the API for a download link; return candidate URLs (primary + reserved)."""
|
| 36 |
+
url = f"https://{RAPIDAPI_HOST}/download_video/{video_id}"
|
| 37 |
+
try:
|
| 38 |
+
r = requests.get(url, params={"quality": quality}, headers=_headers(), timeout=timeout)
|
| 39 |
+
except requests.RequestException as exc:
|
| 40 |
+
raise DownloadError(f"download API unreachable: {exc}") from exc
|
| 41 |
+
if r.status_code in (401, 403):
|
| 42 |
+
raise DownloadError(f"download API auth failed (HTTP {r.status_code}); "
|
| 43 |
+
"check RAPIDAPI_KEY / that you're subscribed.")
|
| 44 |
+
if r.status_code != 200:
|
| 45 |
+
raise DownloadError(f"download API HTTP {r.status_code}: {r.text[:160]}")
|
| 46 |
+
data = r.json()
|
| 47 |
+
urls = [u for u in (data.get("file"), data.get("reserved_file")) if u]
|
| 48 |
+
if not urls:
|
| 49 |
+
raise DownloadError(f"no download URL in API response: {str(data)[:200]}")
|
| 50 |
+
return urls, data
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def download_video(video_id: str, dest: str, quality: str = DEFAULT_QUALITY,
|
| 54 |
+
poll_timeout: int = 330, chunk: int = 1 << 20,
|
| 55 |
+
progress=None) -> str:
|
| 56 |
+
"""Download ``video_id`` to ``dest`` and return the path.
|
| 57 |
+
|
| 58 |
+
Polls the (async) provider link until ready (404 -> wait), then streams it to ``dest``.
|
| 59 |
+
Raises DownloadError if it never becomes ready within ``poll_timeout`` seconds.
|
| 60 |
+
"""
|
| 61 |
+
urls, _ = _request_urls(video_id, quality)
|
| 62 |
+
deadline = time.time() + poll_timeout
|
| 63 |
+
last = "not ready"
|
| 64 |
+
while time.time() < deadline:
|
| 65 |
+
for url in urls:
|
| 66 |
+
try:
|
| 67 |
+
with requests.get(url, stream=True, timeout=90) as resp:
|
| 68 |
+
if resp.status_code == 404:
|
| 69 |
+
last = "404 (server still preparing)"
|
| 70 |
+
continue
|
| 71 |
+
resp.raise_for_status()
|
| 72 |
+
with open(dest, "wb") as fh:
|
| 73 |
+
for c in resp.iter_content(chunk):
|
| 74 |
+
if c:
|
| 75 |
+
fh.write(c)
|
| 76 |
+
if os.path.getsize(dest) > 0:
|
| 77 |
+
return dest
|
| 78 |
+
except requests.RequestException as exc:
|
| 79 |
+
last = f"{type(exc).__name__}: {exc}"
|
| 80 |
+
if progress:
|
| 81 |
+
progress(0.0, desc="Preparing video (server-side, up to ~5 min)…")
|
| 82 |
+
time.sleep(8)
|
| 83 |
+
raise DownloadError(f"video not ready after {poll_timeout}s ({last}).")
|