Update app.py
Browse files
app.py
CHANGED
@@ -29,12 +29,13 @@ from typing import Dict, Any
|
|
29 |
import re
|
30 |
import asyncio
|
31 |
import cloudscraper
|
32 |
-
|
33 |
-
from bs4 import BeautifulSoup
|
34 |
-
from pydantic import BaseModel
|
35 |
|
36 |
tmp_dir = tempfile.gettempdir()
|
37 |
-
BASE_URL = "https://chrunos-
|
|
|
|
|
|
|
38 |
|
39 |
|
40 |
def env_to_cookies(env_content: str, output_file: str) -> None:
|
@@ -126,119 +127,10 @@ async def get_video_url(youtube_url: str):
|
|
126 |
|
127 |
|
128 |
|
129 |
-
|
130 |
-
@app.get("/script")
|
131 |
-
async def get_transcript(youtube_url: str, language: str = None):
|
132 |
-
try:
|
133 |
-
# Set up yt-dlp options
|
134 |
-
ydl_opts = {
|
135 |
-
'skip_download': True,
|
136 |
-
'writesubtitles': True,
|
137 |
-
'writeautomaticsub': True,
|
138 |
-
'outtmpl': '%(id)s.%(ext)s',
|
139 |
-
'noplaylist': True,
|
140 |
-
'cookiefile': "firefox-cookies.txt"
|
141 |
-
}
|
142 |
-
|
143 |
-
# If a language is specified, only download that language
|
144 |
-
# Otherwise, we'll first get video info to determine the original language
|
145 |
-
if language:
|
146 |
-
ydl_opts['subtitleslangs'] = [language]
|
147 |
-
|
148 |
-
env_to_cookies_from_env("firefox-cookies.txt")
|
149 |
-
logger.info(f"Current directory files (before): {os.listdir('.')}")
|
150 |
-
|
151 |
-
# First, get video info without downloading anything
|
152 |
-
with yt_dlp.YoutubeDL({**ydl_opts, 'skip_download': True, 'writesubtitles': False, 'writeautomaticsub': False}) as ydl:
|
153 |
-
info = ydl.extract_info(youtube_url, download=False)
|
154 |
-
video_id = info['id']
|
155 |
-
logger.info(f"Video ID: {video_id}")
|
156 |
-
|
157 |
-
# If no language specified, try to use the original language
|
158 |
-
if not language:
|
159 |
-
# Try to determine the original language if available in the info
|
160 |
-
if 'subtitles' in info and info['subtitles']:
|
161 |
-
# Use the first available subtitle language
|
162 |
-
available_languages = list(info['subtitles'].keys())
|
163 |
-
if available_languages:
|
164 |
-
language = available_languages[0]
|
165 |
-
logger.info(f"Using detected language: {language}")
|
166 |
-
ydl_opts['subtitleslangs'] = [language]
|
167 |
-
else:
|
168 |
-
# Fall back to 'en' if can't determine
|
169 |
-
language = 'en'
|
170 |
-
ydl_opts['subtitleslangs'] = [language]
|
171 |
-
|
172 |
-
# Now download the subtitle in the selected language
|
173 |
-
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
174 |
-
ydl.extract_info(youtube_url, download=True)
|
175 |
-
|
176 |
-
# Check actual downloaded files
|
177 |
-
logger.info(f"Current directory files (after extraction): {os.listdir('.')}")
|
178 |
-
|
179 |
-
# Look for the subtitle file with the specified language
|
180 |
-
subtitle_files = [f for f in os.listdir('.')
|
181 |
-
if f.startswith(video_id) and any(ext in f for ext in ['.vtt', '.srt', '.ttml', '.json3'])]
|
182 |
-
|
183 |
-
# If specific language requested, filter for that language
|
184 |
-
if language:
|
185 |
-
lang_subtitle_files = [f for f in subtitle_files if language in f]
|
186 |
-
if lang_subtitle_files:
|
187 |
-
subtitle_files = lang_subtitle_files
|
188 |
-
|
189 |
-
logger.info(f"Potential subtitle files: {subtitle_files}")
|
190 |
-
|
191 |
-
if subtitle_files:
|
192 |
-
# Process the first found subtitle file
|
193 |
-
subtitle_file = subtitle_files[0]
|
194 |
-
logger.info(f"Processing subtitle file: {subtitle_file}")
|
195 |
-
|
196 |
-
with open(subtitle_file, 'r', encoding='utf-8') as f:
|
197 |
-
content = f.read()
|
198 |
-
|
199 |
-
# Add format-specific parsing
|
200 |
-
if subtitle_file.endswith('.json3'):
|
201 |
-
import json
|
202 |
-
subs = json.loads(content)
|
203 |
-
text = ' '.join([e['segs'][0]['utf8'] for e in subs['events'] if e.get('segs')])
|
204 |
-
elif subtitle_file.endswith('.vtt'):
|
205 |
-
text = ' '.join(line.strip() for line in content.split('\n')
|
206 |
-
if not line.startswith('WEBVTT')
|
207 |
-
and '-->' not in line
|
208 |
-
and not line.strip().isdigit()
|
209 |
-
and line.strip())
|
210 |
-
elif subtitle_file.endswith('.srt'):
|
211 |
-
# Simple SRT parsing - skip timestamps and numbers
|
212 |
-
lines = []
|
213 |
-
for line in content.split('\n'):
|
214 |
-
if not line.strip().isdigit() and '-->' not in line and line.strip():
|
215 |
-
lines.append(line.strip())
|
216 |
-
text = ' '.join(lines)
|
217 |
-
else:
|
218 |
-
text = f"Unsupported format: {subtitle_file}"
|
219 |
-
|
220 |
-
# Clean up files to avoid cluttering the directory
|
221 |
-
for f in subtitle_files:
|
222 |
-
try:
|
223 |
-
os.remove(f)
|
224 |
-
except:
|
225 |
-
logger.warning(f"Could not remove file: {f}")
|
226 |
-
|
227 |
-
detected_language = subtitle_file.split('.')[-2] if '.' in subtitle_file else "unknown"
|
228 |
-
return {"transcript": text, "language": detected_language}
|
229 |
-
|
230 |
-
return {"transcript": f"No subtitle files found for {video_id}", "language": "none"}
|
231 |
-
except Exception as e:
|
232 |
-
logger.error(f"Error: {str(e)}", exc_info=True)
|
233 |
-
raise HTTPException(status_code=500, detail=str(e))
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
# Define a global temporary download directory
|
238 |
global_download_dir = tempfile.mkdtemp()
|
239 |
|
240 |
-
|
241 |
-
|
242 |
class RateLimiter:
|
243 |
def __init__(self, max_requests: int, time_window: timedelta):
|
244 |
self.max_requests = max_requests
|
@@ -275,7 +167,7 @@ class RateLimiter:
|
|
275 |
|
276 |
# Initialize rate limiter with 100 requests per day
|
277 |
rate_limiter = RateLimiter(
|
278 |
-
max_requests=
|
279 |
time_window=timedelta(days=1)
|
280 |
)
|
281 |
|
@@ -308,16 +200,18 @@ class ApiRotator:
|
|
308 |
|
309 |
# In your function:
|
310 |
api_rotator = ApiRotator([
|
311 |
-
"
|
312 |
-
"https://
|
313 |
-
"
|
|
|
314 |
"https://cobalt-api.ayo.tf",
|
315 |
-
"https://
|
|
|
316 |
])
|
317 |
|
318 |
|
319 |
|
320 |
-
async def get_track_download_url(video_url: str) -> str:
|
321 |
apis = api_rotator.get_prioritized_apis()
|
322 |
session = cloudscraper.create_scraper() # Requires cloudscraper package
|
323 |
headers = {
|
@@ -325,6 +219,11 @@ async def get_track_download_url(video_url: str) -> str:
|
|
325 |
"Content-Type": "application/json",
|
326 |
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
327 |
}
|
|
|
|
|
|
|
|
|
|
|
328 |
|
329 |
for i, api_url in enumerate(apis):
|
330 |
try:
|
@@ -333,7 +232,7 @@ async def get_track_download_url(video_url: str) -> str:
|
|
333 |
response = session.post(
|
334 |
api_url,
|
335 |
timeout=20,
|
336 |
-
json=
|
337 |
headers=headers
|
338 |
)
|
339 |
logger.info(f"Response status: {response.status_code}")
|
@@ -343,7 +242,7 @@ async def get_track_download_url(video_url: str) -> str:
|
|
343 |
json_response = response.json()
|
344 |
error_code = json_response.get("error", {}).get("code", "")
|
345 |
|
346 |
-
if error_code == "error.api.content.video.
|
347 |
logger.warning(f"Video unavailable error from {api_url}")
|
348 |
break # Only break for specific error
|
349 |
|
@@ -359,192 +258,168 @@ async def get_track_download_url(video_url: str) -> str:
|
|
359 |
return {"error": "Download URL not found"}
|
360 |
|
361 |
|
362 |
-
def jio_search(query: str, quality: str) -> str:
|
363 |
-
try:
|
364 |
-
# Construct the API URL
|
365 |
-
api_url = f"https://saavn.dev/api/search/songs?query={query}"
|
366 |
-
session = cloudscraper.create_scraper()
|
367 |
-
# Make the API request
|
368 |
-
response = session.get(api_url)
|
369 |
-
# Check if the request was successful
|
370 |
-
response.raise_for_status()
|
371 |
-
# Get the data from the response
|
372 |
-
data = response.json().get("data")
|
373 |
-
if not data:
|
374 |
-
logger.error("No data found in the response.")
|
375 |
-
raise HTTPException(status_code=404, detail="No data found for the given query.")
|
376 |
-
# Get the song results
|
377 |
-
song_results = data.get("results")
|
378 |
-
if not song_results or len(song_results) == 0:
|
379 |
-
logger.error("No song results found in the response.")
|
380 |
-
raise HTTPException(status_code=404, detail="No song results found for the given query.")
|
381 |
-
# Iterate through each song result
|
382 |
-
for song in song_results:
|
383 |
-
download_urls = song.get("downloadUrl")
|
384 |
-
if download_urls:
|
385 |
-
for download_url in download_urls:
|
386 |
-
if download_url.get("quality") == quality:
|
387 |
-
return download_url.get("url")
|
388 |
-
logger.error(f"No download URL found for quality {quality} in the search results for query {query}.")
|
389 |
-
raise HTTPException(status_code=404, detail=f"No download URL found for quality {quality} in the search results for query {query}.")
|
390 |
-
except cloudscraper.exceptions.CloudflareChallengeError as cf_err:
|
391 |
-
logger.error(f"Cloudflare challenge error while searching for {query}: {cf_err}")
|
392 |
-
raise HTTPException(status_code=503, detail="Cloudflare challenge failed")
|
393 |
-
except HTTPException:
|
394 |
-
# Re - raise the HTTPException if it's already raised
|
395 |
-
raise
|
396 |
-
except Exception as e:
|
397 |
-
logger.error(f"Error while searching for {query}: {e}")
|
398 |
-
raise HTTPException(status_code=500, detail=f"An error occurred while searching: {str(e)}")
|
399 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
400 |
|
401 |
-
|
402 |
-
try:
|
403 |
-
# Construct the API URL
|
404 |
-
api_url = f"https://saavn.dev/api/songs?link={url}"
|
405 |
-
session = cloudscraper.create_scraper()
|
406 |
-
# Make the API request
|
407 |
-
response = session.get(api_url)
|
408 |
-
# Check if the request was successful
|
409 |
-
response.raise_for_status()
|
410 |
-
data = response.json()
|
411 |
-
song_data = data.get("data")
|
412 |
-
if not song_data or len(song_data) == 0:
|
413 |
-
logger.error("No data found in the response.")
|
414 |
-
raise HTTPException(status_code=404, detail="No data found for the given URL.")
|
415 |
-
download_urls = song_data[0].get("downloadUrl")
|
416 |
-
if not download_urls:
|
417 |
-
logger.error("No download URLs found in the response.")
|
418 |
-
raise HTTPException(status_code=404, detail="No download URLs found for the given song.")
|
419 |
-
for download_url in download_urls:
|
420 |
-
if download_url.get("quality") == quality:
|
421 |
-
return download_url.get("url")
|
422 |
-
logger.error(f"No download URL found for quality {quality}.")
|
423 |
-
raise HTTPException(status_code=404, detail=f"No download URL found for quality {quality}.")
|
424 |
-
except cloudscraper.exceptions.CloudflareChallengeError as cf_err:
|
425 |
-
logger.error(f"Cloudflare challenge error while fetching {url}: {cf_err}")
|
426 |
-
raise HTTPException(status_code=503, detail="Cloudflare challenge failed")
|
427 |
-
except HTTPException:
|
428 |
-
# Re - raise the HTTPException if it's already raised
|
429 |
-
raise
|
430 |
-
except Exception as e:
|
431 |
-
logger.error(f"Error while fetching {url}: {e}")
|
432 |
-
raise HTTPException(status_code=500, detail=f"An error occurred while fetching: {str(e)}")
|
433 |
-
|
434 |
-
# Define the request model
|
435 |
-
class JioDownloadRequest(BaseModel):
|
436 |
-
url: str = None
|
437 |
-
query: str = None
|
438 |
-
quality: str = None
|
439 |
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
url
|
444 |
-
query = request.query
|
445 |
-
quality = request.quality
|
446 |
-
if url and quality:
|
447 |
-
logger.info(f'input url: {url}')
|
448 |
-
download_url = jio_fetch(url, quality)
|
449 |
-
return {"download_url": download_url}
|
450 |
-
elif query:
|
451 |
-
logger.info(f'input query: {query}')
|
452 |
-
download_url = jio_search(query, quality)
|
453 |
-
return {"download_url": download_url}
|
454 |
-
else:
|
455 |
-
logger.error("Missing 'url' and 'quality' or 'query' in request data.")
|
456 |
-
raise HTTPException(status_code=400, detail="Missing 'url' and 'quality' or 'query' in request data")
|
457 |
-
except HTTPException:
|
458 |
-
# Re - raise the HTTPException if it's already raised
|
459 |
-
raise
|
460 |
-
except Exception as e:
|
461 |
-
logger.error(f"Error in jio_download: {e}")
|
462 |
-
raise HTTPException(status_code=500, detail=f"An error occurred during the operation: {str(e)}")
|
463 |
|
|
|
|
|
464 |
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
query = request.query
|
471 |
-
if quality == '320kbps':
|
472 |
-
return {
|
473 |
-
"error": "Quality 320kbps is for Premium users only",
|
474 |
-
"premium": "https://chrunos.com/premium-shortcuts/"
|
475 |
-
}
|
476 |
-
if url and quality:
|
477 |
-
logger.info(f'input url: {url}')
|
478 |
-
download_url = jio_fetch(url, quality)
|
479 |
-
return {"download_url": download_url}
|
480 |
-
elif query:
|
481 |
-
logger.info(f'input query: {query}')
|
482 |
-
download_url = jio_search(query, quality)
|
483 |
-
return {"download_url": download_url}
|
484 |
-
else:
|
485 |
-
logger.error("Missing 'url' and 'quality' or 'query' in request data.")
|
486 |
-
raise HTTPException(status_code=400, detail="Missing 'url' and 'quality' or 'query' in request data")
|
487 |
-
except HTTPException:
|
488 |
-
# Re - raise the HTTPException if it's already raised
|
489 |
-
raise
|
490 |
-
except Exception as e:
|
491 |
-
logger.error(f"Error in jio_download: {e}")
|
492 |
-
raise HTTPException(status_code=500, detail=f"An error occurred during the operation: {str(e)}")
|
493 |
-
|
494 |
|
|
|
|
|
495 |
|
|
|
|
|
496 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
497 |
|
|
|
|
|
498 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
499 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
500 |
|
501 |
|
502 |
-
|
503 |
ALT_API = os.getenv("ALT_API")
|
504 |
|
505 |
-
|
506 |
def extract_video_info(video_url: str) -> str:
|
507 |
-
|
508 |
-
|
509 |
-
|
510 |
-
|
511 |
-
|
512 |
-
|
513 |
-
|
514 |
-
|
515 |
-
|
516 |
-
|
517 |
-
|
518 |
-
|
519 |
-
|
520 |
-
|
521 |
-
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
|
528 |
-
|
529 |
-
|
530 |
-
|
531 |
-
|
532 |
-
|
533 |
-
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
|
|
|
|
|
|
541 |
else:
|
542 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
543 |
else:
|
544 |
-
|
545 |
-
|
546 |
-
|
547 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
548 |
|
549 |
|
550 |
@app.post("/test")
|
@@ -571,6 +446,7 @@ async def download_hls_video(request: Request):
|
|
571 |
'noprogress': True,
|
572 |
'merge_output_format': 'mp4'
|
573 |
}
|
|
|
574 |
try:
|
575 |
await run_in_threadpool(lambda: yt_dlp.YoutubeDL(ydl_opts).download([hls_url]))
|
576 |
except Exception as e:
|
@@ -589,61 +465,34 @@ async def download_hls_video(request: Request):
|
|
589 |
|
590 |
|
591 |
|
592 |
-
async def get_audio_download_url(track_id: str, quality: str) -> str:
|
593 |
-
if quality == 'mp3':
|
594 |
-
type = 'audio'
|
595 |
-
quality = 128
|
596 |
-
else:
|
597 |
-
type = 'video'
|
598 |
-
donwnload_url = f'https://chrunos-shadl.hf.space/yt/dl?url={track_id}&type={type}&quality={quality}'
|
599 |
-
return donwnload_url
|
600 |
-
|
601 |
|
602 |
-
@app.post("/
|
603 |
async def download_high_quality_video(request: Request):
|
604 |
-
user_ip = get_user_ip(request)
|
605 |
-
|
606 |
-
if rate_limiter.is_rate_limited(user_ip):
|
607 |
-
current_count = rate_limiter.get_current_count(user_ip)
|
608 |
-
raise HTTPException(
|
609 |
-
status_code=429,
|
610 |
-
detail={
|
611 |
-
"error": "You have exceeded the maximum number of requests per day. Please try again tomorrow.",
|
612 |
-
"url": "https://t.me/chrunoss"
|
613 |
-
}
|
614 |
-
)
|
615 |
-
|
616 |
-
|
617 |
data = await request.json()
|
618 |
-
restricted_domain = "chrunos.com"
|
619 |
video_url = data.get('url')
|
620 |
-
quality = data.get('quality', '
|
621 |
-
|
622 |
-
is_youtube_url = re.search(r'(youtube\.com|youtu\.be|instagram\.com)', video_url) is not None
|
623 |
-
if video_url and restricted_domain in video_url:
|
624 |
-
return {"error": "What is wrong with you?", "url": "https://t.me/chrunoss"}
|
625 |
-
|
626 |
# Check if the requested quality is above 1080p
|
627 |
-
if int(quality) >
|
628 |
-
|
629 |
-
|
630 |
-
|
631 |
|
632 |
cookiefile = "firefox-cookies.txt"
|
633 |
env_to_cookies_from_env("firefox-cookies.txt")
|
634 |
|
635 |
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
|
636 |
-
output_template = str(Path(global_download_dir) / f'%(title)
|
637 |
|
638 |
# Convert quality string to height
|
639 |
height_map = {
|
640 |
-
'240': 240,
|
641 |
-
'360': 360,
|
642 |
'480': 480,
|
643 |
'720': 720,
|
644 |
-
'1080': 1080
|
|
|
|
|
645 |
}
|
646 |
-
max_height = height_map.get(quality,
|
647 |
|
648 |
# Determine format string based on quality
|
649 |
format_str = f'bestvideo[height<={max_height}][vcodec^=avc]+bestaudio/best'
|
@@ -654,22 +503,12 @@ async def download_high_quality_video(request: Request):
|
|
654 |
'quiet': True,
|
655 |
'no_warnings': True,
|
656 |
'noprogress': True,
|
657 |
-
'merge_output_format': 'mp4'
|
|
|
658 |
}
|
659 |
|
660 |
-
if is_youtube_url:
|
661 |
-
ydl_opts["cookiefile"] = "firefox-cookies.txt"
|
662 |
-
'''dl_url = await get_audio_download_url(video_url, quality)
|
663 |
-
if dl_url and "http" in dl_url:
|
664 |
-
return {"url": dl_url, "requests_remaining": rate_limiter.max_requests - rate_limiter.get_current_count(user_ip)}
|
665 |
-
else:
|
666 |
-
return {
|
667 |
-
"error": "Failed to Fetch the video."
|
668 |
-
}
|
669 |
-
'''
|
670 |
-
# else:
|
671 |
await run_in_threadpool(lambda: yt_dlp.YoutubeDL(ydl_opts).download([video_url]))
|
672 |
-
|
673 |
downloaded_files = list(Path(global_download_dir).glob(f"*_{timestamp}.mp4"))
|
674 |
if not downloaded_files:
|
675 |
return {"error": "Download failed"}
|
@@ -677,38 +516,12 @@ async def download_high_quality_video(request: Request):
|
|
677 |
downloaded_file = downloaded_files[0]
|
678 |
encoded_filename = urllib.parse.quote(downloaded_file.name)
|
679 |
download_url = f"{BASE_URL}/file/{encoded_filename}"
|
680 |
-
|
681 |
-
|
682 |
gc.collect()
|
|
|
683 |
|
684 |
-
return {"url": download_url, "requests_remaining": rate_limiter.max_requests - rate_limiter.get_current_count(user_ip)}
|
685 |
-
|
686 |
-
|
687 |
-
|
688 |
-
|
689 |
-
|
690 |
-
|
691 |
-
|
692 |
-
|
693 |
-
api_key_header = APIKeyHeader(name="X-API-Key")
|
694 |
-
|
695 |
-
# Store this securely in your environment variables
|
696 |
-
API_KEY = os.getenv("API_KEY")
|
697 |
-
|
698 |
-
|
699 |
-
async def verify_api_key(api_key: str = Security(api_key_header)):
|
700 |
-
if api_key != API_KEY:
|
701 |
-
raise HTTPException(
|
702 |
-
status_code=403,
|
703 |
-
detail="Invalid API key"
|
704 |
-
)
|
705 |
-
return api_key
|
706 |
|
707 |
@app.post("/audio")
|
708 |
-
async def download_audio(
|
709 |
-
request: Request
|
710 |
-
#api_key: str = Security(verify_api_key)
|
711 |
-
):
|
712 |
user_ip = get_user_ip(request)
|
713 |
|
714 |
if rate_limiter.is_rate_limited(user_ip):
|
@@ -722,6 +535,8 @@ async def download_audio(
|
|
722 |
)
|
723 |
data = await request.json()
|
724 |
video_url = data.get('url')
|
|
|
|
|
725 |
#cookiefile = "firefox-cookies.txt"
|
726 |
#env_to_cookies_from_env("firefox-cookies.txt")
|
727 |
|
@@ -734,7 +549,6 @@ async def download_audio(
|
|
734 |
'quiet': True,
|
735 |
'no_warnings': True,
|
736 |
'noprogress': True,
|
737 |
-
#'cookiefile': cookiefile,
|
738 |
'postprocessors': [{
|
739 |
'key': 'FFmpegExtractAudio',
|
740 |
'preferredcodec': 'mp3',
|
@@ -742,16 +556,15 @@ async def download_audio(
|
|
742 |
}]
|
743 |
|
744 |
}
|
745 |
-
is_youtube_url = re.search(r'(youtube\.com|youtu\.be)', video_url) is not None
|
746 |
if is_youtube_url:
|
747 |
-
dl_url = await
|
748 |
if dl_url and "http" in dl_url:
|
749 |
return {"url": dl_url, "requests_remaining": rate_limiter.max_requests - rate_limiter.get_current_count(user_ip)}
|
750 |
else:
|
751 |
return {
|
752 |
-
"error": "Failed to Fetch the
|
753 |
}
|
754 |
-
else:
|
755 |
await run_in_threadpool(lambda: yt_dlp.YoutubeDL(ydl_opts).download([video_url]))
|
756 |
|
757 |
downloaded_files = list(Path(global_download_dir).glob(f"*_{timestamp}.*"))
|
@@ -766,12 +579,9 @@ async def download_audio(
|
|
766 |
|
767 |
# Configure logging
|
768 |
logging.basicConfig(level=logging.INFO)
|
769 |
-
logger = logging.getLogger(__name__)
|
770 |
|
771 |
@app.post("/search")
|
772 |
-
async def search_and_download_song(request: Request
|
773 |
-
api_key: str = Security(verify_api_key)
|
774 |
-
):
|
775 |
data = await request.json()
|
776 |
song_name = data.get('songname')
|
777 |
artist_name = data.get('artist')
|
@@ -834,14 +644,3 @@ app.mount("/file", StaticFiles(directory=global_download_dir), name="downloads")
|
|
834 |
|
835 |
|
836 |
|
837 |
-
|
838 |
-
|
839 |
-
|
840 |
-
|
841 |
-
@app.middleware("http")
|
842 |
-
async def set_mime_type_middleware(request: Request, call_next):
|
843 |
-
response = await call_next(request)
|
844 |
-
if request.url.path.endswith(".mp4"):
|
845 |
-
response.headers["Content-Type"] = "video/mp4"
|
846 |
-
return response
|
847 |
-
|
|
|
29 |
import re
|
30 |
import asyncio
|
31 |
import cloudscraper
|
32 |
+
|
|
|
|
|
33 |
|
34 |
tmp_dir = tempfile.gettempdir()
|
35 |
+
BASE_URL = "https://chrunos-multi.hf.space"
|
36 |
+
|
37 |
+
logging.basicConfig(level=logging.INFO)
|
38 |
+
logger = logging.getLogger(__name__)
|
39 |
|
40 |
|
41 |
def env_to_cookies(env_content: str, output_file: str) -> None:
|
|
|
127 |
|
128 |
|
129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
130 |
# Define a global temporary download directory
|
131 |
global_download_dir = tempfile.mkdtemp()
|
132 |
|
133 |
+
# Rate limiting dictionary
|
|
|
134 |
class RateLimiter:
|
135 |
def __init__(self, max_requests: int, time_window: timedelta):
|
136 |
self.max_requests = max_requests
|
|
|
167 |
|
168 |
# Initialize rate limiter with 100 requests per day
|
169 |
rate_limiter = RateLimiter(
|
170 |
+
max_requests=12,
|
171 |
time_window=timedelta(days=1)
|
172 |
)
|
173 |
|
|
|
200 |
|
201 |
# In your function:
|
202 |
api_rotator = ApiRotator([
|
203 |
+
"http://109.107.189.229:9000",
|
204 |
+
"https://dl01.yt-dl.click",
|
205 |
+
"http://34.107.254.11",
|
206 |
+
"http://2.56.214.74:9000",
|
207 |
"https://cobalt-api.ayo.tf",
|
208 |
+
"https://dwnld.nichind.dev",
|
209 |
+
"https://cobalt-api.kwiatekmiki.com"
|
210 |
])
|
211 |
|
212 |
|
213 |
|
214 |
+
async def get_track_download_url(video_url: str, quality: str) -> str:
|
215 |
apis = api_rotator.get_prioritized_apis()
|
216 |
session = cloudscraper.create_scraper() # Requires cloudscraper package
|
217 |
headers = {
|
|
|
219 |
"Content-Type": "application/json",
|
220 |
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
221 |
}
|
222 |
+
quality_lower = quality.lower()
|
223 |
+
if quality_lower == "mp3":
|
224 |
+
body_json = {"url": video_url, "audioFormat": "mp3", "downloadMode": "audio"}
|
225 |
+
else:
|
226 |
+
body_json = {"url": video_url, "videoQuality": quality, "filenameStyle": "pretty", "youtubeVideoCodec": "h264"}
|
227 |
|
228 |
for i, api_url in enumerate(apis):
|
229 |
try:
|
|
|
232 |
response = session.post(
|
233 |
api_url,
|
234 |
timeout=20,
|
235 |
+
json=body_json,
|
236 |
headers=headers
|
237 |
)
|
238 |
logger.info(f"Response status: {response.status_code}")
|
|
|
242 |
json_response = response.json()
|
243 |
error_code = json_response.get("error", {}).get("code", "")
|
244 |
|
245 |
+
if error_code == "error.api.content.video.age":
|
246 |
logger.warning(f"Video unavailable error from {api_url}")
|
247 |
break # Only break for specific error
|
248 |
|
|
|
258 |
return {"error": "Download URL not found"}
|
259 |
|
260 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
261 |
|
262 |
+
restricted_domain = "chrunos.com"
|
263 |
+
|
264 |
+
@app.post("/maxs")
|
265 |
+
async def download_high_quality_video(request: Request):
|
266 |
+
user_ip = get_user_ip(request)
|
267 |
+
|
268 |
+
if rate_limiter.is_rate_limited(user_ip):
|
269 |
+
current_count = rate_limiter.get_current_count(user_ip)
|
270 |
+
raise HTTPException(
|
271 |
+
status_code=429,
|
272 |
+
detail={
|
273 |
+
"error": "You have exceeded the maximum number of requests per day. Please try again tomorrow.",
|
274 |
+
"url": "https://t.me/chrunoss"
|
275 |
+
}
|
276 |
+
)
|
277 |
|
278 |
+
data = await request.json()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
279 |
|
280 |
+
video_url = data.get('url')
|
281 |
+
is_youtube_url = re.search(r'(youtube\.com|youtu\.be)', video_url) is not None
|
282 |
+
if "t.me" in video_url or "chrunos.com" in video_url:
|
283 |
+
return {"error": f"{video_url} is not supported", "url": "https://t.me/chrunoss"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
284 |
|
285 |
+
quality = data.get('quality', '720') # Default to 720 if not specified
|
286 |
+
logger.info(f'input: {video_url}, {quality}')
|
287 |
|
288 |
+
# Check if the requested quality is above 1080p
|
289 |
+
if int(quality) > 720:
|
290 |
+
error_message = "Quality above 720p is for Premium Members Only. Please check the URL for more information."
|
291 |
+
help_url = "https://chrunos.com/premium-shortcuts/" # Replace with your actual URL
|
292 |
+
return {"error": error_message, "url": help_url}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
293 |
|
294 |
+
#cookiefile = "firefox-cookies.txt"
|
295 |
+
#env_to_cookies_from_env("firefox-cookies.txt")
|
296 |
|
297 |
+
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
|
298 |
+
output_template = str(Path(global_download_dir) / f'%(title).70s_{timestamp}.%(ext)s')
|
299 |
|
300 |
+
# Convert quality string to height
|
301 |
+
height_map = {
|
302 |
+
'480': 480,
|
303 |
+
'720': 720,
|
304 |
+
'1080': 1080
|
305 |
+
}
|
306 |
+
max_height = height_map.get(quality, 1080) # Use the quality variable correctly
|
307 |
|
308 |
+
# Determine format string based on quality
|
309 |
+
format_str = f'bestvideo[height<={max_height}][vcodec^=avc]+bestaudio/best'
|
310 |
|
311 |
+
ydl_opts = {
|
312 |
+
'format': format_str,
|
313 |
+
'outtmpl': output_template,
|
314 |
+
'quiet': True,
|
315 |
+
'no_warnings': True,
|
316 |
+
'noprogress': True,
|
317 |
+
'merge_output_format': 'mp4'
|
318 |
+
}
|
319 |
|
320 |
+
if is_youtube_url:
|
321 |
+
dl_url = await get_track_download_url(video_url, quality)
|
322 |
+
if dl_url and "http" in dl_url:
|
323 |
+
return {"url": dl_url, "requests_remaining": rate_limiter.max_requests - rate_limiter.get_current_count(user_ip)}
|
324 |
+
else:
|
325 |
+
return {
|
326 |
+
"error": "Failed to Fetch the video."
|
327 |
+
}
|
328 |
+
else:
|
329 |
+
await run_in_threadpool(lambda: yt_dlp.YoutubeDL(ydl_opts).download([video_url]))
|
330 |
+
|
331 |
+
downloaded_files = list(Path(global_download_dir).glob(f"*_{timestamp}.mp4"))
|
332 |
+
if not downloaded_files:
|
333 |
+
return {"error": "Download failed. Report error on Telegram", "url": "https://t.me/chrunoss"}
|
334 |
+
|
335 |
+
downloaded_file = downloaded_files[0]
|
336 |
+
encoded_filename = urllib.parse.quote(downloaded_file.name)
|
337 |
+
download_url = f"{BASE_URL}/file/{encoded_filename}"
|
338 |
+
|
339 |
+
|
340 |
+
|
341 |
+
gc.collect()
|
342 |
+
|
343 |
+
return {"url": download_url}
|
344 |
|
345 |
|
346 |
+
TRACT_API = os.getenv("EXTRACT_API")
|
347 |
ALT_API = os.getenv("ALT_API")
|
348 |
|
|
|
349 |
def extract_video_info(video_url: str) -> str:
|
350 |
+
if "pornhub.com" in video_url:
|
351 |
+
EXTRACT_API = TRACT_API
|
352 |
+
else:
|
353 |
+
EXTRACT_API = ALT_API
|
354 |
+
api_url = f'{EXTRACT_API}?url={video_url}'
|
355 |
+
logger.info(api_url)
|
356 |
+
session = cloudscraper.create_scraper()
|
357 |
+
try:
|
358 |
+
response = session.get(api_url, timeout=20)
|
359 |
+
|
360 |
+
if response.status_code == 200:
|
361 |
+
json_response = response.json()
|
362 |
+
result = []
|
363 |
+
# 检查 formats 列表是否存在且不为空
|
364 |
+
if 'formats' in json_response:
|
365 |
+
for format_item in json_response['formats']:
|
366 |
+
format_url = format_item.get('url')
|
367 |
+
format_id = format_item.get('format_id')
|
368 |
+
p_cookies = format_item.get('cookies')
|
369 |
+
if format_id and format_url:
|
370 |
+
result.append({
|
371 |
+
"url": format_url,
|
372 |
+
"format_id": format_id,
|
373 |
+
"cookies": p_cookies
|
374 |
+
})
|
375 |
+
|
376 |
+
title = json_response.get('title')
|
377 |
+
logger.info(title)
|
378 |
+
if "pornhub.com" in video_url:
|
379 |
+
p_result = [item for item in result if 'hls' in item['format_id']]
|
380 |
+
last_item = p_result[-1]
|
381 |
+
second_last_item = p_result[-2]
|
382 |
+
last_item["format_id"] = f'{last_item["format_id"]} - Chrunos Shortcuts Premium Only'
|
383 |
+
last_item["url"] = 'https://chrunos.com/premium-shortcuts/'
|
384 |
+
second_last_item["format_id"] = f'{second_last_item["format_id"]} - Chrunos Shortcuts Premium Only'
|
385 |
+
second_last_item["url"] = 'https://chrunos.com/premium-shortcuts/'
|
386 |
+
return p_result
|
387 |
else:
|
388 |
+
new_result = result
|
389 |
+
# Check if new_result has more than one item
|
390 |
+
if len(new_result) > 1:
|
391 |
+
# Get the last item
|
392 |
+
last_item = new_result[-1]
|
393 |
+
# Modify the format_id
|
394 |
+
last_item["format_id"] = f'{last_item["format_id"]} - Chrunos Shortcuts Premium Only'
|
395 |
+
# Modify the url
|
396 |
+
last_item["url"] = 'https://chrunos.com/premium-shortcuts/'
|
397 |
+
elif len(new_result) == 1:
|
398 |
+
new_item = {"url": "https://chrunos.com/premium-shortcuts/",
|
399 |
+
"format_id": "Best Qaulity Video - Chrunos Shortcuts Premium Only"
|
400 |
+
}
|
401 |
+
new_result.append(new_item)
|
402 |
+
|
403 |
+
return new_result
|
404 |
else:
|
405 |
+
if 'url' in json_response:
|
406 |
+
download_url = json_response.get('url')
|
407 |
+
thumbnail_url = json_response.get('thumbnail')
|
408 |
+
return [
|
409 |
+
{"url": download_url,
|
410 |
+
"format_id": "Normal Quality Video"
|
411 |
+
},
|
412 |
+
{"url": thumbnail_url,
|
413 |
+
"format_id": "thumbnail"},
|
414 |
+
{"url": "https://chrunos.com/premium-shortcuts/",
|
415 |
+
"format_id": "Best Qaulity Video - Chrunos Shortcuts Premium Only"}
|
416 |
+
]
|
417 |
+
return {"error": "No formats available. Report Error on Telegram"}
|
418 |
+
else:
|
419 |
+
return {"error": f"Request failed with status code {response.status_code}, API: {api_url}"}
|
420 |
+
except Exception as e:
|
421 |
+
logger.error(f"An error occurred: {e}")
|
422 |
+
return {"error": str(e)}
|
423 |
|
424 |
|
425 |
@app.post("/test")
|
|
|
446 |
'noprogress': True,
|
447 |
'merge_output_format': 'mp4'
|
448 |
}
|
449 |
+
|
450 |
try:
|
451 |
await run_in_threadpool(lambda: yt_dlp.YoutubeDL(ydl_opts).download([hls_url]))
|
452 |
except Exception as e:
|
|
|
465 |
|
466 |
|
467 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
468 |
|
469 |
+
@app.post("/max")
|
470 |
async def download_high_quality_video(request: Request):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
471 |
data = await request.json()
|
|
|
472 |
video_url = data.get('url')
|
473 |
+
quality = data.get('quality', '1080') # Default to 1080p if not specified
|
474 |
+
|
|
|
|
|
|
|
|
|
475 |
# Check if the requested quality is above 1080p
|
476 |
+
#if int(quality) > 1080:
|
477 |
+
# error_message = "Quality above 1080p is for premium users. Please check the URL for more information."
|
478 |
+
# help_url = "https://chrunos.com/premium-shortcuts/" # Replace with your actual URL
|
479 |
+
# return {"error": error_message, "url": help_url}
|
480 |
|
481 |
cookiefile = "firefox-cookies.txt"
|
482 |
env_to_cookies_from_env("firefox-cookies.txt")
|
483 |
|
484 |
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
|
485 |
+
output_template = str(Path(global_download_dir) / f'%(title)s_{timestamp}.%(ext)s')
|
486 |
|
487 |
# Convert quality string to height
|
488 |
height_map = {
|
|
|
|
|
489 |
'480': 480,
|
490 |
'720': 720,
|
491 |
+
'1080': 1080,
|
492 |
+
'1440': 1440,
|
493 |
+
'2160': 2160
|
494 |
}
|
495 |
+
max_height = height_map.get(quality, 1080) # Use the quality variable correctly
|
496 |
|
497 |
# Determine format string based on quality
|
498 |
format_str = f'bestvideo[height<={max_height}][vcodec^=avc]+bestaudio/best'
|
|
|
503 |
'quiet': True,
|
504 |
'no_warnings': True,
|
505 |
'noprogress': True,
|
506 |
+
'merge_output_format': 'mp4',
|
507 |
+
'cookiefile': cookiefile
|
508 |
}
|
509 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
510 |
await run_in_threadpool(lambda: yt_dlp.YoutubeDL(ydl_opts).download([video_url]))
|
511 |
+
|
512 |
downloaded_files = list(Path(global_download_dir).glob(f"*_{timestamp}.mp4"))
|
513 |
if not downloaded_files:
|
514 |
return {"error": "Download failed"}
|
|
|
516 |
downloaded_file = downloaded_files[0]
|
517 |
encoded_filename = urllib.parse.quote(downloaded_file.name)
|
518 |
download_url = f"{BASE_URL}/file/{encoded_filename}"
|
|
|
|
|
519 |
gc.collect()
|
520 |
+
return {"url": download_url}
|
521 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
522 |
|
523 |
@app.post("/audio")
|
524 |
+
async def download_audio(request: Request):
|
|
|
|
|
|
|
525 |
user_ip = get_user_ip(request)
|
526 |
|
527 |
if rate_limiter.is_rate_limited(user_ip):
|
|
|
535 |
)
|
536 |
data = await request.json()
|
537 |
video_url = data.get('url')
|
538 |
+
is_youtube_url = re.search(r'(youtube\.com|youtu\.be)', video_url) is not None
|
539 |
+
|
540 |
#cookiefile = "firefox-cookies.txt"
|
541 |
#env_to_cookies_from_env("firefox-cookies.txt")
|
542 |
|
|
|
549 |
'quiet': True,
|
550 |
'no_warnings': True,
|
551 |
'noprogress': True,
|
|
|
552 |
'postprocessors': [{
|
553 |
'key': 'FFmpegExtractAudio',
|
554 |
'preferredcodec': 'mp3',
|
|
|
556 |
}]
|
557 |
|
558 |
}
|
|
|
559 |
if is_youtube_url:
|
560 |
+
dl_url = await get_track_download_url(video_url, 'mp3')
|
561 |
if dl_url and "http" in dl_url:
|
562 |
return {"url": dl_url, "requests_remaining": rate_limiter.max_requests - rate_limiter.get_current_count(user_ip)}
|
563 |
else:
|
564 |
return {
|
565 |
+
"error": "Failed to Fetch the audio."
|
566 |
}
|
567 |
+
else:
|
568 |
await run_in_threadpool(lambda: yt_dlp.YoutubeDL(ydl_opts).download([video_url]))
|
569 |
|
570 |
downloaded_files = list(Path(global_download_dir).glob(f"*_{timestamp}.*"))
|
|
|
579 |
|
580 |
# Configure logging
|
581 |
logging.basicConfig(level=logging.INFO)
|
|
|
582 |
|
583 |
@app.post("/search")
|
584 |
+
async def search_and_download_song(request: Request):
|
|
|
|
|
585 |
data = await request.json()
|
586 |
song_name = data.get('songname')
|
587 |
artist_name = data.get('artist')
|
|
|
644 |
|
645 |
|
646 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|