Spaces:
Sleeping
Sleeping
Enforce monthly request cap (12000/mo plan) via RapidAPI quota headers; optional MONTHLY_DOWNLOAD_LIMIT self-cap; show quota in health check
Browse files- pipeline/downloader.py +64 -1
pipeline/downloader.py
CHANGED
|
@@ -19,11 +19,68 @@ import requests
|
|
| 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:
|
|
@@ -38,6 +95,10 @@ def _request_urls(video_id: str, quality: str, timeout: int = 60) -> tuple[list[
|
|
| 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.")
|
|
@@ -56,8 +117,10 @@ def download_video(video_id: str, dest: str, quality: str = DEFAULT_QUALITY,
|
|
| 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"
|
|
|
|
| 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 |
+
# RapidAPI tracks monthly usage against the plan limit and returns it in every response
|
| 23 |
+
# header. We mirror the latest values here and enforce the cap — no persistence needed,
|
| 24 |
+
# and it's Space-wide + monthly (the plan's billing cycle) by construction.
|
| 25 |
+
_quota = {"limit": None, "remaining": None, "reset": None} # reset = seconds until reset
|
| 26 |
+
|
| 27 |
|
| 28 |
class DownloadError(RuntimeError):
|
| 29 |
"""Raised when the video can't be obtained from the download API."""
|
| 30 |
|
| 31 |
|
| 32 |
+
def _update_quota(headers) -> None:
|
| 33 |
+
def _int(name):
|
| 34 |
+
try:
|
| 35 |
+
return int(headers.get(name))
|
| 36 |
+
except (TypeError, ValueError):
|
| 37 |
+
return None
|
| 38 |
+
for key, hdr in (("limit", "X-RateLimit-Requests-Limit"),
|
| 39 |
+
("remaining", "X-RateLimit-Requests-Remaining"),
|
| 40 |
+
("reset", "X-RateLimit-Requests-Reset")):
|
| 41 |
+
val = _int(hdr)
|
| 42 |
+
if val is not None:
|
| 43 |
+
_quota[key] = val
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def quota_status() -> dict:
|
| 47 |
+
"""Latest known monthly quota: ``{limit, remaining, reset(sec), used, self_cap}``."""
|
| 48 |
+
q = dict(_quota)
|
| 49 |
+
q["used"] = (q["limit"] - q["remaining"]) if (q["limit"] and q["remaining"] is not None) else None
|
| 50 |
+
q["self_cap"] = _self_cap()
|
| 51 |
+
return q
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _reset_days() -> str:
|
| 55 |
+
r = _quota.get("reset")
|
| 56 |
+
return f"~{max(1, r // 86400)} day(s)" if r else "the next cycle"
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _self_cap() -> int | None:
|
| 60 |
+
"""Optional lower monthly cap (env ``MONTHLY_DOWNLOAD_LIMIT``); None = use plan limit."""
|
| 61 |
+
v = os.environ.get("MONTHLY_DOWNLOAD_LIMIT", "").strip()
|
| 62 |
+
try:
|
| 63 |
+
return int(v) if v else None
|
| 64 |
+
except ValueError:
|
| 65 |
+
return None
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _enforce_quota() -> None:
|
| 69 |
+
"""Refuse before spending a request if the monthly cap is already reached."""
|
| 70 |
+
q = _quota
|
| 71 |
+
if q["remaining"] is None:
|
| 72 |
+
return # unknown yet (fresh start) — let the call itself surface a 429
|
| 73 |
+
if q["limit"] is not None:
|
| 74 |
+
used = q["limit"] - q["remaining"]
|
| 75 |
+
cap = _self_cap()
|
| 76 |
+
if cap is not None and used >= cap:
|
| 77 |
+
raise DownloadError(
|
| 78 |
+
f"Self-imposed monthly cap reached: {used} of {cap} used. Resets in {_reset_days()}.")
|
| 79 |
+
if q["remaining"] <= 0:
|
| 80 |
+
raise DownloadError(
|
| 81 |
+
f"Monthly request limit reached (plan: {q['limit']}). Resets in {_reset_days()}.")
|
| 82 |
+
|
| 83 |
+
|
| 84 |
def _headers() -> dict:
|
| 85 |
key = os.environ.get("RAPIDAPI_KEY", "").strip()
|
| 86 |
if not key:
|
|
|
|
| 95 |
r = requests.get(url, params={"quality": quality}, headers=_headers(), timeout=timeout)
|
| 96 |
except requests.RequestException as exc:
|
| 97 |
raise DownloadError(f"download API unreachable: {exc}") from exc
|
| 98 |
+
_update_quota(r.headers)
|
| 99 |
+
if r.status_code == 429:
|
| 100 |
+
raise DownloadError(f"Monthly request limit reached (plan: {_quota.get('limit')}). "
|
| 101 |
+
f"Resets in {_reset_days()}.")
|
| 102 |
if r.status_code in (401, 403):
|
| 103 |
raise DownloadError(f"download API auth failed (HTTP {r.status_code}); "
|
| 104 |
"check RAPIDAPI_KEY / that you're subscribed.")
|
|
|
|
| 117 |
"""Download ``video_id`` to ``dest`` and return the path.
|
| 118 |
|
| 119 |
Polls the (async) provider link until ready (404 -> wait), then streams it to ``dest``.
|
| 120 |
+
Raises DownloadError if it never becomes ready within ``poll_timeout`` seconds, or if
|
| 121 |
+
the monthly request cap is already reached.
|
| 122 |
"""
|
| 123 |
+
_enforce_quota() # refuse before spending a request if the monthly cap is reached
|
| 124 |
urls, _ = _request_urls(video_id, quality)
|
| 125 |
deadline = time.time() + poll_timeout
|
| 126 |
last = "not ready"
|