Spaces:
Running
Running
Commit Β·
437ce9e
1
Parent(s): 3dc5240
Fix cobalt API v7, add pytubefix as Method B, more invidious instances
Browse files- app.py +98 -38
- requirements.txt +1 -0
app.py
CHANGED
|
@@ -173,7 +173,7 @@ def _extract_video_id(url):
|
|
| 173 |
def download_via_cobalt(url, output_path):
|
| 174 |
"""
|
| 175 |
Download via cobalt.tools API β works from datacenter IPs.
|
| 176 |
-
Tries multiple public cobalt instances.
|
| 177 |
"""
|
| 178 |
import requests as _req
|
| 179 |
instances = [
|
|
@@ -181,42 +181,94 @@ def download_via_cobalt(url, output_path):
|
|
| 181 |
'https://cobalt.api.timelessnesses.me',
|
| 182 |
'https://cobalt.tools.nadeko.net',
|
| 183 |
]
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
for instance in instances:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
try:
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
print(f'[
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
print(f'[cobalt] Got URL ({status}), downloading ...')
|
| 207 |
-
with _req.get(dl_url, stream=True, timeout=300,
|
| 208 |
-
headers={'User-Agent': 'Mozilla/5.0'}) as r:
|
| 209 |
-
r.raise_for_status()
|
| 210 |
-
with open(output_path, 'wb') as f:
|
| 211 |
-
for chunk in r.iter_content(65536):
|
| 212 |
-
f.write(chunk)
|
| 213 |
-
if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
|
| 214 |
-
print(f'[cobalt] Success! {os.path.getsize(output_path)//1024} KB')
|
| 215 |
-
return True
|
| 216 |
-
else:
|
| 217 |
-
print(f'[cobalt] Bad status from {instance}: {status} | {data}')
|
| 218 |
-
except Exception as e:
|
| 219 |
-
print(f'[cobalt] Error with {instance}: {e}')
|
| 220 |
return False
|
| 221 |
|
| 222 |
|
|
@@ -237,6 +289,9 @@ def download_via_invidious(url, output_path):
|
|
| 237 |
'https://invidious.privacydev.net',
|
| 238 |
'https://iv.datura.network',
|
| 239 |
'https://invidious.projectsegfau.lt',
|
|
|
|
|
|
|
|
|
|
| 240 |
]
|
| 241 |
for instance in instances:
|
| 242 |
try:
|
|
@@ -490,12 +545,17 @@ def clip_youtube_video():
|
|
| 490 |
# ββ Method A: cobalt.tools (best β no IP restrictions) ββββββββββββββ
|
| 491 |
dl_success = download_via_cobalt(url, raw_download_path)
|
| 492 |
|
| 493 |
-
# ββ Method B:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
if not dl_success:
|
| 495 |
-
print('[download]
|
| 496 |
dl_success = download_via_invidious(url, raw_download_path)
|
| 497 |
|
| 498 |
-
# ββ Method
|
| 499 |
if not dl_success:
|
| 500 |
print('[download] Invidious failed, trying yt-dlp with cookies ...')
|
| 501 |
ytdlp_bin = get_pip_binary('yt-dlp')
|
|
|
|
| 173 |
def download_via_cobalt(url, output_path):
|
| 174 |
"""
|
| 175 |
Download via cobalt.tools API β works from datacenter IPs.
|
| 176 |
+
Tries multiple public cobalt instances with both old and new API formats.
|
| 177 |
"""
|
| 178 |
import requests as _req
|
| 179 |
instances = [
|
|
|
|
| 181 |
'https://cobalt.api.timelessnesses.me',
|
| 182 |
'https://cobalt.tools.nadeko.net',
|
| 183 |
]
|
| 184 |
+
# Try both v7 (new) and v6 (old) payload formats
|
| 185 |
+
payloads = [
|
| 186 |
+
# cobalt v7+ format
|
| 187 |
+
{
|
| 188 |
+
'url': url,
|
| 189 |
+
'videoQuality': '720',
|
| 190 |
+
'filenameStyle': 'basic',
|
| 191 |
+
'downloadMode': 'auto',
|
| 192 |
+
},
|
| 193 |
+
# cobalt v6 format (older instances)
|
| 194 |
+
{
|
| 195 |
+
'url': url,
|
| 196 |
+
'vQuality': '720',
|
| 197 |
+
'filenamePattern': 'basic',
|
| 198 |
+
'isAudioOnly': False,
|
| 199 |
+
'disableMetadata': True,
|
| 200 |
+
},
|
| 201 |
+
]
|
| 202 |
+
headers = {'Accept': 'application/json', 'Content-Type': 'application/json'}
|
| 203 |
for instance in instances:
|
| 204 |
+
for payload in payloads:
|
| 205 |
+
try:
|
| 206 |
+
print(f'[cobalt] Trying {instance} ...')
|
| 207 |
+
resp = _req.post(f'{instance}/', json=payload, headers=headers, timeout=20)
|
| 208 |
+
if resp.status_code not in (200, 201):
|
| 209 |
+
print(f'[cobalt] {instance} returned HTTP {resp.status_code}')
|
| 210 |
+
break # try next instance, not next payload
|
| 211 |
+
data = resp.json()
|
| 212 |
+
status = data.get('status', '')
|
| 213 |
+
dl_url = data.get('url', '')
|
| 214 |
+
if status in ('stream', 'tunnel', 'redirect', 'local', 'picker') and dl_url:
|
| 215 |
+
print(f'[cobalt] Got URL ({status}), downloading ...')
|
| 216 |
+
with _req.get(dl_url, stream=True, timeout=300,
|
| 217 |
+
headers={'User-Agent': 'Mozilla/5.0'}) as r:
|
| 218 |
+
r.raise_for_status()
|
| 219 |
+
with open(output_path, 'wb') as f:
|
| 220 |
+
for chunk in r.iter_content(65536):
|
| 221 |
+
f.write(chunk)
|
| 222 |
+
if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
|
| 223 |
+
print(f'[cobalt] Success! {os.path.getsize(output_path)//1024} KB')
|
| 224 |
+
return True
|
| 225 |
+
else:
|
| 226 |
+
print(f'[cobalt] Unexpected status from {instance}: {status} | {data}')
|
| 227 |
+
break
|
| 228 |
+
except Exception as e:
|
| 229 |
+
print(f'[cobalt] Error with {instance}: {e}')
|
| 230 |
+
break
|
| 231 |
+
return False
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def download_via_pytubefix(url, output_path):
|
| 235 |
+
"""
|
| 236 |
+
Download via pytubefix β uses different request patterns than yt-dlp
|
| 237 |
+
and often works from datacenter IPs where yt-dlp SSL-fails.
|
| 238 |
+
"""
|
| 239 |
+
try:
|
| 240 |
+
from pytubefix import YouTube
|
| 241 |
+
from pytubefix.cli import on_progress
|
| 242 |
+
print(f'[pytubefix] Trying {url} ...')
|
| 243 |
+
yt = YouTube(url, on_progress_callback=on_progress,
|
| 244 |
+
use_oauth=False, allow_oauth_cache=False)
|
| 245 |
+
# Try progressive (combined audio+video) MP4 first
|
| 246 |
+
stream = (
|
| 247 |
+
yt.streams
|
| 248 |
+
.filter(progressive=True, file_extension='mp4')
|
| 249 |
+
.order_by('resolution')
|
| 250 |
+
.last() # highest resolution
|
| 251 |
+
)
|
| 252 |
+
if not stream:
|
| 253 |
+
# Fall back to any MP4
|
| 254 |
+
stream = yt.streams.filter(file_extension='mp4').first()
|
| 255 |
+
if not stream:
|
| 256 |
+
print('[pytubefix] No suitable stream found')
|
| 257 |
+
return False
|
| 258 |
+
print(f'[pytubefix] Downloading {stream.resolution} stream ...')
|
| 259 |
+
import tempfile, shutil
|
| 260 |
+
# pytubefix downloads to a directory, so use a temp dir
|
| 261 |
+
tmp_dir = tempfile.mkdtemp()
|
| 262 |
try:
|
| 263 |
+
dl_path = stream.download(output_path=tmp_dir, filename='video.mp4')
|
| 264 |
+
if dl_path and os.path.exists(dl_path) and os.path.getsize(dl_path) > 0:
|
| 265 |
+
shutil.move(dl_path, output_path)
|
| 266 |
+
print(f'[pytubefix] Success! {os.path.getsize(output_path)//1024} KB')
|
| 267 |
+
return True
|
| 268 |
+
finally:
|
| 269 |
+
shutil.rmtree(tmp_dir, ignore_errors=True)
|
| 270 |
+
except Exception as e:
|
| 271 |
+
print(f'[pytubefix] Error: {e}')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
return False
|
| 273 |
|
| 274 |
|
|
|
|
| 289 |
'https://invidious.privacydev.net',
|
| 290 |
'https://iv.datura.network',
|
| 291 |
'https://invidious.projectsegfau.lt',
|
| 292 |
+
'https://yt.cdaut.de',
|
| 293 |
+
'https://invidious.nerdvpn.de',
|
| 294 |
+
'https://vid.puffyan.us',
|
| 295 |
]
|
| 296 |
for instance in instances:
|
| 297 |
try:
|
|
|
|
| 545 |
# ββ Method A: cobalt.tools (best β no IP restrictions) ββββββββββββββ
|
| 546 |
dl_success = download_via_cobalt(url, raw_download_path)
|
| 547 |
|
| 548 |
+
# ββ Method B: pytubefix (different request path β bypasses SSL block) β
|
| 549 |
+
if not dl_success:
|
| 550 |
+
print('[download] cobalt failed, trying pytubefix ...')
|
| 551 |
+
dl_success = download_via_pytubefix(url, raw_download_path)
|
| 552 |
+
|
| 553 |
+
# ββ Method C: Invidious API (proxy fallback) βββββββββββββββββββββββββ
|
| 554 |
if not dl_success:
|
| 555 |
+
print('[download] pytubefix failed, trying Invidious ...')
|
| 556 |
dl_success = download_via_invidious(url, raw_download_path)
|
| 557 |
|
| 558 |
+
# ββ Method D: yt-dlp with cookies (last resort) ββββββββββββββββββββββ
|
| 559 |
if not dl_success:
|
| 560 |
print('[download] Invidious failed, trying yt-dlp with cookies ...')
|
| 561 |
ytdlp_bin = get_pip_binary('yt-dlp')
|
requirements.txt
CHANGED
|
@@ -8,3 +8,4 @@ Pillow>=10.0.0
|
|
| 8 |
requests
|
| 9 |
gunicorn
|
| 10 |
curl-cffi>=0.7.0
|
|
|
|
|
|
| 8 |
requests
|
| 9 |
gunicorn
|
| 10 |
curl-cffi>=0.7.0
|
| 11 |
+
pytubefix>=9.0.0
|