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 = """ لوحة القيادة | Renoo VIP Control Center

💎 Renoo Control

لوحة التحكم الكاملة - الإصدار 4.0

🛡️ وضع الإنقاذ
يمنع الشاشة البيضاء
🥷 وضع التخفي
تجاوز حجب الميتا
⚡ الوضع السريع
إخفاء الأوصاف للتسريع
📺 القنوات المباشرة
تفعيل البث الحي

✅ تم تجهيز الرابط الخاص بك:

📲 اضغط هنا للتثبيت المباشر في Stremio
""" # --- المنطق الخلفي (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) --- @app.route('/') def dashboard(): return render_template_string(HTML_TEMPLATE) @app.route('//manifest.json') 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 }) @app.route('//catalog//.json') @app.route('//catalog///.json') 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}) # --- الميتا (قلب النظام الجديد) --- @app.route('//meta//.json') 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}) @app.route('//stream//.json') 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)