Spaces:
Sleeping
Sleeping
| from flask import Flask, jsonify, request, render_template_string | |
| import requests | |
| import urllib.parse | |
| import base64 | |
| import json | |
| from concurrent.futures import ThreadPoolExecutor | |
| app = Flask(__name__) | |
| # --- واجهة التحكم الاحترافية (HTML Pro Dashboard) --- | |
| HTML_TEMPLATE = """ | |
| <!DOCTYPE html> | |
| <html lang="ar" dir="rtl"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>لوحة القيادة | Renoo VIP Control Center</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { --primary: #e50914; --bg: #141414; --card: #1f1f1f; --text: #fff; --success: #46d369; } | |
| body { font-family: 'Cairo', sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 20px; min-height: 100vh; display: flex; align-items: center; justify-content: center; } | |
| .dashboard { background: var(--card); width: 100%; max-width: 600px; border-radius: 20px; padding: 30px; box-shadow: 0 20px 50px rgba(0,0,0,0.7); border: 1px solid #333; } | |
| .header { text-align: center; margin-bottom: 30px; border-bottom: 2px solid #333; padding-bottom: 20px; } | |
| .header h1 { margin: 0; color: var(--primary); font-size: 2.5rem; } | |
| .header p { color: #888; margin-top: 10px; } | |
| .control-group { margin-bottom: 20px; } | |
| .control-group label { display: block; margin-bottom: 8px; font-weight: bold; color: #ddd; } | |
| .input-field { width: 100%; padding: 15px; background: #000; border: 2px solid #333; border-radius: 10px; color: #fff; font-family: 'Cairo'; font-size: 16px; box-sizing: border-box; transition: 0.3s; } | |
| .input-field:focus { border-color: var(--primary); outline: none; } | |
| .settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 25px; } | |
| .toggle-btn { background: #2a2a2a; padding: 15px; border-radius: 10px; cursor: pointer; border: 2px solid transparent; text-align: center; font-size: 14px; transition: 0.3s; } | |
| .toggle-btn.active { border-color: var(--success); background: rgba(70, 211, 105, 0.1); color: var(--success); } | |
| .toggle-btn:hover { background: #333; } | |
| .action-btn { width: 100%; padding: 18px; background: var(--primary); color: #fff; border: none; border-radius: 12px; font-size: 18px; font-weight: bold; cursor: pointer; transition: transform 0.2s; } | |
| .action-btn:hover { transform: scale(1.02); background: #f40612; } | |
| .status-box { margin-top: 20px; padding: 15px; border-radius: 10px; display: none; text-align: center; } | |
| .status-box.success { background: rgba(70, 211, 105, 0.2); color: var(--success); border: 1px solid var(--success); } | |
| .status-box.error { background: rgba(229, 9, 20, 0.2); color: var(--primary); border: 1px solid var(--primary); } | |
| .result-area { margin-top: 30px; background: #000; padding: 20px; border-radius: 15px; border: 1px solid #333; display: none; } | |
| code { display: block; word-break: break-all; color: var(--success); font-family: monospace; margin: 10px 0; font-size: 14px; } | |
| .copy-btn { background: #333; color: #fff; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; margin-top: 10px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="dashboard"> | |
| <div class="header"> | |
| <h1>💎 Renoo Control</h1> | |
| <p>لوحة التحكم الكاملة - الإصدار 4.0</p> | |
| </div> | |
| <div class="control-group"> | |
| <label>📡 رابط السيرفر (Host):</label> | |
| <input type="text" id="host" class="input-field" placeholder="http://domain.com:port" value="http://sbeiin.cc:2095"> | |
| </div> | |
| <div class="settings-grid"> | |
| <div class="control-group"> | |
| <label>👤 المستخدم:</label> | |
| <input type="text" id="user" class="input-field" value="renoomon"> | |
| </div> | |
| <div class="control-group"> | |
| <label>🔑 كلمة المرور:</label> | |
| <input type="text" id="pass" class="input-field" value="0506305618"> | |
| </div> | |
| </div> | |
| <label style="margin-bottom: 10px; display:block; color:#888;">⚙️ إعدادات المعالجة (Advanced Features):</label> | |
| <div class="settings-grid"> | |
| <div class="toggle-btn active" id="btn-rescue" onclick="toggle('rescue')"> | |
| 🛡️ وضع الإنقاذ<br><span style="font-size:10px">يمنع الشاشة البيضاء</span> | |
| </div> | |
| <div class="toggle-btn" id="btn-stealth" onclick="toggle('stealth')"> | |
| 🥷 وضع التخفي<br><span style="font-size:10px">تجاوز حجب الميتا</span> | |
| </div> | |
| <div class="toggle-btn" id="btn-fast" onclick="toggle('fast')"> | |
| ⚡ الوضع السريع<br><span style="font-size:10px">إخفاء الأوصاف للتسريع</span> | |
| </div> | |
| <div class="toggle-btn active" id="btn-live" onclick="toggle('live')"> | |
| 📺 القنوات المباشرة<br><span style="font-size:10px">تفعيل البث الحي</span> | |
| </div> | |
| </div> | |
| <button class="action-btn" onclick="generate()">🚀 استخراج وتفعيل الإضافة</button> | |
| <div id="status" class="status-box"></div> | |
| <div id="result" class="result-area"> | |
| <p>✅ تم تجهيز الرابط الخاص بك:</p> | |
| <code id="linkCode"></code> | |
| <button class="copy-btn" onclick="copyLink()">نسخ الرابط</button> | |
| <a id="installBtn" href="#" style="display:block; margin-top:15px; color:var(--primary); text-decoration:none; font-weight:bold;">📲 اضغط هنا للتثبيت المباشر في Stremio</a> | |
| </div> | |
| </div> | |
| <script> | |
| let settings = { rescue: true, stealth: false, fast: false, live: true }; | |
| function toggle(key) { | |
| settings[key] = !settings[key]; | |
| document.getElementById('btn-'+key).classList.toggle('active'); | |
| } | |
| function generate() { | |
| const host = document.getElementById('host').value.trim().replace(/\/$/, ""); | |
| const user = document.getElementById('user').value.trim(); | |
| const pass = document.getElementById('pass').value.trim(); | |
| if(!host || !user || !pass) { | |
| showStatus("الرجاء تعبئة جميع بيانات السيرفر!", "error"); | |
| return; | |
| } | |
| // تشفير الإعدادات | |
| const config = { h: host, u: user, p: pass, s: settings }; | |
| const encoded = btoa(JSON.stringify(config)); | |
| const baseUrl = window.location.protocol + "//" + window.location.host; | |
| const finalLink = `${baseUrl}/${encoded}/manifest.json`; | |
| document.getElementById('linkCode').textContent = finalLink; | |
| document.getElementById('installBtn').href = finalLink.replace("https://", "stremio://").replace("http://", "stremio://"); | |
| document.getElementById('result').style.display = 'block'; | |
| showStatus("تم الاتصال بنجاح وتوليد الإعدادات!", "success"); | |
| } | |
| function showStatus(msg, type) { | |
| const el = document.getElementById('status'); | |
| el.textContent = msg; | |
| el.className = "status-box " + type; | |
| el.style.display = 'block'; | |
| } | |
| function copyLink() { | |
| navigator.clipboard.writeText(document.getElementById('linkCode').textContent); | |
| alert("تم النسخ!"); | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| # --- المنطق الخلفي (Backend Logic) --- | |
| session = requests.Session() | |
| # تمويه ذكي جداً كأنه تطبيق هاتف | |
| session.headers.update({ | |
| 'User-Agent': 'IPTV Smarters Pro/3.1.5 (iPad; iOS 14.4; Scale/2.00)', | |
| 'Accept': '*/*' | |
| }) | |
| def decode_config(config_str): | |
| try: return json.loads(base64.b64decode(config_str).decode('utf-8')) | |
| except: return None | |
| def clean_str(txt): | |
| if not txt: return "" | |
| try: return urllib.parse.unquote(str(txt)).strip() | |
| except: return str(txt) | |
| def fetch_api(conf, action, params=None): | |
| if params is None: params = {} | |
| url = f"{conf['h']}/player_api.php?username={conf['u']}&password={conf['p']}&action={action}" | |
| for k, v in params.items(): url += f"&{k}={v}" | |
| try: | |
| # مهلة قصيرة للاتصال لتجنب التعليق | |
| resp = session.get(url, timeout=15) | |
| if resp.status_code == 200: | |
| return resp.json() | |
| except: pass | |
| return None | |
| # --- المسارات (Routes) --- | |
| def dashboard(): | |
| return render_template_string(HTML_TEMPLATE) | |
| def manifest(config): | |
| conf = decode_config(config) | |
| settings = conf.get('s', {}) | |
| # بناء الكتالوجات بناءً على الإعدادات | |
| catalogs = [] | |
| # 1. القنوات (اختياري) | |
| if settings.get('live'): | |
| catalogs.append({ | |
| "type": "tv", "id": "rn_live", "name": "Live TV 📺", | |
| "extra": [{"name": "search"}, {"name": "genre"}] | |
| }) | |
| # 2. الأفلام | |
| catalogs.append({ | |
| "type": "movie", "id": "rn_movies", "name": "Movies 🎬", | |
| "extra": [{"name": "search"}, {"name": "genre"}] | |
| }) | |
| # 3. المسلسلات | |
| catalogs.append({ | |
| "type": "series", "id": "rn_series", "name": "Series 🍿", | |
| "extra": [{"name": "search"}, {"name": "genre"}] | |
| }) | |
| return jsonify({ | |
| "id": "org.renoo.v4.pro", | |
| "version": "4.0.0", | |
| "name": "Renoo VIP 👑", | |
| "description": "Smart Xtream Player with Rescue Mode", | |
| "logo": "https://cdn-icons-png.flaticon.com/512/3163/3163478.png", | |
| "resources": ["catalog", "meta", "stream"], | |
| "types": ["movie", "series", "tv"], | |
| "idPrefixes": ["rn_"], | |
| "catalogs": catalogs | |
| }) | |
| def catalog(config, type, id, params=""): | |
| conf = decode_config(config) | |
| settings = conf.get('s', {}) | |
| args = {} | |
| if params: | |
| for p in params.split('/'): | |
| if '=' in p: k, v = p.split('=', 1); args[k] = urllib.parse.unquote(v) | |
| mode_map = { | |
| "movie": {"list": "get_vod_streams", "cat": "get_vod_categories"}, | |
| "series": {"list": "get_series", "cat": "get_series_categories"}, | |
| "tv": {"list": "get_live_streams", "cat": "get_live_categories"} | |
| } | |
| current_mode = mode_map.get(type) | |
| if not current_mode: return jsonify({"metas": []}) | |
| # استراتيجية جلب البيانات | |
| api_params = {} | |
| # إذا بحث | |
| if args.get('search'): | |
| # البحث يتطلب جلب الكل ثم الفلترة (ثقيل) | |
| data = fetch_api(conf, current_mode['list']) or [] | |
| s_term = args['search'].lower() | |
| data = [x for x in data if s_term in clean_str(x.get('name')).lower()] | |
| # إذا تصفح مجلد (وهذا الأهم) | |
| elif args.get('genre'): | |
| # جلب ID المجلد | |
| cats = fetch_api(conf, current_mode['cat']) or [] | |
| cat_id = next((c['category_id'] for c in cats if clean_str(c['category_name']) == args['genre']), None) | |
| if cat_id: | |
| api_params['category_id'] = cat_id | |
| data = fetch_api(conf, current_mode['list'], api_params) or [] | |
| else: | |
| data = [] | |
| else: | |
| # الصفحة الرئيسية للكتالوج (بدون فلتر) - نعرض أحدث 50 فقط لتجنب التعليق | |
| data = fetch_api(conf, current_mode['list']) or [] | |
| try: data.sort(key=lambda x: int(x.get('stream_id') or x.get('series_id') or 0), reverse=True) | |
| except: pass | |
| data = data[:50] | |
| metas = [] | |
| for item in data[:100]: | |
| sid = item.get('stream_id') or item.get('series_id') | |
| name = clean_str(item.get('name')) | |
| poster = item.get('stream_icon') or item.get('cover') | |
| # وصف ذكي يعرض التقييم أو السنة | |
| desc = "" if settings.get('fast') else f"Rating: {item.get('rating', 'N/A')}" | |
| metas.append({ | |
| "id": f"rn_{type}_{sid}", | |
| "type": type, | |
| "name": name, | |
| "poster": poster, | |
| "description": desc | |
| }) | |
| return jsonify({"metas": metas}) | |
| # --- الميتا (قلب النظام الجديد) --- | |
| def meta(config, type, id): | |
| conf = decode_config(config) | |
| real_id = id.split('_')[-1] | |
| # 1. إنشاء كائن طوارئ (Rescue Object) | |
| # هذا الكائن يضمن أن الشاشة البيضاء لن تظهر أبداً | |
| # حتى لو السيرفر انفجر، هذا الكائن سيظهر زر التشغيل | |
| meta_obj = { | |
| "id": id, | |
| "type": type, | |
| "name": "محتوى جاهز للتشغيل", | |
| "poster": "https://dummyimage.com/600x900/000/fff&text=Play+Now", | |
| "background": "https://dummyimage.com/1280x720/000/fff&text=Renoo+VIP", | |
| "description": "لم يتم العثور على الوصف من السيرفر، ولكن الرابط جاهز. اضغط تشغيل.", | |
| "behaviorHints": {"defaultVideoId": real_id} | |
| } | |
| try: | |
| # محاولة جلب البيانات الحقيقية | |
| if type == "movie": | |
| data = fetch_api(conf, "get_vod_info", {"vod_id": real_id}) | |
| if data and 'info' in data: | |
| i = data['info'] | |
| meta_obj['name'] = clean_str(i.get('name')) or meta_obj['name'] | |
| meta_obj['poster'] = i.get('movie_image') or meta_obj['poster'] | |
| meta_obj['background'] = i.get('backdrop_path') or meta_obj['background'] | |
| meta_obj['description'] = clean_str(i.get('description')) or meta_obj['description'] | |
| meta_obj['releaseInfo'] = str(i.get('releasedate', ''))[:4] | |
| elif type == "series": | |
| data = fetch_api(conf, "get_series_info", {"series_id": real_id}) | |
| if data and 'info' in data: | |
| i = data['info'] | |
| meta_obj['name'] = clean_str(i.get('name')) or meta_obj['name'] | |
| meta_obj['poster'] = i.get('cover') or meta_obj['poster'] | |
| meta_obj['background'] = i.get('backdrop_path') or meta_obj['background'] | |
| meta_obj['description'] = clean_str(i.get('plot')) or meta_obj['description'] | |
| # معالجة الحلقات (أهم جزء للمسلسلات) | |
| episodes = data.get('episodes', {}) | |
| videos = [] | |
| if episodes: | |
| sorted_seasons = sorted(episodes.keys(), key=lambda x: int(x) if x.isdigit() else 999) | |
| for s in sorted_seasons: | |
| for ep in episodes[s]: | |
| videos.append({ | |
| "id": f"rn_ep_{ep['id']}_{ep['container_extension']}", | |
| "title": clean_str(ep.get('title')) or f"Episode {ep['episode_num']}", | |
| "season": int(s), | |
| "episode": int(ep['episode_num']), | |
| "thumbnail": ep.get('image') or i.get('cover'), | |
| "released": str(ep.get('added', ''))[:10] | |
| }) | |
| meta_obj['videos'] = videos | |
| else: | |
| # إذا لم نجد حلقات، نعرض حلقة وهمية للتشغيل الطارئ | |
| meta_obj['videos'] = [{ | |
| "id": f"rn_ep_force_{real_id}", | |
| "title": "تشغيل مباشر (Force Play)", | |
| "season": 1, "episode": 1 | |
| }] | |
| except Exception as e: | |
| print(f"Rescue Mode Activated: {e}") | |
| # في حالة الخطأ، لا نفعل شيئاً ونعيد كائن الطوارئ كما هو | |
| pass | |
| return jsonify({"meta": meta_obj}) | |
| def stream(config, type, id): | |
| conf = decode_config(config) | |
| parts = id.split('_') | |
| url = "" | |
| # بناء رابط التشغيل | |
| if "ep" in id: | |
| # دعم التشغيل العادي والتشغيل الطارئ | |
| if "force" in id: | |
| # محاولة تخمين الامتداد الشائع | |
| url = f"{conf['h']}/series/{conf['u']}/{conf['p']}/{parts[-1]}.mkv" | |
| else: | |
| url = f"{conf['h']}/series/{conf['u']}/{conf['p']}/{parts[2]}.{parts[3]}" | |
| elif type == "movie": | |
| url = f"{conf['h']}/movie/{conf['u']}/{conf['p']}/{parts[-1]}.mp4" | |
| elif type == "tv": | |
| url = f"{conf['h']}/{conf['u']}/{conf['p']}/{parts[-1]}" | |
| return jsonify({"streams": [{ | |
| "name": "Renoo VIP ⚡", | |
| "title": "Direct Stream 🚀", | |
| "url": url | |
| }]}) | |
| if __name__ == '__main__': | |
| app.run(host='0.0.0.0', port=7860) |