Spaces:
Running
Running
| import streamlit as st | |
| import telebot | |
| import requests | |
| import json | |
| import os | |
| import re | |
| import urllib3 | |
| import threading | |
| import time | |
| import uuid | |
| import asyncio | |
| import random | |
| from datetime import datetime | |
| from urllib.parse import urlparse, parse_qs | |
| import urllib.request | |
| from huggingface_hub import HfApi, hf_hub_download | |
| import streamlit.components.v1 as components | |
| from requests.adapters import HTTPAdapter | |
| from urllib3.util.retry import Retry | |
| from concurrent.futures import ThreadPoolExecutor | |
| urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) | |
| # ========================================== | |
| # ⚙️ الإعدادات الأساسية | |
| # ========================================== | |
| TOKEN = "8622568429:AAF9Dj535-JSt-wMQA5QMSPvOAM8SBbiL8I" | |
| bot = telebot.TeleBot(TOKEN) | |
| HF_TOKEN = "Hf_NrYBkhFdfJTFGIdxGbAzuLICxgvxWcIrHn" | |
| REPO_ID = "Ipnshalaby5/IPTV_DB" | |
| api = HfApi(token=HF_TOKEN) | |
| if 'logged_in' not in st.session_state: | |
| st.session_state.logged_in = False | |
| if 'username' not in st.session_state: | |
| st.session_state.username = "" | |
| if 'reset_exp' not in st.session_state: | |
| st.session_state.reset_exp = 0 | |
| if "user_auth" in st.query_params and not st.session_state.logged_in: | |
| st.session_state.username = st.query_params["user_auth"] | |
| st.session_state.logged_in = True | |
| if not st.session_state.logged_in and "user_auth" not in st.query_params: | |
| components.html("""<script>const savedUser = localStorage.getItem("user_auth"); if(savedUser){const url = new URL(window.location.href); url.searchParams.set("user_auth", savedUser); window.location.href = url.href;}</script>""", height=0, width=0) | |
| def get_user_db_file(): | |
| return f"master_database_{st.session_state.username}.json" | |
| def get_user_servers_file(): | |
| return f"servers_database_{st.session_state.username}.json" | |
| ADMIN_FILE = "admin_chat.txt" | |
| VIP_LOGOS = { | |
| 'bein': 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/1a/BeIN_Sports_logo.svg/1024px-BeIN_Sports_logo.svg.png', | |
| 'ssc': 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/87/SSC_Sports_Logo.png/800px-SSC_Sports_Logo.png', | |
| 'mbc': 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/65/MBC_logo.svg/512px-MBC_logo.svg.png', | |
| 'ad sport': 'https://upload.wikimedia.org/wikipedia/en/thumb/0/05/AD_Sports_Logo.svg/1200px-AD_Sports_Logo.svg.png', | |
| 'ontime': 'https://upload.wikimedia.org/wikipedia/commons/thumb/7/7b/OnTime_Sports_Logo.png/800px-OnTime_Sports_Logo.png', | |
| 'alkass': 'https://upload.wikimedia.org/wikipedia/en/thumb/2/2b/Al_Kass_Sports_Channels_Logo.svg/1200px-Al_Kass_Sports_Channels_Logo.svg.png', | |
| 'fajr': 'https://upload.wikimedia.org/wikipedia/commons/thumb/7/77/Al_Fajr_TV_Logo.png/800px-Al_Fajr_TV_Logo.png', | |
| 'dubai': 'https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Dubai_Sports_logo.svg/512px-Dubai_Sports_logo.svg.png' | |
| } | |
| def get_vip_logo(channel_name): | |
| name_lower = channel_name.lower() | |
| for key, logo_url in VIP_LOGOS.items(): | |
| if key in name_lower: return logo_url | |
| return "" | |
| def sync_file_from_cloud(file_name): | |
| if f"synced_{file_name}" not in st.session_state: | |
| try: | |
| hf_hub_download(repo_id=REPO_ID, filename=file_name, repo_type="dataset", token=HF_TOKEN, local_dir=".", force_download=True) | |
| st.session_state[f"synced_{file_name}"] = True | |
| except: pass | |
| def sync_file_to_cloud(file_name): | |
| try: | |
| api.upload_file(path_or_fileobj=file_name, path_in_repo=file_name, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN) | |
| except: pass | |
| def load_global_vips(): | |
| sync_file_from_cloud("global_vips.json") | |
| if os.path.exists("global_vips.json"): | |
| try: | |
| with open("global_vips.json", 'r', encoding='utf-8') as f: return json.load(f) | |
| except: return [] | |
| return [] | |
| def save_to_global_vips(server_data): | |
| vips = load_global_vips() | |
| uid_check = lambda s: s.get('host') == server_data.get('host') and s.get('user') == server_data.get('user') and s.get('mac') == server_data.get('mac') | |
| if not any(uid_check(s) for s in vips): | |
| vips.append(server_data) | |
| with open("global_vips.json", 'w', encoding='utf-8') as f: json.dump(vips, f, indent=4) | |
| sync_file_to_cloud("global_vips.json") | |
| def load_users_registry(): | |
| registry_file = "all_users_registry.json" | |
| sync_file_from_cloud(registry_file) | |
| if os.path.exists(registry_file): | |
| try: | |
| with open(registry_file, 'r', encoding='utf-8') as f: return json.load(f) | |
| except: return [] | |
| return [] | |
| def save_users_registry(users_list): | |
| registry_file = "all_users_registry.json" | |
| with open(registry_file, 'w', encoding='utf-8') as f: json.dump(users_list, f) | |
| sync_file_to_cloud(registry_file) | |
| def parse_timestamp(ts): | |
| if not ts or str(ts).strip() == "": return "Unlimited" | |
| ts_str = str(ts).strip() | |
| if ts_str.lower() in ['null', 'unlimited', 'none', '0']: return "Unlimited" | |
| if "0000-00-00" in ts_str or "00:00:00 00-00-0000" in ts_str or "1970" in ts_str: return "Unlimited" | |
| try: | |
| if ts_str.isdigit(): | |
| return datetime.fromtimestamp(int(ts_str)).strftime('%Y-%m-%d') | |
| if 'am' in ts_str.lower() or 'pm' in ts_str.lower(): | |
| return datetime.strptime(ts_str, "%B %d, %Y, %I:%M %p").strftime('%Y-%m-%d') | |
| date_match = re.search(r'(\d{1,4}[-/\.]\d{1,2}[-/\.]\d{1,4})', ts_str) | |
| if date_match: | |
| return date_match.group(1).replace('.', '-').replace('/', '-') | |
| return ts_str | |
| except: return "Unlimited" | |
| def calculate_days_left(exp_date_str_or_ts): | |
| if not exp_date_str_or_ts: return "Unlimited" | |
| ts_str = str(exp_date_str_or_ts).strip() | |
| if ts_str.lower() in ['null', 'unlimited', 'none', '0', '']: return "Unlimited" | |
| if "0000-00-00" in ts_str or "00:00:00 00-00-0000" in ts_str or "1970" in ts_str: return "Unlimited" | |
| try: | |
| if ts_str.isdigit(): | |
| exp = datetime.fromtimestamp(int(ts_str)) | |
| else: | |
| ts_str_clean = ts_str.replace('.', '-').replace('/', '-') | |
| try: exp = datetime.strptime(ts_str_clean, '%Y-%m-%d %H:%M:%S') | |
| except: | |
| try: exp = datetime.strptime(ts_str_clean, '%Y-%m-%d') | |
| except: | |
| try: exp = datetime.strptime(ts_str_clean, '%d-%m-%Y') | |
| except: return "Unlimited" | |
| return max(0, (exp - datetime.now()).days) | |
| except: return "Unlimited" | |
| def save_admin(chat_id): | |
| with open(ADMIN_FILE, "w") as f: f.write(str(chat_id)) | |
| def load_servers(): | |
| file_name = get_user_servers_file() | |
| sync_file_from_cloud(file_name) | |
| if os.path.exists(file_name): | |
| try: | |
| with open(file_name, 'r', encoding='utf-8') as f: return json.load(f) | |
| except: return [] | |
| return [] | |
| def save_server_to_db(host, user, pwd, days, auth_type='xtream', created_at="غير معروف", exp_date="غير معروف", mac=None, conns="N/A"): | |
| servers = load_servers() | |
| file_name = get_user_servers_file() | |
| found = False | |
| for s in servers: | |
| if s.get('host') == host and s.get('user') == user and s.get('mac') == mac: | |
| s.update({'pass': pwd, 'days': days, 'type': auth_type, 'created_at': created_at, 'exp_date': exp_date, 'status': '🟢 نشط', 'conns': conns}) | |
| found = True; break | |
| if not found: | |
| servers.append({'host': host, 'user': user, 'pass': pwd, 'days': days, 'type': auth_type, 'created_at': created_at, 'exp_date': exp_date, 'status': '🟢 نشط', 'mac': mac, 'conns': conns}) | |
| with open(file_name, 'w', encoding='utf-8') as f: | |
| json.dump(servers, f, indent=4) | |
| sync_file_to_cloud(file_name) | |
| def save_all_servers_to_db(servers_list): | |
| file_name = get_user_servers_file() | |
| with open(file_name, 'w', encoding='utf-8') as f: | |
| json.dump(servers_list, f, indent=4) | |
| sync_file_to_cloud(file_name) | |
| def load_db(): | |
| file_name = get_user_db_file() | |
| sync_file_from_cloud(file_name) | |
| if os.path.exists(file_name): | |
| try: | |
| with open(file_name, 'r', encoding='utf-8') as f: return json.load(f).get("channels", []) | |
| except: return [] | |
| return [] | |
| def save_db(channels): | |
| file_name = get_user_db_file() | |
| with open(file_name, 'w', encoding='utf-8') as f: | |
| json.dump({"channels": channels}, f, ensure_ascii=False, indent=4) | |
| sync_file_to_cloud(file_name) | |
| def export_to_m3u(channels, file_name="VLC_Playlist.m3u"): | |
| lines = ["#EXTM3U", "# Generated by Shalaby VIP Tool\n"] | |
| for ch in channels: | |
| lines.append(f'#EXTINF:-1 tvg-id="{ch["id"]}" tvg-logo="{ch["logo"]}" group-title="{ch["group"]}",{ch["name"]}') | |
| lines.append(f"{ch['url']}|User-Agent={ch['userAgent']}") | |
| with open(file_name, 'w', encoding='utf-8') as f: | |
| f.write("\n".join(lines)) | |
| return file_name | |
| # ========================================== | |
| # 🧠 دوال الترتيب والفلترة | |
| # ========================================== | |
| def live_sort_key(x): | |
| ping_status = 1 if x.get('ping_ok', False) else 0 | |
| days = x.get('days', 0) | |
| days_val = float('inf') if str(days).lower() in ['unlimited', 'direct', 'mac', 'none', 'null', '-'] else int(days) if str(days).isdigit() else -1 | |
| return (ping_status, days_val) | |
| def is_arabic_sport(name, cat): | |
| n, c = name.lower(), cat.lower() | |
| bad = ['movie', 'series', 'drama', 'cinema', 'kids', 'news', 'documentary', 'وثائقي', 'براعم', 'جيم', 'فتافيت', 'rotana', 'osn', 'art'] | |
| foreign = ['|fr|', '|uk|', '|us|', '|de|', '|it|', '|tr|', '|es|', '|pt|', '|nl|', '|pl|', '|ru|', ' sky ', ' tnt ', 'espn', 'fox ', 'eleven', 'bt sport', 'optus', 'astro', 'super sport', 'nfl', 'nba', 'premier sports', 'digi sport'] | |
| if any(x in n or x in c for x in bad + foreign): | |
| return False | |
| allowed = ['bein', 'ssc', 'mbc', 'alwan', 'fajr', 'ontime', 'on time', 'on sport', 'thamania', 'ad sport', 'abu dhabi', 'ابو ظبي', 'kass', 'الكاس', 'ksa', 'dubai', 'دبي', 'رياضة', 'رياضيه', 'sport ar'] | |
| return any(x in n or x in c for x in allowed) or ' ar ' in f" {n} " | |
| def categorize_vip_sport(name, sn): | |
| nl = name.lower() | |
| if 'alwan' in nl: bg = "Alwan" | |
| elif 'bein' in nl: bg = "Bein sport" | |
| elif 'on time' in nl or 'ontime' in nl or 'on sport' in nl: bg = "On sport" | |
| elif 'fajr' in nl: bg = "Fajr" | |
| elif 'mbc masr 2' in nl or 'مصر 2' in nl: bg = "Mbc masr 2" | |
| elif 'mbc action' in nl or 'action' in nl or 'أكشن' in nl: bg = "Mbc action" | |
| elif 'mbc' in nl and ('masr' in nl or 'مصر' in nl or '1' in nl): bg = "Mbc masr 1" | |
| elif 'kass' in nl or 'الكاس' in nl or 'كأس' in nl: bg = "Alkass" | |
| elif 'ad sport' in nl or 'abu dhabi' in nl or 'ابو ظبي' in nl or 'أبوظبي' in nl: bg = "Abu Dhabi" | |
| elif 'dubai' in nl or 'دبي' in nl: bg = "Dubai Sport" | |
| elif 'ssc' in nl: bg = "SSC" | |
| else: bg = "Other sport" | |
| q_str = "[4K]" if any(k in nl for k in ['4k', 'uhd']) else "[FHD]" if any(k in nl for k in ['fhd', '1080']) else "[HD]" if any(k in nl for k in ['hd', '720']) else "[SD]" | |
| return f"{bg} [{sn}] {q_str}" | |
| def normalize_search_text(text): | |
| t = str(text).lower() | |
| t = re.sub(r'\b(بين|بي ان|be in)\b', 'bein', t) | |
| t = re.sub(r'\b(sports|sport|سبورت|رياضة|رياضه)\b', 'sport', t) | |
| t = re.sub(r'[^a-z0-9\sأ-ي]', '', t) | |
| return t | |
| def smart_search_match(query, line): | |
| q_norm = normalize_search_text(query) | |
| line_norm = normalize_search_text(line) | |
| for word in q_norm.split(): | |
| if word not in line_norm: return False | |
| return True | |
| def get_vip_sort_key(ch): | |
| name, group = ch.get('name', '').lower(), ch.get('group', '').lower() | |
| server_name = re.search(r'\[(.*?)\]', ch.get('name', '')).group(1).lower() if re.search(r'\[(.*?)\]', ch.get('name', '')) else "" | |
| cat_prio = 1 if 'alwan' in group else 2 if 'bein sport' in group else 3 if 'on sport' in group else 4 if 'fajr' in group else 5 if 'mbc masr 1' in group else 6 if 'mbc masr 2' in group else 7 if 'mbc action' in group else 8 if 'alkass' in group else 9 if 'abu dhabi' in group else 10 if 'dubai sport' in group else 11 if 'ssc' in group else 12 | |
| q_prio = 4 if '[4k]' in group else 3 if '[fhd]' in group else 2 if '[hd]' in group else 1 if '[sd]' in group else 1 | |
| sub_prio = 2 if any(x in name for x in ['max', 'afc', 'extra', 'xtra']) else 1 | |
| ch_num = int(re.search(r'\b(\d+)\b', name).group(1)) if re.search(r'\b(\d+)\b', name) else 999 | |
| base_name = re.sub(r'\[.*?\]|\(.*?\)|\|.*?\||\{.*?\}', '', name) | |
| for q in ['4k', 'uhd', 'fhd', '1080p', '1080i', '1080', 'hd', '720p', '720', 'sd', '576p', '480p', 'low', 'mob', 'hevc', 'h265', 'vip', 'premium', 'ar', 'arabic']: | |
| base_name = re.sub(rf'\b{q}\b', '', base_name) | |
| base_name = re.sub(r'[^a-z0-9]', '', base_name) | |
| return (cat_prio, q_prio, sub_prio, ch_num, base_name, server_name) | |
| # ========================================== | |
| # 🛠️ معالج الروابط والاستخراج الشامل والمطور | |
| # ========================================== | |
| def parse_any_link(url): | |
| try: | |
| u_m = re.search(r'username=([^&]+)', url, re.IGNORECASE) | |
| p_m = re.search(r'password=([^&]+)', url, re.IGNORECASE) | |
| if u_m and p_m: | |
| host_match = re.search(r'(https?://[^/]+)', url, re.IGNORECASE) | |
| if host_match: | |
| return host_match.group(1), u_m.group(1), p_m.group(1) | |
| m_path = re.search(r'(https?://[^/]+)/(?:live|movie|series|play)/([^/]+)/([^/]+)/', url, re.IGNORECASE) | |
| if m_path: | |
| return m_path.group(1), m_path.group(2), m_path.group(3) | |
| parsed = urlparse(url.strip()) | |
| path_parts = [p for p in parsed.path.split('/') if p] | |
| ignore_starts = ['download', 'portal', 'stalker_portal', 'c', 'get.php', 'live.php', 'player_api.php', 'enigma2', 'mag'] | |
| if len(path_parts) >= 3 and path_parts[0].lower() not in ignore_starts: | |
| if not '.' in path_parts[0] and not '.' in path_parts[1]: | |
| return f"{parsed.scheme}://{parsed.netloc}", path_parts[0], path_parts[1] | |
| except: pass | |
| return None, None, None | |
| def clean_camouflage(text): | |
| return re.sub(r'[@#]+', '', text) | |
| def extract_all_links_from_text(text): | |
| accounts = [] | |
| seen = set() | |
| clean_text = re.sub(r'(https?)\s*:\s*//\s*([a-zA-Z0-9.-]+)\s*\.\s*([a-zA-Z]+)', r'\1://\2.\3', text) | |
| lines = [line.strip() for line in clean_text.split('\n') if line.strip()] | |
| # 1. التقطيع الذكي: التعامل مع السطور المتتالية (Host, User, Pass) | |
| for i in range(len(lines) - 2): | |
| h_m = re.search(r'(https?://[a-zA-Z0-9.-]+(?::\d+)?)', lines[i]) | |
| if not h_m or 'username=' in lines[i].lower(): continue | |
| u_line, p_line = lines[i+1], lines[i+2] | |
| u_m = re.search(r'(?:user(?:name)?|👤user)\s*[=:]?\s*([a-zA-Z0-9_.\-@#]+)', u_line, re.IGNORECASE) | |
| if not u_m: u_m = re.search(r'^([a-zA-Z0-9_.\-@#]+)$', u_line) | |
| p_m = re.search(r'(?:pass(?:word)?|🔐password)\s*[=:]?\s*([a-zA-Z0-9_.\-@#]+)', p_line, re.IGNORECASE) | |
| if not p_m: p_m = re.search(r'^([a-zA-Z0-9_.\-@#]+)$', p_line) | |
| if h_m and u_m and p_m and 'http' not in u_line and 'http' not in p_line: | |
| h = h_m.group(1).rstrip('/') | |
| u = clean_camouflage(u_m.group(1)) | |
| p = clean_camouflage(p_m.group(1)) | |
| uid_str = f"x_{h}_{u}" | |
| 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) | |
| # 2. الاستخراج المدمج في الروابط | |
| for link in re.findall(r'(https?://[^\s<>"\'\|]+)', clean_text, re.IGNORECASE): | |
| link = link.rstrip('.') | |
| h, u, p = parse_any_link(link) | |
| if h and u and p: | |
| u, p = clean_camouflage(u), clean_camouflage(p) | |
| uid_str = f"x_{h}_{u}" | |
| 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); seen.add(link) | |
| # 3. استخراج البلوكات المتباعدة | |
| for b in re.split(r'\n\s*\n', clean_text): | |
| h_m = re.search(r'(https?://[a-zA-Z0-9.-]+(?::\d+)?)', b) | |
| u_m = re.search(r'(?:User|Username|👤User)[^\w\n]*([a-zA-Z0-9_.\-@#]+)', b, re.IGNORECASE) | |
| p_m = re.search(r'(?:Pass|Password|🔐Password)[^\w\n]*([a-zA-Z0-9_.\-@#]+)', b, re.IGNORECASE) | |
| if h_m and u_m and p_m: | |
| h, u, p = h_m.group(1).rstrip('/'), clean_camouflage(u_m.group(1)), clean_camouflage(p_m.group(1)) | |
| uid_str = f"x_{h}_{u}" | |
| 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) | |
| # 4. الماكات | |
| current_mac_host = None | |
| 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) | |
| url_patt = re.compile(r'(https?://[^\s]+)') | |
| for line in lines: | |
| u_m = url_patt.search(line.replace(" ", "")) | |
| if u_m and 'username=' not in line: | |
| current_mac_host = u_m.group(1).rstrip('/') | |
| m_m = mac_patt.search(line) | |
| if m_m and current_mac_host: | |
| m = m_m.group(1).upper().replace('-', ':') | |
| uid_str = f"m_{current_mac_host}_{m}" | |
| 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) | |
| # 5. الروابط المباشرة المتبقية | |
| direct_links = re.findall(r'(https?://[^\s<>"\'\|]+)', clean_text, re.IGNORECASE) | |
| for link in direct_links: | |
| link = link.rstrip('.') | |
| if link not in seen and not parse_any_link(link)[0]: | |
| if any(x in link.lower() for x in ['.m3u8', '.ts', '.m3u', 'mpegts', '/stream/', 'amazonaws', '.dev', '.top', 'ok.ru', 'token=', 'index.', '/play/', 'get.php?']): | |
| uid_str = f"d_{link}" | |
| if uid_str not in seen: | |
| accounts.append({'type': 'direct', 'url': link, '_uid': str(uuid.uuid4())[:8]}) | |
| seen.add(uid_str) | |
| seen.add(link) | |
| return accounts | |
| # ========================================== | |
| # 📡 محرك الماك المطوّر | |
| # ========================================== | |
| def smart_mac_session(mac, change_ua=False): | |
| session = requests.Session() | |
| 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" | |
| session.headers.update({"User-Agent": ua, "Cookie": f"mac={mac}; stb_lang=en; timezone=Europe/London;"}) | |
| retry_strategy = Retry(total=1, status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["HEAD", "GET", "OPTIONS"], backoff_factor=0.5) | |
| session.mount("http://", HTTPAdapter(max_retries=retry_strategy)) | |
| session.mount("https://", HTTPAdapter(max_retries=retry_strategy)) | |
| return session | |
| def clean_mac_address(mac): | |
| return mac.replace(':', '') | |
| def test_mac_portal(host, mac): | |
| session = smart_mac_session(mac) | |
| conns, is_working, exp = "0 / 1", False, "Unlimited" | |
| try: | |
| r_hs = session.get(f"{host}/portal.php?type=stb&action=handshake&mac={mac}", timeout=5, verify=False).json() | |
| 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']}"}) | |
| prof_req = session.get(f"{host}/portal.php?type=stb&action=get_profile&mac={mac}", timeout=5, verify=False).json() | |
| if 'js' in prof_req and prof_req['js'] == []: | |
| return False, "Unlimited", "N/A" | |
| prof = prof_req.get('js', {}) | |
| if prof and isinstance(prof, dict): | |
| is_working = True | |
| conns = f"{prof.get('active_cons', '0')} / {prof.get('max_connections', '1')}" | |
| for key in ['phone', 'expire_billing_date', 'expiry', 'expire', 'end_date', 'expiration', 'expiration_date', 'account_balance']: | |
| if key in prof and prof[key] and str(prof[key]) not in ["0", "0000-00-00 00:00:00", "0000-00-00", "null", "none"]: | |
| exp = parse_timestamp(str(prof[key])) | |
| break | |
| except: pass | |
| if not is_working: return False, "Unlimited", "N/A" | |
| if exp == "Unlimited": | |
| mac_clean = clean_mac_address(mac) | |
| endpoints = [ | |
| f"/portal.php?action=get_main_info&type=account_info&mac={mac}&JsHttpRequest=1-xml", | |
| f"/portal.php?action=get_agreement_info&type=account_info&mac={mac}&JsHttpRequest=1-xml", | |
| f"/portal.php?action=get_payment_info&type=account_info&mac={mac}&JsHttpRequest=1-xml", | |
| f"/stalker_portal/server/api.php?action=get_subscription_info&mac={mac_clean}", | |
| f"/stalker_portal/api/v2/player/get_subscription_info?mac={mac_clean}", | |
| f"/server/api.php?action=get_subscription_info&mac={mac_clean}" | |
| ] | |
| for ep in endpoints: | |
| try: | |
| resp = session.get(f"{host.rstrip('/')}{ep}", timeout=5, verify=False) | |
| if resp.status_code == 200: | |
| raw = resp.text.strip() | |
| json_match = re.search(r'({.*})', raw, re.DOTALL) | |
| if json_match: raw = json_match.group(1) | |
| try: | |
| data = json.loads(raw) | |
| if 'js' in data and data['js'] == []: continue | |
| js_data = data.get('js', data) if isinstance(data, dict) else data | |
| if isinstance(js_data, dict): | |
| for key in ['phone', 'expire_billing_date', 'expiry', 'expire', 'end_date', 'expiration', 'expiration_date']: | |
| 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"]: | |
| return True, parse_timestamp(str(js_data[key])), conns | |
| except: pass | |
| date_match = re.search(r'(\d{4}-\d{2}-\d{2})|([A-Z][a-z]+ \d{1,2}, \d{4})', raw) | |
| if date_match: return True, parse_timestamp(date_match.group(0)), conns | |
| except: continue | |
| return True, exp, conns | |
| def extract_mac_to_m3u_logic(portal, mac, mode="🍿 الكل (لايف+أفلام+مسلسلات)", add_ua=False, ext_choice=".m3u8"): | |
| session = smart_mac_session(mac) | |
| try: | |
| hs = session.get(f"{portal}/portal.php?type=stb&action=handshake&mac={mac}", timeout=5, verify=False).json() | |
| if hs.get('js') and 'token' in hs['js']: session.headers.update({"Authorization": f"Bearer {hs['js']['token']}"}) | |
| except: pass | |
| 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", []) | |
| except: return False, "❌ فشل الاتصال بالبورتال.", 0 | |
| if not genres: return False, "❌ لم يتم العثور على تصنيفات.", 0 | |
| m3u_lines = ["#EXTM3U", "# Generated by Shalaby VIP Panel\n"] | |
| total_channels = 0 | |
| ua_suffix = "|User-Agent=IPTVSmartersPro" if add_ua else "" | |
| ext_val = "m3u8" if ext_choice == ".m3u8" else "ts" if ext_choice == ".ts" else "" | |
| strategy = "create_link" | |
| first_genre_id = genres[0].get('id') if genres else None | |
| if first_genre_id: | |
| try: | |
| test_data = session.get(f"{portal}/portal.php?type=itv&action=get_ordered_list&genre={first_genre_id}&p=1&mac={mac}&JsHttpRequest=1-xml", timeout=5, verify=False).json().get("js", {}).get("data", []) | |
| if test_data: | |
| test_cmd = test_data[0].get("cmd", "") or test_data[0].get("id") | |
| 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() | |
| if test_link.get("js") and test_link["js"].get("id") is None: | |
| session = smart_mac_session(mac, change_ua=True) | |
| 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() | |
| if test_link_2.get("js") and test_link_2["js"].get("id") is None: strategy = "raw_fallback" | |
| except: strategy = "raw_fallback" | |
| if mode in ["⚽ رياضة فقط VIP", "📺 لايف فقط", "🎬 لايف + أفلام", "🍿 الكل (لايف+أفلام+مسلسلات)"]: | |
| domain_parts = portal.split('//')[-1].split(':')[0].split('.') | |
| valid_parts = [p for p in domain_parts if p.lower() not in ['com', 'net', 'org', 'info', 'tv'] and len(p)>2] | |
| sn = re.sub(r'[^a-zA-Z0-9]', '', valid_parts[0].capitalize() if valid_parts else "MAC") | |
| sports_channels = [] | |
| def grab_genre(g): | |
| res_lines, res_sports = [], [] | |
| g_id, g_name = g.get("id"), g.get("title", "Unknown") | |
| p = 1 | |
| seen = set() | |
| while p <= 15: | |
| try: | |
| data = session.get(f"{portal}/portal.php?type=itv&action=get_ordered_list&genre={g_id}&p={p}&mac={mac}&JsHttpRequest=1-xml", timeout=12, verify=False).json().get("js", {}).get("data", []) | |
| if not data or not isinstance(data, list): break | |
| added = False | |
| for ch in data: | |
| name, cid, cmd_str = ch.get("name", "Unknown"), ch.get("id"), str(ch.get("cmd", "")) | |
| if not cid or cid in seen: continue | |
| seen.add(cid); added = True | |
| if strategy == "create_link": | |
| stream_url = f"{portal}/portal.php?type=itv&action=create_link&cmd={urllib.parse.quote(cmd_str)}&mac={mac}" | |
| else: | |
| stream_url = f"{portal}/play/live.php?mac={mac}&stream={cid}" | |
| if ext_val: stream_url += f"&extension={ext_val}" | |
| logo = ch.get("logo", "") | |
| if mode == "⚽ رياضة فقط VIP": | |
| 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}) | |
| else: res_lines.append(f'#EXTINF:-1 tvg-logo="{logo}" group-title="{g_name}",{name}\n{stream_url}{ua_suffix}') | |
| if not added: break | |
| p += 1 | |
| except: break | |
| return res_lines, res_sports | |
| with ThreadPoolExecutor(max_workers=5) as executor: | |
| results = list(executor.map(grab_genre, genres)) | |
| for res_l, res_s in results: | |
| if mode == "⚽ رياضة فقط VIP": sports_channels.extend(res_s) | |
| else: m3u_lines.extend(res_l); total_channels += len(res_l) | |
| if mode == "⚽ رياضة فقط VIP" and sports_channels: | |
| sports_channels.sort(key=get_vip_sort_key) | |
| 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 | |
| if mode in ["🎬 لايف + أفلام", "🍿 الكل (لايف+أفلام+مسلسلات)"]: | |
| try: | |
| vod_cats = session.get(f"{portal}/portal.php?type=vod&action=get_categories&mac={mac}&JsHttpRequest=1-xml", timeout=10, verify=False).json().get("js", []) | |
| for c in vod_cats: | |
| c_id, c_name = c.get("id"), c.get("title", "Movies") | |
| p = 1 | |
| while p <= 15: | |
| try: | |
| 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", []) | |
| if not vod_data: break | |
| for vod in vod_data: | |
| stream_url = f"{portal}/play/vod.php?mac={mac}&stream={vod.get('cmd', '') or vod.get('id')}&extension=mp4" | |
| 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 | |
| p += 1 | |
| except: break | |
| except: pass | |
| if mode == "🍿 الكل (لايف+أفلام+مسلسلات)": | |
| try: | |
| ser_cats = session.get(f"{portal}/portal.php?type=series&action=get_categories&mac={mac}&JsHttpRequest=1-xml", timeout=10, verify=False).json().get("js", []) | |
| for c in ser_cats: | |
| c_id, c_name = c.get("id"), c.get("title", "Series") | |
| p = 1 | |
| while p <= 15: | |
| try: | |
| 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", []) | |
| if not ser_data: break | |
| for ser in ser_data: | |
| stream_url = f"{portal}/play/series.php?mac={mac}&stream={ser.get('id')}&extension=mp4" | |
| 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 | |
| p += 1 | |
| except: break | |
| except: pass | |
| if total_channels == 0: return False, "❌ لم يتم العثور على قنوات.", 0 | |
| return True, "\n".join(m3u_lines), total_channels | |
| # ========================================== | |
| # 🔥 دالة السحب المباشر الشغالة الأصلية لـ player_api (النسخة الصاروخية الناجحة) 🔥 | |
| # ========================================== | |
| def custom_api_extractor_sync(host, user, pw, mode, ext_choice, add_ua): | |
| output_file = f"Extracted_VIP_{user}_API.m3u" | |
| extracted_count = 0 | |
| live_ext = "" if ext_choice == "بدون صيغة" else ext_choice | |
| ua_suffix = "|User-Agent=IPTVSmartersPro" if add_ua else "" | |
| host = host.rstrip('/') | |
| sn = host.split('//')[-1].split(':')[0].split('.')[0].capitalize() | |
| # 1. سحب أسماء الأقسام أولاً كـ الخريطة المحلية (لعدم إجهاد السيرفر) | |
| cat_map = {} | |
| try: | |
| cats_res = requests.get(f"{host}/player_api.php?username={user}&password={pw}&action=get_live_categories", headers={'User-Agent': 'IPTVSmartersPro'}, timeout=15, verify=False) | |
| if cats_res.status_code == 200: | |
| cats_data = cats_res.json() | |
| if isinstance(cats_data, list): | |
| for c in cats_data: | |
| cat_map[str(c.get('category_id'))] = str(c.get('category_name', 'Live TV')) | |
| except: pass | |
| # 2. سحب جميع قنوات اللايف بضغطة واحدة (أسرع مليون مرة وتتجنب الـ 502 والـ Timeout تماماً) | |
| vip_ch = [] | |
| try: | |
| streams_res = requests.get(f"{host}/player_api.php?username={user}&password={pw}&action=get_live_streams", headers={'User-Agent': 'IPTVSmartersPro'}, timeout=35, verify=False) | |
| if streams_res.status_code == 200: | |
| raw_text = streams_res.text.strip() | |
| if raw_text.startswith('\ufeff'): raw_text = raw_text[1:] | |
| streams_data = json.loads(raw_text) | |
| if isinstance(streams_data, list): | |
| with open(output_file, 'w', encoding='utf-8') as f: | |
| f.write("#EXTM3U\n") | |
| for s in streams_data: | |
| name = str(s.get('name', '')) | |
| cat_id = str(s.get('category_id', '')) | |
| cat_name = cat_map.get(cat_id, 'Live TV') | |
| logo = s.get("stream_icon", "") | |
| stream_id = s.get('stream_id') | |
| if not stream_id: continue | |
| url = f"{host}/live/{user}/{pw}/{stream_id}{live_ext}" | |
| if mode == "⚽ رياضة فقط VIP": | |
| if is_arabic_sport(name, cat_name): | |
| chic_name = f"{name} [{sn}]" | |
| group = categorize_vip_sport(name, sn) | |
| vip_ch.append({"name": chic_name, "group": group, "logo": get_vip_logo(name) or logo, "url": url}) | |
| else: # 📺 لايف فقط (كل قنوات اللايف) | |
| f.write(f'#EXTINF:-1 tvg-logo="{logo}" group-title="{cat_name}",{name}\n{url}{ua_suffix}\n') | |
| extracted_count += 1 | |
| # كتابة باقات الرياضة المرتبة لو تم اختيارها | |
| if mode == "⚽ رياضة فقط VIP" and vip_ch: | |
| vip_ch.sort(key=get_vip_sort_key) | |
| 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 | |
| except: pass | |
| return extracted_count > 0, output_file, extracted_count | |
| def custom_get_php_extractor(host, user, pw, mode, ext_choice, add_ua): | |
| live_ext = "m3u8" if ext_choice == ".m3u8" else "ts" | |
| ua_suffix = "|User-Agent=IPTVSmartersPro" if add_ua else "" | |
| url = f"{host.rstrip('/')}/get.php?username={user}&password={pw}&type=m3u_plus&output={live_ext}" | |
| output_file = f"Extracted_VIP_{user}_GET.m3u" | |
| extracted_count = 0 | |
| try: | |
| r = requests.get(url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}, timeout=35, verify=False) | |
| if r.status_code != 200 or '#EXTM3U' not in r.text[:200]: | |
| return False, "❌ فشل سحب الملف عبر get.php", 0 | |
| raw_lines = r.text.splitlines() | |
| with open(output_file, 'w', encoding='utf-8') as f: | |
| f.write("#EXTM3U\n") | |
| sn = host.split('//')[-1].split(':')[0].split('.')[0].capitalize() | |
| i = 0 | |
| while i < len(raw_lines): | |
| line = raw_lines[i] | |
| if line.startswith("#EXTINF"): | |
| next_line = raw_lines[i+1] if i+1 < len(raw_lines) else "" | |
| name_match = re.search(r',(.*)$', line) | |
| name = name_match.group(1).strip() if name_match else "Unknown" | |
| group_match = re.search(r'group-title="([^"]+)"', line) | |
| group = group_match.group(1) if group_match else "" | |
| logo_match = re.search(r'tvg-logo="([^"]+)"', line) | |
| logo = logo_match.group(1) if logo_match else "" | |
| is_vod = '/movie/' in next_line.lower() or '/series/' in next_line.lower() or next_line.lower().endswith('.mp4') or next_line.lower().endswith('.mkv') | |
| is_live = not is_vod | |
| add_this = False | |
| final_line = line | |
| final_next = next_line + ua_suffix | |
| if mode == "⚽ رياضة فقط VIP": | |
| if is_live and is_arabic_sport(name, group): | |
| chic_name = f"{name} [{sn}]" | |
| final_group = categorize_vip_sport(name, sn) | |
| final_logo = get_vip_logo(name) or logo | |
| final_line = re.sub(r'group-title="[^"]*"', f'group-title="{final_group}"', line) if 'group-title=' in line else f'{line} group-title="{final_group}"' | |
| final_line = re.sub(r'tvg-logo="[^"]*"', f'tvg-logo="{final_logo}"', final_line) if 'tvg-logo=' in line else final_line | |
| final_line = re.sub(r',(.*)$', f',{chic_name}', final_line) | |
| add_this = True | |
| elif mode == "📺 لايف فقط (كل القنوات)": | |
| if is_live: add_this = True | |
| if add_this: | |
| f.write(f"{final_line}\n{final_next}\n") | |
| extracted_count += 1 | |
| i += 2 | |
| else: i += 1 | |
| return extracted_count > 0, output_file, extracted_count | |
| except Exception as e: | |
| return False, f"❌ السيرفر لا يدعم get.php: {str(e)}", 0 | |
| # ========================================== | |
| # ✅ الفحص الذكي ونظام المراوغة المتعدد (مع دعم الـ get_user_info والـ Strict Timeout) | |
| # ========================================== | |
| def check_server_smart(host, user, pwd): | |
| agents = [ | |
| 'IPTVSmartersPro', | |
| 'ExoPlayer/2.18.1 (Linux; Android 11) ExoPlayerLib/2.18.1', | |
| 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' | |
| ] | |
| for agent in agents: | |
| headers = {'User-Agent': agent, 'Accept': '*/*', 'Connection': 'keep-alive'} | |
| try: | |
| r = requests.get(f"{host}/player_api.php?username={user}&password={pwd}", headers=headers, timeout=(4,8), verify=False, allow_redirects=True) | |
| if r.status_code in [200, 301, 302]: | |
| raw = r.text.strip() | |
| if raw.startswith('\ufeff'): raw = raw[1:] | |
| data = json.loads(raw) | |
| info = data.get('user_info', {}) | |
| status = str(info.get('status', '')).lower() | |
| if not info.get('exp_date') or status not in ['active', 'trial', '1']: | |
| try: | |
| r2 = requests.get(f"{host}/player_api.php?username={user}&password={pwd}&action=get_user_info", headers=headers, timeout=(4,8), verify=False) | |
| raw2 = r2.text.strip() | |
| if raw2.startswith('\ufeff'): raw2 = raw2[1:] | |
| data2 = json.loads(raw2) | |
| if data2.get('user_info'): | |
| info.update(data2.get('user_info')) | |
| status = str(info.get('status', '')).lower() | |
| except: pass | |
| if status in ['active', 'trial', '1'] or str(info.get('auth', '')) == '1' or data.get('user_info'): | |
| info['source'] = 'Xtream API' | |
| info['conns'] = f"{info.get('active_cons', '0')} / {info.get('max_connections', '1')}" | |
| info['formats'] = ', '.join(info.get('allowed_output_formats', [])) if isinstance(info.get('allowed_output_formats', []), list) and len(info.get('allowed_output_formats', [])) > 0 else 'HLS (m3u8), TS' | |
| return info | |
| except: pass | |
| for agent in agents: | |
| headers = {'User-Agent': agent, 'Accept': '*/*'} | |
| try: | |
| r = requests.get(f"{host}/get.php?username={user}&password={pwd}&type=m3u_plus", headers=headers, timeout=(4,8), verify=False) | |
| if r.status_code == 200 and '#EXTM3U' in r.text[:200]: | |
| return {'status': 'Active', 'exp_date': None, 'created_at': None, 'ch_count': r.text.count('#EXTINF'), 'source': 'get.php (M3U)', 'formats': 'm3u8, ts', 'conns': 'N/A'} | |
| except: pass | |
| return None | |
| def test_live_stream_ping(host, user, pw): | |
| agents = ['IPTVSmartersPro', 'ExoPlayer/2.18.1'] | |
| for agent in agents: | |
| headers = {'User-Agent': agent, 'Accept': '*/*'} | |
| try: | |
| api_url = f"{host.rstrip('/')}/player_api.php?username={user}&password={pw}&action=get_live_streams" | |
| r = requests.get(api_url, headers=headers, timeout=(4,8), verify=False) | |
| if r.status_code == 200: | |
| data = r.json() | |
| if isinstance(data, list) and len(data) > 0: | |
| test_url = f"{host.rstrip('/')}/live/{user}/{pw}/{data[0].get('stream_id')}.ts" | |
| res = requests.get(test_url, headers=headers, stream=True, timeout=(4,10), verify=False) | |
| status = res.status_code | |
| if status in [200, 302, 206]: | |
| chunk = next(res.iter_content(chunk_size=1024), b"") | |
| res.close() | |
| if b"<html" in chunk.lower() or b"error" in chunk.lower()[:20]: | |
| return False, "❌ صفحة خطأ (البث متوقف)." | |
| return True, "✅ يعمل بقوة." | |
| res.close() | |
| return False, f"❌ متوقف (كود {status})." | |
| return False, "⚠️ الحساب يعمل لكن قنوات اللايف فارغة." | |
| except: continue | |
| return False, "❌ فشل الاتصال." | |
| def advanced_direct_link_test(url): | |
| try: | |
| res = requests.get(url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}, stream=True, timeout=(4,8), verify=False, allow_redirects=True) | |
| if res.status_code in [200, 206, 302, 304]: | |
| chunk = next(res.iter_content(chunk_size=2048), b"") | |
| res.close() | |
| text_chunk = chunk.decode('utf-8', errors='ignore').strip() | |
| if text_chunk.startswith("#EXTM3U") or "#EXTINF" in text_chunk: | |
| return True, True, max(1, text_chunk.count("#EXTINF")) | |
| if "<html" in text_chunk.lower() and not any(x in url for x in ['.dev', '.workers', '.top']): | |
| return False, False, 0 | |
| return True, False, 0 | |
| except: pass | |
| return False, False, 0 | |
| # ========================================== | |
| # ⚡ محرك Asyncio للتجميع المتوازي | |
| # ========================================== | |
| def process_single_account_sync(a): | |
| try: | |
| if a['type'] == 'xtream': | |
| info = check_server_smart(a['host'], a['user'], a['pass']) | |
| if info: | |
| d = calculate_days_left(info.get('exp_date')) | |
| ping_ok, _ = test_live_stream_ping(a['host'], a['user'], a['pass']) | |
| 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')}) | |
| return a | |
| elif a['type'] == 'mac': | |
| prof_ok, exp, conns = test_mac_portal(a['host'], a['mac']) | |
| if prof_ok: | |
| 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}) | |
| else: | |
| a.update({'days': 'MAC', 'type_auth': 'Portal MAC', 'created_at': '-', 'exp_date': '-', 'status': 'لا يعمل', 'ping_ok': False, 'formats': 'Unknown', 'conns': 'N/A'}) | |
| return a | |
| elif a['type'] == 'direct': | |
| ping_ok, is_playlist, ch_count = advanced_direct_link_test(a['url']) | |
| if ping_ok: | |
| if is_playlist: | |
| a.update({'host': 'Playlist M3U', 'days': 'Direct', 'type_auth': 'Playlist', 'status': 'قائمة تشغيل تعمل', 'ping_ok': True, 'formats': 'm3u8/ts', 'ch_count': ch_count}) | |
| else: | |
| a.update({'host': 'Direct Link', 'days': 'Direct', 'type_auth': 'Stream', 'status': 'بث مباشر يعمل', 'ping_ok': True, 'formats': 'ts/m3u8'}) | |
| else: | |
| a.update({'host': 'Direct Link', 'days': 'Direct', 'type_auth': 'Stream', 'status': 'لا يعمل', 'ping_ok': False, 'formats': 'Unknown'}) | |
| return a | |
| except: pass | |
| return None | |
| async def run_async_scan(accs, progress_bar, status_msg, live_placeholder, context): | |
| loop = asyncio.get_running_loop() | |
| valid_found = [] | |
| total = len(accs) | |
| completed = 0 | |
| with ThreadPoolExecutor(max_workers=30) as executor: | |
| tasks = [loop.run_in_executor(executor, process_single_account_sync, a) for a in accs] | |
| for coro in asyncio.as_completed(tasks): | |
| result = await coro | |
| completed += 1 | |
| progress_bar.progress(completed / total) | |
| status_msg.warning(f"⚡ فحص صاروخي بـ Asyncio ({completed}/{total})...") | |
| if result: | |
| valid_found.append(result) | |
| valid_found.sort(key=live_sort_key, reverse=True) | |
| with live_placeholder.container(): | |
| for srv in valid_found: | |
| ping_icon = "🟢" if srv.get('ping_ok') else "🔴" | |
| days_str = f"{srv.get('days', 0)} يوم" if str(srv.get('days', 0)).isdigit() else str(srv.get('days', 0)) | |
| if srv.get('type') == 'direct': | |
| st.success(f"{ping_icon} {srv['status']} | 🌐 `{srv['url']}` | ⏳ {days_str}") | |
| elif srv.get('type') == 'mac': | |
| st.success(f"{ping_icon} {srv['status']} | 🌐 `{srv['host']}` | 💳 `{srv['mac']}` | ⏳ {days_str}") | |
| else: | |
| st.success(f"{ping_icon} {srv['status']} | 🌐 `{srv['host']}` | 👤 `{srv['user']}` | ⏳ {days_str}") | |
| try: | |
| if result.get('type') == 'xtream': save_to_global_vips({'host': result['host'], 'user': result['user'], 'pass': result['pass'], 'days': result.get('days')}) | |
| elif result.get('type') == 'mac': save_to_global_vips({'host': result['host'], 'mac': result['mac'], 'days': result.get('days'), 'type': 'mac'}) | |
| except: pass | |
| live_placeholder.empty() | |
| return valid_found | |
| def safe_async_run(coro): | |
| try: loop = asyncio.get_running_loop() | |
| except RuntimeError: loop = None | |
| if loop and loop.is_running(): | |
| result = [None] | |
| def _run(): | |
| new_loop = asyncio.new_event_loop() | |
| asyncio.set_event_loop(new_loop) | |
| result[0] = new_loop.run_until_complete(coro) | |
| t = threading.Thread(target=_run) | |
| t.start() | |
| t.join() | |
| return result[0] | |
| else: return asyncio.run(coro) | |
| def extract_sports_unified(host, user, pwd, skip_clean=False, ext=".m3u8"): | |
| domain_parts = host.split('//')[-1].split(':')[0].split('.') | |
| valid_parts = [p for p in domain_parts if p.lower() not in ['com', 'net', 'org', 'info', 'tv'] and len(p)>2] | |
| sn = re.sub(r'[^a-zA-Z0-9]', '', valid_parts[0].capitalize() if valid_parts else domain_parts[0].capitalize()) | |
| try: | |
| r = requests.get(f"{host}/player_api.php?username={user}&password={pwd}", headers=get_headers(), timeout=(4,10), verify=False).json() | |
| info = r.get('user_info', {}) | |
| if info.get('status') != 'Active' and str(info.get('auth', '')) != '1': | |
| return False, f"❌ متوقف: {sn}" | |
| 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'))) | |
| # استخدام دالة التجميع السريعة المباشرة لعدم إغلاق السيرفر | |
| cat_map = {} | |
| try: | |
| cats = requests.get(f"{host}/player_api.php?username={user}&password={pwd}&action=get_live_categories", timeout=15, verify=False).json() | |
| if isinstance(cats, list): | |
| for c in cats: cat_map[str(c.get('category_id'))] = str(c.get('category_name', 'Live TV')) | |
| except: pass | |
| streams = requests.get(f"{host}/player_api.php?username={user}&password={pwd}&action=get_live_streams", timeout=25, verify=False).json() | |
| new_chans = [] | |
| if isinstance(streams, list): | |
| for s in streams: | |
| name = str(s.get('name', '')) | |
| cat_name = cat_map.get(str(s.get('category_id')), 'Live TV') | |
| if is_arabic_sport(name, cat_name): | |
| ch_id = f"ch_{s.get('stream_id')}_{re.sub(r'\W','',host.lower())}_{re.sub(r'\W','',user.lower())}" | |
| 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"}) | |
| if not new_chans: return False, f"⚠️ لا يوجد باقات رياضية في {sn}" | |
| db = load_db(); merged = {c['id']: c for c in db} | |
| for c in new_chans: merged[c['id']] = c | |
| final = list(merged.values()); final.sort(key=get_vip_sort_key); save_db(final) | |
| return True, f"✅ تم سحب {len(new_chans)} باقة من [{sn}]" | |
| except: return False, f"❌ خطأ اتصال في {sn}" | |
| # ========================================== | |
| # 🎨 واجهة البطاقات الموحدة (Super UX) | |
| # ========================================== | |
| def render_server_card(server, idx, context="tab1"): | |
| t = server.get('type') | |
| days_val = server.get('days', 0) | |
| days_str = f"{days_val} يوم" if str(days_val).isdigit() else str(days_val) | |
| uid_str = server.get('_uid', str(uuid.uuid4())[:8]) | |
| base_k = f"{context}_{idx}_{uid_str}" | |
| 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 | |
| ping_icon = "🟢" if server.get('ping_ok') else "🔴" | |
| if t == 'direct': | |
| title = f"{ping_icon} {server['status']} | 🌐 {server.get('url', '')} | ⏳ {days_str}" | |
| 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}" | |
| 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}" | |
| title += "\u200B" * st.session_state.reset_exp | |
| with st.expander(title, expanded=force_open): | |
| if t == 'xtream': | |
| st.markdown("### 📋 بيانات السيرفر (للنسخ السريع):") | |
| c1, c2, c3 = st.columns(3) | |
| c1.text_input("🌐 الهوست (Host):", value=server.get('host', ''), key=f"h_{base_k}") | |
| c2.text_input("👤 اليوزر (User):", value=server.get('user', ''), key=f"u_{base_k}") | |
| c3.text_input("🔑 الباسورد (Pass):", value=server.get('pass', ''), key=f"p_{base_k}") | |
| m3u_link = f"{server['host'].rstrip('/')}/get.php?username={server['user']}&password={server['pass']}&type=m3u_plus" | |
| st.text_input("🔗 رابط M3U المباشر:", value=m3u_link, key=f"m3u_link_{base_k}") | |
| extinf_format = f'#EXTINF:-1,{server.get("host", "")} | {server.get("user", "")}\n{m3u_link}' | |
| st.text_area("📄 صيغة ملف M3U الكاملة:", value=extinf_format, height=68, key=f"m3u_txt_{base_k}") | |
| st.markdown("---") | |
| col_ping, col_preview = st.columns(2) | |
| with col_ping: | |
| if st.button("📡 فحص دقة البث الفعلي (GET)", key=f"ping_{base_k}"): | |
| with st.spinner("جاري فحص داتا الفيديو..."): | |
| ok, msg = test_live_stream_ping(server['host'], server['user'], server['pass']) | |
| st.session_state[f"ping_res_{base_k}"] = (ok, msg) | |
| st.rerun() | |
| with col_preview: | |
| if st.button("📺 جلب رابط المعاينة الفورية", key=f"preview_btn_{base_k}"): | |
| with st.spinner("جاري توليد مسار المشغل..."): | |
| try: | |
| 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=12, verify=False).json() | |
| 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" | |
| else: | |
| st.session_state[f"preview_err_{base_k}"] = "لم نجد قنوات للمعاينة." | |
| except: | |
| st.session_state[f"preview_err_{base_k}"] = "فشل توليد اتصال المعاينة." | |
| st.rerun() | |
| if f"ping_res_{base_k}" in st.session_state: | |
| ok, msg = st.session_state[f"ping_res_{base_k}"] | |
| if ok: st.success(msg) | |
| else: st.error(msg) | |
| if f"preview_err_{base_k}" in st.session_state: | |
| st.error(st.session_state[f"preview_err_{base_k}"]) | |
| st.markdown("---") | |
| st.markdown(f"### ⚙️ خيارات السحب المزدوج لللايف (جرب الدالة الأخرى لو الأولى فشلت):") | |
| tab_api, tab_get = st.tabs(["🚀 سحب بـ player_api (السريع والمضمون)", "🌐 سحب بـ get.php (المدمج)"]) | |
| with tab_api: | |
| st.info("💡 دي الدالة المباشرة الخفيفة اللي بتعدي من الحماية وتسحب قنوات اللايف بسرعة.") | |
| col_m1, col_e1, col_u1, col_b1 = st.columns([3, 2, 2, 2]) | |
| selected_mode_api = col_m1.selectbox("🎯 المحتوى المستهدف:", ["⚽ رياضة فقط VIP", "📺 لايف فقط (كل الباقات)"], key=f"mode_api_{base_k}") | |
| selected_ext_api = col_e1.selectbox("صيغة البث:", [".m3u8", ".ts", "بدون صيغة"], key=f"ext_api_{base_k}") | |
| add_ua_api = col_u1.checkbox("إضافة حماية UA", value=False, key=f"ua_api_{base_k}") | |
| if col_b1.button("📥 سحب داتا السيرفر (API)", key=f"btn_api_{base_k}"): | |
| with st.spinner("جاري السحب بالـ API لللايف..."): | |
| success, file, count = custom_api_extractor_sync(server['host'], server['user'], server['pass'], selected_mode_api, selected_ext_api, add_ua_api) | |
| 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']}_API.m3u" | |
| if f"prep_err_{base_k}" in st.session_state: del st.session_state[f"prep_err_{base_k}"] | |
| else: | |
| st.session_state[f"prep_err_{base_k}"] = "❌ السيرفر فارغ أو محظور בـ API بـ قنوات اللايف." | |
| st.rerun() | |
| with tab_get: | |
| st.info("💡 دي دالة تحميل الـ M3U بالكامل وتقطيعه. جربها لو الأولى مسحبتش معاك حاجة.") | |
| col_m2, col_e2, col_u2, col_b2 = st.columns([3, 2, 2, 2]) | |
| selected_mode_get = col_m2.selectbox("🎯 المحتوى المستهدف:", ["⚽ رياضة فقط VIP", "📺 لايف فقط (كل الباقات)"], key=f"mode_get_{base_k}") | |
| selected_ext_get = col_e2.selectbox("صيغة البث:", [".m3u8", ".ts"], key=f"ext_get_{base_k}") | |
| add_ua_get = col_u2.checkbox("إضافة حماية UA", value=False, key=f"ua_get_{base_k}") | |
| if col_b2.button("📥 سحب داتا السيرفر (GET)", key=f"btn_get_{base_k}"): | |
| with st.spinner("جاري تحميل M3U بالكامل وفلترة اللايف..."): | |
| success, file, count = custom_get_php_extractor(server['host'], server['user'], server['pass'], selected_mode_get, selected_ext_get, add_ua_get) | |
| 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']}_GET.m3u" | |
| if f"prep_err_{base_k}" in st.session_state: del st.session_state[f"prep_err_{base_k}"] | |
| else: | |
| st.session_state[f"prep_err_{base_k}"] = "❌ السيرفر ده قافل الـ get.php، استخدم التاب الأول (API) لسحبه." | |
| st.rerun() | |
| if f"prep_err_{base_k}" in st.session_state: | |
| st.error(st.session_state[f"prep_err_{base_k}"]) | |
| elif t == 'direct': | |
| st.markdown("### 📋 بيانات الرابط (للنسخ السريع):") | |
| st.text_input("🔗 رابط الملف التنزيلي السريع:", value=server.get('url', ''), key=f"link_{base_k}") | |
| st.markdown("---") | |
| st.markdown(f"### ⚙️ خيارات التشغيل المعاينة:") | |
| col_p1, col_p2 = st.columns(2) | |
| with col_p1: | |
| if server.get('type_auth') == 'Playlist' and st.button("📥 سحب القنوات (M3U Playlist)", key=f"prep_{base_k}"): | |
| with st.spinner("جاري سحب المحتوى..."): | |
| try: | |
| 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': '*/*'} | |
| txt = requests.get(server['url'], headers=browser_headers, timeout=15, verify=False).text | |
| 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" | |
| st.rerun() | |
| except: st.error("❌ فشل سحب روابط القائمة المباشرة. السيرفر يرفض الاتصال.") | |
| with col_p2: | |
| if st.button("📺 تشغيل معاينة القناة المباشرة", key=f"preview_{base_k}"): | |
| st.session_state[f"preview_url_{base_k}"] = server.get('url', '') | |
| st.rerun() | |
| elif t == 'mac': | |
| st.markdown("### 📋 بيانات الماك (للنسخ السريع):") | |
| c1, c2 = st.columns(2) | |
| c1.text_input("🌐 البورتال (Portal):", value=server.get('host', ''), key=f"h_{base_k}") | |
| c2.text_input("💳 الماك (MAC):", value=server.get('mac', ''), key=f"m_{base_k}") | |
| st.markdown("---") | |
| st.markdown(f"### ⚙️ خيارات سحب الماك الذكية (لايف فقط):") | |
| col_m, col_e, col_u, col_b = st.columns([3, 2, 2, 2]) | |
| mac_mode = col_m.selectbox("🎯 قسم المحتوى:", ["⚽ رياضة فقط VIP", "📺 لايف فقط (كل القنوات)"], key=f"mode_{base_k}") | |
| mac_ext = col_e.selectbox("صيغة البث:", [".m3u8", ".ts", "بدون صيغة"], index=0, key=f"ext_{base_k}") | |
| mac_ua = col_u.checkbox("حماية التخطي UA", value=False, key=f"ua_{base_k}") | |
| if col_b.button("📥 بدء سحب الماك", key=f"prep_btn_{base_k}"): | |
| with st.spinner("جاري الاختراق وسحب القنوات بالتوازي..."): | |
| ok, res_text, count = extract_mac_to_m3u_logic(server['host'], server['mac'], mac_mode, mac_ua, mac_ext) | |
| 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" | |
| if f"prep_err_{base_k}" in st.session_state: del st.session_state[f"prep_err_{base_k}"] | |
| else: | |
| st.session_state[f"prep_err_{base_k}"] = res_text | |
| st.rerun() | |
| if f"prep_err_{base_k}" in st.session_state: | |
| st.error(st.session_state[f"prep_err_{base_k}"]) | |
| if f"preview_url_{base_k}" in st.session_state: | |
| p_url = st.session_state[f"preview_url_{base_k}"] | |
| hls_demo = f"https://hls-js.netlify.app/demo/?src={urllib.parse.quote(p_url)}" | |
| 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) | |
| 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) | |
| if f"file_{base_k}" in st.session_state: | |
| st.success(f"✅ تم السحب بنجاح! ({st.session_state[f'count_{base_k}']} عنصر)") | |
| 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}") | |
| with st.expander("🔍 فرز ونسخ سريع لقنوات الملف المسحوب مباشرة", expanded=True): | |
| raw_text = st.session_state[f"file_{base_k}"].decode('utf-8', errors='ignore') | |
| col_s1, col_s2 = st.columns([3, 1]) | |
| search_q = col_s1.text_input("🔍 اكتب كلمة الفرز هنا (مثل bein, ssc):", key=f"sq_{base_k}") | |
| display_text = raw_text | |
| if col_s2.button("فلترة 🎯", key=f"sbtn_{base_k}") or search_q: | |
| if search_q.strip(): | |
| lines, filtered = raw_text.splitlines(), ["#EXTM3U"] | |
| for i in range(len(lines)): | |
| if lines[i].startswith("#EXTINF") and smart_search_match(search_q, lines[i]): | |
| filtered.append(lines[i]) | |
| if i + 1 < len(lines): filtered.append(lines[i+1]) | |
| display_text = "\n".join(filtered) | |
| st.success(f"🎯 تم العثور على {(len(filtered)-1)//2} باقة متطابقة.") | |
| st.text_area("محتوى الصندوق (تحديد الكل -> نسخ):", value=display_text, height=250, key=f"all_{base_k}") | |
| st.markdown("---") | |
| if context == "saved": | |
| if st.button("🗑️ حذف الحساب من السجل نهائياً", key=f"del_{base_k}"): | |
| svs = load_servers() | |
| svs.pop(idx) | |
| save_all_servers_to_db(svs) | |
| st.success("تم الحذف!") | |
| time.sleep(1) | |
| st.rerun() | |
| else: | |
| if t == 'xtream' and st.button("💾 حفظ السيرفر في قاعدة بياناتي", key=f"save_{base_k}"): | |
| 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')) | |
| st.success("✅ تم حفظ السيرفر بنجاح!") | |
| elif t == 'mac' and st.button("💾 حفظ الماك في قاعدة بياناتي", key=f"save_{base_k}"): | |
| 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')) | |
| st.success("✅ تم حفظ الماك بنجاح!") | |
| # ========================================== | |
| # 🤖 التلغرام والواجهة الرسومية لقاعدة البيانات والتابات | |
| # ========================================== | |
| def send_menu(message): | |
| save_admin(message.chat.id) | |
| bot.send_message(message.chat.id, "📺 **Shalaby IPTV Tool**\nأهلاً بك يا هندسة في لوحة الإدارة الذكية للتلفزيون!", parse_mode="Markdown") | |
| def send_all_servers(message): | |
| vips = load_global_vips() | |
| if not vips: | |
| bot.reply_to(message, "📭 الصندوق الأسود فارغ حالياً.") | |
| return | |
| file_path = "All_Hunted_Servers.txt" | |
| with open(file_path, "w", encoding="utf-8") as f: | |
| f.write(f"--- 📦 إجمالي السيرفرات المصادة الشغالة: {len(vips)} ---\n\n") | |
| for s in vips: | |
| if s.get('type') == 'mac': | |
| f.write(f"Portal: {s.get('host')}\nMAC: {s.get('mac')}\nDays Left: {s.get('days')}\n") | |
| else: | |
| f.write(f"Host: {s.get('host')}\nUser: {s.get('user')}\nPass: {s.get('pass')}\nDays Left: {s.get('days')}\n") | |
| f.write("-" * 30 + "\n") | |
| with open(file_path, "rb") as doc: | |
| bot.send_document(message.chat.id, doc, caption=f"🔥 تفضل يا هندسة، ملف إجمالي السيرفرات الصالحة المصادة ({len(vips)}).") | |
| def handle_all_messages(message): | |
| accs = extract_all_links_from_text(message.text) | |
| if accs: | |
| msg = bot.reply_to(message, "🔍 جاري فحص وتحليل السيرفرات المرسلة من التلغرام...") | |
| valid = [] | |
| for a in accs: | |
| if a['type'] == 'xtream': | |
| info = check_server_smart(a['host'], a['user'], a['pass']) | |
| if info: | |
| valid.append(f"✅ `{a['host']}` | 👤 `{a['user']}` | ⏳ {calculate_days_left(info.get('exp_date'))} يوم") | |
| elif a['type'] == 'mac': | |
| prof_ok, exp, _ = test_mac_portal(a['host'], a['mac']) | |
| if prof_ok: | |
| valid.append(f"✅ `{a['host']}` | 💳 `{a['mac']}` | ⏳ {calculate_days_left(exp)} يوم") | |
| 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") | |
| else: | |
| bot.edit_message_text("❌ عذراً لم تنجح أي روابط مرسلة.", chat_id=msg.chat.id, message_id=msg.message_id) | |
| def run_bot_thread_init(): | |
| for t in threading.enumerate(): | |
| if t.name == "TelegramBotThread": return | |
| def start_polling(): | |
| try: | |
| requests.get(f"https://api.telegram.org/bot{TOKEN}/deleteWebhook?drop_pending_updates=True", timeout=5) | |
| except: pass | |
| time.sleep(2) | |
| bot.infinity_polling(timeout=20, long_polling_timeout=10, skip_pending=True) | |
| threading.Thread(target=start_polling, name="TelegramBotThread", daemon=True).start() | |
| run_bot_thread_init() | |
| st.set_page_config(page_title="Shalaby IPTV Tool", page_icon="📺", layout="wide") | |
| st.markdown(""" | |
| <style> | |
| .block-container { max-width: 100% !important; padding: 1rem !important; padding-bottom: 5rem !important; } | |
| section[data-testid="stSidebar"] { width: 170px !important; min-width: 170px !important; } | |
| .main { text-align: right; direction: rtl; } | |
| h1 { font-size: 1.8rem !important; text-align: center; color: #ff4b4b;} | |
| div[data-baseweb="tab-list"] { display: flex; flex-direction: column; gap: 8px; } | |
| div[data-baseweb="tab"] { width: 100%; justify-content: flex-end; background-color: #2b2b2b; border-radius: 8px; padding: 10px !important; border: 1px solid #444;} | |
| div[data-baseweb="tab"]:hover { background-color: #ff4b4b; color: white !important;} | |
| .stButton>button { width: 100%; border-radius: 8px; font-weight: bold; background-color: #ff4b4b; color: white; margin-top: 5px;} | |
| </style> | |
| """, unsafe_allow_html=True) | |
| if not st.session_state.logged_in: | |
| st.title("🔐 بوابة الدخول التلقائية - Shalaby IPTV Tool") | |
| tab_in, tab_up = st.tabs(["تسجيل دخول بالحساب الحالي", "إنشاء كود مستخدم جديد"]) | |
| with tab_up: | |
| new_user = st.text_input("اختر كود حسابك الجديد:") | |
| if st.button("🚀 إنشاء وتفعيل التلقائي"): | |
| if new_user.strip(): | |
| users = load_users_registry() | |
| if new_user.strip() not in users: | |
| users.append(new_user.strip()) | |
| save_users_registry(users) | |
| st.session_state.username = new_user.strip() | |
| st.session_state.logged_in = True | |
| st.query_params["user_auth"] = new_user.strip() | |
| components.html(f"""<script>localStorage.setItem("user_auth", "{new_user.strip()}");</script>""", height=0, width=0) | |
| time.sleep(0.5) | |
| st.rerun() | |
| else: st.error("❌ الكود مستخدم من قبل.") | |
| with tab_in: | |
| exist_user = st.text_input("أدخل كود الدخول الخاص بك:") | |
| if st.button("🔓 مصادقة ودخول"): | |
| if exist_user.strip() in load_users_registry(): | |
| st.session_state.username = exist_user.strip() | |
| st.session_state.logged_in = True | |
| st.query_params["user_auth"] = exist_user.strip() | |
| components.html(f"""<script>localStorage.setItem("user_auth", "{exist_user.strip()}");</script>""", height=0, width=0) | |
| time.sleep(0.5) | |
| st.rerun() | |
| else: st.error("❌ كود الحساب غير مسجل بالمنظومة.") | |
| st.stop() | |
| with st.sidebar: | |
| st.markdown(f"### 👤 الحساب المتصل:\n`{st.session_state.username}`") | |
| if st.button("🚪 تسجيل خروج"): | |
| components.html("""<script>localStorage.removeItem("user_auth");</script>""", height=0, width=0) | |
| st.query_params.clear() | |
| st.session_state.logged_in = False | |
| time.sleep(0.5) | |
| st.rerun() | |
| st.title("📺 اللوحة الأسطورية الشاملة - Shalaby VIP Tool") | |
| st.markdown("---") | |
| tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs([ | |
| "🧪 محرك الفحص الصاروخي الذكي", "🗄️ سجل سيرفراتي المحفوظة", "🚀 تجميع وتحديث الماستر داتابيز", "🏭 منقي الباقات للعملاء المخصص", "🔥 فاحص ومحول الماك الفوري", "✨ ساحب القنوات المطور (Playlist Extractor)" | |
| ]) | |
| with tab1: | |
| st.markdown("### 🔍 أدخل البيانات أو النصوص المبعثرة للفحص:") | |
| if 'valid_found_tab1' in st.session_state and st.session_state['valid_found_tab1']: | |
| if st.button("🔽 غلق جميع الكروت المفتوحة ومسح الذاكرة التفاعلية", key="close_t1"): | |
| st.session_state.reset_exp += 1 | |
| for k in list(st.session_state.keys()): | |
| if k.startswith("file_") or k.startswith("ping_res_") or k.startswith("prep_err_") or k.startswith("preview_"): del st.session_state[k] | |
| st.rerun() | |
| bulk_input = st.text_area("ضع الروابط أو نصوص الماكات المبعثرة هنا:", height=150) | |
| if st.button("🚀 إطلاق موتور فحص Asyncio الشامل"): | |
| if not bulk_input.strip(): st.warning("⚠️ يرجى تعبئة الحقل بالنصوص أولاً.") | |
| else: | |
| accs = extract_all_links_from_text(bulk_input) | |
| if len(accs) == 0: st.error("❌ لم يتم العثور على أي روابط أو ماكات صالحة للبناء التقني.") | |
| else: | |
| p_bar, s_msg, l_placeholder = st.progress(0), st.empty(), st.empty() | |
| v_found = safe_async_run(run_async_scan(accs, p_bar, s_msg, l_placeholder, "tab1")) | |
| s_msg.success(f"🎉 اكتمل مسح شبكة بايثون بنجاح! تم اصطياد وتحليل {len(v_found)} سيرفر.") | |
| st.session_state['valid_found_tab1'] = v_found | |
| if v_found: | |
| c_db = load_servers() | |
| for v in v_found: | |
| 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) | |
| save_all_servers_to_db(c_db) | |
| if 'valid_found_tab1' in st.session_state and st.session_state['valid_found_tab1']: | |
| st.markdown("#### ⚙️ الحسابات المصادة والجاهزة للنسخ والسحب الفوري:") | |
| for i, acc in enumerate(st.session_state['valid_found_tab1']): render_server_card(acc, i, context="t1") | |
| with tab2: | |
| st.markdown("### 🗄️ السيرفرات المسجلة في داتابيز حسابك") | |
| svs = load_servers() | |
| if not svs: st.warning("لا توجد حسابات محفوظة في السجل الخاص بك حالياً.") | |
| else: | |
| if st.button("🔽 تنظيف ذاكرة الكروت المؤقتة بالسجل", key="close_t2"): | |
| st.session_state.reset_exp += 1 | |
| for k in list(st.session_state.keys()): | |
| if k.startswith("file_") or k.startswith("ping_res_") or k.startswith("prep_err_") or k.startswith("preview_"): del st.session_state[k] | |
| st.rerun() | |
| for i, s in enumerate(svs): render_server_card(s, i, context="saved") | |
| with tab3: | |
| st.markdown("### 🚀 موتور تحديث الماستر داتابيز الشامل وتجميع الرياضة") | |
| if st.button("🔄 بدء التجميع والدمج المتوازي لكافة السيرفرات"): | |
| svs = load_servers() | |
| if not svs: st.error("❌ سجل الحسابات فارغ تماماً!") | |
| else: | |
| p_bar3 = st.progress(0) | |
| for i, s in enumerate(svs): | |
| if s.get('type') in ['xtream', 'Xtream API']: extract_sports_unified(s['host'], s['user'], s['pass'], skip_clean=True, ext=".m3u8") | |
| p_bar3.progress((i + 1) / len(svs)) | |
| f_db = load_db() | |
| m3u_file = export_to_m3u(f_db, "My_Saved_Servers.m3u") | |
| st.success(f"🎉 تم دمج وفحص وإدراج {len(f_db)} باقة رياضية محدثة بالماستر داتابيز!") | |
| with open(m3u_file, "rb") as file: | |
| st.download_button("💾 تحميل ملف اللستة الموحدة الشغالة لقنواتك", data=file, file_name=f"Master_Shalaby_{st.session_state.username}.m3u") | |
| with tab4: | |
| st.markdown("### 🏭 صناعة وتخصيص باقات خاصة للعملاء (Custom Selector)") | |
| all_ch = load_db() | |
| if not all_ch: st.warning("⚠️ قاعدة داتابيز القنوات فارغة، يرجى التجميع أولاً من التاب السابق.") | |
| else: | |
| u_groups = set(ch['group'].replace('[SD]','').replace('[HD]','').replace('[FHD]','').replace('[4K]','').strip() for ch in all_ch) | |
| sel_g = st.multiselect("📌 حدد الباقات المراد تصديرها للعميل:", sorted(list(u_groups))) | |
| smart_ren = st.checkbox("✨ تنظيف وإعادة هيكلة مسميات الجودة تلقائياً") | |
| if st.button("🏭 توليد ملف باقة العميل"): | |
| if not sel_g: st.error("❌ يرجى اختيار باقة واحدة على الأقل للتصدير.") | |
| else: | |
| cust_ch = [] | |
| for ch in all_ch: | |
| if ch['group'].replace('[SD]','').replace('[HD]','').replace('[FHD]','').replace('[4K]','').strip() in sel_g: | |
| 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") | |
| cust_ch.append(ch) | |
| if cust_ch: | |
| c_file = export_to_m3u(cust_ch, "Client.m3u") | |
| st.success(f"🎉 تم توليد قنوات الملف المخصص بنجاح بنجاح! ({len(cust_ch)} قناة)") | |
| with open(c_file, "rb") as file: | |
| st.download_button("📥 تحميل ملف العميل الجاهز المخصص", data=file, file_name=f"Client_{st.session_state.username}.m3u") | |
| else: st.error("❌ لم نجد قنوات متوافقة مع اختيارك.") | |
| with tab5: | |
| st.markdown("### 🔥 فاحص الماك المنفرد والمحول المباشر لملف M3U") | |
| col_h, col_m = st.columns(2) | |
| m_portal = col_h.text_input("🔗 رابط البورتال (Portal URL):", placeholder="http://domain.xyz:8080") | |
| m_addr = col_m.text_input("💳 الماك أدرس (MAC Address):", placeholder="00:1A:79:XX:XX:XX") | |
| col_b1, col_b2, col_b3, col_b4 = st.columns([3, 2, 2, 2]) | |
| standalone_mode = col_b1.selectbox("🎯 فلتر محتوى السحب المطلوب:", ["⚽ رياضة فقط VIP", "📺 لايف فقط (كل القنوات)"], key="st_mode") | |
| standalone_ext = col_b2.selectbox("صيغة المشاهدة المستهدفة:", [".m3u8", ".ts", "بدون صيغة"], key="st_ext") | |
| standalone_ua = col_b3.checkbox("تفعيل حماية وتخطي UA كـ MAG", value=True) | |
| if col_b4.button("🚀 فحص وتحويل الماك"): | |
| if not m_portal or not m_addr: st.warning("⚠️ الرجاء تعبئة بيانات البورتال والماك معاً.") | |
| else: | |
| p_clean = "http://" + m_portal.strip().rstrip('/') if not m_portal.strip().startswith("http") else m_portal.strip().rstrip('/') | |
| with st.spinner("🕵️♂️ جاري كسر الحماية والتحقق من التواريخ وقنوات السيرفر..."): | |
| ok, res_text, count = extract_mac_to_m3u_logic(p_clean, m_addr.strip().upper(), standalone_mode, standalone_ua, standalone_ext) | |
| if ok: | |
| st.success(f"🎉 نجح التحويل! تم استخراج {count} قناة بكفاءة تامة.") | |
| st.download_button("📥 تحميل ملف الـ M3U الناتج فوراً", data=res_text, file_name=f"Converted_{m_addr.replace(':','')}.m3u") | |
| else: st.error(res_text) | |
| with tab6: | |
| st.markdown("### ✨ ساحب ومحلل محتويات قوائم التشغيل المباشرة (M3U Extractor)") | |
| if 'valid_found_tab6' in st.session_state and st.session_state['valid_found_tab6']: | |
| if st.button("🔽 مسح ذاكرة الكروت المؤقتة للتاب", key="close_t6"): | |
| st.session_state.reset_exp += 1 | |
| for k in list(st.session_state.keys()): | |
| if k.startswith("file_") or k.startswith("ping_res_") or k.startswith("prep_err_") or k.startswith("preview_"): del st.session_state[k] | |
| st.rerun() | |
| ex_input = st.text_area("ضع نصوص قوائم التشغيل أو روابط الـ Workers هنا لفرزها:", height=150) | |
| if st.button("⚙️ بدء الفرز والمسح الصاروخي"): | |
| if not ex_input.strip(): st.warning("⚠️ يرجى إدخال روابط أولاً للفرز.") | |
| else: | |
| accs = extract_all_links_from_text(ex_input) | |
| if not accs: st.error("❌ لم يتم العثور على أي بيانات روابط تشغيل صالحة بالفحص.") | |
| else: | |
| p_bar6, s_msg6, l_placeholder6 = st.progress(0), st.empty(), st.empty() | |
| v_accs = safe_async_run(run_async_scan(accs, p_bar6, s_msg6, l_placeholder6, "t6")) | |
| s_msg6.success(f"🎉 اكتمل المسح بنجاح! تم استخراج {len(v_accs)} كارت تشغيل جاهز.") | |
| st.session_state['valid_found_tab6'] = v_accs | |
| if 'valid_found_tab6' in st.session_state and st.session_state['valid_found_tab6']: | |
| st.markdown("#### ⚙️ الكروت النهائية بعد الفحص والترتيب التلقائي:") | |
| for i, acc in enumerate(st.session_state['valid_found_tab6']): render_server_card(acc, i, context="t6") | |
| st.markdown("---") | |
| with st.expander("🔬 فحص رابط قناة مفردة (اختبار الشاشة السوداء)"): | |
| single_link = st.text_input("🔗 أدخل رابط القناة (m3u8 أو ts):", placeholder="http://host.com/live/user/pass/123.m3u8") | |
| if st.button("⚡ فحص استجابة القناة"): | |
| if single_link.strip(): | |
| with st.spinner("جاري فحص القناة..."): | |
| ping_ok, _, _ = advanced_direct_link_test(single_link.strip()) | |
| if ping_ok: st.success("✅ القناة تعمل بكفاءة (تسحب بيانات).") | |
| else: st.error("❌ القناة لا تستجيب (قد تكون شاشة سوداء أو محمية بشدة).") | |
| else: st.warning("الرجاء إدخال الرابط أولاً.") | |