understanding commited on
Commit
eeccd31
·
verified ·
1 Parent(s): a1097bf

Update bot/handlers.py

Browse files
Files changed (1) hide show
  1. bot/handlers.py +130 -101
bot/handlers.py CHANGED
@@ -11,8 +11,12 @@ from hydrogram.types import Message, CallbackQuery
11
 
12
  from bot.config import Workers
13
  from bot.ui import texts
14
- from bot.ui.keyboards import profiles_keyboard, auth_menu_keyboard
15
- from bot.ui.callbacks import parse_cb, AUTH_JSON, AUTH_CI, CANCEL
 
 
 
 
16
 
17
  from bot.core.auth import is_owner_id, require_allowed
18
  from bot.core.progress import SpeedETA, human_bytes, human_eta
@@ -47,19 +51,15 @@ from bot.youtube.uploader import upload_video
47
  from bot.integrations.diag_extra import dns_check
48
  from bot.integrations.http import fetch_status
49
 
50
- # ---------- in-memory auth state (simple FSM) ----------
51
  _AWAIT_AUTH: Dict[int, str] = {}
52
 
53
- # uptime + speedtest cooldown
54
  _STARTED_AT = time.time()
55
  _SPEED_COOLDOWN: dict[int, float] = {}
56
 
57
 
58
  def _pick_login_url(j: dict) -> str:
59
- """Accept login_url from either top-level OR nested {data:{...}} shapes."""
60
  if not isinstance(j, dict):
61
  return ""
62
-
63
  data = j.get("data") if isinstance(j.get("data"), dict) else {}
64
 
65
  def _get(d: dict, k: str) -> str:
@@ -78,14 +78,54 @@ def _pick_login_url(j: dict) -> str:
78
  )
79
 
80
 
81
- def _fmt_gb(b: float) -> float:
82
- return float(b) / (1024.0**3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
 
85
  def setup_handlers(app: Client) -> None:
86
  @app.on_message(filters.command(["start"]) & filters.private)
87
  async def start_handler(_: Client, m: Message):
88
- await safe_reply(m, texts.START_TEXT)
 
 
 
 
89
 
90
  @app.on_message(filters.command(["ping"]) & filters.private)
91
  async def ping_handler(_: Client, m: Message):
@@ -107,50 +147,20 @@ def setup_handlers(app: Client) -> None:
107
  return await safe_reply(m, "⏳ Wait 30s then try again.")
108
  _SPEED_COOLDOWN[uid] = now
109
 
110
- msg = await safe_reply(m, "⚡ Running speed test…")
111
-
112
- # uptime
113
- up_sec = int(max(0, time.time() - _STARTED_AT))
114
- up_txt = human_eta(up_sec)
115
-
116
- # disk
117
- d = disk_total_free("/")
118
- total_gb = _fmt_gb(d["total"])
119
- free_gb = _fmt_gb(d["free"])
120
-
121
- # run parallel
122
- dl_task = asyncio.create_task(net_download_test())
123
- up_task = asyncio.create_task(net_upload_test())
124
- p_task = asyncio.create_task(ping_ms())
125
-
126
- dl, up, p = await asyncio.gather(dl_task, up_task, p_task)
127
-
128
- ping_line = f"{p:.0f} ms" if p is not None else "N/A"
129
-
130
- dl_mb_s = bytes_per_sec_to_mb_s(dl["bps"])
131
- up_mb_s = bytes_per_sec_to_mb_s(up["bps"])
132
-
133
- dl_mb = bytes_to_mb(dl["bytes"])
134
- up_mb = bytes_to_mb(up["bytes"])
135
-
136
- txt = (
137
- "⚡ Server Speed Test\n\n"
138
- "🕒 Uptime\n"
139
- f"└ {up_txt}\n\n"
140
- "📡 Network\n"
141
- f"├ 🟢 Ping: {ping_line}\n"
142
- f"├ ⬇️ Download: {dl_mb_s:.2f} MB/s\n"
143
- f"│ └ {dl_mb:.2f} MB in {dl['seconds']:.2f}s\n"
144
- f"└ ⬆️ Upload: {up_mb_s:.2f} MB/s\n"
145
- f" └ {up_mb:.2f} MB in {up['seconds']:.2f}s\n\n"
146
- "💾 Storage\n"
147
- f"├ Total: {total_gb:.1f} GB\n"
148
- f"└ Free : {free_gb:.1f} GB\n\n"
149
- "🧪 Measured from server (real MB/s, not Mbps)"
150
- )
151
- await safe_edit(msg, txt)
152
 
153
- # -------- OWNER allow/disallow via forward OR id --------
154
  @app.on_message(filters.command(["allow"]) & filters.private)
155
  async def allow_cmd(_: Client, m: Message):
156
  if not is_owner_id(m.from_user.id if m.from_user else None):
@@ -184,7 +194,6 @@ def setup_handlers(app: Client) -> None:
184
  j = await disallow_user(target)
185
  await safe_reply(m, f"✅ disallowed: `{target}`\n`{j}`")
186
 
187
- # -------- Owner stats + diag --------
188
  @app.on_message(filters.command(["stats"]) & filters.private)
189
  async def stats_cmd(_: Client, m: Message):
190
  if not is_owner_id(m.from_user.id if m.from_user else None):
@@ -210,38 +219,64 @@ def setup_handlers(app: Client) -> None:
210
  f"DNS google -> {dns_check('https://www.google.com')}\n",
211
  )
212
 
213
- # -------- AUTH UI --------
214
  @app.on_message(filters.command(["auth"]) & filters.private)
215
  async def auth_cmd(_: Client, m: Message):
216
  uid = m.from_user.id if m.from_user else 0
217
- ok = await require_allowed(uid)
218
- if not ok:
219
  return await safe_reply(m, texts.NOT_ALLOWED)
220
 
221
  _AWAIT_AUTH.pop(uid, None)
222
- await safe_reply(m, texts.AUTH_MENU, reply_markup=auth_menu_keyboard())
223
 
224
  @app.on_message(filters.command(["cancel"]) & filters.private)
225
  async def cancel_cmd(_: Client, m: Message):
226
  _AWAIT_AUTH.pop(m.from_user.id, None)
227
- await safe_reply(m, texts.CANCELLED)
228
 
229
  @app.on_callback_query()
230
  async def cb(_: Client, q: CallbackQuery):
231
  uid = q.from_user.id if q.from_user else 0
232
  action, value = parse_cb(q.data or "")
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  if action in (AUTH_JSON, AUTH_CI, CANCEL):
235
  if action == CANCEL:
236
  _AWAIT_AUTH.pop(uid, None)
237
  return await q.answer("Cancelled")
238
  if action == AUTH_JSON:
239
  _AWAIT_AUTH[uid] = "json"
240
- await q.message.edit_text(texts.ASK_JSON)
241
  return await q.answer("Paste JSON")
242
  if action == AUTH_CI:
243
  _AWAIT_AUTH[uid] = "ci"
244
- await q.message.edit_text(texts.ASK_ID_SECRET)
245
  return await q.answer("Send ID & Secret")
246
  return
247
 
@@ -255,32 +290,18 @@ def setup_handlers(app: Client) -> None:
255
 
256
  await q.answer("Current set ✅", show_alert=False)
257
  try:
258
- await q.message.edit_text(f" Current profile set: `{value}`")
259
  except Exception:
260
  pass
261
  return
262
 
263
  await q.answer("OK")
264
 
265
- # message sink for auth states
266
  @app.on_message(
267
  filters.private
268
  & filters.text
269
- & ~filters.command(
270
- [
271
- "start",
272
- "ping",
273
- "me",
274
- "auth",
275
- "cancel",
276
- "profiles",
277
- "speedtest",
278
- "stats",
279
- "allow",
280
- "disallow",
281
- "diag",
282
- ]
283
- )
284
  )
285
  async def auth_sink(_: Client, m: Message):
286
  uid = m.from_user.id if m.from_user else 0
@@ -304,7 +325,6 @@ def setup_handlers(app: Client) -> None:
304
  client_id, client_secret = parts[0], parts[1]
305
 
306
  j = await profile_add(uid, client_id, client_secret, label="main", ttl_sec=600)
307
-
308
  if not isinstance(j, dict):
309
  return await safe_reply(m, f"❌ profile_add bad response: `{j}`")
310
 
@@ -316,7 +336,7 @@ def setup_handlers(app: Client) -> None:
316
  return await safe_reply(m, f"❌ profile_add OK but login link missing.\nBackend response:\n`{j}`")
317
 
318
  _AWAIT_AUTH.pop(uid, None)
319
- await safe_reply(m, texts.SENT_AUTH_LINK + login_url)
320
 
321
  except Exception as e:
322
  await safe_reply(m, f"{texts.PARSE_FAIL}\n`{type(e).__name__}: {e}`")
@@ -324,8 +344,7 @@ def setup_handlers(app: Client) -> None:
324
  @app.on_message(filters.command(["profiles"]) & filters.private)
325
  async def profiles_cmd(_: Client, m: Message):
326
  uid = m.from_user.id if m.from_user else 0
327
- ok = await require_allowed(uid)
328
- if not ok:
329
  return await safe_reply(m, texts.NOT_ALLOWED)
330
 
331
  j = await profile_list(uid)
@@ -335,7 +354,9 @@ def setup_handlers(app: Client) -> None:
335
  profiles = j.get("profiles") or []
336
  default_id = j.get("default_profile_id")
337
 
338
- txt = f"Current: `{default_id}`\n\n"
 
 
339
  for i, p in enumerate(profiles, start=1):
340
  pid = p.get("profile_id")
341
  ch = (p.get("channel_title") or "—").strip()
@@ -343,17 +364,19 @@ def setup_handlers(app: Client) -> None:
343
  sec = p.get("client_secret_hint") or "****…****"
344
  connected = "✅ Connected" if (p.get("has_refresh") and p.get("channel_id")) else "⏳ Pending"
345
  cur = "🟢 Current" if pid == default_id else ""
346
- txt += f"[{i}] {ch} | {cur} | id: {cid} | secret: {sec} | {connected}\n"
 
 
 
347
 
348
  kb = profiles_keyboard(profiles)
349
  await safe_reply(m, txt, reply_markup=kb)
350
 
351
- # -------- Upload handler --------
352
  @app.on_message(filters.private & (filters.video | filters.document))
353
  async def upload_handler(app_: Client, m: Message):
354
  uid = m.from_user.id if m.from_user else 0
355
- ok = await require_allowed(uid)
356
- if not ok:
357
  return await safe_reply(m, texts.NOT_ALLOWED)
358
 
359
  media = m.video or m.document
@@ -384,8 +407,7 @@ def setup_handlers(app: Client) -> None:
384
  return await safe_reply(m, texts.TOKEN_FAIL.format(tok))
385
  access_tok = tok["access_token"]
386
 
387
- status = await safe_reply(m, texts.UPLOAD_START)
388
-
389
  task_id = f"{uid}:{m.id}"
390
  create_task(task_id, uid)
391
 
@@ -394,15 +416,13 @@ def setup_handlers(app: Client) -> None:
394
  file_name = "video.bin"
395
 
396
  try:
397
- # ---------------- Download (Telegram) ----------------
398
  set_task(task_id, "downloading", "")
399
-
400
  dl_t0 = time.time()
401
  file_path, file_size, file_name = await download_to_temp(app_, m)
402
  dl_sec = max(0.001, time.time() - dl_t0)
403
 
404
- # fallback size from disk if TG didn't give it
405
- if not file_size and file_path and os.path.exists(file_path):
406
  try:
407
  file_size = os.path.getsize(file_path)
408
  except Exception:
@@ -410,10 +430,17 @@ def setup_handlers(app: Client) -> None:
410
 
411
  dl_avg_bps = int((file_size or 0) / dl_sec) if file_size else 0
412
 
413
- # parse title/desc after download
414
- title, desc = extract_title_description(m, file_name)
 
 
 
 
 
 
 
415
 
416
- # ---------------- Upload (YouTube) ----------------
417
  set_task(task_id, "uploading", "")
418
  up_t0 = time.time()
419
 
@@ -427,12 +454,14 @@ def setup_handlers(app: Client) -> None:
427
  return
428
  last_edit = time.time()
429
  txt = (
430
- f"{texts.UPLOAD_TO_YT}\n"
431
  f"{human_bytes(done)}/{human_bytes(total)}\n"
432
  f"speed: {human_bytes(snap['speed_bps'])}/s | eta: {human_eta(snap['eta_sec'])}"
433
  )
434
  await safe_edit(status, txt)
435
 
 
 
436
  yt_url = await upload_video(
437
  access_tok,
438
  file_path,
@@ -446,11 +475,11 @@ def setup_handlers(app: Client) -> None:
446
  up_avg_bps = int((file_size or 0) / up_sec) if file_size else 0
447
 
448
  await record_upload(uid, use_profile_id)
449
-
450
  set_task(task_id, "done", "", yt_url=yt_url)
451
- await safe_reply(
452
- m,
453
- f"{texts.DONE}\n\n"
 
454
  f"🎬 {title}\n"
455
  f"🔗 {yt_url}\n\n"
456
  f"⬇️ Download: {human_eta(int(dl_sec))} | avg {human_bytes(dl_avg_bps)}/s\n"
@@ -459,7 +488,7 @@ def setup_handlers(app: Client) -> None:
459
 
460
  except Exception as e:
461
  set_task(task_id, "error", str(e))
462
- await safe_reply(m, f"❌ Upload failed: `{type(e).__name__}: {e}`")
463
  finally:
464
  if file_path:
465
  cleanup_file(file_path)
 
11
 
12
  from bot.config import Workers
13
  from bot.ui import texts
14
+ from bot.ui.keyboards import profiles_keyboard, auth_menu_keyboard, main_menu_keyboard
15
+ from bot.ui.callbacks import (
16
+ parse_cb,
17
+ AUTH_JSON, AUTH_CI, CANCEL,
18
+ MENU_HELP, MENU_AUTH, MENU_PROFILES, MENU_SPEEDTEST
19
+ )
20
 
21
  from bot.core.auth import is_owner_id, require_allowed
22
  from bot.core.progress import SpeedETA, human_bytes, human_eta
 
51
  from bot.integrations.diag_extra import dns_check
52
  from bot.integrations.http import fetch_status
53
 
 
54
  _AWAIT_AUTH: Dict[int, str] = {}
55
 
 
56
  _STARTED_AT = time.time()
57
  _SPEED_COOLDOWN: dict[int, float] = {}
58
 
59
 
60
  def _pick_login_url(j: dict) -> str:
 
61
  if not isinstance(j, dict):
62
  return ""
 
63
  data = j.get("data") if isinstance(j.get("data"), dict) else {}
64
 
65
  def _get(d: dict, k: str) -> str:
 
78
  )
79
 
80
 
81
+ async def _render_speedtest(mb: int) -> str:
82
+ mb = max(1, min(200, int(mb)))
83
+ dl_bytes = mb * 1024 * 1024
84
+ up_mb = min(20, max(3, mb // 5)) # 3..20MB
85
+ up_bytes = up_mb * 1024 * 1024
86
+
87
+ up_sec = int(max(0, time.time() - _STARTED_AT))
88
+ up_txt = human_eta(up_sec)
89
+
90
+ d = disk_total_free("/")
91
+ total_gb = d["total"] / (1024.0**3)
92
+ free_gb = d["free"] / (1024.0**3)
93
+
94
+ dl_task = asyncio.create_task(net_download_test(bytes_target=dl_bytes, timeout=40.0))
95
+ up_task = asyncio.create_task(net_upload_test(bytes_target=up_bytes, timeout=40.0))
96
+ p_task = asyncio.create_task(ping_ms())
97
+
98
+ dl, up, p = await asyncio.gather(dl_task, up_task, p_task)
99
+
100
+ ping_line = f"{p:.0f} ms" if p is not None else "N/A"
101
+ dl_mb_s = bytes_per_sec_to_mb_s(dl["bps"])
102
+ up_mb_s = bytes_per_sec_to_mb_s(up["bps"])
103
+
104
+ return (
105
+ "⚡ **Server Speed Test**\n\n"
106
+ "🕒 Uptime\n"
107
+ f"└ {up_txt}\n\n"
108
+ "📡 Network\n"
109
+ f"├ 🟢 Ping: {ping_line}\n"
110
+ f"├ ⬇️ Download: {dl_mb_s:.2f} MB/s\n"
111
+ f"│ └ {bytes_to_mb(dl['bytes']):.2f} MB in {dl['seconds']:.2f}s\n"
112
+ f"└ ⬆️ Upload: {up_mb_s:.2f} MB/s\n"
113
+ f" └ {bytes_to_mb(up['bytes']):.2f} MB in {up['seconds']:.2f}s\n\n"
114
+ "💾 Storage\n"
115
+ f"├ Total: {total_gb:.1f} GB\n"
116
+ f"└ Free : {free_gb:.1f} GB\n\n"
117
+ "🧪 Measured from server (real MB/s, not Mbps)"
118
+ )
119
 
120
 
121
  def setup_handlers(app: Client) -> None:
122
  @app.on_message(filters.command(["start"]) & filters.private)
123
  async def start_handler(_: Client, m: Message):
124
+ await safe_reply(m, texts.START_TEXT, reply_markup=main_menu_keyboard())
125
+
126
+ @app.on_message(filters.command(["help"]) & filters.private)
127
+ async def help_handler(_: Client, m: Message):
128
+ await safe_reply(m, texts.HELP_TEXT, reply_markup=main_menu_keyboard())
129
 
130
  @app.on_message(filters.command(["ping"]) & filters.private)
131
  async def ping_handler(_: Client, m: Message):
 
147
  return await safe_reply(m, "⏳ Wait 30s then try again.")
148
  _SPEED_COOLDOWN[uid] = now
149
 
150
+ parts = (m.text or "").split()
151
+ try:
152
+ mb = int(parts[1]) if len(parts) > 1 else 8
153
+ except Exception:
154
+ mb = 8
155
+
156
+ msg = await safe_reply(m, "⚡ Running speedtest…")
157
+ try:
158
+ txt = await _render_speedtest(mb)
159
+ await safe_edit(msg, txt)
160
+ except Exception as e:
161
+ await safe_edit(msg, f"❌ Speedtest failed: `{type(e).__name__}: {e}`")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
+ # -------- OWNER allow/disallow --------
164
  @app.on_message(filters.command(["allow"]) & filters.private)
165
  async def allow_cmd(_: Client, m: Message):
166
  if not is_owner_id(m.from_user.id if m.from_user else None):
 
194
  j = await disallow_user(target)
195
  await safe_reply(m, f"✅ disallowed: `{target}`\n`{j}`")
196
 
 
197
  @app.on_message(filters.command(["stats"]) & filters.private)
198
  async def stats_cmd(_: Client, m: Message):
199
  if not is_owner_id(m.from_user.id if m.from_user else None):
 
219
  f"DNS google -> {dns_check('https://www.google.com')}\n",
220
  )
221
 
222
+ # -------- AUTH --------
223
  @app.on_message(filters.command(["auth"]) & filters.private)
224
  async def auth_cmd(_: Client, m: Message):
225
  uid = m.from_user.id if m.from_user else 0
226
+ if not await require_allowed(uid):
 
227
  return await safe_reply(m, texts.NOT_ALLOWED)
228
 
229
  _AWAIT_AUTH.pop(uid, None)
230
+ await safe_reply(m, "🔐 Add YouTube Profile\n\nChoose method:", reply_markup=auth_menu_keyboard())
231
 
232
  @app.on_message(filters.command(["cancel"]) & filters.private)
233
  async def cancel_cmd(_: Client, m: Message):
234
  _AWAIT_AUTH.pop(m.from_user.id, None)
235
+ await safe_reply(m, texts.CANCELLED, reply_markup=main_menu_keyboard())
236
 
237
  @app.on_callback_query()
238
  async def cb(_: Client, q: CallbackQuery):
239
  uid = q.from_user.id if q.from_user else 0
240
  action, value = parse_cb(q.data or "")
241
 
242
+ # main menu
243
+ if action == MENU_HELP:
244
+ try:
245
+ await q.message.edit_text(texts.HELP_TEXT, reply_markup=main_menu_keyboard())
246
+ except Exception:
247
+ pass
248
+ return await q.answer("Help")
249
+
250
+ if action == MENU_SPEEDTEST:
251
+ await q.answer("Use /speedtest", show_alert=False)
252
+ return
253
+
254
+ if action == MENU_AUTH:
255
+ if not await require_allowed(uid):
256
+ return await q.answer("Not allowed", show_alert=True)
257
+ _AWAIT_AUTH.pop(uid, None)
258
+ try:
259
+ await q.message.edit_text("🔐 Add YouTube Profile\n\nChoose method:", reply_markup=auth_menu_keyboard())
260
+ except Exception:
261
+ pass
262
+ return await q.answer("Auth")
263
+
264
+ if action == MENU_PROFILES:
265
+ await q.answer("Use /profiles", show_alert=False)
266
+ return
267
+
268
+ # auth menu
269
  if action in (AUTH_JSON, AUTH_CI, CANCEL):
270
  if action == CANCEL:
271
  _AWAIT_AUTH.pop(uid, None)
272
  return await q.answer("Cancelled")
273
  if action == AUTH_JSON:
274
  _AWAIT_AUTH[uid] = "json"
275
+ await q.message.edit_text("📄 Paste your client JSON here.\n\n(You can /cancel anytime)")
276
  return await q.answer("Paste JSON")
277
  if action == AUTH_CI:
278
  _AWAIT_AUTH[uid] = "ci"
279
+ await q.message.edit_text("🔑 Send Client ID and Client Secret.\n\nFormat:\n<id> | <secret>\n(or 2 lines)\n\n(/cancel anytime)")
280
  return await q.answer("Send ID & Secret")
281
  return
282
 
 
290
 
291
  await q.answer("Current set ✅", show_alert=False)
292
  try:
293
+ await q.message.edit_text(f"🟢 Current profile set:\n`{value}`", reply_markup=main_menu_keyboard())
294
  except Exception:
295
  pass
296
  return
297
 
298
  await q.answer("OK")
299
 
300
+ # auth sink
301
  @app.on_message(
302
  filters.private
303
  & filters.text
304
+ & ~filters.command(["start", "help", "ping", "me", "auth", "cancel", "profiles", "speedtest", "stats", "allow", "disallow", "diag"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  )
306
  async def auth_sink(_: Client, m: Message):
307
  uid = m.from_user.id if m.from_user else 0
 
325
  client_id, client_secret = parts[0], parts[1]
326
 
327
  j = await profile_add(uid, client_id, client_secret, label="main", ttl_sec=600)
 
328
  if not isinstance(j, dict):
329
  return await safe_reply(m, f"❌ profile_add bad response: `{j}`")
330
 
 
336
  return await safe_reply(m, f"❌ profile_add OK but login link missing.\nBackend response:\n`{j}`")
337
 
338
  _AWAIT_AUTH.pop(uid, None)
339
+ await safe_reply(m, texts.SENT_AUTH_LINK + login_url, reply_markup=main_menu_keyboard())
340
 
341
  except Exception as e:
342
  await safe_reply(m, f"{texts.PARSE_FAIL}\n`{type(e).__name__}: {e}`")
 
344
  @app.on_message(filters.command(["profiles"]) & filters.private)
345
  async def profiles_cmd(_: Client, m: Message):
346
  uid = m.from_user.id if m.from_user else 0
347
+ if not await require_allowed(uid):
 
348
  return await safe_reply(m, texts.NOT_ALLOWED)
349
 
350
  j = await profile_list(uid)
 
354
  profiles = j.get("profiles") or []
355
  default_id = j.get("default_profile_id")
356
 
357
+ txt = "👤 **Profiles**\n\n"
358
+ txt += f"🟢 Current: `{default_id}`\n\n"
359
+
360
  for i, p in enumerate(profiles, start=1):
361
  pid = p.get("profile_id")
362
  ch = (p.get("channel_title") or "—").strip()
 
364
  sec = p.get("client_secret_hint") or "****…****"
365
  connected = "✅ Connected" if (p.get("has_refresh") and p.get("channel_id")) else "⏳ Pending"
366
  cur = "🟢 Current" if pid == default_id else ""
367
+ txt += f"[{i}] {ch} {cur}\n"
368
+ txt += f" id: {cid}\n"
369
+ txt += f" secret: {sec}\n"
370
+ txt += f" {connected}\n\n"
371
 
372
  kb = profiles_keyboard(profiles)
373
  await safe_reply(m, txt, reply_markup=kb)
374
 
375
+ # -------- Upload --------
376
  @app.on_message(filters.private & (filters.video | filters.document))
377
  async def upload_handler(app_: Client, m: Message):
378
  uid = m.from_user.id if m.from_user else 0
379
+ if not await require_allowed(uid):
 
380
  return await safe_reply(m, texts.NOT_ALLOWED)
381
 
382
  media = m.video or m.document
 
407
  return await safe_reply(m, texts.TOKEN_FAIL.format(tok))
408
  access_tok = tok["access_token"]
409
 
410
+ status = await safe_reply(m, "⬇️ Downloading…")
 
411
  task_id = f"{uid}:{m.id}"
412
  create_task(task_id, uid)
413
 
 
416
  file_name = "video.bin"
417
 
418
  try:
419
+ # ---- Download ----
420
  set_task(task_id, "downloading", "")
 
421
  dl_t0 = time.time()
422
  file_path, file_size, file_name = await download_to_temp(app_, m)
423
  dl_sec = max(0.001, time.time() - dl_t0)
424
 
425
+ if (not file_size) and file_path and os.path.exists(file_path):
 
426
  try:
427
  file_size = os.path.getsize(file_path)
428
  except Exception:
 
430
 
431
  dl_avg_bps = int((file_size or 0) / dl_sec) if file_size else 0
432
 
433
+ await safe_edit(
434
+ status,
435
+ "✅ Downloaded\n"
436
+ f"• File: `{file_name}`\n"
437
+ f"• Size: {human_bytes(file_size)}\n"
438
+ f"• Time: {human_eta(int(dl_sec))}\n"
439
+ f"• Avg: {human_bytes(dl_avg_bps)}/s\n\n"
440
+ "⬆️ Starting upload…",
441
+ )
442
 
443
+ # ---- Upload ----
444
  set_task(task_id, "uploading", "")
445
  up_t0 = time.time()
446
 
 
454
  return
455
  last_edit = time.time()
456
  txt = (
457
+ "⬆️ Uploading to YouTube…\n"
458
  f"{human_bytes(done)}/{human_bytes(total)}\n"
459
  f"speed: {human_bytes(snap['speed_bps'])}/s | eta: {human_eta(snap['eta_sec'])}"
460
  )
461
  await safe_edit(status, txt)
462
 
463
+ title, desc = extract_title_description(m, file_name)
464
+
465
  yt_url = await upload_video(
466
  access_tok,
467
  file_path,
 
475
  up_avg_bps = int((file_size or 0) / up_sec) if file_size else 0
476
 
477
  await record_upload(uid, use_profile_id)
 
478
  set_task(task_id, "done", "", yt_url=yt_url)
479
+
480
+ await safe_edit(
481
+ status,
482
+ "✅ **Uploaded!**\n\n"
483
  f"🎬 {title}\n"
484
  f"🔗 {yt_url}\n\n"
485
  f"⬇️ Download: {human_eta(int(dl_sec))} | avg {human_bytes(dl_avg_bps)}/s\n"
 
488
 
489
  except Exception as e:
490
  set_task(task_id, "error", str(e))
491
+ await safe_edit(status, f"❌ Upload failed:\n`{type(e).__name__}: {e}`")
492
  finally:
493
  if file_path:
494
  cleanup_file(file_path)