renoo-iptv / main.py
renoomon's picture
Update main.py
fbd1478 verified
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) ---
@app.route('/')
def dashboard():
return render_template_string(HTML_TEMPLATE)
@app.route('/<config>/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('/<config>/catalog/<type>/<id>.json')
@app.route('/<config>/catalog/<type>/<id>/<path:params>.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('/<config>/meta/<type>/<id>.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('/<config>/stream/<type>/<id>.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)