Update src/streamlit_app.py

#2
Files changed (1) hide show
  1. src/streamlit_app.py +289 -515
src/streamlit_app.py CHANGED
@@ -95,7 +95,9 @@ def save_users_registry(users_list):
95
  with open(registry_file, 'w', encoding='utf-8') as f: json.dump(users_list, f)
96
  sync_file_to_cloud(registry_file)
97
 
98
- # ✅ معالج التواريخ الذكي لتجاهل الأصفار وفهم كل الصيغ (مثل 17.2.2027)
 
 
99
  def parse_timestamp(ts):
100
  if not ts or str(ts).strip() == "": return "Unlimited"
101
  ts_str = str(ts).strip()
@@ -130,12 +132,6 @@ def calculate_days_left(exp_date_str_or_ts):
130
  def save_admin(chat_id):
131
  with open(ADMIN_FILE, "w") as f: f.write(str(chat_id))
132
 
133
- def get_admin():
134
- try:
135
- if os.path.exists(ADMIN_FILE): return int(open(ADMIN_FILE, "r").read().strip())
136
- except: pass
137
- return None
138
-
139
  def load_servers():
140
  file_name = get_user_servers_file()
141
  sync_file_from_cloud(file_name)
@@ -218,13 +214,6 @@ def normalize_search_text(text):
218
  t = str(text).lower()
219
  t = re.sub(r'\b(بين|بي ان|be in)\b', 'bein', t)
220
  t = re.sub(r'\b(sports|sport|سبورت|رياضة|رياضه)\b', 'sport', t)
221
- t = re.sub(r'\b(ام بي سي)\b', 'mbc', t)
222
- t = re.sub(r'\b(اس اس سي)\b', 'ssc', t)
223
- t = re.sub(r'\b(الفجر|فجر)\b', 'fajr', t)
224
- t = re.sub(r'\b(اون تايم|on time|on sport|اون سبورت|اون)\b', 'ontime', t)
225
- t = re.sub(r'\b(ابو ظبي|أبوظبي|ابوظبي)\b', 'abu dhabi', t)
226
- t = re.sub(r'\b(الكاس|كاس|كأس|kass)\b', 'alkass', t)
227
- t = re.sub(r'\b(دبي)\b', 'dubai', t)
228
  t = re.sub(r'[^a-z0-9\sأ-ي]', '', t)
229
  return t
230
 
@@ -264,21 +253,15 @@ def parse_any_link(url):
264
  rest = re.split(r'/(?:live|movie|series)/', url, 1)[1]
265
  rest_clean = re.sub(r'/+', '/', rest).strip('/')
266
  parts = rest_clean.split('/')
267
- if len(parts) >= 2:
268
- return host, parts[0], parts[1]
269
  except: pass
270
  return None, None, None
271
 
272
- def clean_camouflage(text):
273
- return re.sub(r'[@#]+', '', text)
274
 
275
- # ==========================================
276
- # 🚀 الساحب الذكي الشامل (ربط 1 URL لعدة MACs)
277
- # ==========================================
278
  def extract_all_links_from_text(text):
279
  accounts = []
280
  seen = set()
281
-
282
  clean_text = re.sub(r'(https?)\s*:\s*//\s*([a-zA-Z0-9.-]+)\s*\.\s*([a-zA-Z]+)', r'\1://\2.\3', text)
283
 
284
  for link in re.findall(r'(https?://[^\s<>"\'\|]+)', clean_text, re.IGNORECASE):
@@ -299,11 +282,9 @@ def extract_all_links_from_text(text):
299
  if h_m and u_m and p_m:
300
  h, u, p = h_m.group(1).rstrip('/'), clean_camouflage(u_m.group(1)), clean_camouflage(p_m.group(1))
301
  uid_str = f"x_{h}_{u}"
302
- if uid_str not in seen:
303
- accounts.append({'type': 'xtream', 'host': h, 'user': u, 'pass': p, '_uid': str(uuid.uuid4())[:8]}); seen.add(uid_str)
304
 
305
  lines = [line.strip() for line in clean_text.split('\n') if line.strip()]
306
-
307
  current_mac_host = None
308
  mac_patt = re.compile(r'([0-9A-Fa-f]{2}[:\-][0-9A-Fa-f]{2}[:\-][0-9A-Fa-f]{2}[:\-][0-9A-Fa-f]{2}[:\-][0-9A-Fa-f]{2}[:\-][0-9A-Fa-f]{2})', re.IGNORECASE)
309
  url_patt = re.compile(r'(https?://[^\s]+)')
@@ -311,13 +292,11 @@ def extract_all_links_from_text(text):
311
  for line in lines:
312
  u_m = url_patt.search(line.replace(" ", ""))
313
  if u_m and 'username=' not in line: current_mac_host = u_m.group(1).rstrip('/')
314
-
315
  m_m = mac_patt.search(line)
316
  if m_m and current_mac_host:
317
  m = m_m.group(1).upper().replace('-', ':')
318
  uid_str = f"m_{current_mac_host}_{m}"
319
- if uid_str not in seen:
320
- accounts.append({'type': 'mac', 'host': current_mac_host, 'mac': m, '_uid': str(uuid.uuid4())[:8]}); seen.add(uid_str); seen.add(current_mac_host+m)
321
 
322
  direct_links = re.findall(r'(https?://[^\s<>"\'\|]+)', clean_text, re.IGNORECASE)
323
  for link in direct_links:
@@ -325,21 +304,19 @@ def extract_all_links_from_text(text):
325
  if link not in seen and 'username=' not in link and not re.search(r'/(?:live|movie|series)/', link) and 'get.php?' not in link:
326
  if any(x in link.lower() for x in ['.m3u8', '.ts', '.m3u', 'mpegts', '/stream/', 'amazonaws', '.dev', '.top', 'ok.ru', 'token=', 'index.', '/play/']):
327
  uid_str = f"d_{link}"
328
- if uid_str not in seen:
329
- accounts.append({'type': 'direct', 'url': link, '_uid': str(uuid.uuid4())[:8]}); seen.add(uid_str); seen.add(link)
330
  return accounts
331
 
332
  # ==========================================
333
- # 📡 صائد التواريخ الشامل (MAC Date Hunter)
334
  # ==========================================
335
  def smart_mac_session(mac, change_ua=False):
336
  session = requests.Session()
337
- ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0" if change_ua else "Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG322 stbapp/5.0.1"
338
  session.headers.update({"User-Agent": ua, "Cookie": f"mac={mac}; stb_lang=en; timezone=Europe/London;"})
339
  retry_strategy = Retry(total=1, status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["HEAD", "GET", "OPTIONS"], backoff_factor=0.5)
340
- adapter = HTTPAdapter(max_retries=retry_strategy)
341
- session.mount("http://", adapter)
342
- session.mount("https://", adapter)
343
  return session
344
 
345
  def clean_mac_address(mac): return mac.replace(':', '')
@@ -347,13 +324,13 @@ def clean_mac_address(mac): return mac.replace(':', '')
347
  def test_mac_portal(host, mac):
348
  session = smart_mac_session(mac)
349
  conns, is_working, exp = "0 / 1", False, "Unlimited"
350
-
351
  try:
352
  r_hs = session.get(f"{host}/portal.php?type=stb&action=handshake&mac={mac}", timeout=5, verify=False).json()
353
- if r_hs.get('js') and 'token' in r_hs['js']: session.headers.update({"Authorization": f"Bearer {r_hs['js']['token']}"})
354
-
355
- prof = session.get(f"{host}/portal.php?type=stb&action=get_profile&mac={mac}", timeout=5, verify=False).json().get('js', {})
356
- if prof:
 
357
  is_working = True
358
  conns = f"{prof.get('active_cons', '0')} / {prof.get('max_connections', '1')}"
359
  for key in ['phone', 'expire_billing_date', 'expiry', 'expire', 'end_date', 'expiration', 'expiration_date', 'account_balance']:
@@ -366,12 +343,13 @@ def test_mac_portal(host, mac):
366
  if exp == "Unlimited":
367
  mac_clean = clean_mac_address(mac)
368
  endpoints = [
369
- f"/portal.php?action=get_main_info&type=account_info&mac={mac}",
 
 
370
  f"/stalker_portal/server/api.php?action=get_subscription_info&mac={mac_clean}",
371
  f"/stalker_portal/api/v2/player/get_subscription_info?mac={mac_clean}",
372
  f"/server/api.php?action=get_subscription_info&mac={mac_clean}"
373
  ]
374
- possible_keys = ['phone', 'expire_billing_date', 'expiry', 'expire', 'end_date', 'expiration', 'expiration_date']
375
  for ep in endpoints:
376
  try:
377
  resp = session.get(f"{host.rstrip('/')}{ep}", timeout=5, verify=False)
@@ -381,19 +359,18 @@ def test_mac_portal(host, mac):
381
  if json_match: raw = json_match.group(1)
382
  try:
383
  data = json.loads(raw)
 
384
  js_data = data.get('js', data) if isinstance(data, dict) else data
385
  if isinstance(js_data, dict):
386
- for key in possible_keys:
387
  if key in js_data and js_data[key] and str(js_data[key]) not in ["0", "0000-00-00 00:00:00", "0000-00-00", "null", "none"]:
388
- exp = parse_timestamp(str(js_data[key]))
389
- return True, exp, conns
390
- except:
391
- date_match = re.search(r'(\d{1,4}[-/\.]\d{1,2}[-/\.]\d{1,4})|([A-Z][a-z]+ \d{1,2}, \d{4})', raw)
392
- if date_match: exp = parse_timestamp(date_match.group(0)); return True, exp, conns
393
  except: continue
394
  return True, exp, conns
395
 
396
- # 🔥 تطوير extract_mac_to_m3u_logic (نظام الذكاء المتعدد) 🔥
397
  def extract_mac_to_m3u_logic(portal, mac, mode="🍿 الكل (لايف+أفلام+مسلسلات)", add_ua=False, ext_choice=".m3u8"):
398
  session = smart_mac_session(mac)
399
  try:
@@ -401,17 +378,15 @@ def extract_mac_to_m3u_logic(portal, mac, mode="🍿 الكل (لايف+أفلا
401
  if hs.get('js') and 'token' in hs['js']: session.headers.update({"Authorization": f"Bearer {hs['js']['token']}"})
402
  except: pass
403
 
404
- st.write("🔄 جاري الاتصال بالبورتال واستخراج التصنيفات...")
405
  try: genres = session.get(f"{portal}/portal.php?type=itv&action=get_genres&mac={mac}&JsHttpRequest=1-xml", timeout=20, verify=False).json().get("js", [])
406
- except: return False, "❌ فشل الاتصال بالبورتال، السيرفر بطيء جداً.", 0
407
- if not genres: return False, "❌ لم يتم العثور على تصنيفات (حاول تغيير الماك).", 0
408
 
409
  m3u_lines = ["#EXTM3U", "# Generated by Shalaby VIP Panel\n"]
410
  total_channels = 0
411
  ua_suffix = "|User-Agent=IPTVSmartersPro" if add_ua else ""
412
  ext_val = "m3u8" if ext_choice == ".m3u8" else "ts" if ext_choice == ".ts" else ""
413
 
414
- # المستوى الأول: اختبار حماية البورتال
415
  strategy = "create_link"
416
  first_genre_id = genres[0].get('id') if genres else None
417
  if first_genre_id:
@@ -421,7 +396,6 @@ def extract_mac_to_m3u_logic(portal, mac, mode="🍿 الكل (لايف+أفلا
421
  test_cmd = test_data[0].get("cmd", "") or test_data[0].get("id")
422
  test_link = session.get(f"{portal}/portal.php?type=itv&action=create_link&cmd={urllib.parse.quote(str(test_cmd))}&mac={mac}", timeout=5, verify=False).json()
423
  if test_link.get("js") and test_link["js"].get("id") is None:
424
- # Fallback Protocol: تجربة Token/Session جديدة
425
  session = smart_mac_session(mac, change_ua=True)
426
  test_link_2 = session.get(f"{portal}/portal.php?type=itv&action=create_link&cmd={urllib.parse.quote(str(test_cmd))}&mac={mac}", timeout=5, verify=False).json()
427
  if test_link_2.get("js") and test_link_2["js"].get("id") is None: strategy = "raw_fallback"
@@ -447,12 +421,10 @@ def extract_mac_to_m3u_logic(portal, mac, mode="🍿 الكل (لايف+أفلا
447
  name, cid, cmd_str = ch.get("name", "Unknown"), ch.get("id"), str(ch.get("cmd", ""))
448
  if not cid or cid in seen: continue
449
  seen.add(cid); added = True
450
-
451
  if strategy == "create_link": stream_url = f"{portal}/portal.php?type=itv&action=create_link&cmd={urllib.parse.quote(cmd_str)}&mac={mac}"
452
- else: # Raw Link Hunting
453
  stream_url = f"{portal}/play/live.php?mac={mac}&stream={cid}"
454
  if ext_val: stream_url += f"&extension={ext_val}"
455
-
456
  logo = ch.get("logo", "")
457
  if mode == "⚽ رياضة فقط VIP":
458
  if is_arabic_sport(name, g_name): res_sports.append({"name": f"{name} [{sn}]", "group": categorize_vip_sport(name, sn), "logo": get_vip_logo(name) or logo, "url": stream_url})
@@ -469,8 +441,7 @@ def extract_mac_to_m3u_logic(portal, mac, mode="🍿 الكل (لايف+أفلا
469
  else: m3u_lines.extend(res_l); total_channels += len(res_l)
470
  if mode == "⚽ رياضة فقط VIP" and sports_channels:
471
  sports_channels.sort(key=get_vip_sort_key)
472
- for ch in sports_channels:
473
- m3u_lines.append(f'#EXTINF:-1 tvg-logo="{ch["logo"]}" group-title="{ch["group"]}",{ch["name"]}\n{ch["url"]}{ua_suffix}'); total_channels += 1
474
 
475
  if mode in ["🎬 لايف + أفلام", "🍿 الكل (لايف+أفلام+مسلسلات)"]:
476
  try:
@@ -483,9 +454,8 @@ def extract_mac_to_m3u_logic(portal, mac, mode="🍿 الكل (لايف+أفلا
483
  vod_data = session.get(f"{portal}/portal.php?type=vod&action=get_ordered_list&category={c_id}&p={p}&mac={mac}&JsHttpRequest=1-xml", timeout=10, verify=False).json().get("js", {}).get("data", [])
484
  if not vod_data: break
485
  for vod in vod_data:
486
- name, vid, logo = vod.get("name", "Unknown Movie"), vod.get("cmd", "") or vod.get("id"), vod.get("screenshot_uri", "")
487
- stream_url = f"{portal}/play/vod.php?mac={mac}&stream={vid}&extension=mp4"
488
- m3u_lines.append(f'#EXTINF:-1 tvg-logo="{logo}" group-title="Movies - {c_name}",{name}\n{stream_url}{ua_suffix}'); total_channels += 1
489
  p += 1
490
  except: break
491
  except: pass
@@ -501,18 +471,17 @@ def extract_mac_to_m3u_logic(portal, mac, mode="🍿 الكل (لايف+أفلا
501
  ser_data = session.get(f"{portal}/portal.php?type=series&action=get_ordered_list&category={c_id}&p={p}&mac={mac}&JsHttpRequest=1-xml", timeout=10, verify=False).json().get("js", {}).get("data", [])
502
  if not ser_data: break
503
  for ser in ser_data:
504
- name, sid, logo = ser.get("name", "Unknown Series"), ser.get("id"), ser.get("screenshot_uri", "")
505
- stream_url = f"{portal}/play/series.php?mac={mac}&stream={sid}&extension=mp4"
506
- m3u_lines.append(f'#EXTINF:-1 tvg-logo="{logo}" group-title="Series - {c_name}",{name}\n{stream_url}{ua_suffix}'); total_channels += 1
507
  p += 1
508
  except: break
509
  except: pass
510
 
511
- if total_channels == 0: return False, "❌ لم يتم العثور على قنوات.", 0
512
  return True, "\n".join(m3u_lines), total_channels
513
 
514
  # ==========================================
515
- # ✅ الفحص الذكي (Xtream + Direct) + قناص البنج الفعلي
516
  # ==========================================
517
  def check_server_smart(host, user, pwd):
518
  headers = get_headers()
@@ -533,41 +502,35 @@ def check_server_smart(host, user, pwd):
533
  except: pass
534
  return None
535
 
536
- # 🔥 قناص البنج الفعلي (يتخطى البنج الوهمي) 🔥
537
  def test_live_stream_ping(host, user, pw):
538
  try:
539
  api_url = f"{host.rstrip('/')}/player_api.php?username={user}&password={pw}&action=get_live_streams"
540
  r = requests.get(api_url, headers=get_headers(), timeout=5, verify=False).json()
541
  if isinstance(r, list) and len(r) > 0:
542
- stream_id = r[0].get('stream_id')
543
- test_url = f"{host.rstrip('/')}/live/{user}/{pw}/{stream_id}.ts"
544
  res = requests.get(test_url, headers=get_headers(), stream=True, timeout=5, verify=False)
545
  status = res.status_code
546
  if status in [200, 302, 206]:
547
  chunk = next(res.iter_content(chunk_size=128), b"")
548
  res.close()
549
- if b"<html" in chunk.lower() or b"error" in chunk.lower()[:20]: return False, "❌ السيرفر يعيد صفحة خطأ (بث متوقف)."
550
  return True, "✅ يعمل بقوة."
551
  res.close()
552
  return False, f"❌ متوقف (كود {status})."
553
- return False, "⚠️ الحساب يعمل لكن لا توجد قنوات لايف."
554
  except: return False, "❌ فشل الاتصال."
555
 
556
  def advanced_direct_link_test(url):
557
- headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36', 'Accept': '*/*'}
558
  try:
559
  res = requests.get(url, headers=headers, stream=True, timeout=5, verify=False, allow_redirects=True)
560
  if res.status_code in [200, 206, 302, 304]:
561
  chunk = next(res.iter_content(chunk_size=2048), b"")
562
  res.close()
563
  text_chunk = chunk.decode('utf-8', errors='ignore').strip()
564
- if text_chunk.startswith("#EXTM3U") or "#EXTINF" in text_chunk:
565
- count = max(1, text_chunk.count("#EXTINF"))
566
- return True, True, count
567
- lower_chunk = text_chunk.lower()
568
- if "<html" in lower_chunk and not any(x in url for x in ['.dev', '.workers', '.top']): return False, False, 0
569
  return True, False, 0
570
- res.close()
571
  except: pass
572
  return False, False, 0
573
 
@@ -581,34 +544,20 @@ def process_single_account_sync(a):
581
  if info:
582
  d = calculate_days_left(info.get('exp_date'))
583
  ping_ok, _ = test_live_stream_ping(a['host'], a['user'], a['pass'])
584
- formats = info.get('formats', 'HLS (m3u8), TS')
585
- a.update({
586
- 'days': d, 'type_auth': info.get('source', 'Xtream'),
587
- 'created_at': parse_timestamp(info.get('created_at')),
588
- 'exp_date': parse_timestamp(info.get('exp_date')),
589
- 'status': 'يعمل بقوة' if ping_ok else 'متصل (البث متوقف)',
590
- 'ping_ok': ping_ok, 'formats': formats, 'conns': info.get('conns', 'N/A')
591
- })
592
  return a
593
  elif a['type'] == 'mac':
594
  prof_ok, exp, conns = test_mac_portal(a['host'], a['mac'])
595
  if prof_ok:
596
- d = calculate_days_left(exp)
597
- a.update({
598
- 'days': d, 'type_auth': 'Portal MAC',
599
- 'created_at': '-', 'exp_date': exp,
600
- 'status': 'يعمل بقوة', 'ping_ok': True, 'formats': 'Stalker/MAG', 'conns': conns
601
- })
602
- else:
603
- a.update({'days': 'MAC', 'type_auth': 'Portal MAC', 'created_at': '-', 'exp_date': '-', 'status': 'لا يعمل', 'ping_ok': False, 'formats': 'Unknown', 'conns': 'N/A'})
604
  return a
605
  elif a['type'] == 'direct':
606
  ping_ok, is_playlist, ch_count = advanced_direct_link_test(a['url'])
607
  if ping_ok:
608
  if is_playlist: a.update({'host': 'Playlist M3U', 'days': 'Direct', 'type_auth': 'Playlist', 'status': 'قائمة تشغيل تعمل', 'ping_ok': True, 'formats': 'm3u8/ts', 'ch_count': ch_count})
609
  else: a.update({'host': 'Direct Link', 'days': 'Direct', 'type_auth': 'Stream', 'status': 'بث مباشر يعمل', 'ping_ok': True, 'formats': 'ts/m3u8'})
610
- else:
611
- a.update({'host': 'Direct Link', 'days': 'Direct', 'type_auth': 'Stream', 'status': 'لا يعمل', 'ping_ok': False, 'formats': 'Unknown'})
612
  return a
613
  except: pass
614
  return None
@@ -630,17 +579,14 @@ async def run_async_scan(accs, progress_bar, status_msg, live_placeholder, conte
630
  if result:
631
  valid_found.append(result)
632
  valid_found.sort(key=live_sort_key, reverse=True)
633
-
634
  with live_placeholder.container():
635
  for srv in valid_found:
636
  ping_icon = "🟢" if srv.get('ping_ok') else "🔴"
637
- days_val = srv.get('days', 0)
638
- days_str = f"{days_val} يوم" if str(days_val).isdigit() else str(days_val)
639
  if srv.get('type') == 'direct': st.success(f"{ping_icon} {srv['status']} | 🌐 `{srv['url']}` | ⏳ {days_str}")
640
  elif srv.get('type') == 'mac': st.success(f"{ping_icon} {srv['status']} | 🌐 `{srv['host']}` | 💳 `{srv['mac']}` | ⏳ {days_str}")
641
  else: st.success(f"{ping_icon} {srv['status']} | 🌐 `{srv['host']}` | 👤 `{srv['user']}` | ⏳ {days_str}")
642
-
643
- try:
644
  if result.get('type') == 'xtream': save_to_global_vips({'host': result['host'], 'user': result['user'], 'pass': result['pass'], 'days': result.get('days')})
645
  elif result.get('type') == 'mac': save_to_global_vips({'host': result['host'], 'mac': result['mac'], 'days': result.get('days'), 'type': 'mac'})
646
  except: pass
@@ -657,19 +603,76 @@ def safe_async_run(coro):
657
  asyncio.set_event_loop(new_loop)
658
  result[0] = new_loop.run_until_complete(coro)
659
  t = threading.Thread(target=_run)
660
- t.start()
661
- t.join()
662
  return result[0]
663
  else: return asyncio.run(coro)
664
 
665
- def live_sort_key(x):
666
- ping_status = 1 if x.get('ping_ok', False) else 0
667
- days = x.get('days', 0)
668
- days_val = float('inf') if str(days).lower() in ['unlimited', 'direct', 'mac', 'none', 'null', '-'] else int(days) if str(days).isdigit() else -1
669
- return (ping_status, days_val)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
670
 
671
  # ==========================================
672
- # 🎨 واجهة الكروت الموحدة (Header مُحسّن)
673
  # ==========================================
674
  def render_server_card(server, idx, context="tab1"):
675
  t = server.get('type')
@@ -678,308 +681,160 @@ def render_server_card(server, idx, context="tab1"):
678
  uid_str = server.get('_uid', str(uuid.uuid4())[:8])
679
  base_k = f"{context}_{idx}_{uid_str}"
680
 
681
- force_open = False
682
- if f"file_{base_k}" in st.session_state or f"ping_res_{base_k}" in st.session_state or f"prep_err_{base_k}" in st.session_state or f"preview_url_{base_k}" in st.session_state or f"preview_err_{base_k}" in st.session_state:
683
- force_open = True
684
-
685
  ping_icon = "🟢" if server.get('ping_ok') else "🔴"
686
- status_str = server.get('status', 'نشط')
687
 
688
- # 🌟 Header Section: كل البيانات بالخارج لسهولة الـ Copy
689
- if t == 'direct':
690
- title = f"{ping_icon} {status_str} | 🌐 {server.get('url', '')} | ⏳ {days_str}"
691
- elif t == 'mac':
692
- title = f"{ping_icon} {status_str} | 🌐 {server.get('host', '')} | 💳 {server.get('mac', '')} | 👥 {server.get('conns', '0 / 1')} | 🛑 {server.get('exp_date', 'Unlimited')} | ⏳ {days_str}"
693
- else:
694
- title = f"{ping_icon} {status_str} | 🌐 {server.get('host', '')} | 👤 {server.get('user', '')} | 🔑 {server.get('pass', '')} | 👥 {server.get('conns', 'N/A')} | 🛑 {server.get('exp_date', 'Unlimited')} | ⏳ {days_str}"
695
 
696
  title += "\u200B" * st.session_state.reset_exp
697
 
698
  with st.expander(title, expanded=force_open):
699
- # 🔥 داخل البطاقة: رابط M3U المباشر في أول سطر 🔥
700
  if t == 'xtream':
701
  formatted_m3u_url = f"{server['host'].rstrip('/')}/get.php?username={server['user']}&password={server['pass']}&type=m3u_plus"
702
- st.text_input("🔗 الرابط المباشر للمشغل (M3U_PLUS) - للنسخ السريع:", value=formatted_m3u_url, key=f"link_m3u_{base_k}")
703
  st.markdown("---")
704
- st.markdown(f"### ⚙️ خيارات السحب والتشغيل:")
705
-
706
  elif t == 'direct':
707
- st.text_input("🔗 الرابط المباشر - للنسخ السريع:", value=server.get('url', ''), key=f"link_{base_k}")
708
  st.markdown("---")
709
- st.markdown(f"### ⚙️ خيارات التشغيل:")
710
-
711
- elif t == 'mac':
712
- st.markdown(f"### ⚙️ خيارات سحب الماك الذكية:")
713
 
714
- # ================== أزرار التحكم ==================
715
  if t == 'direct':
716
  col_p1, col_p2 = st.columns(2)
717
  with col_p1:
718
- if server.get('type_auth') == 'Playlist':
719
- if st.button("📥 سحب القنوات (قائمة التشغيل)", key=f"prep_{base_k}"):
720
- with st.spinner("جاري سحب المحتوى..."):
721
- try:
722
- txt = requests.get(server['url'], headers=get_headers(), timeout=10, verify=False).text
723
- st.session_state[f"file_{base_k}"] = txt.encode('utf-8')
724
- st.session_state[f"count_{base_k}"] = txt.count("#EXTINF")
725
- st.session_state[f"filename_{base_k}"] = f"Playlist_{uid_str}.m3u"
726
- st.rerun()
727
- except: st.error("❌ فشل الاتصال وسحب القائمة.")
728
  with col_p2:
729
- if st.button("📺 خيارات المعاينة", key=f"preview_{base_k}"):
730
- st.session_state[f"preview_url_{base_k}"] = server.get('url', '')
731
- st.rerun()
732
 
733
  elif t == 'mac':
734
  col_mode, col_ext, col_ua, col_btn = st.columns([3, 2, 2, 2])
735
- mac_mode = col_mode.selectbox("🎯 المحتوى:", ["⚽ رياضة فقط VIP", "📺 لايف فقط", "🎬 لايف + أفلام", "🍿 الكل (لايف+أفلام+مسلسلات)"], key=f"mode_{base_k}")
736
- mac_ext = col_ext.selectbox("صيغة البث:", [".m3u8", ".ts", "بدون صيغة"], index=0, key=f"ext_{base_k}")
737
- mac_ua = col_ua.checkbox("إضافة حماية UA", value=False, key=f"ua_{base_k}")
738
-
739
- if col_btn.button("📥 تجهيز السحب", key=f"prep_btn_{base_k}"):
740
- with st.spinner("جاري سحب قنوات الماك الذكي (يُرجى الانتظار)..."):
741
  ok, res_text, count = extract_mac_to_m3u_logic(server['host'], server['mac'], mac_mode, mac_ua, mac_ext)
742
- if ok:
743
- st.session_state[f"file_{base_k}"] = res_text.encode('utf-8')
744
- st.session_state[f"count_{base_k}"] = count
745
- st.session_state[f"filename_{base_k}"] = f"MAC_{server['mac'].replace(':','')}.m3u"
746
- if f"prep_err_{base_k}" in st.session_state: del st.session_state[f"prep_err_{base_k}"]
747
  else: st.session_state[f"prep_err_{base_k}"] = res_text
748
  st.rerun()
749
 
750
  elif t == 'xtream':
751
  col_ping, col_preview = st.columns(2)
752
  with col_ping:
753
- if st.button("📡 فحص البث الفعلي بدقة", key=f"ping_{base_k}"):
754
- with st.spinner("جاري اختبار سحب الداتا الفعلي..."):
755
  ok, msg = test_live_stream_ping(server['host'], server['user'], server['pass'])
756
- st.session_state[f"ping_res_{base_k}"] = (ok, msg)
757
- st.rerun()
758
  with col_preview:
759
- if st.button("📺 خيارات معاينة البث", key=f"preview_btn_{base_k}"):
760
- with st.spinner("جاري جلب رابط البث للمعاينة..."):
761
  try:
762
- api_url = f"{server['host'].rstrip('/')}/player_api.php?username={server['user']}&password={server['pass']}&action=get_live_streams"
763
- r_streams = requests.get(api_url, headers={'User-Agent': 'IPTVSmartersPro'}, timeout=5, verify=False).json()
764
- if isinstance(r_streams, list) and len(r_streams) > 0:
765
- s_id = r_streams[0].get('stream_id')
766
- play_url = f"{server['host'].rstrip('/')}/live/{server['user']}/{server['pass']}/{s_id}.m3u8"
767
- st.session_state[f"preview_url_{base_k}"] = play_url
768
- else: st.session_state[f"preview_err_{base_k}"] = "لم يتم العثور على قنوات للمعاينة."
769
- except: st.session_state[f"preview_err_{base_k}"] = "فشل الاتصال لجلب البث."
770
  st.rerun()
771
 
772
  if f"ping_res_{base_k}" in st.session_state:
773
  ok, msg = st.session_state[f"ping_res_{base_k}"]
774
  if ok: st.success(msg)
775
  else: st.error(msg)
776
- if f"preview_err_{base_k}" in st.session_state:
777
- st.error(st.session_state[f"preview_err_{base_k}"])
778
-
779
- def custom_api_extractor_sync(host, user, pw, mode, ext_choice, add_ua):
780
- headers = {'User-Agent': 'IPTVSmartersPro', 'Accept': '*/*'}
781
- output_file = f"Extracted_VIP_{user}.m3u"
782
- extracted_count = 0
783
- live_ext = "" if ext_choice == "بدون صيغة" else ext_choice
784
- ua_suffix = "|User-Agent=IPTVSmartersPro" if add_ua else ""
785
- if host.endswith('/'): host = host[:-1]
786
-
787
- with open(output_file, 'w', encoding='utf-8') as f:
788
- f.write("#EXTM3U\n")
789
- if mode == "⚽ رياضة فقط VIP":
790
- api_url = f"{host}/player_api.php?username={user}&password={pw}"
791
- try:
792
- streams = requests.get(f"{api_url}&action=get_live_streams", timeout=25, verify=False).json()
793
- cats = requests.get(f"{host}/player_api.php?username={user}&password={pw}&action=get_live_categories", headers=headers, timeout=25, verify=False).json()
794
- cat_map = {i['category_id']: i['category_name'] for i in cats}
795
- domain_parts = host.split('//')[-1].split(':')[0].split('.')
796
- valid_parts = [p for p in domain_parts if p.lower() not in ['com', 'net', 'org', 'info', 'tv'] and len(p)>2]
797
- sn = re.sub(r'[^a-zA-Z0-9]', '', valid_parts[0].capitalize() if valid_parts else "Server")
798
- vip_channels = []
799
- for s in streams:
800
- name, cat = str(s.get('name', '')), str(cat_map.get(s.get('category_id'), "Other"))
801
- if is_arabic_sport(name, cat):
802
- chic_name = f"{name} [{sn}]"
803
- vip_channels.append({"name": chic_name, "group": categorize_vip_sport(name, sn), "logo": get_vip_logo(name) or s.get("stream_icon", ""), "url": f"{host}/live/{user}/{pw}/{s.get('stream_id')}{live_ext}"})
804
- vip_channels.sort(key=get_vip_sort_key)
805
- for ch in vip_channels:
806
- f.write(f'#EXTINF:-1 tvg-logo="{ch["logo"]}" group-title="{ch["group"]}",{ch["name"]}\n{ch["url"]}{ua_suffix}\n'); extracted_count += 1
807
- except: pass
808
- else:
809
- if mode in ["📺 لايف فقط", "🎬 لايف + أفلام", "🍿 الكل (لايف+أفلام+مسلسلات)"]:
810
- try:
811
- with urllib.request.urlopen(urllib.request.Request(f"{host}/player_api.php?username={user}&password={pw}&action=get_live_streams", headers=headers), timeout=30) as resp:
812
- data = json.loads(resp.read().decode('utf-8'))
813
- if isinstance(data, list):
814
- for ch in data: f.write(f'#EXTINF:-1 tvg-logo="{ch.get("stream_icon", "")}" group-title="Live TV",{ch.get("name", "Unknown Live")}\n{host}/live/{user}/{pw}/{ch.get("stream_id", "")}{live_ext}{ua_suffix}\n'); extracted_count += 1
815
- except: pass
816
- if mode in ["🎬 لايف + أفلام", "🍿 الكل (لايف+أفلام+مسلسلات)"]:
817
- try:
818
- with urllib.request.urlopen(urllib.request.Request(f"{host}/player_api.php?username={user}&password={pw}&action=get_vod_streams", headers=headers), timeout=30) as resp:
819
- data = json.loads(resp.read().decode('utf-8'))
820
- if isinstance(data, list):
821
- for mov in data: f.write(f'#EXTINF:-1 tvg-logo="{mov.get("stream_icon", "")}" group-title="Movies",{mov.get("name", "Unknown Movie")}\n{host}/movie/{user}/{pw}/{mov.get("stream_id", "")}.{mov.get("container_extension", "mp4")}{ua_suffix}\n'); extracted_count += 1
822
- except: pass
823
- if mode == "🍿 الكل (لايف+أفلام+مسلسلات)":
824
- try:
825
- with urllib.request.urlopen(urllib.request.Request(f"{host}/player_api.php?username={user}&password={pw}&action=get_series", headers=headers), timeout=30) as resp:
826
- data = json.loads(resp.read().decode('utf-8'))
827
- if isinstance(data, list):
828
- for ser in data: f.write(f'#EXTINF:-1 tvg-logo="{ser.get("cover", "")}" group-title="Series",{ser.get("name", "Unknown Series")} (Series)\n{host}/series/{user}/{pw}/{ser.get("series_id", "")}{live_ext}{ua_suffix}\n'); extracted_count += 1
829
- except: pass
830
- return True if extracted_count > 0 else False, output_file, extracted_count
831
-
832
- col_mode, col_ext, col_ua, col_btn = st.columns([3, 2, 2, 2])
833
- selected_mode = col_mode.selectbox("🎯 المحتوى:", ["⚽ رياضة فقط VIP", "📺 لايف فقط", "🎬 لايف + أفلام", "🍿 الكل (لايف+أفلام+مسلسلات)"], key=f"mode_{base_k}")
834
- selected_ext = col_ext.selectbox("صيغة البث:", [".m3u8", ".ts", "بدون صيغة"], key=f"ext_{base_k}")
835
- add_ua = col_ua.checkbox("إضافة حماية UA", value=False, key=f"ua_{base_k}")
836
-
837
- if col_btn.button("📥 تجهيز السحب", key=f"prep_btn_{base_k}"):
838
- with st.spinner("جاري سحب المحتوى..."):
839
- success, file, count = custom_api_extractor_sync(server['host'], server['user'], server['pass'], selected_mode, selected_ext, add_ua)
840
- if success:
841
- st.session_state[f"file_{base_k}"] = open(file, 'rb').read()
842
- st.session_state[f"count_{base_k}"] = count
843
- st.session_state[f"filename_{base_k}"] = f"VIP_{server['user']}_{selected_mode.split()[0]}.m3u"
844
- if f"prep_err_{base_k}" in st.session_state: del st.session_state[f"prep_err_{base_k}"]
845
- else: st.session_state[f"prep_err_{base_k}"] = "❌ فشل السحب أو المحتوى فارغ."
846
- st.rerun()
847
-
848
- if f"prep_err_{base_k}" in st.session_state: st.error(st.session_state[f"prep_err_{base_k}"])
849
 
850
- # واجهة مشغل المعاينة المدمج
851
  if f"preview_url_{base_k}" in st.session_state:
852
  p_url = st.session_state[f"preview_url_{base_k}"]
853
- import urllib.parse
854
  hls_demo = f"https://hls-js.netlify.app/demo/?src={urllib.parse.quote(p_url)}"
855
- android_intent = f"intent:{p_url}#Intent;action=android.intent.action.VIEW;type=video/*;end"
856
-
857
- st.info("💡 معاينة المتصفح المدمجة (بالأسفل). إذا لم تعمل بسبب الحماية، استخدم الأزرار لفتح برامج الموبايل:")
858
- player_html = f"""
859
- <link href="https://vjs.zencdn.net/7.11.4/video-js.css" rel="stylesheet" />
860
- <script src="https://vjs.zencdn.net/7.11.4/video.min.js"></script>
861
- <video id="my-video" class="video-js vjs-default-skin vjs-big-play-centered" controls preload="auto" width="100%" height="250" data-setup='{{}}'>
862
- <source src="{p_url}" type="application/x-mpegURL">
863
- </video>
864
- """
865
- components.html(player_html, height=270)
866
-
867
- st.markdown(f"""
868
- <div style="background-color: #2b2b2b; padding: 15px; border-radius: 8px; text-align: center; border: 1px solid #444; margin-bottom: 15px;">
869
- <p style="color: #ff9800; font-weight: bold; font-size: 14px; margin-bottom: 10px;">🛡️ تخطي حماية المتصفح وتشغيل البث الخارجي:</p>
870
- <a href="{android_intent}" style="text-decoration: none;">
871
- <button style="background-color: #00cc66; color: white; border: none; padding: 11px 18px; border-radius: 6px; font-weight: bold; cursor: pointer; margin: 5px; box-shadow: 0px 4px 6px rgba(0,0,0,0.2);">
872
- 📱 فتح برامج الهاتف (VLC / MX Player)
873
- </button>
874
- </a>
875
- <a href="{hls_demo}" target="_blank" style="text-decoration: none;">
876
- <button style="background-color: #ff9800; color: white; border: none; padding: 11px 18px; border-radius: 6px; font-weight: bold; cursor: pointer; margin: 5px;">
877
- 🌐 مشغل ويب خارجي (Netlify)
878
- </button>
879
- </a>
880
- </div>
881
- """, unsafe_allow_html=True)
882
-
883
- # واجهة عرض وتنزيل الملفات
884
  if f"file_{base_k}" in st.session_state:
885
- st.success(f"✅ تم سحب ({st.session_state[f'count_{base_k}']}) عنصر بنجاح!")
886
- st.download_button("💾 تحميل كملف خارجي (.m3u)", data=st.session_state[f"file_{base_k}"], file_name=st.session_state[f"filename_{base_k}"], key=f"dl_{base_k}")
887
-
888
- with st.expander("🔍 بحث ونسخ القنوات المسحوبة (سريع ومضمون)", expanded=True):
889
  raw_text = st.session_state[f"file_{base_k}"].decode('utf-8', errors='ignore')
890
- st.info("💡 اضغط داخل الصندوق لنسخ القنوات. للفلترة استخدم صندوق البحث بالأعلى:")
891
-
892
  col_s1, col_s2 = st.columns([3, 1])
893
- search_q = col_s1.text_input("🔍 ابحث هنا (مثال: bein, ssc, sport):", key=f"sq_{base_k}")
894
- search_clicked = col_s2.button("ابحث 🎯", key=f"sbtn_{base_k}")
895
-
896
- display_text = raw_text
897
- if search_clicked or search_q:
898
  if search_q.strip():
899
- lines = raw_text.splitlines()
900
- filtered = ["#EXTM3U"]
901
  for i in range(len(lines)):
902
- if lines[i].startswith("#EXTINF"):
903
- if smart_search_match(search_q, lines[i]):
904
- filtered.append(lines[i])
905
- if i + 1 < len(lines) and not lines[i+1].startswith("#EXTINF"):
906
- filtered.append(lines[i+1])
907
-
908
- found_count = (len(filtered) - 1) // 2
909
- if found_count > 0:
910
- st.success(f"🎯 وجدنا ({found_count}) قناة! اضغط داخل المربع الأبيض ⬇️ واختر (تحديد الكل) ثم (نسخ).")
911
- display_text = "\n".join(filtered)
912
- else:
913
- st.error("❌ لم يتم العثور على قنوات بهذا الاسم.")
914
- display_text = ""
915
- else: st.warning("يرجى كتابة كلمة للبحث.")
916
-
917
- st.text_area("محتوى الملف (اضغط ضغطة مطولة -> تحديد الكل -> نسخ):", value=display_text, height=300, key=f"all_{base_k}")
918
 
919
  st.markdown("---")
920
  if context == "saved":
921
- if st.button("🗑️ حذف السيرفر نهائياً", key=f"del_{base_k}"):
922
  svs = load_servers(); svs.pop(idx); save_all_servers_to_db(svs)
923
- st.success("تم الحذف بنجاح!"); time.sleep(1); st.rerun()
924
  else:
925
- if t == 'xtream':
926
- if st.button("💾 حفظ في قاعدة بياناتي", key=f"save_{base_k}"):
927
- save_server_to_db(server['host'], server['user'], server['pass'], server['days'], auth_type='Xtream', created_at=server.get('created_at'), exp_date=server.get('exp_date'), conns=server.get('conns'))
928
- st.success(" تم حفظ السيرفر بنجاح!")
929
- elif t == 'mac':
930
- if st.button("💾 حفظ في قاعدة بياناتي", key=f"save_{base_k}"):
931
- save_server_to_db(server['host'], server['mac'], 'MAC Auth', server['days'], auth_type='Portal MAC', created_at=server.get('created_at'), exp_date=server.get('exp_date'), mac=server['mac'], conns=server.get('conns'))
932
- st.success("✅ تم حفظ الماك بنجاح!")
933
 
934
  # ==========================================
935
- # 🤖 تشغيل البوت والواجهة
936
  # ==========================================
937
  @bot.message_handler(commands=['start', 'menu'])
938
  def send_menu(message):
939
- save_admin(message.chat.id)
940
- bot.send_message(message.chat.id, "📺 **Shalaby IPTV Tool**\nأهلاً بك يا هندسة!", parse_mode="Markdown")
941
 
942
  @bot.message_handler(commands=['allservers'])
943
  def send_all_servers(message):
944
  vips = load_global_vips()
945
- if not vips:
946
- bot.reply_to(message, "📭 الصندوق الأسود فارغ حالياً.")
947
- return
948
  file_path = "All_Hunted_Servers.txt"
949
  with open(file_path, "w", encoding="utf-8") as f:
950
- f.write(f"--- 📦 إجمالي السيرفرات المصادة: {len(vips)} ---\n\n")
951
  for s in vips:
952
  if s.get('type') == 'mac': f.write(f"Portal: {s.get('host')}\nMAC: {s.get('mac')}\nDays Left: {s.get('days')}\n")
953
  else: f.write(f"Host: {s.get('host')}\nUser: {s.get('user')}\nPass: {s.get('pass')}\nDays Left: {s.get('days')}\n")
954
  f.write("-" * 30 + "\n")
955
- with open(file_path, "rb") as doc: bot.send_document(message.chat.id, doc, caption=f"🔥 تفضل يا هندسة، إجمالي السيرفرات المصادة ({len(vips)}).")
956
 
957
  @bot.message_handler(func=lambda message: True)
958
  def handle_all_messages(message):
959
- text = message.text
960
- accs = extract_all_links_from_text(text)
961
  if accs:
962
- msg = bot.reply_to(message, "🔍 جاري فحص السيرفرات المرسلة...")
963
  valid = []
964
  for a in accs:
965
  if a['type'] == 'xtream':
966
  info = check_server_smart(a['host'], a['user'], a['pass'])
967
- if info:
968
- days = calculate_days_left(info.get('exp_date'))
969
- days_str = f"{days} يوم" if str(days).isdigit() else str(days)
970
- valid.append(f"✅ `{a['host']}` | 👤 `{a['user']}` | ⏳ {days_str}")
971
- try: save_to_global_vips({'type': 'xtream', 'host': a['host'], 'user': a['user'], 'pass': a['pass'], 'days': days})
972
- except: pass
973
  elif a['type'] == 'mac':
974
  prof_ok, exp, _ = test_mac_portal(a['host'], a['mac'])
975
- if prof_ok:
976
- days = calculate_days_left(exp)
977
- days_str = f"{days} يوم" if str(days).isdigit() else str(days)
978
- valid.append(f"✅ `{a['host']}` | 💳 `{a['mac']}` | ⏳ {days_str}")
979
- try: save_to_global_vips({'type': 'mac', 'host': a['host'], 'mac': a['mac'], 'days': days})
980
- except: pass
981
- if valid: bot.edit_message_text(f"🎉 **تم الفحص بنجاح!**\n\n" + "\n".join(valid), chat_id=msg.chat.id, message_id=msg.message_id, parse_mode="Markdown")
982
- else: bot.edit_message_text("❌ لم أجد أي سيرفرات تعمل.", chat_id=msg.chat.id, message_id=msg.message_id)
983
 
984
  def run_bot_thread_init():
985
  for t in threading.enumerate():
@@ -987,162 +842,108 @@ def run_bot_thread_init():
987
  def start_polling():
988
  try: requests.get(f"https://api.telegram.org/bot{TOKEN}/deleteWebhook?drop_pending_updates=True", timeout=5)
989
  except: pass
990
- time.sleep(2)
991
- bot.infinity_polling(timeout=20, long_polling_timeout=10, skip_pending=True)
992
- t = threading.Thread(target=start_polling, name="TelegramBotThread", daemon=True)
993
- t.start()
994
  run_bot_thread_init()
995
 
996
  st.set_page_config(page_title="Shalaby IPTV Tool", page_icon="📺", layout="wide")
997
 
998
  st.markdown("""
999
  <style>
1000
- .block-container {
1001
- max-width: 100% !important;
1002
- padding-top: 1rem !important;
1003
- padding-left: 1rem !important;
1004
- padding-right: 1rem !important;
1005
- padding-bottom: 5rem !important;
1006
- }
1007
- section[data-testid="stSidebar"] {
1008
- width: 170px !important;
1009
- min-width: 170px !important;
1010
- max-width: 170px !important;
1011
- }
1012
- .main { overflow: visible !important; text-align: right; direction: rtl; }
1013
- h1 { font-size: 1.8rem !important; margin-bottom: 0 !important; padding-bottom: 0.5rem !important; text-align: center; color: #ff4b4b;}
1014
  div[data-baseweb="tab-list"] { display: flex; flex-direction: column; gap: 8px; }
1015
  div[data-baseweb="tab"] { width: 100%; justify-content: flex-end; background-color: #2b2b2b; border-radius: 8px; padding: 10px !important; border: 1px solid #444;}
1016
  div[data-baseweb="tab"]:hover { background-color: #ff4b4b; color: white !important;}
1017
  .stButton>button { width: 100%; border-radius: 8px; font-weight: bold; background-color: #ff4b4b; color: white; margin-top: 5px;}
1018
- .stButton>button:hover { background-color: #ff3333; color: white; border-color: #ff3333; }
1019
 
1020
  [data-testid="stExpander"] details summary {
1021
- background-color: #262730 !important;
1022
- border-left: 5px solid #ff9800 !important;
1023
- border-right: none !important;
1024
- border-radius: 8px !important;
1025
- padding: 12px !important;
1026
- margin-bottom: 5px !important;
1027
- box-shadow: 0px 4px 6px rgba(0,0,0,0.3) !important;
1028
- transition: all 0.3s ease !important;
1029
- }
1030
- [data-testid="stExpander"] details summary p,
1031
- [data-testid="stExpander"] details summary span {
1032
- color: #ffffff !important;
1033
- font-weight: bold !important;
1034
- }
1035
- [data-testid="stExpander"] details[open] summary {
1036
- background-color: #333333 !important;
1037
- border-left: 5px solid #ff9800 !important;
1038
  }
1039
  [data-testid="stExpander"] details[open] > div {
1040
- background-color: #1e1e1e !important;
1041
- border: 1px solid #444 !important;
1042
- border-top: none !important;
1043
- border-radius: 0 0 10px 10px !important;
1044
- padding: 20px 40px 20px 20px !important;
1045
- margin-top: -5px !important;
1046
- margin-left: 0px !important;
1047
- margin-right: 40px !important;
1048
- width: calc(100% - 40px) !important;
1049
- box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5) !important;
1050
- }
1051
- [data-testid="stExpander"] details[open] > div p,
1052
- [data-testid="stExpander"] details[open] > div span,
1053
- [data-testid="stExpander"] details[open] > div label {
1054
- color: #e0e0e0 !important;
1055
- }
1056
- [data-testid="stExpander"] .stTextInput input,
1057
- [data-testid="stExpander"] .stTextArea textarea {
1058
- background-color: #2b2b2b !important;
1059
- color: #ffffff !important;
1060
- border: 1px solid #555 !important;
1061
  }
1062
  </style>
1063
  """, unsafe_allow_html=True)
1064
 
 
1065
  if not st.session_state.logged_in:
1066
- st.title("🔐 بوابة الدخول - Shalaby IPTV Tool")
1067
- tab_in, tab_up = st.tabs(["تسجيل دخول", "إنشاء حساب"])
1068
  with tab_up:
1069
- new_user = st.text_input("اختر كود / اسم مستخدم جديد:")
1070
- if st.button("🚀 إنشاء حساب ودخول"):
1071
  if new_user.strip():
1072
  users = load_users_registry()
1073
  if new_user.strip() not in users:
1074
  users.append(new_user.strip()); save_users_registry(users)
1075
- st.session_state.username = new_user.strip(); st.session_state.logged_in = True
1076
  st.query_params["user_auth"] = new_user.strip()
1077
  components.html(f"""<script>localStorage.setItem("user_auth", "{new_user.strip()}");</script>""", height=0, width=0)
1078
  time.sleep(0.5); st.rerun()
1079
- else: st.error("❌ عذراً، هذا الاسم/الكود مستخدم من قبل.")
1080
  with tab_in:
1081
- exist_user = st.text_input("أدخل الكود الخاص بك:")
1082
- if st.button("🔓 دخول"):
1083
  if exist_user.strip() in load_users_registry():
1084
- st.session_state.username = exist_user.strip(); st.session_state.logged_in = True
1085
  st.query_params["user_auth"] = exist_user.strip()
1086
  components.html(f"""<script>localStorage.setItem("user_auth", "{exist_user.strip()}");</script>""", height=0, width=0)
1087
  time.sleep(0.5); st.rerun()
1088
- else: st.error("❌ هذا الحساب غير موجود.")
1089
  st.stop()
1090
 
1091
  with st.sidebar:
1092
- st.markdown(f"### 👤 مرحباً بك: `{st.session_state.username}`")
1093
- st.success("✅ لوحة تحكمك جاهزة.")
1094
  if st.button("🚪 تسجيل خروج"):
1095
  components.html("""<script>localStorage.removeItem("user_auth");</script>""", height=0, width=0)
1096
- st.query_params.clear(); st.session_state.logged_in = False; st.session_state.username = ""; time.sleep(0.5); st.rerun()
1097
 
1098
- st.title("📺 Shalaby IPTV Tool")
1099
  st.markdown("---")
1100
 
1101
  tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs([
1102
- "🧪 سحب سرفرات ذكي", "🗄️ سرفراتي المحفوظة", "🚀 تحديث كل السرفرات لسحبها", "🏭 m3u categories selector", "🔥 Mac to m3u", "✨ Smart m3u list extractor"
1103
  ])
1104
 
1105
  with tab1:
1106
- st.markdown("### 🔍 أدخل السيرفرات المبعثرة:")
1107
  if 'valid_found_tab1' in st.session_state and st.session_state['valid_found_tab1']:
1108
- if st.button("🔽 طي/إغلاق جميع السيرفرات وتنظيف الذاكرة", key="close_t1"):
1109
  st.session_state.reset_exp += 1
1110
  for k in list(st.session_state.keys()):
1111
  if k.startswith("file_") or k.startswith("ping_res_") or k.startswith("prep_err_") or k.startswith("preview_"): del st.session_state[k]
1112
  st.rerun()
1113
 
1114
- bulk_input = st.text_area("النص المبعثر:", height=150)
1115
- if st.button("🚀 بدء الفحص والاستخراج الذكي"):
1116
- if not bulk_input.strip(): st.warning("⚠️ الرجاء إدخال نص أولاً.")
1117
  else:
1118
  accs = extract_all_links_from_text(bulk_input)
1119
- total_to_check = len(accs)
1120
- if total_to_check == 0: st.error("❌ لم يتم العثور على روابط أو سيرفرات صالحة.")
1121
  else:
1122
- progress_bar = st.progress(0)
1123
- status_msg = st.empty()
1124
- live_placeholder = st.empty()
1125
- valid_found = safe_async_run(run_async_scan(accs, progress_bar, status_msg, live_placeholder, "t1"))
1126
- status_msg.success(f"🎉 اكتمل الفحص بصاروخ Asyncio! وجدنا {len(valid_found)} سيرفرات/روابط.")
1127
- st.session_state['valid_found_tab1'] = valid_found
1128
-
1129
- if valid_found:
1130
- current_db = load_servers()
1131
- for v in valid_found:
1132
- if v.get('type') == 'xtream' and not any(s.get('host') == v['host'] and s.get('user') == v['user'] for s in current_db):
1133
- current_db.append(v)
1134
- save_all_servers_to_db(current_db)
1135
 
1136
  if 'valid_found_tab1' in st.session_state and st.session_state['valid_found_tab1']:
1137
- st.markdown("#### ⚙️ الكروت النهائية بعد الفحص والترتيب التلقائي:")
1138
  for i, acc in enumerate(st.session_state['valid_found_tab1']): render_server_card(acc, i, context="t1")
1139
 
1140
  with tab2:
1141
- st.markdown("### 🗄️ لوحة تحكم السيرفرات المحفوظة")
1142
  svs = load_servers()
1143
- if not svs: st.warning("لا يوجد سيرفرات محفوظة.")
1144
  else:
1145
- if st.button("🔽 طي/إغلاق جميع السيرفرات وتنظيف الذاكرة", key="close_t2"):
1146
  st.session_state.reset_exp += 1
1147
  for k in list(st.session_state.keys()):
1148
  if k.startswith("file_") or k.startswith("ping_res_") or k.startswith("prep_err_") or k.startswith("preview_"): del st.session_state[k]
@@ -1150,109 +951,82 @@ with tab2:
1150
  for i, s in enumerate(svs): render_server_card(s, i, context="saved")
1151
 
1152
  with tab3:
1153
- st.markdown("### 🚀 تجميع وتحديث الماستر داتابيز")
1154
- if st.button("🔄 بدء التحديث الشامل"):
1155
  svs = load_servers()
1156
- if not svs: st.error("❌ لا يوجد سيرفرات محفوظة.")
1157
  else:
1158
- progress_bar2 = st.progress(0)
1159
  for i, s in enumerate(svs):
1160
- if s.get('type') != 'mac' and s.get('type') != 'direct': extract_sports_unified(s['host'], s['user'], s['pass'], skip_clean=True, ext=".m3u8")
1161
- progress_bar2.progress((i + 1) / len(svs))
1162
- f_db = load_db()
1163
- m3u_file = export_to_m3u(f_db, "My_Saved_Servers.m3u")
1164
- st.success(f" تم تحديث {len(f_db)} قناة بنجاح!")
1165
- with open(m3u_file, "rb") as file: st.download_button(label="💾 تحميل اللستة المحدثة", data=file, file_name=f"Updated_Shalaby_{st.session_state.username}.m3u", mime="audio/x-mpegurl")
1166
 
1167
  with tab4:
1168
- st.markdown("### 🏭 إنشاء لستة M3U مخصصة للعملاء")
1169
- all_channels = load_db()
1170
- if not all_channels: st.warning("⚠️ قاعدة البيانات فارغة. قم بتحديث الكل أولاً من التاب السابق.")
1171
  else:
1172
- unique_groups = set()
1173
- for ch in all_channels: unique_groups.add(ch['group'].replace('[SD]', '').replace('[HD]', '').replace('[FHD]', '').replace('[4K]', '').strip())
1174
- selected_groups = st.multiselect("📌 اختر الباقات المطلوبة:", sorted(list(unique_groups)), default=sorted(list(unique_groups))[:2] if unique_groups else None)
1175
- smart_rename = st.checkbox(" تفعيل التنظيف الذكي لأسماء الباقات")
1176
- if st.button(" إنشاء اللستة المخصصة"):
1177
- if not selected_groups: st.error("❌ الرجاء اختيار باقة واحدة على الأقل.")
1178
  else:
1179
- with st.spinner("جاري تجهيز الملف..."):
1180
- custom_channels = []
1181
- for ch in all_channels:
1182
- bg = ch['group'].replace('[SD]', '').replace('[HD]', '').replace('[FHD]', '').replace('[4K]', '').strip()
1183
- if bg in selected_groups:
1184
- if smart_rename: ch['group'] = categorize_vip_sport(ch['name'], re.search(r'\[(.*?)\]', ch['name']).group(1) if re.search(r'\[(.*?)\]', ch['name']) else "")
1185
- custom_channels.append(ch)
1186
- if custom_channels:
1187
- custom_file = export_to_m3u(custom_channels, "Custom_Client_Playlist.m3u")
1188
- st.success(f"🎉 تم إنشاء اللستة بنجاح! ({len(custom_channels)} قناة).")
1189
- with open(custom_file, "rb") as file: st.download_button(label="📥 تحميل اللستة المخصصة", data=file, file_name=f"Custom_VIP_{st.session_state.username}.m3u", mime="audio/x-mpegurl")
1190
- else: st.error("❌ لم يتم العثور على قنوات.")
1191
 
1192
  with tab5:
1193
- st.markdown("### 🔥 اختراق وتحويل الماك إلى M3U")
1194
- col1, col2 = st.columns(2)
1195
- with col1: mac_portal = st.text_input("🔗 رابط البورت (Portal URL):", placeholder="http://example.com:8080")
1196
- with col2: mac_address = st.text_input("💳 الماك أدرس (MAC Address):", placeholder="00:1A:79:XX:XX:XX")
1197
-
1198
- col_m, col_e, col_u, col_b = st.columns([3, 2, 2, 2])
1199
- mac_mode = col_m.selectbox("🎯 المحتوى:", ["⚽ رياضة فقط VIP", "📺 لايف فقط", "🎬 لايف + أفلام", "🍿 الكل (لايف+أفلام+مسلسلات)"], key="mac_standalone_mode")
1200
- mac_ext = col_e.selectbox("صيغة البث:", [".m3u8", ".ts", "بدون صيغة"], index=0, key="mac_standalone_ext")
1201
- mac_ua = col_u.checkbox("إضافة حماية UA", value=False, key="mac_standalone_ua")
1202
-
1203
- if col_b.button("🚀 بدء الاختراق والتحويل"):
1204
- if not mac_portal or not mac_address: st.warning("⚠️ الرجاء إدخال البورت والماك.")
1205
  else:
1206
- mac_portal = mac_portal.strip().rstrip("/")
1207
- if not mac_portal.startswith("http"): mac_portal = "http://" + mac_portal
1208
- with st.spinner("🕵️‍♂️ جاري تحليل الحماية واستخراج القنوات..."):
1209
- success, result, count = extract_mac_to_m3u_logic(mac_portal, mac_address.strip().upper(), mac_mode, mac_ua, mac_ext)
1210
- if success:
1211
- st.success(f"🎉 تمت عملية الاختراق والتحويل بنجاح! ({count} قناة)")
1212
- st.download_button(label="📥 تحميل ملف الـ M3U", data=result, file_name=f"Converted_{mac_address.replace(':', '')}_VIP.m3u", mime="audio/x-mpegurl")
1213
- else: st.error(result)
1214
 
1215
  with tab6:
1216
- st.markdown("### ✨ ساحب القنوات المطور (Extractor)")
1217
  if 'valid_found_tab6' in st.session_state and st.session_state['valid_found_tab6']:
1218
- if st.button("🔽 طي/إغلاق جميع السيرفرات وتنظيف الذاكرة", key="close_t6"):
1219
  st.session_state.reset_exp += 1
1220
  for k in list(st.session_state.keys()):
1221
  if k.startswith("file_") or k.startswith("ping_res_") or k.startswith("prep_err_") or k.startswith("preview_"): del st.session_state[k]
1222
  st.rerun()
1223
 
1224
- ex_input = st.text_area("📦 النص المبعثر (للسحب):", height=150)
1225
- if st.button("⚙️ فحص السيرفرات وتجهيزها", key="tab6_btn"):
1226
- if not ex_input.strip(): st.warning("⚠️ الرجاء إدخال بيانات السيرفر(ات) أولاً.")
1227
  else:
1228
  accs = extract_all_links_from_text(ex_input)
1229
- if not accs: st.error("❌ لم يتم العثور على روابط أو سيرفرات صالحة.")
1230
  else:
1231
- progress_bar = st.progress(0)
1232
- status_msg = st.empty()
1233
- live_placeholder_tab6 = st.empty()
1234
- valid_accs = safe_async_run(run_async_scan(accs, progress_bar, status_msg, live_placeholder_tab6, "t6"))
1235
- status_msg.success(f"🎉 اكتمل الفحص بصاروخ Asyncio! وجدنا {len(valid_accs)} سيرفرات/روابط.")
1236
- st.session_state['valid_found_tab6'] = valid_accs
1237
-
1238
- if valid_accs:
1239
- current_db = load_servers()
1240
- for v in valid_accs:
1241
- if v.get('type') == 'xtream' and not any(s.get('host') == v['host'] and s.get('user') == v['user'] for s in current_db):
1242
- current_db.append(v)
1243
- save_all_servers_to_db(current_db)
1244
 
1245
  if 'valid_found_tab6' in st.session_state and st.session_state['valid_found_tab6']:
1246
  st.markdown("#### ⚙️ الكروت النهائية بعد الفحص والترتيب التلقائي:")
1247
  for i, acc in enumerate(st.session_state['valid_found_tab6']): render_server_card(acc, i, context="t6")
1248
-
1249
- st.markdown("---")
1250
- with st.expander("🔬 فحص رابط قناة مفردة (اختبار الشاشة السوداء)"):
1251
- single_link = st.text_input("🔗 أدخل رابط القناة (m3u8 أو ts):", placeholder="http://host.com/live/user/pass/123.m3u8")
1252
- if st.button("⚡ فحص استجابة القناة"):
1253
- if single_link.strip():
1254
- with st.spinner("جاري فحص القناة..."):
1255
- ping_ok, _, _ = advanced_direct_link_test(single_link.strip())
1256
- if ping_ok: st.success("✅ القناة تعمل بكفاءة (تسحب بيانات).")
1257
- else: st.error("❌ القناة لا تستجيب (قد تكون شاشة سوداء أو محمية بشدة).")
1258
- else: st.warning("الرجاء إدخال الرابط أولاً.")
 
95
  with open(registry_file, 'w', encoding='utf-8') as f: json.dump(users_list, f)
96
  sync_file_to_cloud(registry_file)
97
 
98
+ # ==========================================
99
+ # 📅 معالج التواريخ الذكي
100
+ # ==========================================
101
  def parse_timestamp(ts):
102
  if not ts or str(ts).strip() == "": return "Unlimited"
103
  ts_str = str(ts).strip()
 
132
  def save_admin(chat_id):
133
  with open(ADMIN_FILE, "w") as f: f.write(str(chat_id))
134
 
 
 
 
 
 
 
135
  def load_servers():
136
  file_name = get_user_servers_file()
137
  sync_file_from_cloud(file_name)
 
214
  t = str(text).lower()
215
  t = re.sub(r'\b(بين|بي ان|be in)\b', 'bein', t)
216
  t = re.sub(r'\b(sports|sport|سبورت|رياضة|رياضه)\b', 'sport', t)
 
 
 
 
 
 
 
217
  t = re.sub(r'[^a-z0-9\sأ-ي]', '', t)
218
  return t
219
 
 
253
  rest = re.split(r'/(?:live|movie|series)/', url, 1)[1]
254
  rest_clean = re.sub(r'/+', '/', rest).strip('/')
255
  parts = rest_clean.split('/')
256
+ if len(parts) >= 2: return host, parts[0], parts[1]
 
257
  except: pass
258
  return None, None, None
259
 
260
+ def clean_camouflage(text): return re.sub(r'[@#]+', '', text)
 
261
 
 
 
 
262
  def extract_all_links_from_text(text):
263
  accounts = []
264
  seen = set()
 
265
  clean_text = re.sub(r'(https?)\s*:\s*//\s*([a-zA-Z0-9.-]+)\s*\.\s*([a-zA-Z]+)', r'\1://\2.\3', text)
266
 
267
  for link in re.findall(r'(https?://[^\s<>"\'\|]+)', clean_text, re.IGNORECASE):
 
282
  if h_m and u_m and p_m:
283
  h, u, p = h_m.group(1).rstrip('/'), clean_camouflage(u_m.group(1)), clean_camouflage(p_m.group(1))
284
  uid_str = f"x_{h}_{u}"
285
+ if uid_str not in seen: accounts.append({'type': 'xtream', 'host': h, 'user': u, 'pass': p, '_uid': str(uuid.uuid4())[:8]}); seen.add(uid_str)
 
286
 
287
  lines = [line.strip() for line in clean_text.split('\n') if line.strip()]
 
288
  current_mac_host = None
289
  mac_patt = re.compile(r'([0-9A-Fa-f]{2}[:\-][0-9A-Fa-f]{2}[:\-][0-9A-Fa-f]{2}[:\-][0-9A-Fa-f]{2}[:\-][0-9A-Fa-f]{2}[:\-][0-9A-Fa-f]{2})', re.IGNORECASE)
290
  url_patt = re.compile(r'(https?://[^\s]+)')
 
292
  for line in lines:
293
  u_m = url_patt.search(line.replace(" ", ""))
294
  if u_m and 'username=' not in line: current_mac_host = u_m.group(1).rstrip('/')
 
295
  m_m = mac_patt.search(line)
296
  if m_m and current_mac_host:
297
  m = m_m.group(1).upper().replace('-', ':')
298
  uid_str = f"m_{current_mac_host}_{m}"
299
+ if uid_str not in seen: accounts.append({'type': 'mac', 'host': current_mac_host, 'mac': m, '_uid': str(uuid.uuid4())[:8]}); seen.add(uid_str); seen.add(current_mac_host+m)
 
300
 
301
  direct_links = re.findall(r'(https?://[^\s<>"\'\|]+)', clean_text, re.IGNORECASE)
302
  for link in direct_links:
 
304
  if link not in seen and 'username=' not in link and not re.search(r'/(?:live|movie|series)/', link) and 'get.php?' not in link:
305
  if any(x in link.lower() for x in ['.m3u8', '.ts', '.m3u', 'mpegts', '/stream/', 'amazonaws', '.dev', '.top', 'ok.ru', 'token=', 'index.', '/play/']):
306
  uid_str = f"d_{link}"
307
+ if uid_str not in seen: accounts.append({'type': 'direct', 'url': link, '_uid': str(uuid.uuid4())[:8]}); seen.add(uid_str); seen.add(link)
 
308
  return accounts
309
 
310
  # ==========================================
311
+ # 📡 محرك فحص بورتالات الماك الاستثنائي
312
  # ==========================================
313
  def smart_mac_session(mac, change_ua=False):
314
  session = requests.Session()
315
+ ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36" if change_ua else "Mozilla/5.0 (QtEmbedded; U; Linux; C) AppleWebKit/533.3 (KHTML, like Gecko) MAG322 stbapp/5.0.1"
316
  session.headers.update({"User-Agent": ua, "Cookie": f"mac={mac}; stb_lang=en; timezone=Europe/London;"})
317
  retry_strategy = Retry(total=1, status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["HEAD", "GET", "OPTIONS"], backoff_factor=0.5)
318
+ session.mount("http://", HTTPAdapter(max_retries=retry_strategy))
319
+ session.mount("https://", HTTPAdapter(max_retries=retry_strategy))
 
320
  return session
321
 
322
  def clean_mac_address(mac): return mac.replace(':', '')
 
324
  def test_mac_portal(host, mac):
325
  session = smart_mac_session(mac)
326
  conns, is_working, exp = "0 / 1", False, "Unlimited"
 
327
  try:
328
  r_hs = session.get(f"{host}/portal.php?type=stb&action=handshake&mac={mac}", timeout=5, verify=False).json()
329
+ if r_hs.get('js') and isinstance(r_hs['js'], dict) and 'token' in r_hs['js']: session.headers.update({"Authorization": f"Bearer {r_hs['js']['token']}"})
330
+ prof_req = session.get(f"{host}/portal.php?type=stb&action=get_profile&mac={mac}", timeout=5, verify=False).json()
331
+ if 'js' in prof_req and prof_req['js'] == []: return False, "Unlimited", "N/A"
332
+ prof = prof_req.get('js', {})
333
+ if prof and isinstance(prof, dict):
334
  is_working = True
335
  conns = f"{prof.get('active_cons', '0')} / {prof.get('max_connections', '1')}"
336
  for key in ['phone', 'expire_billing_date', 'expiry', 'expire', 'end_date', 'expiration', 'expiration_date', 'account_balance']:
 
343
  if exp == "Unlimited":
344
  mac_clean = clean_mac_address(mac)
345
  endpoints = [
346
+ f"/portal.php?action=get_main_info&type=account_info&mac={mac}&JsHttpRequest=1-xml",
347
+ f"/portal.php?action=get_agreement_info&type=account_info&mac={mac}&JsHttpRequest=1-xml",
348
+ f"/portal.php?action=get_payment_info&type=account_info&mac={mac}&JsHttpRequest=1-xml",
349
  f"/stalker_portal/server/api.php?action=get_subscription_info&mac={mac_clean}",
350
  f"/stalker_portal/api/v2/player/get_subscription_info?mac={mac_clean}",
351
  f"/server/api.php?action=get_subscription_info&mac={mac_clean}"
352
  ]
 
353
  for ep in endpoints:
354
  try:
355
  resp = session.get(f"{host.rstrip('/')}{ep}", timeout=5, verify=False)
 
359
  if json_match: raw = json_match.group(1)
360
  try:
361
  data = json.loads(raw)
362
+ if 'js' in data and data['js'] == []: continue
363
  js_data = data.get('js', data) if isinstance(data, dict) else data
364
  if isinstance(js_data, dict):
365
+ for key in ['phone', 'expire_billing_date', 'expiry', 'expire', 'end_date', 'expiration', 'expiration_date']:
366
  if key in js_data and js_data[key] and str(js_data[key]) not in ["0", "0000-00-00 00:00:00", "0000-00-00", "null", "none"]:
367
+ return True, parse_timestamp(str(js_data[key])), conns
368
+ except: pass
369
+ date_match = re.search(r'(\d{4}-\d{2}-\d{2})|([A-Z][a-z]+ \d{1,2}, \d{4})', raw)
370
+ if date_match: return True, parse_timestamp(date_match.group(0)), conns
 
371
  except: continue
372
  return True, exp, conns
373
 
 
374
  def extract_mac_to_m3u_logic(portal, mac, mode="🍿 الكل (لايف+أفلام+مسلسلات)", add_ua=False, ext_choice=".m3u8"):
375
  session = smart_mac_session(mac)
376
  try:
 
378
  if hs.get('js') and 'token' in hs['js']: session.headers.update({"Authorization": f"Bearer {hs['js']['token']}"})
379
  except: pass
380
 
 
381
  try: genres = session.get(f"{portal}/portal.php?type=itv&action=get_genres&mac={mac}&JsHttpRequest=1-xml", timeout=20, verify=False).json().get("js", [])
382
+ except: return False, "❌ فشل الاتصال بالبورتال.", 0
383
+ if not genres: return False, "❌ لم يتم العثور على تصنيفات.", 0
384
 
385
  m3u_lines = ["#EXTM3U", "# Generated by Shalaby VIP Panel\n"]
386
  total_channels = 0
387
  ua_suffix = "|User-Agent=IPTVSmartersPro" if add_ua else ""
388
  ext_val = "m3u8" if ext_choice == ".m3u8" else "ts" if ext_choice == ".ts" else ""
389
 
 
390
  strategy = "create_link"
391
  first_genre_id = genres[0].get('id') if genres else None
392
  if first_genre_id:
 
396
  test_cmd = test_data[0].get("cmd", "") or test_data[0].get("id")
397
  test_link = session.get(f"{portal}/portal.php?type=itv&action=create_link&cmd={urllib.parse.quote(str(test_cmd))}&mac={mac}", timeout=5, verify=False).json()
398
  if test_link.get("js") and test_link["js"].get("id") is None:
 
399
  session = smart_mac_session(mac, change_ua=True)
400
  test_link_2 = session.get(f"{portal}/portal.php?type=itv&action=create_link&cmd={urllib.parse.quote(str(test_cmd))}&mac={mac}", timeout=5, verify=False).json()
401
  if test_link_2.get("js") and test_link_2["js"].get("id") is None: strategy = "raw_fallback"
 
421
  name, cid, cmd_str = ch.get("name", "Unknown"), ch.get("id"), str(ch.get("cmd", ""))
422
  if not cid or cid in seen: continue
423
  seen.add(cid); added = True
 
424
  if strategy == "create_link": stream_url = f"{portal}/portal.php?type=itv&action=create_link&cmd={urllib.parse.quote(cmd_str)}&mac={mac}"
425
+ else:
426
  stream_url = f"{portal}/play/live.php?mac={mac}&stream={cid}"
427
  if ext_val: stream_url += f"&extension={ext_val}"
 
428
  logo = ch.get("logo", "")
429
  if mode == "⚽ رياضة فقط VIP":
430
  if is_arabic_sport(name, g_name): res_sports.append({"name": f"{name} [{sn}]", "group": categorize_vip_sport(name, sn), "logo": get_vip_logo(name) or logo, "url": stream_url})
 
441
  else: m3u_lines.extend(res_l); total_channels += len(res_l)
442
  if mode == "⚽ رياضة فقط VIP" and sports_channels:
443
  sports_channels.sort(key=get_vip_sort_key)
444
+ for ch in sports_channels: m3u_lines.append(f'#EXTINF:-1 tvg-logo="{ch["logo"]}" group-title="{ch["group"]}",{ch["name"]}\n{ch["url"]}{ua_suffix}'); total_channels += 1
 
445
 
446
  if mode in ["🎬 لايف + أفلام", "🍿 الكل (لايف+أفلام+مسلسلات)"]:
447
  try:
 
454
  vod_data = session.get(f"{portal}/portal.php?type=vod&action=get_ordered_list&category={c_id}&p={p}&mac={mac}&JsHttpRequest=1-xml", timeout=10, verify=False).json().get("js", {}).get("data", [])
455
  if not vod_data: break
456
  for vod in vod_data:
457
+ stream_url = f"{portal}/play/vod.php?mac={mac}&stream={vod.get('cmd', '') or vod.get('id')}&extension=mp4"
458
+ m3u_lines.append(f'#EXTINF:-1 tvg-logo="{vod.get("screenshot_uri", "")}" group-title="Movies - {c_name}",{vod.get("name", "Unknown Movie")}\n{stream_url}{ua_suffix}'); total_channels += 1
 
459
  p += 1
460
  except: break
461
  except: pass
 
471
  ser_data = session.get(f"{portal}/portal.php?type=series&action=get_ordered_list&category={c_id}&p={p}&mac={mac}&JsHttpRequest=1-xml", timeout=10, verify=False).json().get("js", {}).get("data", [])
472
  if not ser_data: break
473
  for ser in ser_data:
474
+ stream_url = f"{portal}/play/series.php?mac={mac}&stream={ser.get('id')}&extension=mp4"
475
+ m3u_lines.append(f'#EXTINF:-1 tvg-logo="{ser.get("screenshot_uri", "")}" group-title="Series - {c_name}",{ser.get("name", "Unknown Series")}\n{stream_url}{ua_suffix}'); total_channels += 1
 
476
  p += 1
477
  except: break
478
  except: pass
479
 
480
+ if total_channels == 0: return False, "❌ لم يتم العثور على قنوات محتوى صالحة.", 0
481
  return True, "\n".join(m3u_lines), total_channels
482
 
483
  # ==========================================
484
+ # ✅ الفحص الذكي وقناص البنج الفعلي (تخطي الوهمي)
485
  # ==========================================
486
  def check_server_smart(host, user, pwd):
487
  headers = get_headers()
 
502
  except: pass
503
  return None
504
 
 
505
  def test_live_stream_ping(host, user, pw):
506
  try:
507
  api_url = f"{host.rstrip('/')}/player_api.php?username={user}&password={pw}&action=get_live_streams"
508
  r = requests.get(api_url, headers=get_headers(), timeout=5, verify=False).json()
509
  if isinstance(r, list) and len(r) > 0:
510
+ test_url = f"{host.rstrip('/')}/live/{user}/{pw}/{r[0].get('stream_id')}.ts"
 
511
  res = requests.get(test_url, headers=get_headers(), stream=True, timeout=5, verify=False)
512
  status = res.status_code
513
  if status in [200, 302, 206]:
514
  chunk = next(res.iter_content(chunk_size=128), b"")
515
  res.close()
516
+ if b"<html" in chunk.lower() or b"error" in chunk.lower()[:20]: return False, "❌ السيرفر يعيد صفحة خطأ (البث متوقف)."
517
  return True, "✅ يعمل بقوة."
518
  res.close()
519
  return False, f"❌ متوقف (كود {status})."
520
+ return False, "⚠️ الحساب يعمل لكن قنوات اللايف فارغة."
521
  except: return False, "❌ فشل الاتصال."
522
 
523
  def advanced_direct_link_test(url):
524
+ headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36'}
525
  try:
526
  res = requests.get(url, headers=headers, stream=True, timeout=5, verify=False, allow_redirects=True)
527
  if res.status_code in [200, 206, 302, 304]:
528
  chunk = next(res.iter_content(chunk_size=2048), b"")
529
  res.close()
530
  text_chunk = chunk.decode('utf-8', errors='ignore').strip()
531
+ if text_chunk.startswith("#EXTM3U") or "#EXTINF" in text_chunk: return True, True, max(1, text_chunk.count("#EXTINF"))
532
+ if "<html" in text_chunk.lower() and not any(x in url for x in ['.dev', '.workers', '.top']): return False, False, 0
 
 
 
533
  return True, False, 0
 
534
  except: pass
535
  return False, False, 0
536
 
 
544
  if info:
545
  d = calculate_days_left(info.get('exp_date'))
546
  ping_ok, _ = test_live_stream_ping(a['host'], a['user'], a['pass'])
547
+ a.update({'days': d, 'type_auth': info.get('source', 'Xtream'), 'created_at': parse_timestamp(info.get('created_at')), 'exp_date': parse_timestamp(info.get('exp_date')), 'status': 'يعمل بقوة' if ping_ok else 'متصل (البث متوقف)', 'ping_ok': ping_ok, 'formats': info.get('formats', 'HLS, TS'), 'conns': info.get('conns', 'N/A')})
 
 
 
 
 
 
 
548
  return a
549
  elif a['type'] == 'mac':
550
  prof_ok, exp, conns = test_mac_portal(a['host'], a['mac'])
551
  if prof_ok:
552
+ a.update({'days': calculate_days_left(exp), 'type_auth': 'Portal MAC', 'created_at': '-', 'exp_date': exp, 'status': 'يعمل بقوة', 'ping_ok': True, 'formats': 'Stalker/MAG', 'conns': conns})
553
+ else: a.update({'days': 'MAC', 'type_auth': 'Portal MAC', 'created_at': '-', 'exp_date': '-', 'status': 'لا يعمل', 'ping_ok': False, 'formats': 'Unknown', 'conns': 'N/A'})
 
 
 
 
 
 
554
  return a
555
  elif a['type'] == 'direct':
556
  ping_ok, is_playlist, ch_count = advanced_direct_link_test(a['url'])
557
  if ping_ok:
558
  if is_playlist: a.update({'host': 'Playlist M3U', 'days': 'Direct', 'type_auth': 'Playlist', 'status': 'قائمة تشغيل تعمل', 'ping_ok': True, 'formats': 'm3u8/ts', 'ch_count': ch_count})
559
  else: a.update({'host': 'Direct Link', 'days': 'Direct', 'type_auth': 'Stream', 'status': 'بث مباشر يعمل', 'ping_ok': True, 'formats': 'ts/m3u8'})
560
+ else: a.update({'host': 'Direct Link', 'days': 'Direct', 'type_auth': 'Stream', 'status': 'لا يعمل', 'ping_ok': False, 'formats': 'Unknown'})
 
561
  return a
562
  except: pass
563
  return None
 
579
  if result:
580
  valid_found.append(result)
581
  valid_found.sort(key=live_sort_key, reverse=True)
 
582
  with live_placeholder.container():
583
  for srv in valid_found:
584
  ping_icon = "🟢" if srv.get('ping_ok') else "🔴"
585
+ days_str = f"{srv.get('days', 0)} يوم" if str(srv.get('days', 0)).isdigit() else str(srv.get('days', 0))
 
586
  if srv.get('type') == 'direct': st.success(f"{ping_icon} {srv['status']} | 🌐 `{srv['url']}` | ⏳ {days_str}")
587
  elif srv.get('type') == 'mac': st.success(f"{ping_icon} {srv['status']} | 🌐 `{srv['host']}` | 💳 `{srv['mac']}` | ⏳ {days_str}")
588
  else: st.success(f"{ping_icon} {srv['status']} | 🌐 `{srv['host']}` | 👤 `{srv['user']}` | ⏳ {days_str}")
589
+ try:
 
590
  if result.get('type') == 'xtream': save_to_global_vips({'host': result['host'], 'user': result['user'], 'pass': result['pass'], 'days': result.get('days')})
591
  elif result.get('type') == 'mac': save_to_global_vips({'host': result['host'], 'mac': result['mac'], 'days': result.get('days'), 'type': 'mac'})
592
  except: pass
 
603
  asyncio.set_event_loop(new_loop)
604
  result[0] = new_loop.run_until_complete(coro)
605
  t = threading.Thread(target=_run)
606
+ t.start(); t.join()
 
607
  return result[0]
608
  else: return asyncio.run(coro)
609
 
610
+ def extract_sports_unified(host, user, pwd, skip_clean=False, ext=".m3u8"):
611
+ domain_parts = host.split('//')[-1].split(':')[0].split('.')
612
+ valid_parts = [p for p in domain_parts if p.lower() not in ['com', 'net', 'org', 'info', 'tv'] and len(p)>2]
613
+ sn = re.sub(r'[^a-zA-Z0-9]', '', valid_parts[0].capitalize() if valid_parts else domain_parts[0].capitalize())
614
+ try:
615
+ r = requests.get(f"{host}/player_api.php?username={user}&password={pwd}", headers=get_headers(), timeout=12, verify=False).json()
616
+ info = r.get('user_info', {})
617
+ if info.get('status') != 'Active': return False, f"❌ متوقف: {sn}"
618
+ save_server_to_db(host, user, pwd, calculate_days_left(info.get('exp_date')), auth_type='Xtream', created_at=parse_timestamp(info.get('created_at')), exp_date=parse_timestamp(info.get('exp_date')))
619
+ streams = requests.get(f"{host}/player_api.php?username={user}&password={pwd}&action=get_live_streams", timeout=25, verify=False).json()
620
+ cats = requests.get(f"{host}/player_api.php?username={user}&password={pwd}&action=get_live_categories", timeout=25, verify=False).json()
621
+ cat_map = {i['category_id']: i['category_name'] for i in cats}
622
+ new_chans = []
623
+ for s in streams:
624
+ name, cat = str(s.get('name', '')), str(cat_map.get(s.get('category_id'), "Other"))
625
+ if is_arabic_sport(name, cat):
626
+ ch_id = f"ch_{s.get('stream_id')}_{re.sub(r'\W','',host.lower())}_{re.sub(r'\W','',user.lower())}"
627
+ new_chans.append({"id": ch_id, "name": f"{name} [{sn}]", "group": categorize_vip_sport(name, sn), "logo": get_vip_logo(name) or s.get("stream_icon", ""), "url": f"{host}/live/{user}/{pwd}/{s.get('stream_id')}{ext}", "userAgent": "IPTVSmartersPro"})
628
+ if not new_chans: return False, f"⚠️ لا يوجد باقات رياضية في {sn}"
629
+ db = load_db(); merged = {c['id']: c for c in db}
630
+ for c in new_chans: merged[c['id']] = c
631
+ final = list(merged.values()); final.sort(key=get_vip_sort_key); save_db(final)
632
+ return True, f"✅ تم سحب {len(new_chans)} باقة من [{sn}]"
633
+ except: return False, f"❌ خطأ اتصال في {sn}"
634
+
635
+ def custom_api_extractor(host, user, pw, mode, ext_choice, add_ua):
636
+ output_file = f"Extracted_VIP_{user}.m3u"
637
+ extracted_count = 0
638
+ live_ext = "" if ext_choice == "بدون صيغة" else ext_choice
639
+ ua_suffix = "|User-Agent=IPTVSmartersPro" if add_ua else ""
640
+ host = host.rstrip('/')
641
+ with open(output_file, 'w', encoding='utf-8') as f:
642
+ f.write("#EXTM3U\n")
643
+ if mode == "⚽ رياضة فقط VIP":
644
+ try:
645
+ streams = requests.get(f"{host}/player_api.php?username={user}&password={pw}&action=get_live_streams", timeout=25, verify=False).json()
646
+ cats = requests.get(f"{host}/player_api.php?username={user}&password={pw}&action=get_live_categories", timeout=25, verify=False).json()
647
+ cat_map = {i['category_id']: i['category_name'] for i in cats}
648
+ sn = host.split('//')[-1].split('.')[0].capitalize()
649
+ vip_ch = []
650
+ for s in streams:
651
+ if is_arabic_sport(str(s.get('name','')), str(cat_map.get(s.get('category_id'), ''))):
652
+ vip_ch.append({"name": f"{s.get('name')} [{sn}]", "group": categorize_vip_sport(s.get('name'), sn), "logo": get_vip_logo(s.get('name')) or s.get("stream_icon", ""), "url": f"{host}/live/{user}/{pw}/{s.get('stream_id')}{live_ext}"})
653
+ vip_ch.sort(key=get_vip_sort_key)
654
+ for ch in vip_ch: f.write(f'#EXTINF:-1 tvg-logo="{ch["logo"]}" group-title="{ch["group"]}",{ch["name"]}\n{ch["url"]}{ua_suffix}\n'); extracted_count += 1
655
+ except: pass
656
+ else:
657
+ if mode in ["📺 لايف فقط", "🎬 لايف + أفلام", "🍿 الكل (لايف+أفلام+مسلسلات)"]:
658
+ try:
659
+ data = requests.get(f"{host}/player_api.php?username={user}&password={pw}&action=get_live_streams", timeout=25, verify=False).json()
660
+ for ch in data: f.write(f'#EXTINF:-1 tvg-logo="{ch.get("stream_icon", "")}" group-title="Live TV",{ch.get("name")}\n{host}/live/{user}/{pw}/{ch.get("stream_id")}{live_ext}{ua_suffix}\n'); extracted_count += 1
661
+ except: pass
662
+ if mode in ["🎬 لايف + أفلام", "🍿 الكل (لايف+أفلام+مسلسلات)"]:
663
+ try:
664
+ data = requests.get(f"{host}/player_api.php?username={user}&password={pw}&action=get_vod_streams", timeout=25, verify=False).json()
665
+ for mov in data: f.write(f'#EXTINF:-1 tvg-logo="{mov.get("stream_icon", "")}" group-title="Movies",{mov.get("name")}\n{host}/movie/{user}/{pw}/{mov.get("stream_id")}.{mov.get("container_extension", "mp4")}{ua_suffix}\n'); extracted_count += 1
666
+ except: pass
667
+ if mode == "🍿 الكل (لايف+أفلام+مسلسلات)":
668
+ try:
669
+ data = requests.get(f"{host}/player_api.php?username={user}&password={pw}&action=get_series", timeout=25, verify=False).json()
670
+ for ser in data: f.write(f'#EXTINF:-1 tvg-logo="{ser.get("cover", "")}" group-title="Series",{ser.get("name")}\n{host}/series/{user}/{pw}/{ser.get("series_id")}{live_ext}{ua_suffix}\n'); extracted_count += 1
671
+ except: pass
672
+ return extracted_count > 0, output_file, extracted_count
673
 
674
  # ==========================================
675
+ # 🎨 واجهة الكروت الذكية الفائقة (Super UX Header)
676
  # ==========================================
677
  def render_server_card(server, idx, context="tab1"):
678
  t = server.get('type')
 
681
  uid_str = server.get('_uid', str(uuid.uuid4())[:8])
682
  base_k = f"{context}_{idx}_{uid_str}"
683
 
684
+ force_open = f"file_{base_k}" in st.session_state or f"ping_res_{base_k}" in st.session_state or f"prep_err_{base_k}" in st.session_state or f"preview_url_{base_k}" in st.session_state
 
 
 
685
  ping_icon = "🟢" if server.get('ping_ok') else "🔴"
 
686
 
687
+ if t == 'direct': title = f"{ping_icon} {server['status']} | 🌐 {server.get('url', '')} | ⏳ {days_str}"
688
+ elif t == 'mac': title = f"{ping_icon} {server['status']} | 🌐 {server.get('host', '')} | 💳 {server.get('mac', '')} | 👥 {server.get('conns', '0/1')} | 🛑 {server.get('exp_date', 'Unlimited')} | ⏳ {days_str}"
689
+ else: title = f"{ping_icon} {server['status']} | 🌐 {server.get('host', '')} | 👤 {server.get('user', '')} | 🔑 {server.get('pass', '')} | 👥 {server.get('conns', 'N/A')} | 🛑 {server.get('exp_date', 'Unlimited')} | ⏳ {days_str}"
 
 
 
 
690
 
691
  title += "\u200B" * st.session_state.reset_exp
692
 
693
  with st.expander(title, expanded=force_open):
 
694
  if t == 'xtream':
695
  formatted_m3u_url = f"{server['host'].rstrip('/')}/get.php?username={server['user']}&password={server['pass']}&type=m3u_plus"
696
+ st.text_input("🔗 رابط M3U المباشر للمشغل الخارجي (للنسخ السريع):", value=formatted_m3u_url, key=f"link_m3u_{base_k}")
697
  st.markdown("---")
698
+ st.markdown(f"### ⚙️ خيارات التحكم والسحب:")
 
699
  elif t == 'direct':
700
+ st.text_input("🔗 رابط الملف التنزيلي السريع:", value=server.get('url', ''), key=f"link_{base_k}")
701
  st.markdown("---")
702
+ st.markdown(f"### ⚙️ خيارات التشغيل المعاينة:")
 
 
 
703
 
 
704
  if t == 'direct':
705
  col_p1, col_p2 = st.columns(2)
706
  with col_p1:
707
+ if server.get('type_auth') == 'Playlist' and st.button("📥 سحب القنوات (M3U Playlist)", key=f"prep_{base_k}"):
708
+ with st.spinner("جاري سحب المحتوى..."):
709
+ try:
710
+ # 🛡️ تخطي حماية سيرفرات Cloudflare Workers بالتنكر كمتصفح Chrome 🛡️
711
+ browser_headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36', 'Accept': '*/*'}
712
+ txt = requests.get(server['url'], headers=browser_headers, timeout=15, verify=False).text
713
+ st.session_state[f"file_{base_k}"], st.session_state[f"count_{base_k}"], st.session_state[f"filename_{base_k}"] = txt.encode('utf-8'), txt.count("#EXTINF"), f"Playlist_{uid_str}.m3u"
714
+ st.rerun()
715
+ except: st.error("❌ فشل سحب روابط القائمة المباشرة. السيرفر يرفض الاتصال.")
 
716
  with col_p2:
717
+ if st.button("📺 تشغيل معاينة القناة المباشرة", key=f"preview_{base_k}"): st.session_state[f"preview_url_{base_k}"] = server.get('url', ''); st.rerun()
 
 
718
 
719
  elif t == 'mac':
720
  col_mode, col_ext, col_ua, col_btn = st.columns([3, 2, 2, 2])
721
+ mac_mode = col_mode.selectbox("🎯 قسم المحتوى الباقات:", ["⚽ رياضة فقط VIP", "📺 لايف فقط", "🎬 لايف + أفلام", "🍿 الكل (لايف+أفلام+مسلسلات)"], key=f"mode_{base_k}")
722
+ mac_ext = col_ext.selectbox("صيغة مشغل البث الفعلي:", [".m3u8", ".ts", "بدون صيغة"], index=0, key=f"ext_{base_k}")
723
+ mac_ua = col_ua.checkbox("حماية التخطي UA", value=False, key=f"ua_{base_k}")
724
+ if col_btn.button("📥 بدء سحب الماك", key=f"prep_btn_{base_k}"):
725
+ with st.spinner("جاري الاختراق وسحب القنوات بالتوازي..."):
 
726
  ok, res_text, count = extract_mac_to_m3u_logic(server['host'], server['mac'], mac_mode, mac_ua, mac_ext)
727
+ if ok: st.session_state[f"file_{base_k}"], st.session_state[f"count_{base_k}"], st.session_state[f"filename_{base_k}"] = res_text.encode('utf-8'), count, f"MAC_{server['mac'].replace(':','')}.m3u"
 
 
 
 
728
  else: st.session_state[f"prep_err_{base_k}"] = res_text
729
  st.rerun()
730
 
731
  elif t == 'xtream':
732
  col_ping, col_preview = st.columns(2)
733
  with col_ping:
734
+ if st.button("📡 فحص دقة البث الفعلي (GET البايتات)", key=f"ping_{base_k}"):
735
+ with st.spinner("جاري فحص سلامة داتا الفيديو للبث..."):
736
  ok, msg = test_live_stream_ping(server['host'], server['user'], server['pass'])
737
+ st.session_state[f"ping_res_{base_k}"] = (ok, msg); st.rerun()
 
738
  with col_preview:
739
+ if st.button("📺 جلب داتا مشغل المعاينة", key=f"preview_btn_{base_k}"):
740
+ with st.spinner("جاري توليد مسار المشغل..."):
741
  try:
742
+ r_streams = requests.get(f"{server['host'].rstrip('/')}/player_api.php?username={server['user']}&password={server['pass']}&action=get_live_streams", headers=get_headers(), timeout=5, verify=False).json()
743
+ if isinstance(r_streams, list) and len(r_streams) > 0: st.session_state[f"preview_url_{base_k}"] = f"{server['host'].rstrip('/')}/live/{server['user']}/{server['pass']}/{r_streams[0].get('stream_id')}.m3u8"
744
+ else: st.session_state[f"preview_err_{base_k}"] = "لم نجد قنوات للمعاينة الفورية."
745
+ except: st.session_state[f"preview_err_{base_k}"] = "فشل توليد اتصال المعاينة."
 
 
 
 
746
  st.rerun()
747
 
748
  if f"ping_res_{base_k}" in st.session_state:
749
  ok, msg = st.session_state[f"ping_res_{base_k}"]
750
  if ok: st.success(msg)
751
  else: st.error(msg)
752
+ if f"preview_err_{base_k}" in st.session_state: st.error(st.session_state[f"preview_err_{base_k}"])
753
+
754
+ col_m, col_e, col_u, col_b = st.columns([3, 2, 2, 2])
755
+ selected_mode = col_m.selectbox("🎯 المحتوى المستهدف:", ["⚽ رياضة فقط VIP", "📺 لايف فقط", "🎬 لايف + أفلام", "🍿 الكل (لايف+أفلام+مسلسلات)"], key=f"mode_x_{base_k}")
756
+ selected_ext = col_e.selectbox("صيغة البث المخرج:", [".m3u8", ".ts", "بدون صيغة"], key=f"ext_x_{base_k}")
757
+ add_ua = col_u.checkbox("إضافة حماية متقدمة UA", value=False, key=f"ua_x_{base_k}")
758
+ if col_b.button("📥 سحب داتا السيرفر", key=f"prep_btn_x_{base_k}"):
759
+ with st.spinner("جاري تجهيز وتحميل الملف..."):
760
+ success, file, count = custom_api_extractor(server['host'], server['user'], server['pass'], selected_mode, selected_ext, add_ua)
761
+ if success: st.session_state[f"file_{base_k}"], st.session_state[f"count_{base_k}"], st.session_state[f"filename_{base_k}"] = open(file, 'rb').read(), count, f"VIP_{server['user']}.m3u"
762
+ else: st.session_state[f"prep_err_{base_k}"] = "❌ السيرفر فارغ أو محظور."
763
+ st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
764
 
 
765
  if f"preview_url_{base_k}" in st.session_state:
766
  p_url = st.session_state[f"preview_url_{base_k}"]
 
767
  hls_demo = f"https://hls-js.netlify.app/demo/?src={urllib.parse.quote(p_url)}"
768
+ components.html(f"""<link href="https://vjs.zencdn.net/7.11.4/video-js.css" rel="stylesheet" /><script src="https://vjs.zencdn.net/7.11.4/video.min.js"></script><video id="my-video" class="video-js vjs-default-skin vjs-big-play-centered" controls preload="auto" width="100%" height="250"><source src="{p_url}" type="application/x-mpegURL"></video>""", height=270)
769
+ st.markdown(f"""<div style="background-color: #2b2b2b; padding: 15px; border-radius: 8px; text-align: center; border: 1px solid #444; margin-bottom: 15px;"><a href="intent:{p_url}#Intent;action=android.intent.action.VIEW;type=video/*;end" style="text-decoration: none;"><button style="background-color: #00cc66; color: white; border: none; padding: 11px 18px; border-radius: 6px; font-weight: bold; cursor: pointer; margin: 5px;">📱 تشغيل مشغل خارجي (VLC / MX)</button></a><a href="{hls_demo}" target="_blank" style="text-decoration: none;"><button style="background-color: #ff9800; color: white; border: none; padding: 11px 18px; border-radius: 6px; font-weight: bold; cursor: pointer; margin: 5px;">🌐 مشغل ويب نتلايف خارجي</button></a></div>""", unsafe_allow_html=True)
770
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
771
  if f"file_{base_k}" in st.session_state:
772
+ st.success(f"✅ تم السحب بنجاح! ({st.session_state[f'count_{base_k}']} عنصر)")
773
+ st.download_button("💾 تحميل ملف السحب الخارجي (.m3u)", data=st.session_state[f"file_{base_k}"], file_name=st.session_state[f"filename_{base_k}"], key=f"dl_{base_k}")
774
+ with st.expander("🔍 فرز ونسخ سريع لقنوات الملف المسحوب مباشرة", expanded=True):
 
775
  raw_text = st.session_state[f"file_{base_k}"].decode('utf-8', errors='ignore')
 
 
776
  col_s1, col_s2 = st.columns([3, 1])
777
+ search_q = col_s1.text_input("🔍 اكتب كلمة الفرز هنا (مثل bein, ssc):", key=f"sq_{base_k}")
778
+ display_text = raw_text
779
+ if col_s2.button("فلترة 🎯", key=f"sbtn_{base_k}") or search_q:
 
 
780
  if search_q.strip():
781
+ lines, filtered = raw_text.splitlines(), ["#EXTM3U"]
 
782
  for i in range(len(lines)):
783
+ if lines[i].startswith("#EXTINF") and smart_search_match(search_q, lines[i]):
784
+ filtered.append(lines[i])
785
+ if i + 1 < len(lines): filtered.append(lines[i+1])
786
+ display_text = "\n".join(filtered)
787
+ st.success(f"🎯 تم العثور على {(len(filtered)-1)//2} باقة متطابقة.")
788
+ st.text_area("محتوى الصندوق (تحديد الكل -> نسخ):", value=display_text, height=250, key=f"all_{base_k}")
 
 
 
 
 
 
 
 
 
 
789
 
790
  st.markdown("---")
791
  if context == "saved":
792
+ if st.button("🗑️ حذف الحساب من السجل نهائياً", key=f"del_{base_k}"):
793
  svs = load_servers(); svs.pop(idx); save_all_servers_to_db(svs)
794
+ st.success("تم الحذف!"); time.sleep(1); st.rerun()
795
  else:
796
+ if t == 'xtream' and st.button("💾 حفظ السيرفر في قاعدة بياناتي", key=f"save_{base_k}"):
797
+ save_server_to_db(server['host'], server['user'], server['pass'], server['days'], auth_type='Xtream', created_at=server.get('created_at'), exp_date=server.get('exp_date'), conns=server.get('conns'))
798
+ st.success("✅ تم حفظ السيرفر بنجاح!")
799
+ elif t == 'mac' and st.button("💾 حفظ الماك في قاعدة بياناتي", key=f"save_{base_k}"):
800
+ save_server_to_db(server['host'], server['mac'], 'MAC Auth', server['days'], auth_type='Portal MAC', created_at=server.get('created_at'), exp_date=server.get('exp_date'), mac=server['mac'], conns=server.get('conns'))
801
+ st.success(" تم حفظ الماك بنجاح!")
 
 
802
 
803
  # ==========================================
804
+ # 🤖 التلغرام والواجهة الرسومية الكاملة
805
  # ==========================================
806
  @bot.message_handler(commands=['start', 'menu'])
807
  def send_menu(message):
808
+ save_admin(message.chat.id); bot.send_message(message.chat.id, "📺 **Shalaby IPTV Tool**\nأهلاً بك يا هندسة في لوحة الإدارة الذكية للتلفزيون!", parse_mode="Markdown")
 
809
 
810
  @bot.message_handler(commands=['allservers'])
811
  def send_all_servers(message):
812
  vips = load_global_vips()
813
+ if not vips: bot.reply_to(message, "📭 الصندوق الأسود فارغ حالياً."); return
 
 
814
  file_path = "All_Hunted_Servers.txt"
815
  with open(file_path, "w", encoding="utf-8") as f:
816
+ f.write(f"--- 📦 إجمالي السيرفرات المصادة الشغالة: {len(vips)} ---\n\n")
817
  for s in vips:
818
  if s.get('type') == 'mac': f.write(f"Portal: {s.get('host')}\nMAC: {s.get('mac')}\nDays Left: {s.get('days')}\n")
819
  else: f.write(f"Host: {s.get('host')}\nUser: {s.get('user')}\nPass: {s.get('pass')}\nDays Left: {s.get('days')}\n")
820
  f.write("-" * 30 + "\n")
821
+ with open(file_path, "rb") as doc: bot.send_document(message.chat.id, doc, caption=f"🔥 تفضل يا هندسة، ملف إجمالي السيرفرات الصالحة المصادة ({len(vips)}).")
822
 
823
  @bot.message_handler(func=lambda message: True)
824
  def handle_all_messages(message):
825
+ accs = extract_all_links_from_text(message.text)
 
826
  if accs:
827
+ msg = bot.reply_to(message, "🔍 جاري فحص وتحليل السيرفرات المرسلة من التلغرام...")
828
  valid = []
829
  for a in accs:
830
  if a['type'] == 'xtream':
831
  info = check_server_smart(a['host'], a['user'], a['pass'])
832
+ if info: valid.append(f"✅ `{a['host']}` | 👤 `{a['user']}` | ⏳ {calculate_days_left(info.get('exp_date'))} يوم")
 
 
 
 
 
833
  elif a['type'] == 'mac':
834
  prof_ok, exp, _ = test_mac_portal(a['host'], a['mac'])
835
+ if prof_ok: valid.append(f"✅ `{a['host']}` | 💳 `{a['mac']}` | ⏳ {calculate_days_left(exp)} يوم")
836
+ if valid: bot.edit_message_text(f"🎉 **تم الفحص من الموتور بنجاح!**\n\n" + "\n".join(valid), chat_id=msg.chat.id, message_id=msg.message_id, parse_mode="Markdown")
837
+ else: bot.edit_message_text(" عذراً لم تنجح أي روابط مرسلة.", chat_id=msg.chat.id, message_id=msg.message_id)
 
 
 
 
 
838
 
839
  def run_bot_thread_init():
840
  for t in threading.enumerate():
 
842
  def start_polling():
843
  try: requests.get(f"https://api.telegram.org/bot{TOKEN}/deleteWebhook?drop_pending_updates=True", timeout=5)
844
  except: pass
845
+ time.sleep(2); bot.infinity_polling(timeout=20, long_polling_timeout=10, skip_pending=True)
846
+ threading.Thread(target=start_polling, name="TelegramBotThread", daemon=True).start()
 
 
847
  run_bot_thread_init()
848
 
849
  st.set_page_config(page_title="Shalaby IPTV Tool", page_icon="📺", layout="wide")
850
 
851
  st.markdown("""
852
  <style>
853
+ .block-container { max-width: 100% !important; padding: 1rem !important; padding-bottom: 5rem !important; }
854
+ section[data-testid="stSidebar"] { width: 170px !important; min-width: 170px !important; }
855
+ .main { text-align: right; direction: rtl; }
856
+ h1 { font-size: 1.8rem !important; text-align: center; color: #ff4b4b;}
 
 
 
 
 
 
 
 
 
 
857
  div[data-baseweb="tab-list"] { display: flex; flex-direction: column; gap: 8px; }
858
  div[data-baseweb="tab"] { width: 100%; justify-content: flex-end; background-color: #2b2b2b; border-radius: 8px; padding: 10px !important; border: 1px solid #444;}
859
  div[data-baseweb="tab"]:hover { background-color: #ff4b4b; color: white !important;}
860
  .stButton>button { width: 100%; border-radius: 8px; font-weight: bold; background-color: #ff4b4b; color: white; margin-top: 5px;}
 
861
 
862
  [data-testid="stExpander"] details summary {
863
+ background-color: #262730 !important; border-left: 5px solid #ff9800 !important; border-radius: 8px !important; padding: 12px !important; margin-bottom: 5px !important; box-shadow: 0px 4px 6px rgba(0,0,0,0.3) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
864
  }
865
  [data-testid="stExpander"] details[open] > div {
866
+ background-color: #1e1e1e !important; border: 1px solid #444 !important; border-radius: 0 0 10px 10px !important; padding: 20px !important; margin-right: 40px !important; width: calc(100% - 40px) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
867
  }
868
  </style>
869
  """, unsafe_allow_html=True)
870
 
871
+ # نظام الدخول وحفظ الجلسة في المتصفح تلقائياً
872
  if not st.session_state.logged_in:
873
+ st.title("🔐 بوابة الدخول التلقائية - Shalaby IPTV Tool")
874
+ tab_in, tab_up = st.tabs(["تسجيل دخول بالحساب الحالي", "إنشاء كود مستخدم جديد"])
875
  with tab_up:
876
+ new_user = st.text_input("اختر كود حسابك الجديد:")
877
+ if st.button("🚀 إنشاء وتفعيل التلقائي"):
878
  if new_user.strip():
879
  users = load_users_registry()
880
  if new_user.strip() not in users:
881
  users.append(new_user.strip()); save_users_registry(users)
882
+ st.session_state.username, st.session_state.logged_in = new_user.strip(), True
883
  st.query_params["user_auth"] = new_user.strip()
884
  components.html(f"""<script>localStorage.setItem("user_auth", "{new_user.strip()}");</script>""", height=0, width=0)
885
  time.sleep(0.5); st.rerun()
886
+ else: st.error("❌ الكود مستخدم من قبل.")
887
  with tab_in:
888
+ exist_user = st.text_input("أدخل كود الدخول الخاص بك:")
889
+ if st.button("🔓 مصادقة ودخول"):
890
  if exist_user.strip() in load_users_registry():
891
+ st.session_state.username, st.session_state.logged_in = exist_user.strip(), True
892
  st.query_params["user_auth"] = exist_user.strip()
893
  components.html(f"""<script>localStorage.setItem("user_auth", "{exist_user.strip()}");</script>""", height=0, width=0)
894
  time.sleep(0.5); st.rerun()
895
+ else: st.error("❌ كود الحساب غير مسجل بالمنظومة.")
896
  st.stop()
897
 
898
  with st.sidebar:
899
+ st.markdown(f"### 👤 الحساب المتصل:\n`{st.session_state.username}`")
 
900
  if st.button("🚪 تسجيل خروج"):
901
  components.html("""<script>localStorage.removeItem("user_auth");</script>""", height=0, width=0)
902
+ st.query_params.clear(); st.session_state.logged_in = False; time.sleep(0.5); st.rerun()
903
 
904
+ st.title("📺 اللوحة الأسطورية الشاملة - Shalaby VIP Tool")
905
  st.markdown("---")
906
 
907
  tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs([
908
+ "🧪 محرك الفحص الصاروخي الذكي", "🗄️ سجل سيرفراتي المحفوظة", "🚀 تجميع وتحديث الماستر داتابيز", "🏭 منقي الباقات للعملاء المخصص", "🔥 فاحص ومحول الماك الفوري", "✨ ساحب القنوات المطور (Playlist Extractor)"
909
  ])
910
 
911
  with tab1:
912
+ st.markdown("### 🔍 أدخل البيانات أو النصوص المبعثرة للفحص:")
913
  if 'valid_found_tab1' in st.session_state and st.session_state['valid_found_tab1']:
914
+ if st.button("🔽 غلق جميع الكروت المفتوحة ومسح الذاكرة التفاعلية", key="close_t1"):
915
  st.session_state.reset_exp += 1
916
  for k in list(st.session_state.keys()):
917
  if k.startswith("file_") or k.startswith("ping_res_") or k.startswith("prep_err_") or k.startswith("preview_"): del st.session_state[k]
918
  st.rerun()
919
 
920
+ bulk_input = st.text_area("ضع الروابط أو نصوص الماكات المبعثرة هنا:", height=150)
921
+ if st.button("🚀 إطلاق موتور فحص Asyncio الشامل"):
922
+ if not bulk_input.strip(): st.warning("⚠️ يرجى تعبئة الحقل بالنصوص أولاً.")
923
  else:
924
  accs = extract_all_links_from_text(bulk_input)
925
+ if len(accs) == 0: st.error("❌ لم يتم العثور على أ�� روابط أو ماكات صالحة للبناء التقني.")
 
926
  else:
927
+ p_bar, s_msg, l_placeholder = st.progress(0), st.empty(), st.empty()
928
+ v_found = safe_async_run(run_async_scan(accs, p_bar, s_msg, l_placeholder, "tab1"))
929
+ s_msg.success(f"🎉 اكتمل مسح شبكة بايثون بنجاح! تم اصطياد وتحليل {len(v_found)} سيرفر.")
930
+ st.session_state['valid_found_tab1'] = v_found
931
+ if v_found:
932
+ c_db = load_servers()
933
+ for v in v_found:
934
+ if v.get('type') == 'xtream' and not any(s.get('host') == v['host'] and s.get('user') == v['user'] for s in c_db): c_db.append(v)
935
+ save_all_servers_to_db(c_db)
 
 
 
 
936
 
937
  if 'valid_found_tab1' in st.session_state and st.session_state['valid_found_tab1']:
938
+ st.markdown("#### ⚙️ الحسابات المصادة والجاهزة للنسخ والسحب الفوري:")
939
  for i, acc in enumerate(st.session_state['valid_found_tab1']): render_server_card(acc, i, context="t1")
940
 
941
  with tab2:
942
+ st.markdown("### 🗄️ السيرفرات المسجلة في داتابيز حسابك")
943
  svs = load_servers()
944
+ if not svs: st.warning("لا توجد حسابات محفوظة في السجل الخاص بك حالياً.")
945
  else:
946
+ if st.button("🔽 تنظيف ذاكرة الكروت المؤقتة بالسجل", key="close_t2"):
947
  st.session_state.reset_exp += 1
948
  for k in list(st.session_state.keys()):
949
  if k.startswith("file_") or k.startswith("ping_res_") or k.startswith("prep_err_") or k.startswith("preview_"): del st.session_state[k]
 
951
  for i, s in enumerate(svs): render_server_card(s, i, context="saved")
952
 
953
  with tab3:
954
+ st.markdown("### 🚀 موتور تحديث الماستر داتابيز الشامل وتجميع الرياضة")
955
+ if st.button("🔄 بدء التجميع والدمج المتوازي لكافة السيرفرات"):
956
  svs = load_servers()
957
+ if not svs: st.error("❌ سجل الحسابات فارغ تماماً!")
958
  else:
959
+ p_bar3 = st.progress(0)
960
  for i, s in enumerate(svs):
961
+ if s.get('type') in ['xtream', 'Xtream API']: extract_sports_unified(s['host'], s['user'], s['pass'], skip_clean=True, ext=".m3u8")
962
+ p_bar3.progress((i + 1) / len(svs))
963
+ f_db = load_db(); m3u_file = export_to_m3u(f_db, "My_Saved_Servers.m3u")
964
+ st.success(f"🎉 تم دمج وفحص وإدراج {len(f_db)} باقة رياضية محدثة بالماستر داتابيز!")
965
+ with open(m3u_file, "rb") as file: st.download_button("💾 تحميل ملف اللستة الموحدة الشغالة لقنواتك", data=file, file_name=f"Master_Shalaby_{st.session_state.username}.m3u")
 
966
 
967
  with tab4:
968
+ st.markdown("### 🏭 صناعة وتخصيص باقات خاصة للعملاء (Custom Selector)")
969
+ all_ch = load_db()
970
+ if not all_ch: st.warning("⚠️ قاعدة داتابيز القنوات فارغة، يرجى التجميع أولاً من التاب السابق.")
971
  else:
972
+ u_groups = set(ch['group'].replace('[SD]','').replace('[HD]','').replace('[FHD]','').replace('[4K]','').strip() for ch in all_ch)
973
+ sel_g = st.multiselect("📌 حدد الباقات المراد تصديرها للعميل:", sorted(list(u_groups)))
974
+ smart_ren = st.checkbox(" تنظيف وإعادة هيكلة مسميات الجودة تلقائياً")
975
+ if st.button("🏭 توليد ملف باقة العميل"):
976
+ if not sel_g: st.error(" يرجى اختيار باقة واحدة على الأقل للتصدير.")
 
977
  else:
978
+ cust_ch = []
979
+ for ch in all_ch:
980
+ if ch['group'].replace('[SD]','').replace('[HD]','').replace('[FHD]','').replace('[4K]','').strip() in sel_g:
981
+ if smart_ren: ch['group'] = categorize_vip_sport(ch['name'], re.search(r'\[(.*?)\]', ch['name']).group(1) if re.search(r'\[(.*?)\]', ch['name']) else "VIP")
982
+ cust_ch.append(ch)
983
+ if cust_ch:
984
+ c_file = export_to_m3u(cust_ch, "Client.m3u")
985
+ st.success(f"🎉 تم توليد قنوات الملف المخصص بنجاح بنجاح! ({len(cust_ch)} قناة)")
986
+ with open(c_file, "rb") as file: st.download_button("📥 تحميل ملف العميل الجاهز المخصص", data=file, file_name=f"Client_{st.session_state.username}.m3u")
987
+ else: st.error(" لم نجد قنوات متوافقة مع اختيارك.")
 
 
988
 
989
  with tab5:
990
+ st.markdown("### 🔥 فاحص الماك المنفرد والمحول المباشر لملف M3U")
991
+ col_h, col_m = st.columns(2)
992
+ m_portal = col_h.text_input("🔗 رابط البورتال (Portal URL):", placeholder="http://domain.xyz:8080")
993
+ m_addr = col_m.text_input("💳 الماك أدرس (MAC Address):", placeholder="00:1A:79:XX:XX:XX")
994
+ col_b1, col_b2, col_b3, col_b4 = st.columns([3, 2, 2, 2])
995
+ standalone_mode = col_b1.selectbox("🎯 فلتر محتوى السحب المطلوب:", ["⚽ رياضة فقط VIP", "📺 لايف فقط", "🎬 لايف + أفلام", "🍿 الكل (لايف+أفلام+مسلسلات)"], key="st_mode")
996
+ standalone_ext = col_b2.selectbox("صيغة المشاهدة المستهدفة:", [".m3u8", ".ts", "بدون صيغة"], key="st_ext")
997
+ standalone_ua = col_b3.checkbox("تفعيل حماية وتخطي UA كـ MAG", value=True)
998
+ if col_b4.button("🚀 فحص وتحويل الماك"):
999
+ if not m_portal or not m_addr: st.warning("⚠️ الرجاء تعبئة بيانات البورتال والماك معاً.")
 
 
1000
  else:
1001
+ p_clean = "http://" + m_portal.strip().rstrip('/') if not m_portal.strip().startswith("http") else m_portal.strip().rstrip('/')
1002
+ with st.spinner("🕵️‍♂️ جاري كسر الحماية والتحقق من التواريخ وقنوات السيرفر..."):
1003
+ ok, res_text, count = extract_mac_to_m3u_logic(p_clean, m_addr.strip().upper(), standalone_mode, standalone_ua, standalone_ext)
1004
+ if ok:
1005
+ st.success(f"🎉 نجح التحويل! تم استخراج {count} قناة بكفاءة تامة.")
1006
+ st.download_button("📥 تحميل ملف الـ M3U الناتج فوراً", data=res_text, file_name=f"Converted_{m_addr.replace(':','')}.m3u")
1007
+ else: st.error(res_text)
 
1008
 
1009
  with tab6:
1010
+ st.markdown("### ✨ ساحب ومحلل محتويات قوائم التشغيل المباشرة (M3U Extractor)")
1011
  if 'valid_found_tab6' in st.session_state and st.session_state['valid_found_tab6']:
1012
+ if st.button("🔽 مسح ذاكرة الكروت المؤقتة للتاب", key="close_t6"):
1013
  st.session_state.reset_exp += 1
1014
  for k in list(st.session_state.keys()):
1015
  if k.startswith("file_") or k.startswith("ping_res_") or k.startswith("prep_err_") or k.startswith("preview_"): del st.session_state[k]
1016
  st.rerun()
1017
 
1018
+ ex_input = st.text_area("ضع نصوص قوائم التشغيل أو روابط الـ Workers هنا لفرزها:", height=150)
1019
+ if st.button("⚙️ بدء الفرز والمسح الصاروخي"):
1020
+ if not ex_input.strip(): st.warning("⚠️ يرجى إدخال روابط أولاً للفرز.")
1021
  else:
1022
  accs = extract_all_links_from_text(ex_input)
1023
+ if not accs: st.error("❌ لم يتم العثور على أي بيانات روابط تشغيل صالحة بالفحص.")
1024
  else:
1025
+ p_bar6, s_msg6, l_placeholder6 = st.progress(0), st.empty(), st.empty()
1026
+ v_accs = safe_async_run(run_async_scan(accs, p_bar6, s_msg6, l_placeholder6, "t6"))
1027
+ s_msg6.success(f"🎉 اكتمل المسح بنجاح! تم استخراج {len(v_accs)} كارت تشغيل جاهز.")
1028
+ st.session_state['valid_found_tab6'] = v_accs
 
 
 
 
 
 
 
 
 
1029
 
1030
  if 'valid_found_tab6' in st.session_state and st.session_state['valid_found_tab6']:
1031
  st.markdown("#### ⚙️ الكروت النهائية بعد الفحص والترتيب التلقائي:")
1032
  for i, acc in enumerate(st.session_state['valid_found_tab6']): render_server_card(acc, i, context="t6")