huuyfytryr commited on
Commit
437ce9e
Β·
1 Parent(s): 3dc5240

Fix cobalt API v7, add pytubefix as Method B, more invidious instances

Browse files
Files changed (2) hide show
  1. app.py +98 -38
  2. 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
- headers = {
185
- 'Accept': 'application/json',
186
- 'Content-Type': 'application/json',
187
- }
188
- payload = {
189
- 'url': url,
190
- 'vQuality': '720',
191
- 'filenamePattern': 'basic',
192
- 'isAudioOnly': False,
193
- 'disableMetadata': True,
194
- }
 
 
 
 
 
 
 
 
195
  for instance in instances:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  try:
197
- print(f'[cobalt] Trying {instance} ...')
198
- resp = _req.post(f'{instance}/', json=payload, headers=headers, timeout=20)
199
- if resp.status_code != 200:
200
- print(f'[cobalt] {instance} returned HTTP {resp.status_code}')
201
- continue
202
- data = resp.json()
203
- status = data.get('status', '')
204
- dl_url = data.get('url', '')
205
- if status in ('stream', 'tunnel', 'redirect', 'local') and dl_url:
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: Invidious API (fallback) ───────────────────────────────
 
 
 
 
 
494
  if not dl_success:
495
- print('[download] cobalt failed, trying Invidious ...')
496
  dl_success = download_via_invidious(url, raw_download_path)
497
 
498
- # ── Method C: yt-dlp with cookies (last resort) ──────────────────────
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