Spaces:
Running
Running
Upload 3 files
Browse files- app.py +90 -811
- requirements.txt +2 -6
app.py
CHANGED
@@ -1,33 +1,14 @@
|
|
1 |
import gradio as gr
|
2 |
import yt_dlp
|
3 |
import aiohttp
|
4 |
-
from aiohttp_proxy import ProxyConnector
|
5 |
import asyncio
|
6 |
import os
|
7 |
import tempfile
|
8 |
-
from urllib.parse import urlparse
|
9 |
import time
|
10 |
import mimetypes
|
11 |
import concurrent.futures
|
12 |
import math
|
13 |
-
import json
|
14 |
-
from datetime import datetime
|
15 |
-
import random
|
16 |
-
from bs4 import BeautifulSoup
|
17 |
-
import re
|
18 |
-
import urllib3
|
19 |
-
import uuid
|
20 |
-
import ssl
|
21 |
-
import undetected_chromedriver as uc
|
22 |
-
from selenium.webdriver.common.by import By
|
23 |
-
from selenium.webdriver.support.ui import WebDriverWait
|
24 |
-
from selenium.webdriver.support import expected_conditions as EC
|
25 |
-
import threading
|
26 |
-
from fake_useragent import UserAgent
|
27 |
-
import requests
|
28 |
-
|
29 |
-
# تعطيل تحذيرات SSL
|
30 |
-
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
31 |
|
32 |
class FastDownloader:
|
33 |
def __init__(self):
|
@@ -35,612 +16,22 @@ class FastDownloader:
|
|
35 |
'format': 'best',
|
36 |
'quiet': True,
|
37 |
'no_warnings': True,
|
38 |
-
'concurrent_fragments': 10,
|
39 |
-
'nocheckcertificate': True,
|
40 |
}
|
41 |
-
self.chunk_size = 1024 * 1024 * 5
|
42 |
-
self.max_workers = 5
|
43 |
-
self.archive_file = "download_archive.json"
|
44 |
-
|
45 |
-
# إنشاء UserAgent عشوائي
|
46 |
-
self.ua = UserAgent()
|
47 |
-
|
48 |
-
# إعداد SSL context
|
49 |
-
self.ssl_context = ssl.create_default_context()
|
50 |
-
self.ssl_context.check_hostname = False
|
51 |
-
self.ssl_context.verify_mode = ssl.CERT_NONE
|
52 |
-
|
53 |
-
# تهيئة المتصفح
|
54 |
-
self.driver = None
|
55 |
-
self.driver_lock = threading.Lock()
|
56 |
-
|
57 |
-
# تهيئة الأرشيف
|
58 |
-
self.load_archive()
|
59 |
-
|
60 |
-
# تهيئة قائمة البروكسيات
|
61 |
-
self.proxies = []
|
62 |
-
self.update_proxies()
|
63 |
-
|
64 |
-
def update_proxies(self):
|
65 |
-
"""تحديث قائمة البروكسيات"""
|
66 |
-
fallback_proxies = [
|
67 |
-
'http://51.159.115.233:3128',
|
68 |
-
'http://20.111.54.16:8123',
|
69 |
-
'http://51.159.115.233:3128',
|
70 |
-
'http://20.206.106.192:80',
|
71 |
-
'http://51.79.50.31:9300'
|
72 |
-
]
|
73 |
-
|
74 |
-
try:
|
75 |
-
# محاولة الحصول على بروكسيات من مصدر خارجي
|
76 |
-
response = requests.get(
|
77 |
-
'https://proxylist.geonode.com/api/proxy-list',
|
78 |
-
params={
|
79 |
-
'limit': '100',
|
80 |
-
'page': '1',
|
81 |
-
'sort_by': 'lastChecked',
|
82 |
-
'sort_type': 'desc',
|
83 |
-
'protocols': 'http,https',
|
84 |
-
'anonymityLevel': 'elite,anonymous'
|
85 |
-
},
|
86 |
-
timeout=10
|
87 |
-
)
|
88 |
-
|
89 |
-
if response.status_code == 200:
|
90 |
-
data = response.json()
|
91 |
-
if isinstance(data, dict) and 'data' in data:
|
92 |
-
for proxy in data['data']:
|
93 |
-
protocol = proxy.get('protocols', ['http'])[0]
|
94 |
-
ip = proxy.get('ip')
|
95 |
-
port = proxy.get('port')
|
96 |
-
if ip and port:
|
97 |
-
self.proxies.append(f'{protocol}://{ip}:{port}')
|
98 |
-
|
99 |
-
# إضافة البروكسيات الاحتياطية
|
100 |
-
self.proxies.extend(fallback_proxies)
|
101 |
-
|
102 |
-
except Exception as e:
|
103 |
-
print(f"Error updating proxies: {str(e)}")
|
104 |
-
# استخدام البروكسيات الاحتياطية في حالة الفشل
|
105 |
-
self.proxies = fallback_proxies.copy()
|
106 |
-
|
107 |
-
def get_random_proxy(self):
|
108 |
-
"""الحصول على بروكسي عشوائي"""
|
109 |
-
# تحديث البروكسيات إذا كانت القائمة فارغة
|
110 |
-
if not self.proxies:
|
111 |
-
self.update_proxies()
|
112 |
-
|
113 |
-
# إرجاع بروكسي عشوائي إذا كانت هناك بروكسيات متاحة
|
114 |
-
return random.choice(self.proxies) if self.proxies else None
|
115 |
-
|
116 |
-
def get_headers(self, url):
|
117 |
-
"""إنشاء ترويسات عشوائية"""
|
118 |
-
parsed_url = urlparse(url)
|
119 |
-
domain = parsed_url.netloc.lower()
|
120 |
-
|
121 |
-
headers = {
|
122 |
-
'User-Agent': self.ua.random,
|
123 |
-
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
124 |
-
'Accept-Language': 'en-US,en;q=0.9,ar;q=0.8',
|
125 |
-
'Accept-Encoding': 'gzip, deflate, br',
|
126 |
-
'Connection': 'keep-alive',
|
127 |
-
'Upgrade-Insecure-Requests': '1',
|
128 |
-
'Cache-Control': 'max-age=0',
|
129 |
-
'TE': 'Trailers',
|
130 |
-
'DNT': '1',
|
131 |
-
'Sec-GPC': '1',
|
132 |
-
'Pragma': 'no-cache'
|
133 |
-
}
|
134 |
-
|
135 |
-
if 'milocdn' in domain or 'anime4up' in domain:
|
136 |
-
headers.update({
|
137 |
-
'Referer': f"https://{domain}/",
|
138 |
-
'Origin': f"https://{domain}",
|
139 |
-
'Host': domain,
|
140 |
-
'Sec-Fetch-Dest': 'document',
|
141 |
-
'Sec-Fetch-Mode': 'navigate',
|
142 |
-
'Sec-Fetch-Site': 'same-origin',
|
143 |
-
'Sec-Fetch-User': '?1',
|
144 |
-
'X-Requested-With': f'XMLHttpRequest-{random.randint(1000, 9999)}',
|
145 |
-
'X-Forwarded-For': f'192.168.{random.randint(1, 255)}.{random.randint(1, 255)}'
|
146 |
-
})
|
147 |
-
|
148 |
-
return headers
|
149 |
-
|
150 |
-
def get_cookies(self, url):
|
151 |
-
"""إنشاء كوكيز عشوائية"""
|
152 |
-
domain = urlparse(url).netloc
|
153 |
-
timestamp = int(time.time())
|
154 |
-
cookies = {
|
155 |
-
'cf_clearance': f"{uuid.uuid4().hex}",
|
156 |
-
'_ga': f"GA1.2.{random.randint(1000000, 9999999)}.{timestamp}",
|
157 |
-
'_gid': f"GA1.2.{random.randint(1000000, 9999999)}.{timestamp}",
|
158 |
-
'PHPSESSID': f"{uuid.uuid4().hex}",
|
159 |
-
'visited': '1',
|
160 |
-
'last_visit': str(timestamp),
|
161 |
-
'session_depth': str(random.randint(1, 5)),
|
162 |
-
'user_preferences': 'theme=light',
|
163 |
-
'timezone_offset': '-180',
|
164 |
-
'_fbp': f"fb.1.{timestamp}.{random.randint(1000000, 9999999)}",
|
165 |
-
'_ym_uid': f"{random.randint(1000000000, 9999999999)}",
|
166 |
-
'_ym_d': str(timestamp),
|
167 |
-
'wordpress_test_cookie': 'WP Cookie check',
|
168 |
-
'wp-settings-time-1': str(timestamp),
|
169 |
-
}
|
170 |
-
return cookies
|
171 |
-
|
172 |
-
def init_browser(self):
|
173 |
-
"""تهيئة المتصفح"""
|
174 |
-
try:
|
175 |
-
options = uc.ChromeOptions()
|
176 |
-
options.add_argument('--headless')
|
177 |
-
options.add_argument('--no-sandbox')
|
178 |
-
options.add_argument('--disable-dev-shm-usage')
|
179 |
-
options.add_argument('--disable-gpu')
|
180 |
-
options.add_argument(f'--user-agent={self.ua.random}')
|
181 |
-
|
182 |
-
# Set Chrome binary location for different platforms
|
183 |
-
if os.name == 'nt': # Windows
|
184 |
-
chrome_paths = [
|
185 |
-
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
186 |
-
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
187 |
-
]
|
188 |
-
for path in chrome_paths:
|
189 |
-
if os.path.exists(path):
|
190 |
-
options.binary_location = path
|
191 |
-
break
|
192 |
-
else: # Linux/Unix
|
193 |
-
chrome_paths = [
|
194 |
-
"/usr/bin/google-chrome",
|
195 |
-
"/usr/bin/chromium-browser",
|
196 |
-
]
|
197 |
-
for path in chrome_paths:
|
198 |
-
if os.path.exists(path):
|
199 |
-
options.binary_location = path
|
200 |
-
break
|
201 |
-
|
202 |
-
with self.driver_lock:
|
203 |
-
if not self.driver:
|
204 |
-
self.driver = uc.Chrome(options=options)
|
205 |
-
except Exception as e:
|
206 |
-
print(f"Warning: Browser initialization failed: {str(e)}")
|
207 |
-
self.driver = None
|
208 |
-
|
209 |
-
def close_browser(self):
|
210 |
-
"""إغلاق المتصفح"""
|
211 |
-
with self.driver_lock:
|
212 |
-
if self.driver:
|
213 |
-
try:
|
214 |
-
self.driver.quit()
|
215 |
-
except:
|
216 |
-
pass
|
217 |
-
self.driver = None
|
218 |
-
|
219 |
-
async def extract_direct_link(self, url):
|
220 |
-
"""استخراج رابط التحميل المباشر"""
|
221 |
-
try:
|
222 |
-
# Parse URL components
|
223 |
-
parsed = urlparse(url)
|
224 |
-
query_params = parse_qs(parsed.query)
|
225 |
-
|
226 |
-
# If URL already has a token, try to refresh it
|
227 |
-
if 't' in query_params:
|
228 |
-
current_time = int(time.time())
|
229 |
-
# Generate new token parameters
|
230 |
-
new_params = {
|
231 |
-
't': query_params.get('t', [''])[0],
|
232 |
-
's': str(current_time),
|
233 |
-
'e': '86400',
|
234 |
-
'f': query_params.get('f', [''])[0],
|
235 |
-
'sp': '1000',
|
236 |
-
'i': '0.0'
|
237 |
-
}
|
238 |
-
|
239 |
-
# Reconstruct URL with new parameters
|
240 |
-
base_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
|
241 |
-
new_url = f"{base_url}?{urlencode(new_params)}"
|
242 |
-
return new_url
|
243 |
-
|
244 |
-
return url
|
245 |
-
except Exception:
|
246 |
-
return url
|
247 |
-
|
248 |
-
async def download_with_ytdlp(self, url, browser_cookies=None, use_proxy=False):
|
249 |
-
"""استخدام yt-dlp للتحميل"""
|
250 |
-
try:
|
251 |
-
temp_dir = tempfile.mkdtemp()
|
252 |
-
|
253 |
-
# Try to refresh/extract direct link
|
254 |
-
url = await self.extract_direct_link(url)
|
255 |
-
|
256 |
-
# Enhanced headers with streaming-specific settings
|
257 |
-
headers = {
|
258 |
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
259 |
-
'Accept': '*/*',
|
260 |
-
'Accept-Language': 'en-US,en;q=0.9',
|
261 |
-
'Accept-Encoding': 'gzip, deflate, br',
|
262 |
-
'Connection': 'keep-alive',
|
263 |
-
'Range': 'bytes=0-',
|
264 |
-
'Referer': url,
|
265 |
-
'Origin': urlparse(url).scheme + '://' + urlparse(url).netloc,
|
266 |
-
'Sec-Fetch-Dest': 'video',
|
267 |
-
'Sec-Fetch-Mode': 'cors',
|
268 |
-
'Sec-Fetch-Site': 'same-origin',
|
269 |
-
}
|
270 |
-
|
271 |
-
# Get cookies
|
272 |
-
if not browser_cookies:
|
273 |
-
browser_cookies = self.get_cookies(url)
|
274 |
-
|
275 |
-
# Custom format selection
|
276 |
-
format_selector = lambda ctx: [{
|
277 |
-
'format_id': 'best',
|
278 |
-
'ext': 'mp4',
|
279 |
-
'protocol': 'https'
|
280 |
-
}]
|
281 |
-
|
282 |
-
ydl_opts = {
|
283 |
-
'format': 'best',
|
284 |
-
'format_sort': ['res:1080', 'ext:mp4:m4a'],
|
285 |
-
'outtmpl': os.path.join(temp_dir, '%(title)s.%(ext)s'),
|
286 |
-
'quiet': True,
|
287 |
-
'no_warnings': True,
|
288 |
-
'nocheckcertificate': True,
|
289 |
-
'http_headers': headers,
|
290 |
-
'socket_timeout': 30,
|
291 |
-
'retries': 3,
|
292 |
-
'fragment_retries': 3,
|
293 |
-
'retry_sleep_functions': {'fragment': lambda n: min(5 * (n + 1), 30)},
|
294 |
-
'format_selector': format_selector,
|
295 |
-
'concurrent_fragment_downloads': 10,
|
296 |
-
'file_access_retries': 5,
|
297 |
-
'hls_prefer_native': True,
|
298 |
-
'hls_split_discontinuity': True,
|
299 |
-
'downloader': 'native',
|
300 |
-
'downloader_options': {
|
301 |
-
'http_chunk_size': 10485760, # 10MB
|
302 |
-
}
|
303 |
-
}
|
304 |
-
|
305 |
-
# Prepare cookies
|
306 |
-
cookies_file = os.path.join(temp_dir, 'cookies.txt')
|
307 |
-
with open(cookies_file, 'w', encoding='utf-8') as f:
|
308 |
-
domain = f".{urlparse(url).netloc}"
|
309 |
-
expire_time = int(time.time()) + 86400 * 7 # 7 days
|
310 |
-
for k, v in browser_cookies.items():
|
311 |
-
f.write(f"{domain}\tTRUE\t/\tFALSE\t{expire_time}\t{k}\t{v}\n")
|
312 |
-
ydl_opts['cookies'] = cookies_file
|
313 |
-
|
314 |
-
# First try direct download
|
315 |
-
try:
|
316 |
-
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
317 |
-
info = ydl.extract_info(url, download=True)
|
318 |
-
filename = ydl.prepare_filename(info)
|
319 |
-
except Exception as e:
|
320 |
-
if not use_proxy:
|
321 |
-
raise
|
322 |
-
|
323 |
-
# If direct download fails, try with proxies
|
324 |
-
proxies = []
|
325 |
-
for _ in range(3):
|
326 |
-
proxy = self.get_random_proxy()
|
327 |
-
if proxy and proxy not in proxies:
|
328 |
-
try:
|
329 |
-
async with aiohttp.ClientSession() as session:
|
330 |
-
async with session.head('https://www.google.com',
|
331 |
-
proxy=proxy,
|
332 |
-
timeout=5,
|
333 |
-
ssl=False) as resp:
|
334 |
-
if resp.status == 200:
|
335 |
-
proxies.append(proxy)
|
336 |
-
except Exception:
|
337 |
-
continue
|
338 |
-
|
339 |
-
if not proxies:
|
340 |
-
# Try one last time with direct connection
|
341 |
-
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
342 |
-
info = ydl.extract_info(url, download=True)
|
343 |
-
filename = ydl.prepare_filename(info)
|
344 |
-
else:
|
345 |
-
# Try each working proxy
|
346 |
-
last_error = None
|
347 |
-
for proxy in proxies:
|
348 |
-
try:
|
349 |
-
ydl_opts['proxy'] = proxy
|
350 |
-
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
351 |
-
info = ydl.extract_info(url, download=True)
|
352 |
-
filename = ydl.prepare_filename(info)
|
353 |
-
break
|
354 |
-
except Exception as e:
|
355 |
-
last_error = e
|
356 |
-
continue
|
357 |
-
else:
|
358 |
-
# If all proxies fail, try one last time without proxy
|
359 |
-
ydl_opts['proxy'] = None
|
360 |
-
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
361 |
-
info = ydl.extract_info(url, download=True)
|
362 |
-
filename = ydl.prepare_filename(info)
|
363 |
-
|
364 |
-
file_info = {
|
365 |
-
"name": os.path.basename(filename),
|
366 |
-
"path": filename,
|
367 |
-
"mime_type": self.get_mime_type(filename),
|
368 |
-
"size": os.path.getsize(filename)
|
369 |
-
}
|
370 |
-
|
371 |
-
archive_text = self.add_to_archive(file_info)
|
372 |
-
return file_info, archive_text
|
373 |
-
|
374 |
-
except Exception as e:
|
375 |
-
error_msg = f"فشل التحميل باستخدام yt-dlp: {str(e)}"
|
376 |
-
raise gr.Error(error_msg)
|
377 |
-
|
378 |
-
async def get_session_token(self, url):
|
379 |
-
"""الحصول على توكن الجلسة"""
|
380 |
-
try:
|
381 |
-
# Parse URL and get base domain
|
382 |
-
parsed = urlparse(url)
|
383 |
-
base_url = f"{parsed.scheme}://{parsed.netloc}"
|
384 |
-
|
385 |
-
# Setup session with browser-like headers
|
386 |
-
headers = {
|
387 |
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
388 |
-
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
389 |
-
'Accept-Language': 'en-US,en;q=0.9',
|
390 |
-
'Accept-Encoding': 'gzip, deflate, br',
|
391 |
-
'Connection': 'keep-alive',
|
392 |
-
'Upgrade-Insecure-Requests': '1',
|
393 |
-
'Sec-Fetch-Dest': 'document',
|
394 |
-
'Sec-Fetch-Mode': 'navigate',
|
395 |
-
'Sec-Fetch-Site': 'none',
|
396 |
-
'Sec-Fetch-User': '?1',
|
397 |
-
'Cache-Control': 'max-age=0',
|
398 |
-
}
|
399 |
-
|
400 |
-
connector = aiohttp.TCPConnector(ssl=False)
|
401 |
-
timeout = aiohttp.ClientTimeout(total=30)
|
402 |
-
|
403 |
-
async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
|
404 |
-
# First get the main page to get initial cookies
|
405 |
-
async with session.get(base_url, headers=headers) as response:
|
406 |
-
if response.status != 200:
|
407 |
-
return None, None
|
408 |
-
|
409 |
-
# Get cookies from response
|
410 |
-
cookies = {k: v.value for k, v in response.cookies.items()}
|
411 |
-
|
412 |
-
# Extract any session or token information from the response
|
413 |
-
text = await response.text()
|
414 |
-
token_patterns = [
|
415 |
-
r'token["\']?\s*:\s*["\']([^"\']+)["\']',
|
416 |
-
r'auth["\']?\s*:\s*["\']([^"\']+)["\']',
|
417 |
-
r'session["\']?\s*:\s*["\']([^"\']+)["\']',
|
418 |
-
]
|
419 |
-
|
420 |
-
session_token = None
|
421 |
-
for pattern in token_patterns:
|
422 |
-
match = re.search(pattern, text, re.IGNORECASE)
|
423 |
-
if match:
|
424 |
-
session_token = match.group(1)
|
425 |
-
break
|
426 |
-
|
427 |
-
return cookies, session_token
|
428 |
-
|
429 |
-
except Exception as e:
|
430 |
-
print(f"Error getting session token: {str(e)}")
|
431 |
-
return None, None
|
432 |
-
|
433 |
-
async def refresh_video_token(self, url):
|
434 |
-
"""تحديث توكن الفيديو"""
|
435 |
-
try:
|
436 |
-
parsed = urlparse(url)
|
437 |
-
query_params = parse_qs(parsed.query)
|
438 |
-
|
439 |
-
# Get current timestamp
|
440 |
-
current_time = int(time.time())
|
441 |
-
|
442 |
-
# Generate new token parameters
|
443 |
-
new_params = {
|
444 |
-
't': query_params.get('t', [''])[0],
|
445 |
-
's': str(current_time),
|
446 |
-
'e': '86400',
|
447 |
-
'f': query_params.get('f', [''])[0],
|
448 |
-
'sp': query_params.get('sp', ['1000'])[0],
|
449 |
-
'i': '0.0',
|
450 |
-
'_': str(current_time * 1000) # Add millisecond timestamp
|
451 |
-
}
|
452 |
-
|
453 |
-
# Add any additional parameters from original URL
|
454 |
-
for k, v in query_params.items():
|
455 |
-
if k not in new_params and k not in ['s', 'e', '_']:
|
456 |
-
new_params[k] = v[0]
|
457 |
-
|
458 |
-
# Reconstruct URL with new parameters
|
459 |
-
base_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
|
460 |
-
new_url = f"{base_url}?{urlencode(new_params)}"
|
461 |
-
|
462 |
-
return new_url
|
463 |
-
|
464 |
-
except Exception as e:
|
465 |
-
print(f"Error refreshing token: {str(e)}")
|
466 |
-
return url
|
467 |
-
|
468 |
-
async def stream_download(self, url, output_path, headers=None, proxy=None):
|
469 |
-
"""تنزيل الملف باستخدام التدفق"""
|
470 |
-
if not headers:
|
471 |
-
headers = {
|
472 |
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
473 |
-
'Accept': '*/*',
|
474 |
-
'Accept-Language': 'en-US,en;q=0.9',
|
475 |
-
'Accept-Encoding': 'gzip, deflate, br',
|
476 |
-
'Range': 'bytes=0-',
|
477 |
-
'Connection': 'keep-alive',
|
478 |
-
'Sec-Fetch-Dest': 'video',
|
479 |
-
'Sec-Fetch-Mode': 'cors',
|
480 |
-
'Sec-Fetch-Site': 'cross-site',
|
481 |
-
'Pragma': 'no-cache',
|
482 |
-
'Cache-Control': 'no-cache',
|
483 |
-
}
|
484 |
-
|
485 |
-
# Get session token and cookies
|
486 |
-
cookies, session_token = await self.get_session_token(url)
|
487 |
-
if cookies:
|
488 |
-
cookie_string = "; ".join([f"{k}={v}" for k, v in cookies.items()])
|
489 |
-
headers['Cookie'] = cookie_string
|
490 |
-
|
491 |
-
if session_token:
|
492 |
-
headers['Authorization'] = f'Bearer {session_token}'
|
493 |
-
|
494 |
-
# Add referer and origin from the URL
|
495 |
-
parsed_url = urlparse(url)
|
496 |
-
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
497 |
-
headers['Referer'] = base_url
|
498 |
-
headers['Origin'] = base_url
|
499 |
-
|
500 |
-
connector = aiohttp.TCPConnector(ssl=False, limit=1)
|
501 |
-
timeout = aiohttp.ClientTimeout(total=3600, connect=60)
|
502 |
-
|
503 |
-
async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
|
504 |
-
try:
|
505 |
-
# First try HEAD request
|
506 |
-
async with session.head(url, headers=headers, proxy=proxy, allow_redirects=True) as response:
|
507 |
-
if response.status == 403:
|
508 |
-
# If 403, try refreshing the URL token
|
509 |
-
url = await self.refresh_video_token(url)
|
510 |
-
# Add updated timestamp to headers
|
511 |
-
headers['X-Requested-With'] = 'XMLHttpRequest'
|
512 |
-
headers['If-Modified-Since'] = time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime())
|
513 |
-
total_size = int(response.headers.get('Content-Length', 0))
|
514 |
-
except Exception:
|
515 |
-
total_size = 0
|
516 |
-
|
517 |
-
# Prepare for chunked download
|
518 |
-
chunk_size = 1024 * 1024 # 1MB chunks
|
519 |
-
downloaded_size = 0
|
520 |
-
retries = 3
|
521 |
-
|
522 |
-
while retries > 0:
|
523 |
-
try:
|
524 |
-
async with session.get(url, headers=headers, proxy=proxy, allow_redirects=True) as response:
|
525 |
-
if response.status == 403 and retries > 1:
|
526 |
-
# Try refreshing token and retry
|
527 |
-
url = await self.refresh_video_token(url)
|
528 |
-
retries -= 1
|
529 |
-
continue
|
530 |
-
|
531 |
-
if response.status != 200:
|
532 |
-
raise aiohttp.ClientError(f"HTTP {response.status}")
|
533 |
-
|
534 |
-
with open(output_path, 'wb') as f:
|
535 |
-
async for chunk in response.content.iter_chunked(chunk_size):
|
536 |
-
if chunk:
|
537 |
-
f.write(chunk)
|
538 |
-
downloaded_size += len(chunk)
|
539 |
-
|
540 |
-
# Update progress
|
541 |
-
if total_size:
|
542 |
-
progress = (downloaded_size / total_size) * 100
|
543 |
-
print(f"\rDownloading: {progress:.1f}%", end="")
|
544 |
-
break
|
545 |
-
except Exception as e:
|
546 |
-
print(f"Download attempt failed: {str(e)}")
|
547 |
-
retries -= 1
|
548 |
-
if retries == 0:
|
549 |
-
raise
|
550 |
-
await asyncio.sleep(2)
|
551 |
-
|
552 |
-
async def try_download(self, url, browser_cookies=None, proxy=None):
|
553 |
-
"""محاولة تحميل الملف"""
|
554 |
-
temp_dir = tempfile.mkdtemp()
|
555 |
-
output_path = os.path.join(temp_dir, "video.mp4")
|
556 |
-
|
557 |
-
headers = {
|
558 |
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
559 |
-
'Accept': '*/*',
|
560 |
-
'Accept-Language': 'en-US,en;q=0.9',
|
561 |
-
'Accept-Encoding': 'gzip, deflate, br',
|
562 |
-
'Range': 'bytes=0-',
|
563 |
-
'Connection': 'keep-alive',
|
564 |
-
'Sec-Fetch-Dest': 'video',
|
565 |
-
'Sec-Fetch-Mode': 'cors',
|
566 |
-
'Sec-Fetch-Site': 'cross-site',
|
567 |
-
'Pragma': 'no-cache',
|
568 |
-
'Cache-Control': 'no-cache',
|
569 |
-
}
|
570 |
-
|
571 |
-
# Add cookies to headers
|
572 |
-
if browser_cookies:
|
573 |
-
cookie_string = "; ".join([f"{k}={v}" for k, v in browser_cookies.items()])
|
574 |
-
headers['Cookie'] = cookie_string
|
575 |
-
|
576 |
-
try:
|
577 |
-
await self.stream_download(url, output_path, headers, proxy)
|
578 |
-
|
579 |
-
if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
|
580 |
-
file_info = {
|
581 |
-
"name": os.path.basename(output_path),
|
582 |
-
"path": output_path,
|
583 |
-
"mime_type": self.get_mime_type(output_path),
|
584 |
-
"size": os.path.getsize(output_path)
|
585 |
-
}
|
586 |
-
return file_info
|
587 |
-
|
588 |
-
except Exception as e:
|
589 |
-
print(f"Download failed: {str(e)}")
|
590 |
-
if os.path.exists(output_path):
|
591 |
-
os.remove(output_path)
|
592 |
-
|
593 |
-
return None
|
594 |
-
|
595 |
-
async def download_file(self, url):
|
596 |
-
"""تحميل الملف"""
|
597 |
-
try:
|
598 |
-
# Get cookies
|
599 |
-
browser_cookies = self.get_cookies(url)
|
600 |
-
|
601 |
-
# First try direct download
|
602 |
-
result = await self.try_download(url, browser_cookies)
|
603 |
-
if result:
|
604 |
-
archive_text = self.add_to_archive(result)
|
605 |
-
return result, archive_text
|
606 |
-
|
607 |
-
# If direct fails, try with proxies
|
608 |
-
max_retries = 3
|
609 |
-
for i in range(max_retries):
|
610 |
-
proxy = self.get_random_proxy()
|
611 |
-
if not proxy:
|
612 |
-
continue
|
613 |
-
|
614 |
-
try:
|
615 |
-
result = await self.try_download(url, browser_cookies, proxy)
|
616 |
-
if result:
|
617 |
-
archive_text = self.add_to_archive(result)
|
618 |
-
return result, archive_text
|
619 |
-
except Exception as e:
|
620 |
-
print(f"Proxy attempt {i+1} failed: {str(e)}")
|
621 |
-
continue
|
622 |
-
|
623 |
-
# If all attempts fail, try yt-dlp as last resort
|
624 |
-
return await self.download_with_ytdlp(url, browser_cookies, use_proxy=True)
|
625 |
-
|
626 |
-
except Exception as e:
|
627 |
-
error_msg = f"حدث خطأ في التحميل: {str(e)}"
|
628 |
-
raise gr.Error(error_msg)
|
629 |
|
630 |
def get_filename_from_url(self, url, headers=None):
|
631 |
try:
|
632 |
if headers and 'content-disposition' in headers:
|
|
|
633 |
matches = re.findall('filename="(.+)"', headers['content-disposition'])
|
634 |
if matches:
|
635 |
return matches[0]
|
636 |
|
637 |
path = urlparse(url).path
|
638 |
filename = os.path.basename(path)
|
639 |
-
filename
|
640 |
-
filename = filename.replace('[', '').replace(']', '')
|
641 |
-
if not filename or filename == '':
|
642 |
-
return 'downloaded_file'
|
643 |
-
return filename
|
644 |
except:
|
645 |
return 'downloaded_file'
|
646 |
|
@@ -648,199 +39,95 @@ class FastDownloader:
|
|
648 |
mime_type, _ = mimetypes.guess_type(filename)
|
649 |
return mime_type or 'application/octet-stream'
|
650 |
|
651 |
-
def
|
652 |
-
|
653 |
-
|
654 |
-
with open(self.archive_file, 'r', encoding='utf-8') as f:
|
655 |
-
self.archive = json.load(f)
|
656 |
-
else:
|
657 |
-
self.archive = []
|
658 |
-
except:
|
659 |
-
self.archive = []
|
660 |
-
|
661 |
-
def save_archive(self):
|
662 |
-
with open(self.archive_file, 'w', encoding='utf-8') as f:
|
663 |
-
json.dump(self.archive, f, ensure_ascii=False, indent=2)
|
664 |
-
|
665 |
-
def add_to_archive(self, file_info):
|
666 |
-
archive_entry = {
|
667 |
-
"name": file_info["name"],
|
668 |
-
"path": file_info["path"],
|
669 |
-
"size": file_info["size"],
|
670 |
-
"mime_type": file_info["mime_type"],
|
671 |
-
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
672 |
-
}
|
673 |
-
self.archive.insert(0, archive_entry)
|
674 |
-
self.save_archive()
|
675 |
-
return self.format_archive()
|
676 |
-
|
677 |
-
def format_archive(self):
|
678 |
-
archive_text = "📚 أرشيف التحميلات:\n\n"
|
679 |
-
for entry in self.archive:
|
680 |
-
size_mb = entry["size"] / (1024 * 1024)
|
681 |
-
archive_text += f"📥 {entry['name']}\n"
|
682 |
-
archive_text += f" 📅 {entry['date']}\n"
|
683 |
-
archive_text += f" 📦 {size_mb:.1f} MB\n"
|
684 |
-
archive_text += f" 📎 {entry['mime_type']}\n"
|
685 |
-
archive_text += "─" * 50 + "\n"
|
686 |
-
return archive_text
|
687 |
-
|
688 |
-
async def handle_successful_download(self, response):
|
689 |
-
"""معالجة التحميل الناجح"""
|
690 |
-
filename = self.get_filename_from_url(response.url, dict(response.headers))
|
691 |
-
temp_dir = tempfile.mkdtemp()
|
692 |
-
file_path = os.path.join(temp_dir, filename)
|
693 |
-
|
694 |
-
total_size = int(response.headers.get('content-length', 0))
|
695 |
-
chunk_size = 1024 * 1024 # 1MB chunks
|
696 |
-
|
697 |
-
with open(file_path, 'wb') as f:
|
698 |
-
downloaded = 0
|
699 |
-
async for chunk in response.content.iter_chunked(chunk_size):
|
700 |
-
if chunk:
|
701 |
-
f.write(chunk)
|
702 |
-
downloaded += len(chunk)
|
703 |
-
if total_size:
|
704 |
-
progress = (downloaded / total_size) * 100
|
705 |
-
print(f"\rDownloading: {progress:.1f}%", end="")
|
706 |
|
707 |
-
|
708 |
-
|
709 |
-
|
710 |
-
|
711 |
-
|
712 |
-
|
713 |
-
|
714 |
-
|
715 |
-
|
716 |
-
|
717 |
-
|
718 |
-
|
719 |
-
headers = {
|
720 |
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
721 |
-
'Accept': '*/*',
|
722 |
-
'Accept-Language': 'en-US,en;q=0.9',
|
723 |
-
'Accept-Encoding': 'gzip, deflate, br',
|
724 |
-
'Origin': 'https://cdn-tube.xyz',
|
725 |
-
'Sec-Fetch-Site': 'cross-site',
|
726 |
-
'Sec-Fetch-Mode': 'cors',
|
727 |
-
'Sec-Fetch-Dest': 'empty',
|
728 |
-
'Referer': 'https://cdn-tube.xyz/',
|
729 |
-
}
|
730 |
|
|
|
|
|
731 |
try:
|
732 |
-
#
|
733 |
-
if not url.endswith('.m3u8'):
|
734 |
-
parsed = urlparse(url)
|
735 |
-
path_parts = parsed.path.split('/')
|
736 |
-
if len(path_parts) > 1:
|
737 |
-
video_id = path_parts[-2]
|
738 |
-
m3u8_url = f"{parsed.scheme}://{parsed.netloc}/hls/{video_id}/index.m3u8"
|
739 |
-
# Add original query parameters
|
740 |
-
m3u8_url += '?' + parsed.query if parsed.query else ''
|
741 |
-
else:
|
742 |
-
return None
|
743 |
-
|
744 |
async with aiohttp.ClientSession() as session:
|
745 |
-
|
746 |
-
|
747 |
-
|
748 |
-
|
749 |
-
|
750 |
-
|
751 |
-
# Parse m3u8 content
|
752 |
-
segments = []
|
753 |
-
base_url = m3u8_url.rsplit('/', 1)[0] + '/'
|
754 |
-
|
755 |
-
for line in m3u8_content.splitlines():
|
756 |
-
if line.endswith('.ts'):
|
757 |
-
if line.startswith('http'):
|
758 |
-
segments.append(line)
|
759 |
-
else:
|
760 |
-
segments.append(base_url + line)
|
761 |
-
|
762 |
-
return segments
|
763 |
-
except Exception as e:
|
764 |
-
print(f"Error getting m3u8 segments: {str(e)}")
|
765 |
-
return None
|
766 |
|
767 |
-
|
768 |
-
|
769 |
-
segments = await self.get_m3u8_segments(url)
|
770 |
-
if not segments:
|
771 |
-
return False
|
772 |
|
773 |
-
|
774 |
-
|
775 |
-
|
776 |
-
'Accept-Language': 'en-US,en;q=0.9',
|
777 |
-
'Accept-Encoding': 'gzip, deflate, br',
|
778 |
-
'Origin': 'https://cdn-tube.xyz',
|
779 |
-
'Sec-Fetch-Site': 'cross-site',
|
780 |
-
'Sec-Fetch-Mode': 'cors',
|
781 |
-
'Sec-Fetch-Dest': 'empty',
|
782 |
-
'Referer': 'https://cdn-tube.xyz/',
|
783 |
-
}
|
784 |
|
785 |
-
|
786 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
787 |
|
788 |
-
try:
|
789 |
-
with open(output_path, 'wb') as outfile:
|
790 |
-
async with aiohttp.ClientSession() as session:
|
791 |
-
for segment_url in segments:
|
792 |
-
retries = 3
|
793 |
-
while retries > 0:
|
794 |
-
try:
|
795 |
-
async with session.get(segment_url, headers=headers) as response:
|
796 |
-
if response.status == 200:
|
797 |
-
chunk = await response.read()
|
798 |
-
outfile.write(chunk)
|
799 |
-
downloaded_segments += 1
|
800 |
-
progress = (downloaded_segments / total_segments) * 100
|
801 |
-
print(f"\rDownloading: {progress:.1f}%", end="")
|
802 |
-
break
|
803 |
-
elif response.status == 403:
|
804 |
-
# Try refreshing token
|
805 |
-
segment_url = await self.refresh_video_token(segment_url)
|
806 |
-
retries -= 1
|
807 |
-
await asyncio.sleep(1)
|
808 |
-
continue
|
809 |
-
else:
|
810 |
-
retries -= 1
|
811 |
-
except Exception:
|
812 |
-
retries -= 1
|
813 |
-
await asyncio.sleep(1)
|
814 |
-
|
815 |
-
if retries == 0:
|
816 |
-
print(f"\nFailed to download segment: {segment_url}")
|
817 |
-
return False
|
818 |
-
|
819 |
-
return True
|
820 |
except Exception as e:
|
821 |
-
|
822 |
-
return False
|
823 |
-
|
824 |
-
async def try_download(self, url, browser_cookies=None, proxy=None):
|
825 |
-
"""محاولة تحميل الملف"""
|
826 |
-
temp_dir = tempfile.mkdtemp()
|
827 |
-
output_path = os.path.join(temp_dir, "video.mp4")
|
828 |
-
|
829 |
-
# Try HLS download first
|
830 |
-
if await self.download_with_segments(url, output_path):
|
831 |
-
return output_path, temp_dir
|
832 |
|
833 |
-
|
|
|
834 |
try:
|
835 |
-
|
836 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
837 |
except Exception as e:
|
838 |
-
|
839 |
-
try:
|
840 |
-
await self.download_with_ytdlp(url, output_path)
|
841 |
-
return output_path, temp_dir
|
842 |
-
except Exception as ytdlp_error:
|
843 |
-
raise Exception(f"All download methods failed. Last error: {str(ytdlp_error)}")
|
844 |
|
845 |
def create_interface():
|
846 |
downloader = FastDownloader()
|
@@ -868,40 +155,32 @@ def create_interface():
|
|
868 |
with gr.Row():
|
869 |
status = gr.Textbox(label="الحالة", value="جاهز للتحميل", interactive=False)
|
870 |
file_output = gr.File(label="الملف المحمل")
|
871 |
-
|
872 |
-
with gr.Row():
|
873 |
-
archive = gr.Textbox(
|
874 |
-
label="أرشيف التحميلات",
|
875 |
-
value=downloader.format_archive(),
|
876 |
-
interactive=False,
|
877 |
-
lines=10
|
878 |
-
)
|
879 |
|
880 |
def download_and_update(url):
|
881 |
if not url:
|
882 |
-
return "الرجاء إدخال رابط صحيح", None
|
883 |
|
884 |
try:
|
885 |
domain = urlparse(url).netloc
|
886 |
|
887 |
if 'youtube.com' in domain or 'youtu.be' in domain:
|
888 |
-
file_info
|
889 |
else:
|
|
|
890 |
loop = asyncio.new_event_loop()
|
891 |
asyncio.set_event_loop(loop)
|
892 |
-
file_info
|
893 |
loop.close()
|
894 |
|
895 |
-
return "تم التحميل بنجاح!", file_info["path"]
|
896 |
|
897 |
except Exception as e:
|
898 |
-
|
899 |
-
raise gr.Error(error_msg)
|
900 |
|
901 |
download_btn.click(
|
902 |
fn=download_and_update,
|
903 |
inputs=url_input,
|
904 |
-
outputs=[status, file_output
|
905 |
)
|
906 |
|
907 |
return demo
|
|
|
1 |
import gradio as gr
|
2 |
import yt_dlp
|
3 |
import aiohttp
|
|
|
4 |
import asyncio
|
5 |
import os
|
6 |
import tempfile
|
7 |
+
from urllib.parse import urlparse
|
8 |
import time
|
9 |
import mimetypes
|
10 |
import concurrent.futures
|
11 |
import math
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
class FastDownloader:
|
14 |
def __init__(self):
|
|
|
16 |
'format': 'best',
|
17 |
'quiet': True,
|
18 |
'no_warnings': True,
|
19 |
+
'concurrent_fragments': 10, # زيادة عدد التحميلات المتزامنة
|
|
|
20 |
}
|
21 |
+
self.chunk_size = 1024 * 1024 * 5 # 5MB chunks
|
22 |
+
self.max_workers = 5 # عدد العمليات المتزامنة
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
|
24 |
def get_filename_from_url(self, url, headers=None):
|
25 |
try:
|
26 |
if headers and 'content-disposition' in headers:
|
27 |
+
import re
|
28 |
matches = re.findall('filename="(.+)"', headers['content-disposition'])
|
29 |
if matches:
|
30 |
return matches[0]
|
31 |
|
32 |
path = urlparse(url).path
|
33 |
filename = os.path.basename(path)
|
34 |
+
return filename if filename else 'downloaded_file'
|
|
|
|
|
|
|
|
|
35 |
except:
|
36 |
return 'downloaded_file'
|
37 |
|
|
|
39 |
mime_type, _ = mimetypes.guess_type(filename)
|
40 |
return mime_type or 'application/octet-stream'
|
41 |
|
42 |
+
async def download_chunk(self, session, url, start, end, file, semaphore):
|
43 |
+
"""تحميل جزء من الملف"""
|
44 |
+
headers = {'Range': f'bytes={start}-{end}'}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
|
46 |
+
async with semaphore:
|
47 |
+
try:
|
48 |
+
async with session.get(url, headers=headers) as response:
|
49 |
+
response.raise_for_status()
|
50 |
+
chunk = await response.read()
|
51 |
+
with open(file, 'r+b') as f:
|
52 |
+
f.seek(start)
|
53 |
+
f.write(chunk)
|
54 |
+
return len(chunk)
|
55 |
+
except Exception as e:
|
56 |
+
print(f"Error downloading chunk {start}-{end}: {str(e)}")
|
57 |
+
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
|
59 |
+
async def download_file(self, url):
|
60 |
+
"""تحميل الملف بشكل متزامن"""
|
61 |
try:
|
62 |
+
# إنشاء جلسة HTTP
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
63 |
async with aiohttp.ClientSession() as session:
|
64 |
+
# الحصول على معلومات الملف
|
65 |
+
async with session.head(url) as response:
|
66 |
+
response.raise_for_status()
|
67 |
+
total_size = int(response.headers.get('content-length', 0))
|
68 |
+
filename = self.get_filename_from_url(url, response.headers)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
|
70 |
+
if total_size == 0:
|
71 |
+
raise gr.Error("لا يمكن تحديد حجم الملف")
|
|
|
|
|
|
|
72 |
|
73 |
+
# إنشاء ملف مؤقت
|
74 |
+
temp_dir = tempfile.mkdtemp()
|
75 |
+
file_path = os.path.join(temp_dir, filename)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
76 |
|
77 |
+
# تحديد حجم كل جزء
|
78 |
+
chunk_size = math.ceil(total_size / self.max_workers)
|
79 |
+
|
80 |
+
# إنشاء الملف بالحجم المطلوب
|
81 |
+
with open(file_path, 'wb') as f:
|
82 |
+
f.seek(total_size - 1)
|
83 |
+
f.write(b'\0')
|
84 |
+
|
85 |
+
# إنشاء مهام التحميل
|
86 |
+
semaphore = asyncio.Semaphore(self.max_workers)
|
87 |
+
tasks = []
|
88 |
+
|
89 |
+
for i in range(self.max_workers):
|
90 |
+
start = i * chunk_size
|
91 |
+
end = min(start + chunk_size - 1, total_size - 1)
|
92 |
+
task = self.download_chunk(session, url, start, end, file_path, semaphore)
|
93 |
+
tasks.append(task)
|
94 |
+
|
95 |
+
# تنفيذ المهام بشكل متزامن
|
96 |
+
await asyncio.gather(*tasks)
|
97 |
+
|
98 |
+
return {
|
99 |
+
"name": filename,
|
100 |
+
"path": file_path,
|
101 |
+
"mime_type": self.get_mime_type(filename),
|
102 |
+
"size": os.path.getsize(file_path)
|
103 |
+
}
|
104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
105 |
except Exception as e:
|
106 |
+
raise gr.Error(f"حدث خطأ في التحميل: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
107 |
|
108 |
+
def download_youtube(self, url):
|
109 |
+
"""تحميل فيديو يوتيوب"""
|
110 |
try:
|
111 |
+
temp_dir = tempfile.mkdtemp()
|
112 |
+
|
113 |
+
opts = dict(self.ydl_opts)
|
114 |
+
opts.update({
|
115 |
+
'outtmpl': os.path.join(temp_dir, '%(title)s.%(ext)s'),
|
116 |
+
})
|
117 |
+
|
118 |
+
with yt_dlp.YoutubeDL(opts) as ydl:
|
119 |
+
info = ydl.extract_info(url, download=True)
|
120 |
+
filename = ydl.prepare_filename(info)
|
121 |
+
|
122 |
+
return {
|
123 |
+
"name": os.path.basename(filename),
|
124 |
+
"path": filename,
|
125 |
+
"mime_type": self.get_mime_type(filename),
|
126 |
+
"size": os.path.getsize(filename)
|
127 |
+
}
|
128 |
+
|
129 |
except Exception as e:
|
130 |
+
raise gr.Error(f"حدث خطأ في تحميل الفيديو: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
131 |
|
132 |
def create_interface():
|
133 |
downloader = FastDownloader()
|
|
|
155 |
with gr.Row():
|
156 |
status = gr.Textbox(label="الحالة", value="جاهز للتحميل", interactive=False)
|
157 |
file_output = gr.File(label="الملف المحمل")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
158 |
|
159 |
def download_and_update(url):
|
160 |
if not url:
|
161 |
+
return "الرجاء إدخال رابط صحيح", None
|
162 |
|
163 |
try:
|
164 |
domain = urlparse(url).netloc
|
165 |
|
166 |
if 'youtube.com' in domain or 'youtu.be' in domain:
|
167 |
+
file_info = downloader.download_youtube(url)
|
168 |
else:
|
169 |
+
# استخدام asyncio للتحميل المتزامن
|
170 |
loop = asyncio.new_event_loop()
|
171 |
asyncio.set_event_loop(loop)
|
172 |
+
file_info = loop.run_until_complete(downloader.download_file(url))
|
173 |
loop.close()
|
174 |
|
175 |
+
return "تم التحميل بنجاح!", file_info["path"]
|
176 |
|
177 |
except Exception as e:
|
178 |
+
return f"حدث خطأ: {str(e)}", None
|
|
|
179 |
|
180 |
download_btn.click(
|
181 |
fn=download_and_update,
|
182 |
inputs=url_input,
|
183 |
+
outputs=[status, file_output]
|
184 |
)
|
185 |
|
186 |
return demo
|
requirements.txt
CHANGED
@@ -1,9 +1,5 @@
|
|
1 |
gradio>=4.7.1
|
2 |
yt-dlp>=2023.12.30
|
3 |
aiohttp>=3.9.1
|
4 |
-
|
5 |
-
|
6 |
-
beautifulsoup4>=4.12.2
|
7 |
-
urllib3<2.0.0
|
8 |
-
aiohttp-proxy>=0.1.2
|
9 |
-
fake-useragent>=1.4.0
|
|
|
1 |
gradio>=4.7.1
|
2 |
yt-dlp>=2023.12.30
|
3 |
aiohttp>=3.9.1
|
4 |
+
requests>=2.31.0
|
5 |
+
pytube>=15.0.0
|
|
|
|
|
|
|
|