geopromini commited on
Commit
293f92d
·
verified ·
1 Parent(s): b8d23d6

Upload 2 files

Browse files
Files changed (2) hide show
  1. app..py +425 -0
  2. requirements.txt +10 -0
app..py ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py (Modified for Hugging Face + Telegram + Cloudflare)
2
+ import nest_asyncio
3
+ nest_asyncio.apply()
4
+
5
+ import os
6
+ import shutil
7
+ import asyncio
8
+ import traceback
9
+ import re
10
+ import time
11
+ from pathlib import Path
12
+
13
+ # Web Server & Bot
14
+ import httpx
15
+ from fastapi import FastAPI, BackgroundTasks
16
+ from pydantic import BaseModel
17
+ from contextlib import asynccontextmanager
18
+ import uvicorn
19
+
20
+ # Video Processing
21
+ try:
22
+ from moviepy.editor import VideoFileClip
23
+ except ImportError:
24
+ print("FATAL ERROR: moviepy is not installed. Run: pip install moviepy")
25
+ # You might want to handle this more gracefully depending on HF deployment
26
+ sys.exit(1)
27
+ import yt_dlp
28
+
29
+ # DNS Fix (Keep from reference)
30
+ import socket
31
+ import aiodns
32
+ original_getaddrinfo = socket.getaddrinfo
33
+ async def custom_getaddrinfo_async(host, port, family=0, type=0, proto=0, flags=0):
34
+ resolver = aiodns.DNSResolver(nameservers=['8.8.8.8', '8.8.4.4'])
35
+ try:
36
+ result = await resolver.query(host, 'A'); addrlist = []
37
+ for record in result: addrlist.append((socket.AF_INET, socket.SOCK_STREAM, 6, '', (record.host, port)))
38
+ return addrlist
39
+ except aiodns.error.DNSError:
40
+ loop = asyncio.get_running_loop(); return await loop.run_in_executor(None, original_getaddrinfo, host, port, family, type, proto, flags)
41
+ def custom_getaddrinfo_sync(*args, **kwargs):
42
+ try: loop = asyncio.get_running_loop()
43
+ except RuntimeError: loop = asyncio.new_event_loop(); asyncio.set_event_loop(loop)
44
+ return loop.run_until_complete(custom_getaddrinfo_async(*args, **kwargs))
45
+ socket.getaddrinfo = custom_getaddrinfo_sync
46
+
47
+ # --- CONFIGURATION ---
48
+ WORKER_URL = os.environ.get("WORKER_URL") # Get Worker URL from HF Secrets
49
+ LOCAL_TEMP_FOLDER = "temp_processing"
50
+ # Ensure the temp folder exists at startup
51
+ os.makedirs(LOCAL_TEMP_FOLDER, exist_ok=True)
52
+
53
+ # User session storage
54
+ USER_SESSIONS = {}
55
+
56
+ # --- TELEGRAM UTILS (Assumed to exist in utils/telegram_utils.py or similar) ---
57
+ # You need functions that POST to your WORKER_URL
58
+ # Example (replace with your actual implementation):
59
+ async def send_telegram_message(chat_id, text, reply_markup=None):
60
+ url = f"{WORKER_URL}"
61
+ payload = {'action': 'sendMessage', 'chat_id': str(chat_id), 'text': text}
62
+ if reply_markup:
63
+ payload['reply_markup'] = json.dumps(reply_markup)
64
+ try:
65
+ async with httpx.AsyncClient() as client:
66
+ response = await client.post(url, json=payload, timeout=60)
67
+ response.raise_for_status()
68
+ print(f"Sent message to {chat_id}: {response.json()}")
69
+ except Exception as e:
70
+ print(f"ERROR sending message to {chat_id}: {e}")
71
+
72
+ async def send_telegram_video(chat_id, video_path, caption):
73
+ url = f"{WORKER_URL}"
74
+ with open(video_path, 'rb') as video_file:
75
+ files = { 'video': (os.path.basename(video_path), video_file, 'video/mp4') }
76
+ data = { 'action': 'sendVideo', 'chat_id': str(chat_id), 'caption': caption }
77
+ try:
78
+ async with httpx.AsyncClient() as client:
79
+ # Increased timeout for potentially large files
80
+ response = await client.post(url, data=data, files=files, timeout=600)
81
+ response.raise_for_status()
82
+ print(f"Sent video to {chat_id}: {response.json()}")
83
+ return True # Indicate success
84
+ except Exception as e:
85
+ print(f"ERROR sending video to {chat_id}: {e}")
86
+ await send_telegram_message(chat_id, f"❌ Lỗi khi gửi file video: {e}")
87
+ return False # Indicate failure
88
+
89
+ async def answer_telegram_callback_query(callback_query_id, text="OK"):
90
+ url = f"{WORKER_URL}"
91
+ payload = {'action': 'answerCallbackQuery', 'callback_query_id': callback_query_id, 'text': text}
92
+ try:
93
+ async with httpx.AsyncClient() as client:
94
+ response = await client.post(url, json=payload, timeout=30)
95
+ response.raise_for_status()
96
+ except Exception as e:
97
+ print(f"ERROR answering callback query {callback_query_id}: {e}")
98
+
99
+ async def edit_telegram_message_text(chat_id, message_id, text, reply_markup=None):
100
+ url = f"{WORKER_URL}"
101
+ payload = {'action': 'editMessageText', 'chat_id': str(chat_id), 'message_id': message_id, 'text': text}
102
+ if reply_markup:
103
+ payload['reply_markup'] = json.dumps(reply_markup)
104
+ try:
105
+ async with httpx.AsyncClient() as client:
106
+ response = await client.post(url, json=payload, timeout=60)
107
+ response.raise_for_status()
108
+ except Exception as e:
109
+ print(f"ERROR editing message {message_id} in chat {chat_id}: {e}")
110
+
111
+
112
+ # --- VIDEO PROCESSING FUNCTIONS (Adapted from GUI code) ---
113
+
114
+ # Helper to get script directory (important for finding cookies.txt)
115
+ def get_script_dir():
116
+ # In HF Spaces, the current working directory is usually the repo root
117
+ return os.getcwd()
118
+
119
+ async def mute_video_with_moviepy_hf(original_video_path, chat_id):
120
+ """ Mutes video using MoviePy, sends status messages via Telegram. """
121
+ await send_telegram_message(chat_id, f" Bắt đầu tắt tiếng: {os.path.basename(original_video_path)}")
122
+ original_path = Path(original_video_path)
123
+ muted_filename = original_path.parent / f"{original_path.stem}_MUTED.mp4"
124
+ video_clip = None
125
+ loop = asyncio.get_running_loop()
126
+
127
+ try:
128
+ def sync_mute_copy():
129
+ nonlocal video_clip # Allow modifying outer scope variable
130
+ video_clip = VideoFileClip(str(original_path))
131
+ final_clip = video_clip.set_audio(None)
132
+ final_clip.write_videofile(str(muted_filename), codec='copy', logger=None)
133
+
134
+ await send_telegram_message(chat_id, " Đang thử tắt tiếng (copy - nhanh)...")
135
+ await loop.run_in_executor(None, sync_mute_copy) # Run synchronous moviepy code in executor
136
+ await send_telegram_message(chat_id, f" Tắt tiếng (copy) thành công: {muted_filename.name}")
137
+
138
+ except Exception as e:
139
+ await send_telegram_message(chat_id, f" Lỗi 'codec=copy': {str(e)}. Đang thử nén lại (chậm hơn)...")
140
+ try:
141
+ def sync_mute_reencode():
142
+ # Need to reopen the clip within the executor context
143
+ clip_retry = VideoFileClip(str(original_path))
144
+ final_clip_retry = clip_retry.set_audio(None)
145
+ final_clip_retry.write_videofile(
146
+ str(muted_filename), threads=4, codec="libx264", audio_codec="aac", logger=None
147
+ )
148
+ clip_retry.close() # Close the clip explicitly
149
+
150
+ # Close the original clip if it exists before retrying
151
+ if video_clip: video_clip.close(); video_clip = None
152
+
153
+ await loop.run_in_executor(None, sync_mute_reencode) # Run re-encode in executor
154
+ await send_telegram_message(chat_id, f" Tắt tiếng (nén lại) thành công: {muted_filename.name}")
155
+ return (True, str(muted_filename))
156
+ except Exception as e2:
157
+ # Explicitly close clip on re-encode error if it's still somehow open
158
+ # (though sync_mute_reencode should handle it)
159
+ if 'clip_retry' in locals() and locals()['clip_retry']: locals()['clip_retry'].close()
160
+
161
+ await send_telegram_message(chat_id, f" LỖI MOVIEPY (lần 2):\n{str(e2)}")
162
+ return (False, f"LỖI MOVIEPY:\n{str(e2)}")
163
+ finally:
164
+ if video_clip: video_clip.close()
165
+ return (True, str(muted_filename))
166
+
167
+ async def download_single_video_hf(url, output_path, chat_id):
168
+ """ Downloads a single video using yt-dlp, sends status via Telegram.
169
+ Uses COOKIE_STRING secret instead of cookies.txt file. """
170
+ await send_telegram_message(chat_id, f"Bắt đầu tải: {url}")
171
+ original_filename = None
172
+ loop = asyncio.get_running_loop()
173
+
174
+ # === THAY ĐỔI: Lấy cookie từ Secret ===
175
+ cookie_string = os.environ.get("COOKIE_STRING")
176
+ if not cookie_string:
177
+ await send_telegram_message(chat_id, "❌ LỖI NGHIÊM TRỌNG: Secret 'COOKIE_STRING' chưa được thiết lập trên Hugging Face!")
178
+ return (False, "Lỗi Server: Thiếu Secret Cookie", None)
179
+ # =======================================
180
+
181
+ # Giữ nguyên Headers khác, nhưng thêm header 'Cookie'
182
+ YTDLP_HEADERS = {
183
+ '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',
184
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
185
+ 'Accept-Language': 'en-US,en;q=0.9,vi;q=0.8',
186
+ # === THAY ĐỔI: Thêm header Cookie ===
187
+ 'Cookie': cookie_string
188
+ # =======================================
189
+ }
190
+ output_template = os.path.join(output_path, '%(title)s - %(id)s.%(ext)s')
191
+ downloaded_file_holder = []
192
+
193
+ def progress_hook(d):
194
+ if d['status'] == 'finished':
195
+ filepath = d.get('filename') or d.get('info_dict', {}).get('_filename')
196
+ if filepath and filepath not in downloaded_file_holder:
197
+ downloaded_file_holder.append(filepath)
198
+
199
+ ydl_opts = {
200
+ 'format': 'best',
201
+ 'outtmpl': output_template,
202
+ 'merge_output_format': 'mp4',
203
+ 'noplaylist': True,
204
+ # 'cookiefile': cookie_file_path, # <<<=== XÓA DÒNG NÀY
205
+ 'http_headers': YTDLP_HEADERS, # Headers giờ đã chứa cookie
206
+ 'quiet': True, 'verbose': False, 'no_warnings': True,
207
+ 'ignoreerrors': False,
208
+ 'progress_hooks': [progress_hook],
209
+ 'paths': {'temp': os.path.join(LOCAL_TEMP_FOLDER, 'ytdlp_temp')},
210
+ 'fragment_retries': 10,
211
+ 'retries': 10,
212
+ }
213
+
214
+ try:
215
+ def sync_download():
216
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
217
+ ydl.download([url])
218
+
219
+ await loop.run_in_executor(None, sync_download)
220
+
221
+ if not downloaded_file_holder:
222
+ raise yt_dlp.utils.DownloadError("Không xác định được tên file đã tải.")
223
+ original_filename = downloaded_file_holder[0]
224
+ await send_telegram_message(chat_id, f" ✅ Tải gốc xong: {os.path.basename(original_filename)}")
225
+ return (True, "Tải gốc OK", original_filename)
226
+
227
+ except Exception as e:
228
+ error_message = str(e)
229
+ await send_telegram_message(chat_id, f" ❌ LỖI KHI TẢI GỐC:\n{error_message}")
230
+ # Specific error checking
231
+ # === THAY ĐỔI: Kiểm tra lỗi cookie kỹ hơn ===
232
+ if "HTTP Error 403" in error_message or "401 Unauthorized" in error_message or "Fresh cookies" in error_message:
233
+ return (False, "Lỗi: Cookies hết hạn hoặc không hợp lệ (kiểm tra Secret).", None)
234
+ # ===========================================
235
+ if "Unsupported URL" in error_message: return (False, "Lỗi: Link không hỗ trợ.", None)
236
+ if "Private video" in error_message: return (False, "Lỗi: Video riêng tư.", None)
237
+ if "Video unavailable" in error_message: return (False, "Lỗi: Video không khả dụng.", None)
238
+ # General error
239
+ return (False, f"Lỗi tải: {error_message[:100]}...", None)
240
+
241
+ # --- Background Task for Processing ---
242
+ async def process_links_task(chat_id, urls, is_muting):
243
+ """ Background task to download and optionally mute multiple videos. """
244
+ success_count = 0
245
+ fail_count = 0
246
+ mute_fail_count = 0
247
+ total_links = len(urls)
248
+ processed_files_info = [] # Store tuples of (original_path, muted_path_or_error)
249
+
250
+ # Create a unique temp folder for this task
251
+ task_temp_folder = os.path.join(LOCAL_TEMP_FOLDER, f"task_{chat_id}_{int(time.time())}")
252
+ os.makedirs(task_temp_folder, exist_ok=True)
253
+
254
+ await send_telegram_message(chat_id, f"🚀 Bắt đầu xử lý {total_links} link...")
255
+
256
+ for i, url in enumerate(urls):
257
+ await send_telegram_message(chat_id, f"--- Xử lý link {i+1}/{total_links} ---")
258
+ original_filepath = None
259
+ muted_filepath_or_error = None
260
+ try:
261
+ # Download to the task's temp folder
262
+ dl_success, dl_msg, original_filepath = await download_single_video_hf(url, task_temp_folder, chat_id)
263
+
264
+ if dl_success:
265
+ if is_muting:
266
+ mute_success, mute_result = await mute_video_with_moviepy_hf(original_filepath, chat_id)
267
+ if mute_success:
268
+ muted_filepath_or_error = mute_result
269
+ else:
270
+ mute_fail_count += 1
271
+ muted_filepath_or_error = f"Lỗi mute: {mute_result}" # Store mute error
272
+ # Still counts as a download success
273
+ success_count += 1
274
+ processed_files_info.append((original_filepath, muted_filepath_or_error))
275
+ else:
276
+ fail_count += 1
277
+ # dl_msg contains the error message from download_single_video_hf
278
+ await send_telegram_message(chat_id, f"⚠️ Lỗi với link {i+1}: {dl_msg}")
279
+
280
+ except Exception as e:
281
+ fail_count += 1
282
+ await send_telegram_message(chat_id, f"❌ Lỗi hệ thống nghiêm trọng khi xử lý link {i+1}: {e}")
283
+ traceback.print_exc() # Print full traceback to server logs
284
+
285
+ # Optional: Short delay between downloads
286
+ await asyncio.sleep(1)
287
+
288
+ # --- Sending results back ---
289
+ await send_telegram_message(chat_id, f"--- Hoàn tất tải {success_count}/{total_links} video gốc ---")
290
+ if is_muting:
291
+ await send_telegram_message(chat_id, f"--- Bắt đầu gửi {success_count} video (bản gốc hoặc đã tắt tiếng) ---")
292
+
293
+ sent_count = 0
294
+ send_errors = 0
295
+ for i, (orig_path, mute_path_or_err) in enumerate(processed_files_info):
296
+ file_to_send = None
297
+ caption = os.path.basename(orig_path or "video")
298
+
299
+ if is_muting:
300
+ if isinstance(mute_path_or_err, str) and not mute_path_or_err.startswith("Lỗi mute"):
301
+ file_to_send = mute_path_or_err # Send muted version if success
302
+ caption = os.path.basename(mute_path_or_err) + " (Đã tắt tiếng)"
303
+ else:
304
+ file_to_send = orig_path # Send original if muting failed
305
+ caption += f" (Lỗi tắt tiếng: {mute_path_or_err})"
306
+ else:
307
+ file_to_send = orig_path # Send original if not muting
308
+
309
+ if file_to_send and os.path.exists(file_to_send):
310
+ await send_telegram_message(chat_id, f" Đang gửi video {i+1}/{len(processed_files_info)}...")
311
+ if await send_telegram_video(chat_id, file_to_send, caption):
312
+ sent_count += 1
313
+ else:
314
+ send_errors += 1
315
+ else:
316
+ # This case means download failed, already logged earlier
317
+ pass
318
+
319
+ # --- Final Summary ---
320
+ summary_message = f"🏁 Hoàn thành!\n- Tải thành công: {success_count}/{total_links}"
321
+ if fail_count > 0: summary_message += f"\n- Tải thất bại: {fail_count}"
322
+ if is_muting:
323
+ if mute_fail_count > 0: summary_message += f"\n- Lỗi tắt tiếng: {mute_fail_count}"
324
+ if send_errors > 0: summary_message += f"\n- Lỗi gửi file: {send_errors}"
325
+
326
+ await send_telegram_message(chat_id, summary_message)
327
+
328
+ # --- Cleanup ---
329
+ if chat_id in USER_SESSIONS: del USER_SESSIONS[chat_id]
330
+ try:
331
+ shutil.rmtree(task_temp_folder)
332
+ print(f"Cleaned up temp folder: {task_temp_folder}")
333
+ except Exception as e:
334
+ print(f"ERROR cleaning up temp folder {task_temp_folder}: {e}")
335
+
336
+
337
+ # --- FastAPI Setup ---
338
+ @asynccontextmanager
339
+ async def lifespan(app: FastAPI):
340
+ print("🚀 Server is starting...")
341
+ # Initial cleanup just in case
342
+ if os.path.exists(LOCAL_TEMP_FOLDER): shutil.rmtree(LOCAL_TEMP_FOLDER)
343
+ os.makedirs(LOCAL_TEMP_FOLDER, exist_ok=True)
344
+ print("✅ Bot ready.")
345
+ yield
346
+ print("👋 Server shutting down.")
347
+
348
+ app = FastAPI(lifespan=lifespan)
349
+
350
+ class TelegramUpdate(BaseModel):
351
+ message: dict | None = None
352
+ callback_query: dict | None = None
353
+
354
+ @app.post("/webhook")
355
+ async def handle_webhook(update: TelegramUpdate, background_tasks: BackgroundTasks):
356
+ if update.callback_query:
357
+ # --- Handle Button Clicks ---
358
+ callback_query = update.callback_query
359
+ chat_id = callback_query["message"]["chat"]["id"]
360
+ message_id = callback_query["message"]["message_id"]
361
+ callback_data = callback_query["data"]
362
+ callback_id = callback_query["id"]
363
+
364
+ session = USER_SESSIONS.get(chat_id)
365
+ if not session or session.get("step") != "awaiting_mute_choice":
366
+ await answer_telegram_callback_query(callback_id, "⚠️ Lựa chọn đã hết hạn hoặc không hợp lệ. Gửi lại link nhé.")
367
+ return {"status": "ok, session invalid"}
368
+
369
+ # Remove the inline keyboard from the original message
370
+ await edit_telegram_message_text(chat_id, message_id, callback_query["message"]["text"], reply_markup={})
371
+ await answer_telegram_callback_query(callback_id, "OK, bắt đầu xử lý!") # Acknowledge button press
372
+
373
+ # Store mute choice and start background task
374
+ is_muting = (callback_data == "mute_yes")
375
+ session["is_muting"] = is_muting
376
+ session["step"] = "processing"
377
+
378
+ background_tasks.add_task(process_links_task, chat_id, session["urls"], is_muting)
379
+ return {"status": "ok, processing started"}
380
+
381
+ elif update.message:
382
+ # --- Handle Incoming Messages ---
383
+ message = update.message
384
+ chat_id = message["chat"]["id"]
385
+ text = message.get("text", "").strip()
386
+
387
+ if text == "/start":
388
+ if chat_id in USER_SESSIONS: del USER_SESSIONS[chat_id]
389
+ await send_telegram_message(chat_id, "👋 Chào bạn! Gửi 1 hoặc nhiều link video (Douyin/XHS), mỗi link một dòng.")
390
+ return {"status": "ok, start message"}
391
+
392
+ # Check if currently processing for this user
393
+ if USER_SESSIONS.get(chat_id, {}).get("step") == "processing":
394
+ await send_telegram_message(chat_id, "⏳ Bot đang xử lý yêu cầu trước đó của bạn. Vui lòng chờ nhé.")
395
+ return {"status": "ok, busy"}
396
+
397
+ # Extract URLs from the message
398
+ url_pattern = r'https?://[^\s]+'
399
+ urls = [match.group(0) for match in re.finditer(url_pattern, text)]
400
+
401
+ if not urls:
402
+ await send_telegram_message(chat_id, "⚠️ Không tìm thấy link hợp lệ nào. Vui lòng gửi link video.")
403
+ return {"status": "ok, no links found"}
404
+
405
+ # Store URLs and ask about muting
406
+ USER_SESSIONS[chat_id] = {
407
+ "step": "awaiting_mute_choice",
408
+ "urls": urls
409
+ }
410
+ keyboard = { "inline_keyboard": [
411
+ [{"text": "🔇 Có, tắt tiếng", "callback_data": "mute_yes"}],
412
+ [{"text": "🔊 Không, giữ nguyên", "callback_data": "mute_no"}]
413
+ ]}
414
+ await send_telegram_message(chat_id, f"Tìm thấy {len(urls)} link. Bạn có muốn tạo thêm bản *tắt tiếng* cho video không?", reply_markup=keyboard)
415
+ return {"status": "ok, links received, asking mute"}
416
+
417
+ return {"status": "ok, ignored"} # Ignore other update types
418
+
419
+
420
+ @app.get("/")
421
+ def read_root(): return {"message": "Downloader Bot is running."}
422
+
423
+ # --- To run locally (optional) ---
424
+ # if __name__ == "__main__":
425
+ # uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ httpx
4
+ yt-dlp>=2023.10.13 # Nên dùng phiên bản gần đây
5
+ moviepy==1.0.3 # Dùng bản ổn định
6
+ aiodns
7
+ numpy # Cần cho moviepy
8
+ # google-auth-transport-requests # Không cần nữa
9
+ # google-oauth2-credentials # Không cần nữa
10
+ # google-api-python-client # Không cần nữa