geopromini commited on
Commit
8cf49a5
·
verified ·
1 Parent(s): a02fa81

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +425 -533
app.py CHANGED
@@ -1,533 +1,425 @@
1
- # app.py
2
- import nest_asyncio
3
- nest_asyncio.apply()
4
-
5
- import os
6
- import random
7
- import shutil
8
- import asyncio
9
- import traceback
10
- import re
11
- import base64
12
- import io
13
- import json
14
- import pickle
15
- import time
16
- import numpy as np
17
-
18
- # Thư viện cho Web Server và Bot
19
- import httpx
20
- from fastapi import FastAPI, BackgroundTasks
21
- from pydantic import BaseModel
22
- from contextlib import asynccontextmanager
23
- import uvicorn
24
-
25
- # Import các module đã tách
26
- from utils.telegram_utils import send_telegram_message, edit_telegram_message_text, answer_telegram_callback_query
27
-
28
- # Thư viện cho xử lý video
29
- from moviepy.editor import (VideoFileClip, AudioFileClip, CompositeVideoClip, ImageClip,
30
- ColorClip, CompositeAudioClip, vfx, TextClip)
31
- from moviepy.video.fx.all import colorx, lum_contrast, speedx, blackwhite
32
- import yt_dlp
33
-
34
- # Thư viện cho DNS fix
35
- import socket
36
- import aiodns
37
-
38
- # Thư viện cho Google Drive API
39
- from google.auth.transport.requests import Request as GoogleRequest
40
- from google.oauth2.credentials import Credentials
41
- from googleapiclient.discovery import build
42
- from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload
43
-
44
- # ==============================================================================
45
- # CẤU HÌNH
46
- # ==============================================================================
47
- WORKER_URL = os.environ.get("WORKER_URL")
48
- TOKEN_BASE64 = os.environ.get("TOKEN_BASE64")
49
-
50
- DRIVE_OUTPUT_FOLDER_ID = "1n0-J6p3pdRuYKPUVwqvjjjzrwNvxig-c"
51
- DRIVE_MUSIC_FOLDER_ID = "1n3Wo62mQB0kuHjsWVnrQSN4HnCWUqtKu"
52
-
53
- LOCAL_TEMP_FOLDER = "temp_processing"
54
-
55
- # Biến toàn cục để lưu trữ trạng thái các phiên làm việc của người dùng
56
- USER_SESSIONS = {}
57
-
58
- # ==============================================================================
59
- # DNS FIX (Giữ nguyên)
60
- # ==============================================================================
61
- original_getaddrinfo = socket.getaddrinfo
62
- async def custom_getaddrinfo_async(host, port, family=0, type=0, proto=0, flags=0):
63
- resolver = aiodns.DNSResolver(nameservers=['8.8.8.8', '8.8.4.4'])
64
- try:
65
- result = await resolver.query(host, 'A')
66
- addrlist = []
67
- for record in result:
68
- addrlist.append((socket.AF_INET, socket.SOCK_STREAM, 6, '', (record.host, port)))
69
- return addrlist
70
- except aiodns.error.DNSError:
71
- loop = asyncio.get_running_loop()
72
- return await loop.run_in_executor(None, original_getaddrinfo, host, port, family, type, proto, flags)
73
-
74
- def custom_getaddrinfo_sync(*args, **kwargs):
75
- try:
76
- loop = asyncio.get_running_loop()
77
- except RuntimeError:
78
- loop = asyncio.new_event_loop()
79
- asyncio.set_event_loop(loop)
80
- return loop.run_until_complete(custom_getaddrinfo_async(*args, **kwargs))
81
- socket.getaddrinfo = custom_getaddrinfo_sync
82
-
83
- async def send_telegram_video(chat_id, video_path, caption):
84
- url = f"{WORKER_URL}"
85
- with open(video_path, 'rb') as video_file:
86
- files = { 'video': (os.path.basename(video_path), video_file, 'video/mp4') }
87
- data = { 'action': 'sendVideo', 'chat_id': str(chat_id), 'caption': caption }
88
- try:
89
- async with httpx.AsyncClient() as client:
90
- response = await client.post(url, data=data, files=files, timeout=300)
91
- response.raise_for_status()
92
- telegram_response = response.json()
93
- if not telegram_response.get("ok", False):
94
- error_msg = telegram_response.get("description", "Unknown Telegram error")
95
- raise Exception(f"Telegram API error: {error_msg}")
96
- except Exception as e:
97
- error_msg = f"Lỗi khi gửi video qua Worker: {e}"
98
- print(f"ERROR: {error_msg}")
99
- await send_telegram_message(chat_id, f"❌ Đã xảy ra lỗi khi gửi video thành phẩm: {e}")
100
-
101
- # ==============================================================================
102
- # CÁC HÀM TIỆN ÍCH KHÁC
103
- # ==============================================================================
104
- def cleanup():
105
- if os.path.exists(LOCAL_TEMP_FOLDER):
106
- shutil.rmtree(LOCAL_TEMP_FOLDER)
107
- os.makedirs(LOCAL_TEMP_FOLDER, exist_ok=True)
108
-
109
- def ensure_base_assets_exist():
110
- pass
111
-
112
- # ==============================================================================
113
- # COLOR GRADING HELPERS (Không thay đổi)
114
- # ==============================================================================
115
- def _apply_saturation_frame(frame: np.ndarray, factor: float) -> np.ndarray:
116
- f = frame.astype(np.float32) / 255.0
117
- luminance = (0.299 * f[:, :, 0] + 0.587 * f[:, :, 1] + 0.114 * f[:, :, 2])
118
- luminance = luminance[:, :, None]
119
- f = luminance + (f - luminance) * factor
120
- f = np.clip(f, 0.0, 1.0)
121
- return (f * 255.0).astype(np.uint8)
122
-
123
- def _apply_contrast_frame(frame: np.ndarray, factor: float) -> np.ndarray:
124
- f = frame.astype(np.float32) / 255.0
125
- f = (f - 0.5) * factor + 0.5
126
- f = np.clip(f, 0.0, 1.0)
127
- return (f * 255.0).astype(np.uint8)
128
-
129
- def _apply_gamma_frame(frame: np.ndarray, gamma: float) -> np.ndarray:
130
- if gamma <= 0: return frame
131
- f = frame.astype(np.float32) / 255.0
132
- f = np.power(f, gamma)
133
- f = np.clip(f, 0.0, 1.0)
134
- return (f * 255.0).astype(np.uint8)
135
-
136
- def _apply_gray_world_balance_frame(frame: np.ndarray) -> np.ndarray:
137
- f = frame.astype(np.float32)
138
- means = np.maximum(f.reshape(-1, 3).mean(axis=0), 1e-6)
139
- gray = means.mean()
140
- gains = gray / means
141
- f = f * gains[None, None, 🙂
142
- f = np.clip(f, 0.0, 255.0)
143
- return f.astype(np.uint8)
144
-
145
- def _apply_tint_frame(frame: np.ndarray, multipliers: tuple[float, float, float]) -> np.ndarray:
146
- f = frame.astype(np.float32)
147
- r_mul, g_mul, b_mul = multipliers
148
- f[:, :, 0] *= r_mul
149
- f[:, :, 1] *= g_mul
150
- f[:, :, 2] *= b_mul
151
- f = np.clip(f, 0.0, 255.0)
152
- return f.astype(np.uint8)
153
-
154
- # ==============================================================================
155
- # GOOGLE DRIVE & XỬ LÝ VIDEO (Không thay đổi)
156
- # ==============================================================================
157
- def get_drive_service():
158
- if not TOKEN_BASE64: raise ValueError("Secret 'TOKEN_BASE64' chưa được thiết lập.")
159
- try:
160
- decoded_token = base64.b64decode(TOKEN_BASE64)
161
- creds = pickle.loads(decoded_token)
162
- except Exception as e:
163
- raise ValueError(f"Lỗi khi giải mã TOKEN_BASE64: {e}")
164
- if creds and creds.expired and creds.refresh_token:
165
- try:
166
- creds.refresh(GoogleRequest())
167
- print("✅ Token Google đã được làm mới.")
168
- except Exception as e:
169
- raise Exception(f"Không thể làm mới token Google: {e}")
170
- return build('drive', 'v3', credentials=creds)
171
-
172
- def download_file_from_drive(service, file_id, destination):
173
- request = service.files().get_media(fileId=file_id)
174
- fh = io.FileIO(destination, 'wb')
175
- downloader = MediaIoBaseDownload(fh, request)
176
- done = False
177
- while not done: status, done = downloader.next_chunk()
178
- return destination
179
-
180
- def get_random_music_from_drive(service, folder_id, destination_folder):
181
- query = f"'{folder_id}' in parents and mimeType contains 'audio/'"
182
- results = service.files().list(q=query, fields="files(id, name)").execute()
183
- items = results.get('files', [])
184
- if not items: raise FileNotFoundError("Không tìm thấy file nhạc nào trong thư mục Google Drive.")
185
- random_file = random.choice(items)
186
- local_path = os.path.join(destination_folder, random_file['name'])
187
- return download_file_from_drive(service, random_file['id'], local_path)
188
-
189
- def upload_to_google_drive(service, file_path, folder_id):
190
- file_metadata = {'name': os.path.basename(file_path), 'parents': [folder_id]}
191
- media = MediaFileUpload(file_path, mimetype='video/mp4', resumable=True)
192
- request = service.files().create(body=file_metadata, media_body=media, fields='id, webViewLink')
193
- response = None
194
- while response is None: status, response = request.next_chunk()
195
- file_id = response.get('id')
196
- service.permissions().create(fileId=file_id, body={'type': 'anyone', 'role': 'reader'}).execute()
197
- file_info = service.files().get(fileId=file_id, fields='webViewLink').execute()
198
- return file_info.get('webViewLink')
199
-
200
- def download_video_from_url(url, output_path):
201
- YTDLP_HEADERS = {
202
- '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',
203
- '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',
204
- 'Accept-Language': 'en-US,en;q=0.9,vi;q=0.8'
205
- }
206
- ydl_opts = {
207
- 'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
208
- 'outtmpl': os.path.join(output_path, '%(id)s.%(ext)s'),
209
- 'merge_output_format': 'mp4',
210
- 'noplaylist': True,
211
- 'cookiefile': 'cookies.txt',
212
- 'http_headers': YTDLP_HEADERS
213
- }
214
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
215
- info = ydl.extract_info(url, download=True)
216
- return ydl.prepare_filename(info)
217
-
218
- # ==============================================================================
219
- # HÀM XỬ LÝ VIDEO CHÍNH (ĐÃ NÂNG CẤP)
220
- # ==============================================================================
221
- def process_video(video_path, output_path, keep_original_audio=True, music_path=None, speed_factor=1.0, filter_choice='default', ratio_choice='9_16', frame_choice='frame1'):
222
- clip = VideoFileClip(video_path)
223
- output_size = (1080, 1920)
224
-
225
- # 1. Xử tỷ lệ khung hình và tạo base_clip
226
- if ratio_choice == 'keep':
227
- base_clip = clip
228
- else:
229
- background = ColorClip(size=output_size, color=(0, 0, 0), duration=clip.duration)
230
- video_resized = clip.resize(width=output_size[0])
231
- base_clip = CompositeVideoClip([background, video_resized.set_position('center')])
232
-
233
- # 2. Xử âm thanh t���c độ
234
- final_audio = None
235
- if keep_original_audio:
236
- original_audio = base_clip.audio
237
- if original_audio:
238
- final_audio = original_audio.fx(speedx, factor=speed_factor)
239
- else:
240
- if music_path and os.path.exists(music_path):
241
- background_music = AudioFileClip(music_path)
242
- video_duration_after_speedup = base_clip.duration / speed_factor
243
- final_audio = background_music.subclip(0, video_duration_after_speedup) if background_music.duration > video_duration_after_speedup else background_music
244
-
245
- final_clip_with_audio = base_clip.set_audio(final_audio)
246
-
247
- # 3. Thêm Frame (ĐÃ NÂNG CẤP)
248
- # Ánh xạ lựa chọn frame tới tên file
249
- frame_map = {
250
- "frame1": "frame.png",
251
- "frame2": "frame1.png",
252
- "frame3": "frame2.png",
253
- "frame4": "frame3.png",
254
- }
255
-
256
- base_dir = os.path.dirname(os.path.abspath(_file_))
257
- # Lấy tên file từ map, nếu không thì mặc định là frame.png
258
- frame_filename = frame_map.get(frame_choice, "frame.png")
259
- frame_path = os.path.join(base_dir, frame_filename)
260
-
261
- # *NÂNG CẤP*: Luôn áp dụng frame nếu file tồn tại, không còn phụ thuộc vào ratio_choice
262
- if os.path.exists(frame_path):
263
- frame_clip = ImageClip(frame_path).set_duration(final_clip_with_audio.duration).resize(final_clip_with_audio.size)
264
- final_video = CompositeVideoClip([final_clip_with_audio, frame_clip])
265
- else:
266
- print(f"CẢNH BÁO: Không tìm thấy file frame '{frame_filename}'. Video sẽ được xuất mà không có khung.")
267
- final_video = final_clip_with_audio
268
-
269
- # 4. Xây dựng chuỗi lệnh FFmpeg để tăng tốc và chỉnh màu
270
- ffmpeg_video_filters = []
271
- ffmpeg_audio_filters = []
272
-
273
- if abs(speed_factor - 1.0) > 1e-6:
274
- ffmpeg_video_filters.append(f"setpts=PTS/{speed_factor}")
275
-
276
- if filter_choice == 'bw':
277
- ffmpeg_video_filters.append("format=gray")
278
- elif filter_choice == 'sharpen':
279
- ffmpeg_video_filters.append("eq=contrast=1.12:saturation=1.08")
280
- elif filter_choice == 'vintage':
281
- ffmpeg_video_filters.append("curves=r='0/0.1':g='0/0.05':b='0/0.2', eq=contrast=0.95:saturation=1.1")
282
- elif filter_choice == 'action_film':
283
- ffmpeg_video_filters.append("curves=preset=cross_process, eq=contrast=1.35")
284
- elif filter_choice == 'cyber_blue':
285
- ffmpeg_video_filters.append("eq=contrast=1.25, curves=r='0/0 0.5/0.4 1/1':b='0/0 0.5/0.6 1/1'")
286
- elif filter_choice == 'bleach_bypass':
287
- ffmpeg_video_filters.append("eq=contrast=1.6:saturation=0.6")
288
- else: # default
289
- ffmpeg_video_filters.append("eq=contrast=1.05:saturation=1.06")
290
-
291
- ffmpeg_params_list = ['-pix_fmt', 'yuv420p']
292
- if ffmpeg_video_filters:
293
- ffmpeg_params_list.extend(['-vf', ",".join(ffmpeg_video_filters)])
294
- if ffmpeg_audio_filters:
295
- ffmpeg_params_list.extend(['-af', ",".join(ffmpeg_audio_filters)])
296
-
297
- # 5. Xuất video
298
- final_video_path = os.path.join(output_path, f"processed_{os.path.basename(video_path)}")
299
- final_video.write_videofile(final_video_path,
300
- codec="libx264",
301
- audio_codec="aac",
302
- threads=4,
303
- ffmpeg_params=ffmpeg_params_list)
304
- clip.close()
305
- return final_video_path
306
-
307
- # ==============================================================================
308
- # WORKFLOW CHÍNH (Không thay đổi)
309
- # ==============================================================================
310
- async def part1_download_and_ask_audio(chat_id: int, url: str):
311
- task_id = str(int(time.time()))
312
- task_temp_folder = os.path.join(LOCAL_TEMP_FOLDER, task_id)
313
- os.makedirs(task_temp_folder, exist_ok=True)
314
-
315
- try:
316
- await send_telegram_message(chat_id, "✅ Đã nhận yêu cầu. Bắt đầu tải video từ link...")
317
- loop = asyncio.get_running_loop()
318
- downloaded_path = await loop.run_in_executor(None, download_video_from_url, url, task_temp_folder)
319
-
320
- if not downloaded_path: raise Exception("Tải video thất bại.")
321
- await send_telegram_message(chat_id, " Tải video thành công!")
322
-
323
- USER_SESSIONS[chat_id] = {
324
- "step": "awaiting_audio_choice",
325
- "data": {
326
- "task_id": task_id,
327
- "task_temp_folder": task_temp_folder,
328
- "original_video_path": downloaded_path,
329
- "original_url": url,
330
- }
331
- }
332
-
333
- keyboard = { "inline_keyboard": [[{"text": "✅ Giữ nhạc gốc", "callback_data": "keep_audio"}], [{"text": "🎵 Thay nhạc mới", "callback_data": "replace_audio"}]] }
334
- await send_telegram_message(chat_id, "Bạn muốn giữ lại âm thanh gốc của video không?", reply_markup=keyboard)
335
-
336
- except Exception as e:
337
- error_message = f"❌ Đã xảy ra lỗi ở bước tải video: {str(e)}"
338
- print(error_message)
339
- traceback.print_exc()
340
- await send_telegram_message(chat_id, error_message)
341
- if chat_id in USER_SESSIONS: del USER_SESSIONS[chat_id]
342
-
343
- async def part2_process_and_upload(chat_id: int):
344
- session = USER_SESSIONS.get(chat_id)
345
- if not session or "data" not in session:
346
- return await send_telegram_message(chat_id, " Phiên làm việc đã hết hạn, vui lòng gửi lại link.")
347
-
348
- data = session["data"]
349
- loop = asyncio.get_running_loop()
350
- service = None
351
-
352
- try:
353
- task_temp_folder = data["task_temp_folder"]
354
- downloaded_path = data["original_video_path"]
355
- url = data["original_url"]
356
- keep_audio = data["keep_audio"]
357
- speed_factor = data.get("speed_factor", 1.0)
358
- filter_choice = data.get("filter_choice", "default")
359
- ratio_choice = data.get("ratio_choice", "9_16")
360
- # *NÂNG CẤP*: Lấy frame_choice từ session
361
- frame_choice = data.get("frame_choice", "frame1")
362
- music_path = None
363
-
364
- await send_telegram_message(chat_id, "⚙️ Bắt đầu xử lý video với các tùy chọn của bạn...")
365
-
366
- if not keep_audio:
367
- await send_telegram_message(chat_id, "☁️ Kết nối Google Drive để lấy nhạc...")
368
- service = await loop.run_in_executor(None, get_drive_service)
369
- music_path = await loop.run_in_executor(None, get_random_music_from_drive, service, DRIVE_MUSIC_FOLDER_ID, task_temp_folder)
370
-
371
- processed_path = await loop.run_in_executor(
372
- None,
373
- process_video,
374
- downloaded_path,
375
- task_temp_folder,
376
- keep_audio,
377
- music_path,
378
- speed_factor,
379
- filter_choice,
380
- ratio_choice,
381
- # *NÂNG CẤP*: Truyền frame_choice vào hàm xử lý
382
- frame_choice
383
- )
384
- if not processed_path: raise Exception("Xử lý video thất bại.")
385
-
386
- await send_telegram_message(chat_id, "✅ Xử lý xong! Đang gửi video cho bạn...")
387
- await send_telegram_video(chat_id, processed_path, caption=f"Video của bạn từ link: {url}")
388
-
389
- await send_telegram_message(chat_id, "☁️ Bắt đầu tải video lên Google Drive...")
390
- if not service: service = await loop.run_in_executor(None, get_drive_service)
391
- drive_link = await loop.run_in_executor(None, upload_to_google_drive, service, processed_path, DRIVE_OUTPUT_FOLDER_ID)
392
-
393
- if drive_link:
394
- await send_telegram_message(chat_id, f"🎉 Tải lên Google Drive thành công!\n\n🔗 Link:\n{drive_link}")
395
- else:
396
- await send_telegram_message(chat_id, "❌ Lỗi khi tải video lên Google Drive.")
397
-
398
- except Exception as e:
399
- error_message = f"❌ Đã xảy ra lỗi: {str(e)}"
400
- print(error_message)
401
- traceback.print_exc()
402
- await send_telegram_message(chat_id, error_message)
403
- finally:
404
- if os.path.exists(data.get("task_temp_folder", "")): shutil.rmtree(data["task_temp_folder"])
405
- if chat_id in USER_SESSIONS: del USER_SESSIONS[chat_id]
406
-
407
- # ==============================================================================
408
- # FASTAPI APP (ĐÃ NÂNG CẤP LUỒNG HỘI THOẠI)
409
- # ==============================================================================
410
- @asynccontextmanager
411
- async def lifespan(app: FastAPI):
412
- print("🚀 Server đang khởi động...")
413
- cleanup()
414
- ensure_base_assets_exist()
415
- print(" Bot đã sẵn sàng nhận yêu cầu.")
416
- yield
417
- print("👋 Server đã tắt.")
418
-
419
- app = FastAPI(lifespan=lifespan)
420
-
421
- class TelegramUpdate(BaseModel):
422
- message: dict | None = None
423
- callback_query: dict | None = None
424
-
425
- @app.post("/webhook")
426
- async def handle_webhook(update: TelegramUpdate, background_tasks: BackgroundTasks):
427
- main_keyboard = { "keyboard": [[{"text": "👽 Reddit"}, {"text": "🎵 TikTok"}], [{"text": "🐦 X / Twitter"}, {"text": "▶️ YouTube"}]], "resize_keyboard": True }
428
-
429
- if update.callback_query:
430
- chat_id = update.callback_query["message"]["chat"]["id"]
431
- message_id = update.callback_query["message"]["message_id"]
432
- callback_data = update.callback_query["data"]
433
-
434
- session = USER_SESSIONS.get(chat_id)
435
- if not session:
436
- await answer_telegram_callback_query(update.callback_query["id"], "Phiên làm việc đã hết hạn. Vui lòng /start lại.")
437
- return {"status": "ok, session expired"}
438
-
439
- current_step = session.get("step")
440
- await edit_telegram_message_text(chat_id, message_id, update.callback_query["message"]["text"], reply_markup={})
441
-
442
- if current_step == "awaiting_audio_choice":
443
- session["data"]["keep_audio"] = (callback_data == "keep_audio")
444
- session["step"] = "awaiting_ratio_choice"
445
- keyboard = json.dumps({"inline_keyboard": [[{"text": "9:16 (Dọc)", "callback_data": "ratio_9_16"}, {"text": "Giữ nguyên", "callback_data": "ratio_keep"}]]})
446
- await edit_telegram_message_text(chat_id, message_id, "OK. Giờ hãy chọn tỷ lệ khung hình:", reply_markup=keyboard)
447
-
448
- elif current_step == "awaiting_ratio_choice":
449
- try:
450
- # *NÂNG CẤP*: Chuyển sang bước chọn Frame
451
- ratio_choice = callback_data.split('_', 1)[1]
452
- session["data"]["ratio_choice"] = ratio_choice
453
- session["step"] = "awaiting_frame_choice" # <-- THAY ĐỔI
454
- keyboard = json.dumps({"inline_keyboard": [
455
- [{"text": "Frame 1", "callback_data": "frame_frame1"}, {"text": "Frame 2", "callback_data": "frame_frame2"}],
456
- [{"text": "Frame 3", "callback_data": "frame_frame3"}, {"text": "Frame 4", "callback_data": "frame_frame4"}]
457
- ]})
458
- await edit_telegram_message_text(chat_id, message_id, "Tuyệt vời! Bây giờ hãy chọn khung (frame) cho video:", reply_markup=keyboard)
459
- except (ValueError, IndexError):
460
- await send_telegram_message(chat_id, "⚠️ Lựa chọn tỷ lệ không hợp lệ, vui lòng thử lại từ đầu.")
461
-
462
- # *NÂNG CẤP*: Thêm bước xử lý lựa chọn Frame
463
- elif current_step == "awaiting_frame_choice":
464
- try:
465
- frame_choice = callback_data.split('_', 1)[1]
466
- session["data"]["frame_choice"] = frame_choice
467
- session["step"] = "awaiting_speed_choice" # <-- Chuyển tiếp tới bước chọn tốc độ
468
- keyboard = json.dumps({"inline_keyboard": [
469
- [{"text": "1.0x (Gốc)", "callback_data": "speed_1.0"}, {"text": "1.05x", "callback_data": "speed_1.05"}],
470
- [{"text": "1.2x", "callback_data": "speed_1.2"}, {"text": "1.5x", "callback_data": "speed_1.5"}]
471
- ]})
472
- await edit_telegram_message_text(chat_id, message_id, "Tiếp theo, chọn tốc độ cho video:", reply_markup=keyboard)
473
- except (ValueError, IndexError):
474
- await send_telegram_message(chat_id, "⚠️ Lựa chọn frame không hợp lệ, vui lòng thử lại từ đầu.")
475
-
476
- elif current_step == "awaiting_speed_choice":
477
- try:
478
- speed_factor = float(callback_data.split('_')[1])
479
- session["data"]["speed_factor"] = speed_factor
480
- session["step"] = "awaiting_filter_choice"
481
- keyboard = json.dumps({ "inline_keyboard": [
482
- [{"text": "Mặc định ✨", "callback_data": "filter_default"}, {"text": "Cổ điển 🎞️", "callback_data": "filter_vintage"}],
483
- [{"text": "Phim Hành Động 💥", "callback_data": "filter_action_film"}, {"text": "Xanh Neon Lạnh 🥶", "callback_data": "filter_cyber_blue"}],
484
- [{"text": "Bleach Bypass 🎬", "callback_data": "filter_bleach_bypass"}, {"text": "Đen Trắng 🎥", "callback_data": "filter_bw"}]
485
- ]})
486
- await edit_telegram_message_text(chat_id, message_id, "Sắp xong rồi! Cuối cùng, hãy chọn một hiệu ứng màu (filter):", reply_markup=keyboard)
487
- except (ValueError, IndexError):
488
- await send_telegram_message(chat_id, "⚠️ Lựa chọn tốc độ không hợp lệ, vui lòng thử lại từ đầu.")
489
-
490
- elif current_step == "awaiting_filter_choice":
491
- filter_choice = callback_data.split('_')[1]
492
- session["data"]["filter_choice"] = filter_choice
493
- session["step"] = "processing"
494
- await edit_telegram_message_text(chat_id, message_id, "✅ Đã nhận đủ thông tin. Bot bắt đầu làm việc, bạn chờ nhé! Quá trình này có thể mất vài phút.")
495
- background_tasks.add_task(part2_process_and_upload, chat_id)
496
-
497
- elif update.message:
498
- chat_id = update.message["chat"]["id"]
499
- text = update.message.get("text", "")
500
- session = USER_SESSIONS.get(chat_id)
501
-
502
- if text == "/start":
503
- if chat_id in USER_SESSIONS: del USER_SESSIONS[chat_id]
504
- await send_telegram_message(chat_id, "👋 Chào bạn! Vui lòng chọn một nguồn từ bàn phím bên dưới để bắt đầu.", reply_markup=main_keyboard)
505
- return {"status": "ok"}
506
-
507
- source_buttons = ["👽 Reddit", "🎵 TikTok", "🐦 X / Twitter", "▶️ YouTube"]
508
- if text in source_buttons:
509
- source_name = text.split(" ")[1]
510
- USER_SESSIONS[chat_id] = {"step": "awaiting_link", "source": source_name}
511
- await send_telegram_message(chat_id, f"✅ Đã chọn: *{source_name}*. \n\nBây giờ, hãy gửi link video từ nguồn này:")
512
- return {"status": "ok, source selected"}
513
-
514
- if session and session.get("step") == "awaiting_link":
515
- url_pattern = r'https?://[^\s]+'
516
- match = re.search(url_pattern, text)
517
- if match:
518
- background_tasks.add_task(part1_download_and_ask_audio, chat_id, match.group(0))
519
- else:
520
- await send_telegram_message(chat_id, "⚠️ Link không hợp lệ. Vui lòng gửi một link video hợp lệ.")
521
- return {"status": "ok, link received"}
522
-
523
- if session and session.get("step") != "processing":
524
- await send_telegram_message(chat_id, "⏳ Bạn đang trong một quy trình, vui lòng hoàn tất các lựa chọn hiện tại hoặc gõ /start để bắt đầu lại.", reply_markup=main_keyboard)
525
- elif session and session.get("step") == "processing":
526
- await send_telegram_message(chat_id, "⏳ Bot đang xử lý yêu cầu trước đó của bạn. Vui lòng chờ một chút nhé.", reply_markup=main_keyboard)
527
- else:
528
- await send_telegram_message(chat_id, "Vui lòng chọn một tùy chọn từ bàn phím hoặc gõ /start để bắt đầu.", reply_markup=main_keyboard)
529
-
530
- return {"status": "ok"}
531
-
532
- @app.get("/")
533
- def read_root(): return {"message": "Telegram Bot is running."}
 
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ử 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ử 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)