المطلوب: إنشاء منصة بث احترافية شبيهة بنتفليكس لع...
Создано: 13 сентября 2025 г.
Отвечено с использованием GPT-5 Thinking от Chat01
Создано: 13 сентября 2025 г.
Отвечено с использованием GPT-5 Thinking от Chat01
المطلوب: إنشاء منصة بث احترافية شبيهة بنتفليكس لعرض الأفلام والمسلسلات، بملف واحد index.php يحتوي على كل HTML/CSS/JS/PHP، مع:
ابدا فورا بكتابة كود , اريد الصميم جذاب وراقي ,أرسل لك نسخة كاملة جاهزة دفعة واحدة
1️⃣ البنية الأساسية:
لا تستخدم Node.js. استخدم PHP (vanilla) + SQLite/MySQL + HTML/CSS/JS.
/db → قاعدة بيانات SQLite (db/app.db)
/movies → ملف JSON للمحتوى
/uploads → صور البوسترات والخلفيات
إنشاء تلقائي للجداول عند أول تشغيل.
Router داخلي للتعامل مع الصفحات (?route=...).
2️⃣ دعم الأفلام والمسلسلات:
جدول movies: بيانات عامة (type='movie' أو 'series').
جدول episodes: لكل حلقة (season, episode, title, duration, mp4_src).
لوحة تحكم لإضافة/تعديل/حذف المواسم والحلقات.
واجهة المستخدم تعرض المسلسلات بمواسم وحلقات منظمة.
Auto-Next للحلقة التالية.
إشعارات عند إضافة حلقات جديدة لمسلسلات يتابعها المستخدم.
4️⃣ المشغل الاحترافي:
HTML5 Video مع تصميم مخصص وأزرار تحكم (تشغيل/إيقاف، تقديم/ترجيع، صوت، سرعة، ملء الشاشة، وضع سينما).
يدعم جميع الروابط
دعم Range Requests للبث الفوري.
استئناف المشاهدة من آخر موضع.
قائمة حلقات جانبية للمسلسلات.
إدارة الجودة (Multi-quality): إمكانية إضافة روابط متعددة للفيديو بجودات مختلفة (480p, 720p, 1080p, 4K) حسب المتوفر.
5️⃣ الإشعارات الداخلية:
جدول notifications(id, user_id, message_ar, message_en, link, is_read, created_at).
إشعار عند إضافة فيلم/مسلسل جديد أو حلقة جديدة لمسلسل يتابعه المستخدم.
الأدمن يمكنه إرسال إشعارات يدوية لأي مستخدم أو للجميع من لوحة التحكم.
أيقونة جرس بالشريط العلوي مع قائمة منسدلة لآخر الإشعارات.
6️⃣ وضع ليلي/نهاري:
زر تبديل الوضع (شمس/قمر) في الشريط العلوي.
حفظ التفضيل في localStorage أو settings للمستخدم.
تطبيق الوضع عبر CSS Variables.
الوضع الليلي افتراضي.
7️⃣ نظام التقييم والتعليقات:
جدول ratings(user_id, movie_id, rating).
جدول comments(user_id, movie_id, comment, created_at).
تقييم من 1-5 نجوم مع عرض المتوسط.
التعليقات مرتبة من الأحدث للأقدم.
تعديل/حذف التعليق لصاحبه أو المدير.
8️⃣ البحث المتقدم والفلاتر:
صفحة بحث متقدم مع فلاتر: النوع، التصنيف، السنة، التقييم، المدة، الجودة، اللغة، الترتيب.
API: action=searchAdvanced مع المعاملات اللازمة.
عرض النتائج في شبكة مع شارات التقييم والجودة.
🔟 قائمة المشاهدة Watchlist:
جدول watchlist(user_id, movie_id).
زر إضافة/إزالة من القائمة.
قسم خاص في الحساب وعلى الرئيسية.
1️⃣1️⃣ الاستمرار في المشاهدة متعدد الأجهزة:
توسيع watch_history بحقول device, last_watched_at.
مزامنة الموضع عبر الأجهزة.
Auto resume عند فتح المحتوى.
1️⃣2️⃣ إحصائيات المستخدم:
عرض ساعات المشاهدة، التصنيفات المفضلة، تاريخ المشاهدة، مستوى التفاعل.
صفحة "إحصاءاتي" ببطاقات ورسوم بيانية بسيطة.
1️⃣3️⃣ نظام ترشيحات ذكي:
توصيات بناءً على التصنيفات المفضلة، التقييمات، المشاهدات.
أقسام: "مقترح لك"، "لأنك أحببت X"، "رائج الآن".
تحديث دوري للتوصيات.
1️⃣4️⃣ إدارة المستخدمين:
أدوار وصلاحيات (Admin, Editor, Moderator, User).
حظر/تجميد المستخدم مع سبب وفترة.
نظام تسجيل بالموافقة اليدوية: المستخدم يسجل، وحسابه يبقى pending حتى يوافق الأدمن أو يرفض مع سبب.
عرض قائمة طلبات التسجيل في لوحة التحكم.
1️⃣5️⃣ المراقبة والتحليل:
لوحة إحصاءات تعرض:
أكثر المحتوى مشاهدة.
أوقات الذروة.
نشاط المستخدمين.
الروابط المعطوبة.
سجل النشاطات (Activity Log) لكل العمليات الإدارية.
مراقبة أداء الخادم (إذا متاح).
1️⃣6️⃣ الأمان والأداء:
PDO + Prepared Statements.
CSRF Tokens.
password_hash لكلمات المرور.
ترميز HTML عند العرض.
Lazy Loading للصور.
Cache-Control و GZIP.
1️⃣7️⃣ التهيئة الأولية:
عند أول تشغيل: إنشاء المجلدات، القاعدة، الجداول، ملف movies.json بعينات باللغتين.
أول حساب يُسجَّل يصبح Admin تلقائيًا.
✅ النجاح يتحقق إذا:
كل الميزات تعمل بكامل وظائفها.
الأداء سريع، والواجهة متجاوبة، والمحتوى منظم.
دعم كامل للغة العربية .
هذا الملف النصي يحتوي على كل المتطلبات التفصيلية لبناء تطبيق
php<?php /** * index.php — منصة بث احترافية (نسخة واحدة شاملة) * PHP + SQLite (افتراضي) / MySQL (اختياري) + HTML/CSS/JS (بدون أُطر) * واجهة عربية بالكامل + دعم إنجليزي للرسائل/العينات * * ملاحظات مهمة: * - أول تشغيل سيقوم بإنشاء المجلدات /db /movies /uploads وإنشاء قاعدة SQLite والجداول وملف movies.json بعينات. * - يمكن تشغيل MySQL بدلاً من SQLite عبر تفعيل ثابت USE_MYSQL وملء إعدادات الاتصال. * - يحتوي الملف على Router داخلي عبر ?route=... وعلى API عبر ?action=... * - يدعم البث مع Range Requests للملفات المحلية (داخل /uploads/videos). للروابط الخارجية يُستخدم <video> مباشرة. * - يطبق أمان أساسي: PDO Prepared, CSRF, password_hash, ترميز HTML, Lazy Loading، وبعض التخزين المؤقت. * - هذه نسخة مرجعية أنيقة وخفيفة وقابلة للتوسّع، جاهزة للاستخدام مباشرة. */ /* =========================[ الإعدادات العامة ]========================= */ // فعّل MySQL (true) أو استخدم SQLite (false) const USE_MYSQL = false; const MYSQL_DSN = 'mysql:host=localhost;dbname=streamapp;charset=utf8mb4'; const MYSQL_USER = 'root'; const MYSQL_PASS = 'root'; // اسم ملف SQLite const SQLITE_FILE = __DIR__ . '/db/app.db'; // مسارات أساسية const DIR_DB = __DIR__ . '/db'; const DIR_MOVIES = __DIR__ . '/movies'; const DIR_UPLOADS = __DIR__ . '/uploads'; const DIR_VIDEOS = __DIR__ . '/uploads/videos'; // للفيديوهات المحلية (اختياري) const DIR_POSTERS = __DIR__ . '/uploads/posters'; const DIR_BACKS = __DIR__ . '/uploads/backdrops'; // ضبط المنطقة الزمنية date_default_timezone_set('UTC'); // تفعيل GZIP إن وُجد if (extension_loaded('zlib') && !ini_get('zlib.output_compression')) { @ob_start('ob_gzhandler'); } /* =========================[ الجلسات والأمان ]========================= */ session_start(); // CSRF if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(16)); } function csrf_field() { echo '<input type="hidden" name="csrf" value="'.esc($_SESSION['csrf']).'">'; } function csrf_check() { if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!isset($_POST['csrf']) || !hash_equals($_SESSION['csrf'], $_POST['csrf'])) { http_response_code(403); exit('CSRF token mismatch.'); } } } /* =========================[ أدوات مساعدة ]========================= */ function esc($s){ return htmlspecialchars($s ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function now(){ return date('Y-m-d H:i:s'); } function is_logged(){ return !empty($_SESSION['user']); } function user(){ return $_SESSION['user'] ?? null; } function is_admin(){ $u = user(); return $u && in_array($u['role'], ['Admin','Editor','Moderator']); } function is_role($roles){ $u=user(); return $u && in_array($u['role'], (array)$roles); } function device_name(){ $ua = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown'; if (stripos($ua,'Android')!==false) return 'Android'; if (stripos($ua,'iPhone')!==false) return 'iPhone'; if (stripos($ua,'iPad')!==false) return 'iPad'; if (stripos($ua,'Windows')!==false) return 'Windows'; if (stripos($ua,'Mac OS')!==false) return 'macOS'; if (stripos($ua,'Linux')!==false) return 'Linux'; return 'جهاز'; } /* =========================[ قاعدة البيانات ]========================= */ function db(){ static $pdo; if ($pdo) return $pdo; if (USE_MYSQL) { $pdo = new PDO(MYSQL_DSN, MYSQL_USER, MYSQL_PASS, [ PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC, ]); } else { $pdo = new PDO('sqlite:'.SQLITE_FILE, null, null, [ PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC, ]); $pdo->exec('PRAGMA foreign_keys = ON;'); $pdo->exec('PRAGMA journal_mode = WAL;'); } return $pdo; } function ensure_dirs_and_db(){ // مجلدات @is_dir(DIR_DB) || @mkdir(DIR_DB, 0775, true); @is_dir(DIR_MOVIES) || @mkdir(DIR_MOVIES, 0775, true); @is_dir(DIR_UPLOADS) || @mkdir(DIR_UPLOADS, 0775, true); @is_dir(DIR_VIDEOS) || @mkdir(DIR_VIDEOS, 0775, true); @is_dir(DIR_POSTERS) || @mkdir(DIR_POSTERS, 0775, true); @is_dir(DIR_BACKS) || @mkdir(DIR_BACKS, 0775, true); // قاعدة البيانات + الجداول $pdo = db(); $pdo->exec("CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, email TEXT UNIQUE, password_hash TEXT, role TEXT DEFAULT 'User', status TEXT DEFAULT 'pending', -- pending/approved/rejected/banned ban_reason TEXT, banned_until TEXT, created_at TEXT )"); $pdo->exec("CREATE TABLE IF NOT EXISTS movies ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT CHECK(type IN ('movie','series')) NOT NULL, title TEXT, title_en TEXT, description TEXT, description_en TEXT, genre TEXT, year INTEGER, duration INTEGER, -- بالدقائق للأفلام، افتراضي quality TEXT, -- 480p/720p/1080p/4K language TEXT, poster TEXT, backdrop TEXT, is_featured INTEGER DEFAULT 0, rating_avg REAL DEFAULT 0, rating_count INTEGER DEFAULT 0, created_at TEXT, updated_at TEXT )"); $pdo->exec("CREATE TABLE IF NOT EXISTS episodes ( id INTEGER PRIMARY KEY AUTOINCREMENT, movie_id INTEGER, season INTEGER, episode INTEGER, title TEXT, title_en TEXT, duration INTEGER, mp4_src TEXT, -- مصدر افتراضي sources_json TEXT, -- جودات متعددة: [{label:'1080p',src:'...',type:'mp4'}] created_at TEXT, FOREIGN KEY(movie_id) REFERENCES movies(id) ON DELETE CASCADE )"); $pdo->exec("CREATE TABLE IF NOT EXISTS ratings ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, movie_id INTEGER, rating INTEGER CHECK(rating BETWEEN 1 AND 5), created_at TEXT, updated_at TEXT, UNIQUE(user_id, movie_id), FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(movie_id) REFERENCES movies(id) ON DELETE CASCADE )"); $pdo->exec("CREATE TABLE IF NOT EXISTS comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, movie_id INTEGER, comment TEXT, created_at TEXT, updated_at TEXT, is_deleted INTEGER DEFAULT 0, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(movie_id) REFERENCES movies(id) ON DELETE CASCADE )"); $pdo->exec("CREATE TABLE IF NOT EXISTS watchlist ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, movie_id INTEGER, created_at TEXT, UNIQUE(user_id, movie_id), FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(movie_id) REFERENCES movies(id) ON DELETE CASCADE )"); $pdo->exec("CREATE TABLE IF NOT EXISTS watch_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, movie_id INTEGER, episode_id INTEGER, position REAL DEFAULT 0, -- بالثواني device TEXT, last_watched_at TEXT, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(movie_id) REFERENCES movies(id) ON DELETE CASCADE, FOREIGN KEY(episode_id) REFERENCES episodes(id) ON DELETE CASCADE )"); $pdo->exec("CREATE TABLE IF NOT EXISTS notifications ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, -- null = للجميع message_ar TEXT, message_en TEXT, link TEXT, is_read INTEGER DEFAULT 0, created_at TEXT, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE )"); $pdo->exec("CREATE TABLE IF NOT EXISTS follows ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, movie_id INTEGER, -- للسلاسل فقط UNIQUE(user_id, movie_id), FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(movie_id) REFERENCES movies(id) ON DELETE CASCADE )"); $pdo->exec("CREATE TABLE IF NOT EXISTS activity_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, action TEXT, details TEXT, created_at TEXT, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE SET NULL )"); // ملف JSON بعينات $sampleFile = DIR_MOVIES . '/movies.json'; if (!file_exists($sampleFile)) { $samples = [ [ "type"=>"movie", "title"=>"الفيلم التجريبي", "title_en"=>"Sample Movie", "description"=>"وصف عربي للفيلم التجريبي.", "description_en"=>"English description of sample movie.", "genre"=>"دراما", "year"=>2024, "duration"=>110, "quality"=>"1080p", "language"=>"ar", "poster"=>"", "backdrop"=>"" ], [ "type"=>"series", "title"=>"المسلسل التجريبي", "title_en"=>"Sample Series", "description"=>"سلسلة تجريبية بموسم واحد وثلاث حلقات.", "description_en"=>"A sample series with one season and three episodes.", "genre"=>"إثارة", "year"=>2025, "duration"=>0, "quality"=>"720p", "language"=>"ar", "poster"=>"", "backdrop"=>"" ] ]; file_put_contents($sampleFile, json_encode($samples, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT)); } } ensure_dirs_and_db(); /* =========================[ تسجيل نشاط ]========================= */ function log_action($action, $details){ $u = user(); $stmt = db()->prepare("INSERT INTO activity_log(user_id, action, details, created_at) VALUES (?,?,?,?)"); $stmt->execute([$u['id']??null, $action, $details, now()]); } /* =========================[ إدخالات أولية (عند عدم وجود بيانات) ]========================= */ function maybe_seed(){ $pdo = db(); $cnt = (int)$pdo->query("SELECT COUNT(*) FROM movies")->fetchColumn(); if ($cnt===0){ // إدراج عينات $pdo->prepare("INSERT INTO movies(type,title,title_en,description,description_en,genre,year,duration,quality,language,poster,backdrop,is_featured,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)")->execute( ['movie','الفيلم التجريبي','Sample Movie','وصف عربي للفيلم التجريبي.','English description.','دراما',2024,110,'1080p','ar','','',1,now(),now()] ); $movieId = (int)$pdo->lastInsertId(); $pdo->prepare("INSERT INTO movies(type,title,title_en,description,description_en,genre,year,duration,quality,language,poster,backdrop,is_featured,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)")->execute( ['series','المسلسل التجريبي','Sample Series','سلسلة تجريبية بموسم واحد وثلاث حلقات.','Sample series.','إثارة',2025,0,'720p','ar','','',1,now(),now()] ); $seriesId = (int)$pdo->lastInsertId(); // حلقات $ep = $pdo->prepare("INSERT INTO episodes(movie_id,season,episode,title,title_en,duration,mp4_src,sources_json,created_at) VALUES(?,?,?,?,?,?,?,?,?)"); for($i=1;$i<=3;$i++){ $sources = [ ["label"=>"720p","src"=>"https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4","type"=>"video/mp4"], ["label"=>"480p","src"=>"https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/480/Big_Buck_Bunny_480_10s_1MB.mp4","type"=>"video/mp4"] ]; $ep->execute([$seriesId,1,$i,"حلقة $i","Episode $i",10,$sources[0]['src'],json_encode($sources,JSON_UNESCAPED_UNICODE),now()]); } } } maybe_seed(); /* =========================[ إشعارات ]========================= */ function notify($user_id, $ar, $en, $link=null){ $stmt = db()->prepare("INSERT INTO notifications(user_id, message_ar, message_en, link, is_read, created_at) VALUES (?,?,?,?,0,?)"); $stmt->execute([$user_id, $ar, $en, $link, now()]); } function notify_all($ar,$en,$link=null){ notify(null, $ar,$en,$link); } /* =========================[ العمليات (API) ]========================= */ if (isset($_GET['action'])) { header('X-Content-Type-Options: nosniff'); $action = $_GET['action']; // CORS خفيف للطلبات XHR داخل نفس الأصل header('Content-Type: application/json; charset=utf-8'); if ($action === 'login' && $_SERVER['REQUEST_METHOD']==='POST') { csrf_check(); $email = trim($_POST['email']??''); $pass = $_POST['password']??''; $stmt = db()->prepare("SELECT * FROM users WHERE email = ?"); $stmt->execute([$email]); $u = $stmt->fetch(); if ($u && password_verify($pass, $u['password_hash'])) { if ($u['status']==='approved') { $_SESSION['user'] = $u; echo json_encode(['ok'=>true]); exit; } else { echo json_encode(['ok'=>false,'error'=>'حسابك غير مُفعّل بعد.']); exit; } } echo json_encode(['ok'=>false,'error'=>'بيانات الدخول غير صحيحة']); exit; } if ($action === 'register' && $_SERVER['REQUEST_METHOD']==='POST') { csrf_check(); $username = trim($_POST['username']??''); $email = trim($_POST['email']??''); $pass = $_POST['password']??''; if (!$username || !$email || !$pass) { echo json_encode(['ok'=>false,'error'=>'الرجاء إدخال جميع الحقول']); exit; } $hash = password_hash($pass, PASSWORD_DEFAULT); try { // أول حساب يصبح Admin تلقائيًا $cnt = (int)db()->query("SELECT COUNT(*) FROM users")->fetchColumn(); $role = $cnt===0 ? 'Admin' : 'User'; $status = $cnt===0 ? 'approved' : 'pending'; $stmt = db()->prepare("INSERT INTO users(username,email,password_hash,role,status,created_at) VALUES(?,?,?,?,?,?)"); $stmt->execute([$username,$email,$hash,$role,$status,now()]); $id = (int)db()->lastInsertId(); $u = db()->query("SELECT * FROM users WHERE id=$id")->fetch(); // إشعار للإدارة notify_all("تسجيل مستخدم جديد: $username","New user registered: $username","?route=admin&tab=users"); echo json_encode(['ok'=>true,'pending'=>$status!=='approved']); exit; } catch(Exception $e){ echo json_encode(['ok'=>false,'error'=>'البريد أو الاسم مستخدم مسبقًا']); exit; } } if ($action === 'logout') { unset($_SESSION['user']); echo json_encode(['ok'=>true]); exit; } if ($action === 'toggleWatchlist' && is_logged()) { csrf_check(); $movie_id = (int)($_POST['movie_id']??0); $pdo = db(); $stmt = $pdo->prepare("SELECT id FROM watchlist WHERE user_id=? AND movie_id=?"); $stmt->execute([user()['id'],$movie_id]); $r = $stmt->fetch(); if ($r){ $pdo->prepare("DELETE FROM watchlist WHERE id=?")->execute([$r['id']]); echo json_encode(['ok'=>true,'in'=>false]); exit; } else { $pdo->prepare("INSERT INTO watchlist(user_id,movie_id,created_at) VALUES(?,?,?)")->execute([user()['id'],$movie_id,now()]); echo json_encode(['ok'=>true,'in'=>true]); exit; } } if ($action === 'rate' && is_logged()) { csrf_check(); $movie_id = (int)($_POST['movie_id']??0); $rating = max(1,min(5,(int)($_POST['rating']??0))); $pdo = db(); // upsert $exists = $pdo->prepare("SELECT id FROM ratings WHERE user_id=? AND movie_id=?"); $exists->execute([user()['id'],$movie_id]); if ($exists->fetch()){ $pdo->prepare("UPDATE ratings SET rating=?, updated_at=? WHERE user_id=? AND movie_id=?") ->execute([$rating, now(), user()['id'],$movie_id]); } else { $pdo->prepare("INSERT INTO ratings(user_id,movie_id,rating,created_at,updated_at) VALUES(?,?,?,?,?)") ->execute([user()['id'],$movie_id,$rating,now(),now()]); } // تحديث المتوسط $row = $pdo->query("SELECT AVG(rating) as avg, COUNT(*) as cnt FROM ratings WHERE movie_id=$movie_id")->fetch(); $pdo->prepare("UPDATE movies SET rating_avg=?, rating_count=?, updated_at=? WHERE id=?") ->execute([$row['avg'],$row['cnt'],now(),$movie_id]); echo json_encode(['ok'=>true,'avg'=>round($row['avg'],2),'count'=>(int)$row['cnt']]); exit; } if ($action === 'comment' && is_logged()) { csrf_check(); $movie_id = (int)($_POST['movie_id']??0); $text = trim($_POST['text']??''); if (!$text){ echo json_encode(['ok'=>false,'error'=>'اكتب تعليقًا.']); exit; } db()->prepare("INSERT INTO comments(user_id,movie_id,comment,created_at,updated_at) VALUES(?,?,?,?,?)") ->execute([user()['id'],$movie_id,$text,now(),now()]); echo json_encode(['ok'=>true]); exit; } if ($action === 'commentEdit' && is_logged()) { csrf_check(); $id = (int)($_POST['id']??0); $text = trim($_POST['text']??''); $c = db()->prepare("SELECT * FROM comments WHERE id=?"); $c->execute([$id]); $row=$c->fetch(); if (!$row) { echo json_encode(['ok'=>false]); exit; } if ($row['user_id']!=user()['id'] && !is_admin()) { echo json_encode(['ok'=>false]); exit; } db()->prepare("UPDATE comments SET comment=?, updated_at=? WHERE id=?")->execute([$text,now(),$id]); echo json_encode(['ok'=>true]); exit; } if ($action === 'commentDelete' && is_logged()) { csrf_check(); $id = (int)($_POST['id']??0); $c = db()->prepare("SELECT * FROM comments WHERE id=?"); $c->execute([$id]); $row=$c->fetch(); if (!$row) { echo json_encode(['ok'=>false]); exit; } if ($row['user_id']!=user()['id'] && !is_admin()) { echo json_encode(['ok'=>false]); exit; } db()->prepare("UPDATE comments SET is_deleted=1, updated_at=? WHERE id=?")->execute([now(),$id]); echo json_encode(['ok'=>true]); exit; } if ($action === 'follow' && is_logged()) { csrf_check(); $movie_id = (int)($_POST['movie_id']??0); $pdo = db(); $ex = $pdo->prepare("SELECT id FROM follows WHERE user_id=? AND movie_id=?"); $ex->execute([user()['id'],$movie_id]); if ($ex->fetch()){ $pdo->prepare("DELETE FROM follows WHERE user_id=? AND movie_id=?")->execute([user()['id'],$movie_id]); echo json_encode(['ok'=>true,'following'=>false]); exit; } else { $pdo->prepare("INSERT INTO follows(user_id,movie_id) VALUES(?,?)")->execute([user()['id'],$movie_id]); echo json_encode(['ok'=>true,'following'=>true]); exit; } } if ($action === 'saveProgress' && is_logged()) { // حفظ موضع المشاهدة $movie_id = (int)($_POST['movie_id']??0); $episode_id = (int)($_POST['episode_id']??0); $position = (float)($_POST['position']??0); $pdo = db(); $row = $pdo->prepare("SELECT id FROM watch_history WHERE user_id=? AND movie_id=? AND episode_id ".($episode_id?'= ?':'IS NULL')); $row->execute($episode_id ? [user()['id'],$movie_id,$episode_id] : [user()['id'],$movie_id]); $r = $row->fetch(); if ($r){ $pdo->prepare("UPDATE watch_history SET position=?, device=?, last_watched_at=? WHERE id=?") ->execute([$position, device_name(), now(), $r['id']]); } else { $pdo->prepare("INSERT INTO watch_history(user_id,movie_id,episode_id,position,device,last_watched_at) VALUES (?,?,?,?,?,?)") ->execute([user()['id'],$movie_id, $episode_id?:null, $position, device_name(), now()]); } echo json_encode(['ok'=>true]); exit; } if ($action === 'getProgress' && is_logged()) { $movie_id = (int)($_GET['movie_id']??0); $episode_id = (int)($_GET['episode_id']??0); $pdo = db(); $stmt = $pdo->prepare("SELECT position FROM watch_history WHERE user_id=? AND movie_id=? AND (episode_id ".($episode_id?'= ?':'IS NULL').") ORDER BY last_watched_at DESC LIMIT 1"); $stmt->execute($episode_id ? [user()['id'],$movie_id,$episode_id] : [user()['id'],$movie_id]); $pos = $stmt->fetchColumn(); echo json_encode(['ok'=>true,'position'=>floatval($pos)]); exit; } if ($action === 'notifications') { $uid = user()['id'] ?? null; if ($uid){ $stmt = db()->prepare("SELECT * FROM notifications WHERE (user_id IS NULL OR user_id=?) ORDER BY id DESC LIMIT 20"); $stmt->execute([$uid]); } else { $stmt = db()->query("SELECT * FROM notifications WHERE user_id IS NULL ORDER BY id DESC LIMIT 10"); } echo json_encode(['ok'=>true,'items'=>$stmt->fetchAll()]); exit; } if ($action === 'markRead' && is_logged()) { $id = (int)($_POST['id']??0); db()->prepare("UPDATE notifications SET is_read=1 WHERE id=?")->execute([$id]); echo json_encode(['ok'=>true]); exit; } if ($action === 'searchAdvanced') { // معايير: النوع، التصنيف، السنة، التقييم، المدة، الجودة، اللغة، الترتيب، النص $pdo = db(); $where = []; $args = []; if (!empty($_GET['type'])) { $where[] = "type = ?"; $args[] = $_GET['type']==='series'?'series':'movie'; } if (!empty($_GET['genre'])){ $where[] = "genre LIKE ?"; $args[] = '%'.$_GET['genre'].'%'; } if (!empty($_GET['year'])) { $where[] = "year = ?"; $args[] = (int)$_GET['year']; } if (!empty($_GET['rating'])){ $where[] = "rating_avg >= ?"; $args[] = (float)$_GET['rating']; } if (!empty($_GET['quality'])){ $where[] = "quality = ?"; $args[] = $_GET['quality']; } if (!empty($_GET['language'])){ $where[] = "language = ?"; $args[] = $_GET['language']; } if (!empty($_GET['q'])){ $where[] = "(title LIKE ? OR title_en LIKE ?)"; $args[] = '%'.$_GET['q'].'%'; $args[] = '%'.$_GET['q'].'%'; } $order = "created_at DESC"; if (!empty($_GET['sort'])){ $sort = $_GET['sort']; $order = match($sort){ 'rating' => "rating_avg DESC", 'year' => "year DESC", 'alpha' => "title COLLATE NOCASE ASC", default => "created_at DESC" }; } $sql = "SELECT * FROM movies ".(count($where)?"WHERE ".implode(" AND ",$where):"")." ORDER BY $order LIMIT 100"; $stmt = $pdo->prepare($sql); $stmt->execute($args); $rows = $stmt->fetchAll(); header('Cache-Control: no-store'); echo json_encode(['ok'=>true,'results'=>$rows], JSON_UNESCAPED_UNICODE); exit; } if ($action === 'adminAddMovie' && is_admin()) { csrf_check(); $p = $_POST; $stmt = db()->prepare("INSERT INTO movies(type,title,title_en,description,description_en,genre,year,duration,quality,language,poster,backdrop,is_featured,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); $stmt->execute([ $p['type'], trim($p['title']), trim($p['title_en']), trim($p['description']), trim($p['description_en']), trim($p['genre']), (int)$p['year'], (int)$p['duration'], $p['quality'],$p['language'], trim($p['poster']), trim($p['backdrop']), !empty($p['is_featured'])?1:0, now(), now() ]); $id = (int)db()->lastInsertId(); notify_all("تم إضافة ".($p['type']==='series'?'مسلسل':'فيلم')." جديد: ".$p['title'], "New ".($p['type']==='series'?'series':'movie')." added: ".$p['title'], "?route=details&id=$id"); log_action('adminAddMovie', json_encode(['id'=>$id,'title'=>$p['title']],JSON_UNESCAPED_UNICODE)); echo json_encode(['ok'=>true,'id'=>$id]); exit; } if ($action === 'adminAddEpisode' && is_admin()) { csrf_check(); $p = $_POST; $sources = $p['sources_json'] ?: '[]'; $stmt = db()->prepare("INSERT INTO episodes(movie_id,season,episode,title,title_en,duration,mp4_src,sources_json,created_at) VALUES(?,?,?,?,?,?,?,?,?)"); $stmt->execute([(int)$p['movie_id'],(int)$p['season'],(int)$p['episode'], trim($p['title']), trim($p['title_en']), (int)$p['duration'], trim($p['mp4_src']), $sources, now()]); $id = (int)db()->lastInsertId(); // إشعار للمتابعين $title = db()->query("SELECT title FROM movies WHERE id=".(int)$p['movie_id'])->fetchColumn(); $followers = db()->query("SELECT user_id FROM follows WHERE movie_id=".(int)$p['movie_id'])->fetchAll(PDO::FETCH_COLUMN); foreach($followers as $uid){ notify($uid, "تمت إضافة حلقة جديدة لمسلسل $title", "New episode added to $title", "?route=watch&episode_id=$id"); } log_action('adminAddEpisode', json_encode(['episode_id'=>$id,'movie_id'=>(int)$p['movie_id']],JSON_UNESCAPED_UNICODE)); echo json_encode(['ok'=>true,'id'=>$id]); exit; } if ($action === 'adminUsers' && is_admin()) { // الموافقات / الحظر if ($_SERVER['REQUEST_METHOD']==='POST'){ csrf_check(); $id = (int)$_POST['id']; $op = $_POST['op'] ?? ''; if ($op==='approve'){ db()->prepare("UPDATE users SET status='approved' WHERE id=?")->execute([$id]); } elseif ($op==='reject'){ db()->prepare("UPDATE users SET status='rejected' WHERE id=?")->execute([$id]); } elseif ($op==='ban'){ $reason = trim($_POST['reason']??''); $until = trim($_POST['until']??''); db()->prepare("UPDATE users SET status='banned', ban_reason=?, banned_until=? WHERE id=?")->execute([$reason,$until,$id]); } elseif ($op==='role' && isset($_POST['role'])){ db()->prepare("UPDATE users SET role=? WHERE id=?")->execute([$_POST['role'],$id]); } echo json_encode(['ok'=>true]); exit; } else { $users = db()->query("SELECT * FROM users ORDER BY created_at DESC")->fetchAll(); echo json_encode(['ok'=>true,'users'=>$users]); exit; } } if ($action === 'adminNotify' && is_admin()) { csrf_check(); $uid = !empty($_POST['user_id']) ? (int)$_POST['user_id'] : null; notify($uid, trim($_POST['message_ar']), trim($_POST['message_en']), trim($_POST['link'])); echo json_encode(['ok'=>true]); exit; } if ($action === 'stream') { // بث فيديو محلي مع دعم Range Requests // الاستخدام: <video src="?action=stream&path=uploads/videos/sample.mp4"> $path = $_GET['path'] ?? ''; $clean = realpath(__DIR__ . '/' . $path); $base = realpath(DIR_VIDEOS); if (!$clean || !str_starts_with($clean, $base)) { http_response_code(403); exit('Forbidden'); } if (!is_file($clean)) { http_response_code(404); exit('Not found'); } $size = filesize($clean); $start = 0; $length = $size; $end = $size - 1; header('Content-Type: video/mp4'); header('Accept-Ranges: bytes'); if (isset($_SERVER['HTTP_RANGE'])) { if (preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $m)) { $start = intval($m[1]); $end = ($m[2] !== '') ? intval($m[2]) : $end; $length = $end - $start + 1; header("HTTP/1.1 206 Partial Content"); header("Content-Range: bytes $start-$end/$size"); } } header("Content-Length: $length"); $fp = fopen($clean, 'rb'); fseek($fp, $start); $chunk = 8192; while(!feof($fp) && $length > 0){ $read = ($length > $chunk) ? $chunk : $length; echo fread($fp, $read); $length -= $read; @ob_flush(); flush(); } fclose($fp); exit; } // افتراضي: خطأ echo json_encode(['ok'=>false,'error'=>'Unknown action']); exit; } /* =========================[ واجهة العرض ]========================= */ // استرجاع بيانات function get_featured(){ return db()->query("SELECT * FROM movies WHERE is_featured=1 ORDER BY created_at DESC LIMIT 12")->fetchAll(); } function get_latest($type=null){ $sql = "SELECT * FROM movies ".($type?"WHERE type='".($type==='series'?'series':'movie')."'":"")." ORDER BY created_at DESC LIMIT 18"; return db()->query($sql)->fetchAll(); } function get_movie($id){ $stmt = db()->prepare("SELECT * FROM movies WHERE id=?"); $stmt->execute([$id]); return $stmt->fetch(); } function get_series_episodes($movie_id){ $stmt = db()->prepare("SELECT * FROM episodes WHERE movie_id=? ORDER BY season ASC, episode ASC"); $stmt->execute([$movie_id]); return $stmt->fetchAll(); } function get_episode($id){ $stmt = db()->prepare("SELECT * FROM episodes WHERE id=?"); $stmt->execute([$id]); return $stmt->fetch(); } function get_next_episode($movie_id,$season,$episode){ $stmt = db()->prepare("SELECT * FROM episodes WHERE movie_id=? AND (season>? OR (season=? AND episode>?)) ORDER BY season ASC, episode ASC LIMIT 1"); $stmt->execute([$movie_id,$season,$season,$episode]); return $stmt->fetch(); } /* =========================[ القالب العام (HTML/CSS/JS) ]========================= */ function render_header($title=''){ $u = user(); $rtl = 'dir="rtl" lang="ar"'; $csrf = esc($_SESSION['csrf']); echo <<<HTML <!doctype html> <html $rtl> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>سينماكس - {$title}</title> <meta name="description" content="منصة بث احترافية للأفلام والمسلسلات"> <style> :root{ --bg: #0b0e13; --bg-soft:#121621; --text:#E6E9EF; --muted:#A9B1BD; --primary:#5b8cff; --accent:#00d4aa; --card:#141a25; --border:#1f2633; --star:#ffcc00; } :root.light{ --bg:#f8fafc; --bg-soft:#ffffff; --text:#0b0e13; --muted:#4b5563; --card:#ffffff; --border:#e5e7eb; --primary:#3b82f6; --accent:#10b981; } *{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--text);font:16px/1.6 system-ui, -apple-system, Segoe UI, Roboto} a{color:inherit;text-decoration:none} img{max-width:100%;height:auto;display:block} .container{max-width:1200px;margin:auto;padding:20px} .nav{position:sticky;top:0;z-index:50;background:var(--bg-soft);border-bottom:1px solid var(--border)} .nav .inner{display:flex;align-items:center;gap:14px;justify-content:space-between;padding:12px 20px} .brand{display:flex;align-items:center;gap:10px;font-weight:800} .brand .logo{width:28px;height:28px;border-radius:6px;background:linear-gradient(135deg,var(--primary),var(--accent))} .search{flex:1;max-width:520px;position:relative} .search input{width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:var(--bg);color:var(--text)} .top-actions{display:flex;align-items:center;gap:10px} .btn{display:inline-flex;align-items:center;gap:8px;padding:9px 12px;border:1px solid var(--border);background:var(--card);border-radius:10px;color:var(--text);cursor:pointer} .btn.primary{background:linear-gradient(135deg,var(--primary),var(--accent));border:0;color:white} .btn.ghost{background:transparent} .badge{display:inline-block;padding:2px 8px;border-radius:999px;background:var(--border);color:var(--muted);font-size:12px} .grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px} @media(min-width:600px){.grid{grid-template-columns:repeat(3,1fr)}} @media(min-width:900px){.grid{grid-template-columns:repeat(6,1fr)}} .card{background:var(--card);border:1px solid var(--border);border-radius:14px;overflow:hidden;position:relative} .card .thumb{aspect-ratio:2/3;background:#222} .card .meta{padding:10px} .card .title{font-weight:700} .card .sub{color:var(--muted);font-size:13px;display:flex;gap:8px;align-items:center} .header-hero{position:relative;border-radius:16px;overflow:hidden;background:linear-gradient(120deg,rgba(91,140,255,.25),transparent 40%), linear-gradient(0deg,rgba(0,0,0,.4),rgba(0,0,0,.4)), url('') center/cover no-repeat} .hero-inner{padding:28px} .section-title{display:flex;align-items:center;justify-content:space-between;margin:24px 0 12px} .kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:10px} .kpi{padding:14px;border:1px solid var(--border);border-radius:12px;background:var(--card)} .player{background:#000;border-radius:14px;overflow:hidden;border:1px solid var(--border);position:relative} .player video{width:100%;display:block;background:#000} .player .controls{position:absolute;inset:auto 0 0 0;background:linear-gradient(0deg,rgba(0,0,0,.6),transparent);padding:8px;display:flex;gap:8px;align-items:center;flex-wrap:wrap} .player .controls button,.player .controls select,.player .controls input[type=range]{background:rgba(255,255,255,.08);border:0;color:#fff;padding:8px;border-radius:8px} .player .controls .time{color:#ddd;font-size:12px;margin-inline-start:auto;margin-inline-end:8px} .episodes{display:flex;gap:10px;overflow:auto;padding:8px} .ep-item{min-width:200px;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:8px} .comment{border-bottom:1px dashed var(--border);padding:10px 0} .stars{display:inline-flex;gap:2px;cursor:pointer} .stars i{font-style:normal;color:var(--star)} .table{width:100%;border-collapse:collapse} .table th,.table td{border-bottom:1px solid var(--border);padding:8px;text-align:start} .admin-grid{display:grid;grid-template-columns:1fr;gap:12px} @media(min-width:900px){.admin-grid{grid-template-columns:1fr 1fr}} .toast{position:fixed;bottom:16px;inset-inline:16px;background:var(--bg-soft);border:1px solid var(--border);color:var(--text);padding:12px;border-radius:10px;display:none} .bell{position:relative} .bell .dot{position:absolute;top:0;right:0;width:8px;height:8px;background:#f43f5e;border-radius:50%} .menu{position:absolute;right:0;margin-top:8px;background:var(--card);border:1px solid var(--border);border-radius:12px;min-width:280px;display:none} .menu ul{list-style:none;margin:0;padding:6px} .menu li{padding:8px;border-radius:8px} .menu li:hover{background:var(--bg)} .small{font-size:12px;color:var(--muted)} .tag{padding:2px 6px;border-radius:6px;background:var(--border);font-size:12px} .cinema{background:#0b0e13;} footer{padding:20px;color:var(--muted)} </style> </head> <body> <nav class="nav"> <div class="inner"> <div class="brand"> <div class="logo"></div> <a href="?route=home"><span>سينماكس</span></a> <span class="badge">منصة بث</span> </div> <div class="search"> <form action="?route=search" method="get"> <input type="hidden" name="route" value="search"> <input name="q" placeholder="ابحث عن فيلم أو مسلسل..." value=""> </form> </div> <div class="top-actions"> <button class="btn" id="themeBtn" title="وضع ليلي/نهاري">🌙/☀️</button> <div class="bell"> <button class="btn" id="bellBtn">🔔 الإشعارات</button> <span class="dot" id="bellDot" style="display:none;"></span> <div class="menu" id="bellMenu"><ul id="notifList"></ul></div> </div> <a class="btn" href="?route=watchlist">قائمتي</a> <a class="btn" href="?route=stats">إحصاءاتي</a> HTML; if ($u){ echo '<span class="badge">مرحبًا، '.esc($u['username']).'</span>'; if (is_admin()) echo ' <a class="btn primary" href="?route=admin">لوحة التحكم</a>'; echo ' <form id="logoutForm" method="post" action="?action=logout" style="display:inline"><input type="hidden" name="csrf" value="'.$csrf.'"><button class="btn">خروج</button></form>'; } else { echo '<a class="btn" href="?route=login">دخول</a> <a class="btn primary" href="?route=register">تسجيل</a>'; } echo <<<HTML </div> </div> </nav> <div class="container"> HTML; } function render_footer(){ $csrf = esc($_SESSION['csrf']); echo <<<HTML </div> <footer class="container small">© <script>document.write(new Date().getFullYear())</script> سينماكس — تجربة بث راقية</footer> <div class="toast" id="toast"></div> <script> // وضع ليلي/نهاري const root = document.documentElement; function applyTheme(){ const t = localStorage.getItem('theme') || 'dark'; if (t==='light') root.classList.add('light'); else root.classList.remove('light'); } applyTheme(); document.getElementById('themeBtn').onclick = () => { const cur = localStorage.getItem('theme') || 'dark'; localStorage.setItem('theme', cur==='dark'?'light':'dark'); applyTheme(); }; // إشعارات const bellBtn = document.getElementById('bellBtn'); const bellMenu = document.getElementById('bellMenu'); const bellDot = document.getElementById('bellDot'); function fetchNotifs(initial=false){ fetch('?action=notifications') .then(r=>r.json()).then(d=>{ if(!d.ok) return; const ul = document.getElementById('notifList'); ul.innerHTML=''; let unread = 0; d.items.forEach(n=>{ const li=document.createElement('li'); const msg = n.message_ar || n.message_en || ''; li.innerHTML = '<div>'+msg+'</div><div class="small">'+(n.created_at||'')+'</div>'; if(n.link){ li.innerHTML += '<div><a class="tag" href="'+n.link+'">فتح</a></div>'; } if(n.is_read==0){ unread++; } li.onclick = () => { if(n.id){ fetch('?action=markRead',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:'id='+encodeURIComponent(n.id)}) } if(n.link) location.href = n.link; }; ul.appendChild(li); }); bellDot.style.display = unread>0 ? 'block' : 'none'; if (!initial && unread>0) showToast('لديك إشعارات جديدة'); }); } bellBtn?.addEventListener('click',()=>{ bellMenu.style.display = bellMenu.style.display==='block'?'none':'block'; if(bellMenu.style.display==='block') fetchNotifs(true); }); setInterval(fetchNotifs, 30000); fetchNotifs(true); // خروج document.getElementById('logoutForm')?.addEventListener('submit', async (e)=>{ e.preventDefault(); const fd = new FormData(e.target); const res = await fetch('?action=logout',{method:'POST',body:fd}); const d=await res.json(); if(d.ok) location.href='?route=home'; }); // أدوات عامة function showToast(msg){ const t=document.getElementById('toast'); t.textContent=msg; t.style.display='block'; setTimeout(()=>t.style.display='none',2500); } // تقييم نجوم function sendRating(movieId, val){ fetch('?action=rate',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:'csrf={$csrf}&movie_id='+movieId+'&rating='+val}) .then(r=>r.json()).then(d=>{ if(d.ok){ showToast('تم حفظ تقييمك'); const el=document.getElementById('avg_'+movieId); if(el) el.textContent=d.avg+' ⭐ ('+d.count+')'; } }); } window.sendRating = sendRating; // قائمة المشاهدة function toggleWatch(movieId, btn){ fetch('?action=toggleWatchlist',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:'csrf={$csrf}&movie_id='+movieId}) .then(r=>r.json()).then(d=>{ if(d.ok){ btn.textContent = d.in ? 'إزالة من قائمتي' : 'إضافة إلى قائمتي'; showToast(d.in?'أضيف إلى قائمتك':'أزيل من قائمتك'); } }); } window.toggleWatch = toggleWatch; // متابعة مسلسل function toggleFollow(movieId, btn){ fetch('?action=follow',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:'csrf={$csrf}&movie_id='+movieId}) .then(r=>r.json()).then(d=>{ if(d.ok){ btn.textContent = d.following ? 'إلغاء المتابعة' : 'متابعة المسلسل'; showToast(d.following?'بدأت المتابعة':'تم الإلغاء'); } }); } window.toggleFollow = toggleFollow; // حفظ التقدم async function saveProgress(movieId, episodeId, pos){ try{ await fetch('?action=saveProgress',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:'movie_id='+movieId+'&episode_id='+(episodeId||0)+'&position='+pos}); }catch(e){} } window.saveProgress = saveProgress; // استئناف تلقائي async function getProgress(movieId, episodeId){ try{ const r = await fetch('?action=getProgress&movie_id='+movieId+'&episode_id='+(episodeId||0)); const d = await r.json(); if(d.ok) return d.position||0; }catch(e){} return 0; } window.getProgress = getProgress; </script> </body></html> HTML; } /* =========================[ الصفحات ]========================= */ $route = $_GET['route'] ?? 'home'; if ($route === 'home') { render_header('الرئيسية'); $featured = get_featured(); echo '<div class="header-hero"><div class="hero-inner"><h2>مرحبًا بك في <b>سينماكس</b> 🍿</h2><p>شاهد أحدث الأفلام والمسلسلات بجودة عالية مع تجربة مشاهدة راقية وذكية.</p></div></div>'; echo '<div class="section-title"><h3>مميز لدينا</h3> <a class="btn ghost" href="?route=search">استعراض الكل →</a></div>'; echo '<div class="grid">'; foreach($featured as $m){ card_movie($m); } echo '</div>'; echo '<div class="section-title"><h3>أحدث الأفلام</h3></div><div class="grid">'; foreach(get_latest('movie') as $m){ card_movie($m); } echo '</div>'; echo '<div class="section-title"><h3>أحدث المسلسلات</h3></div><div class="grid">'; foreach(get_latest('series') as $m){ card_movie($m); } echo '</div>'; render_footer(); exit; } if ($route === 'search') { render_header('البحث المتقدم'); $q = trim($_GET['q'] ?? ''); echo <<<HTML <form method="get" class="card" style="padding:12px;margin-bottom:12px"> <input type="hidden" name="route" value="search"> <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px"> <input name="q" placeholder="كلمة مفتاحية" value="{$q}"> <select name="type"><option value="">الكل</option><option value="movie">أفلام</option><option value="series">مسلسلات</option></select> <input name="genre" placeholder="التصنيف (دراما، إثارة...)"> <input name="year" type="number" placeholder="السنة"> <input name="rating" type="number" step="0.1" placeholder="التقييم الأدنى"> <select name="quality"><option value="">الجودة</option><option>480p</option><option>720p</option><option>1080p</option><option>4K</option></select> <input name="language" placeholder="اللغة (ar,en,...)"> <select name="sort"><option value="">الأحدث</option><option value="rating">الأعلى تقييمًا</option><option value="year">الأحدث (سنة)</option><option value="alpha">أبجديًا</option></select> </div> <div style="margin-top:8px"><button class="btn primary">بحث</button></div> </form> <div id="results" class="grid"></div> <script> const params = new URLSearchParams(location.search); fetch('?action=searchAdvanced&'+params.toString()).then(r=>r.json()).then(d=>{ if(!d.ok) return; const g=document.getElementById('results'); g.innerHTML=''; d.results.forEach(m=>{ const el=document.createElement('a'); el.href='?route=details&id='+m.id; el.className='card'; el.innerHTML = '<div class="thumb"><img loading="lazy" src="'+(m.poster||'https://placehold.co/400x600')+'" alt=""></div>'+ '<div class="meta"><div class="title">'+(m.title||m.title_en||'')+'</div>'+ '<div class="sub"><span class="badge">'+m.type+'</span> <span>'+ (m.year||'') +'</span> <span>'+ (m.quality||'') +'</span>'+ ' <span id="avg_'+m.id+'">'+(m.rating_avg? (m.rating_avg.toFixed(1)+' ⭐ ('+m.rating_count+')') : 'لا تقييم بعد')+'</span></div></div>'; g.appendChild(el); }); }); </script> HTML; render_footer(); exit; } if ($route === 'details') { $id = (int)($_GET['id'] ?? 0); $m = get_movie($id); if (!$m){ http_response_code(404); render_header('غير موجود'); echo '<p>العنصر غير موجود.</p>'; render_footer(); exit; } render_header(esc($m['title'])); echo '<div class="card" style="display:grid;grid-template-columns:200px 1fr;gap:14px;padding:14px">'; echo '<div><img loading="lazy" src="'.esc($m['poster'] ?: 'https://placehold.co/400x600').'" alt=""></div>'; echo '<div>'; echo '<h2>'.esc($m['title'] ?: $m['title_en']).' <span class="badge">'.esc($m['type']).'</span></h2>'; echo '<div class="small">'.esc($m['genre']).' • '.esc($m['year']).' • '.esc($m['quality']).'</div>'; echo '<p>'.esc($m['description'] ?: $m['description_en']).'</p>'; $avg = $m['rating_avg']? round($m['rating_avg'],2).' ⭐ ('.$m['rating_count'].')' : 'لا تقييم بعد'; echo '<div id="avg_'.$m['id'].'" class="badge">'.$avg.'</div><br>'; if (is_logged()){ echo '<button class="btn" onclick="toggleWatch('.$m['id'].',this)">إضافة إلى قائمتي</button> '; if ($m['type']==='series'){ echo '<button class="btn" onclick="toggleFollow('.$m['id'].',this)">متابعة المسلسل</button> '; } } else { echo '<a class="btn" href="?route=login">سجّل الدخول لإدارة قائمتك</a> '; } echo '<div style="margin-top:10px">قيّم العمل: <span class="stars">'; for($i=1;$i<=5;$i++) echo '<i onclick="sendRating('.$m['id'].','.$i.')">★</i>'; echo '</span></div>'; if ($m['type']==='movie'){ // زر مشاهدة الفيلم echo '<div style="margin-top:12px"><a class="btn primary" href="?route=watch&movie_id='.$m['id'].'">▶ مشاهدة الآن</a></div>'; } else { // عرض الحلقات $eps = get_series_episodes($m['id']); $grouped = []; foreach($eps as $e){ $grouped[$e['season']][] = $e; } foreach($grouped as $season=>$list){ echo '<h3>الموسم '.$season.'</h3><div class="episodes">'; foreach($list as $e){ echo '<a class="ep-item" href="?route=watch&episode_id='.$e['id'].'"><div class="small">الحلقة '.$e['episode'].'</div>'. '<div>'.esc($e['title'] ?: $e['title_en']).'</div></a>'; } echo '</div>'; } } echo '</div></div>'; // التعليقات $comments = db()->query("SELECT c.*, u.username FROM comments c JOIN users u ON u.id=c.user_id WHERE c.movie_id=".$m['id']." AND c.is_deleted=0 ORDER BY c.id DESC")->fetchAll(); echo '<div class="card" style="padding:12px;margin-top:12px"><h3>التعليقات</h3>'; if (is_logged()){ echo '<form id="cform"><textarea name="text" rows="3" style="width:100%;padding:8px;border-radius:8px;background:var(--bg);color:var(--text);border:1px solid var(--border)" placeholder="اكتب تعليقك..."></textarea>'; echo '<input type="hidden" name="movie_id" value="'.$m['id'].'">'.'<input type="hidden" name="csrf" value="'.esc($_SESSION['csrf']).'">'; echo '<div><button class="btn primary">إرسال</button></div></form>'; echo <<<JS <script> document.getElementById('cform').addEventListener('submit', async (e)=>{ e.preventDefault(); const fd=new FormData(e.target); const r=await fetch('?action=comment',{method:'POST',body:fd}); const d=await r.json(); if(d.ok) location.reload(); }); </script> JS; } else { echo '<p class="small">سجّل الدخول لتستطيع التعليق.</p>'; } foreach($comments as $c){ echo '<div class="comment"><div><b>'.esc($c['username']).'</b> <span class="small">'.esc($c['created_at']).'</span></div><div>'.esc($c['comment']).'</div>'; if (is_logged() && (user()['id']==$c['user_id'] || is_admin())){ echo '<div class="small"><a href="#" onclick="editC('.$c['id'].',\''.esc($c['comment']).'\');return false;">تعديل</a> • <a href="#" onclick="delC('.$c['id'].');return false;">حذف</a></div>'; } echo '</div>'; } echo <<<JS <script> function editC(id, old){ const nv = prompt('حرّر تعليقك:', old); if(nv==null) return; fetch('?action=commentEdit',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:'csrf={$_SESSION['csrf']}&id='+id+'&text='+encodeURIComponent(nv)}).then(r=>r.json()).then(d=>{ if(d.ok) location.reload(); }); } function delC(id){ if(!confirm('حذف التعليق؟')) return; fetch('?action=commentDelete',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:'csrf={$_SESSION['csrf']}&id='+id}).then(r=>r.json()).then(d=>{ if(d.ok) location.reload(); }); } </script> JS; echo '</div>'; render_footer(); exit; } if ($route === 'watch') { $movie_id = isset($_GET['movie_id']) ? (int)$_GET['movie_id'] : null; $episode_id = isset($_GET['episode_id']) ? (int)$_GET['episode_id'] : null; if ($episode_id){ $ep = get_episode($episode_id); $mv = get_movie($ep['movie_id'] ?? 0); if (!$ep || !$mv){ http_response_code(404); render_header('غير موجود'); echo '<p>الحلقة غير موجودة.</p>'; render_footer(); exit; } render_header('مشاهدة - '.esc($mv['title']).' / '.esc($ep['title'])); player_block($mv, $ep); render_footer(); exit; } else { $mv = get_movie($movie_id); if (!$mv){ http_response_code(404); render_header('غير موجود'); echo '<p>غير موجود.</p>'; render_footer(); exit; } render_header('مشاهدة - '.esc($mv['title'])); player_block($mv, null); render_footer(); exit; } } if ($route === 'watchlist' && is_logged()) { render_header('قائمتي'); $stmt = db()->prepare("SELECT m.* FROM watchlist w JOIN movies m ON m.id=w.movie_id WHERE w.user_id=? ORDER BY w.id DESC"); $stmt->execute([user()['id']]); echo '<div class="grid">'; foreach($stmt->fetchAll() as $m) card_movie($m); echo '</div>'; render_footer(); exit; } if ($route === 'stats' && is_logged()) { render_header('إحصاءاتي'); $uid = user()['id']; $rows = db()->prepare("SELECT h.*, m.title, m.type, e.season, e.episode FROM watch_history h JOIN movies m ON m.id=h.movie_id LEFT JOIN episodes e ON e.id=h.episode_id WHERE h.user_id=? ORDER BY h.last_watched_at DESC LIMIT 200"); $rows->execute([$uid]); $hist=$rows->fetchAll(); $hours = 0; foreach($hist as $h){ $hours += ($h['position'] ?? 0) / 3600; } $genres = db()->prepare("SELECT genre, COUNT(*) c FROM movies m JOIN watch_history h ON h.movie_id=m.id WHERE h.user_id=? GROUP BY genre ORDER BY c DESC"); $genres->execute([$uid]); $g=$genres->fetchAll(); echo '<div class="kpis">'; echo '<div class="kpi"><div class="small">ساعات المشاهدة</div><div style="font-size:26px">'.round($hours,1).'</div></div>'; echo '<div class="kpi"><div class="small">عناصر في قائمتي</div><div style="font-size:26px">'.(int)db()->query("SELECT COUNT(*) FROM watchlist WHERE user_id=".$uid)->fetchColumn().'</div></div>'; echo '<div class="kpi"><div class="small">تقييماتي</div><div style="font-size:26px">'.(int)db()->query("SELECT COUNT(*) FROM ratings WHERE user_id=".$uid)->fetchColumn().'</div></div>'; echo '<div class="kpi"><div class="small">تعليقاتي</div><div style="font-size:26px">'.(int)db()->query("SELECT COUNT(*) FROM comments WHERE user_id=".$uid." AND is_deleted=0")->fetchColumn().'</div></div>'; echo '</div>'; echo '<h3>سجل المشاهدة</h3><table class="table"><tr><th>العنصر</th><th>الموضع</th><th>الجهاز</th><th>آخر مشاهدة</th></tr>'; foreach($hist as $h){ $nm = esc($h['title']).($h['type']==='series'?' • م'.$h['season'].'ح'.$h['episode']:''); echo '<tr><td>'.$nm.'</td><td>'.gmdate('H:i:s', (int)$h['position']).'</td><td>'.esc($h['device']).'</td><td>'.esc($h['last_watched_at']).'</td></tr>'; } echo '</table>'; echo '<h3>التصنيفات المفضلة</h3><div style="display:flex;gap:8px;flex-wrap:wrap">'; foreach($g as $row) echo '<span class="tag">'.esc($row['genre']).' ('.$row['c'].')</span>'; echo '</div>'; // رسم بسيط على Canvas (مشاهدات زمنية) $daily = db()->prepare("SELECT substr(last_watched_at,1,10) d, COUNT(*) c FROM watch_history WHERE user_id=? GROUP BY d ORDER BY d ASC"); $daily->execute([$uid]); $labels=[]; $vals=[]; foreach($daily->fetchAll() as $r){ $labels[]=$r['d']; $vals[]=(int)$r['c']; } $labelsJS = json_encode($labels); $valsJS = json_encode($vals); echo <<<HTML <canvas id="chart" width="600" height="240" style="border:1px solid var(--border);border-radius:8px;background:var(--card)"></canvas> <script> // رسم بسيط بخطوط بدون مكتبات (function(){ const labels = $labelsJS; const vals = $valsJS; const c = document.getElementById('chart').getContext('2d'); const w=c.canvas.width, h=c.canvas.height, pad=30; const max = Math.max(1, ...vals); // محاور c.clearRect(0,0,w,h); c.strokeStyle='#888'; c.beginPath(); c.moveTo(pad,10); c.lineTo(pad,h-pad); c.lineTo(w-10,h-pad); c.stroke(); // خط c.strokeStyle='#5b8cff'; c.beginPath(); vals.forEach((v,i)=>{ const x = pad + (i/(Math.max(1,vals.length-1)))*(w-pad-20); const y = (h-pad) - (v/max)*(h-pad-20); if(i===0) c.moveTo(x,y); else c.lineTo(x,y); }); c.stroke(); // نقاط + تسميات مختصرة c.fillStyle='#5b8cff'; vals.forEach((v,i)=>{ const x = pad + (i/(Math.max(1,vals.length-1)))*(w-pad-20); const y = (h-pad) - (v/max)*(h-pad-20); c.beginPath(); c.arc(x,y,2.5,0,Math.PI*2); c.fill(); }); })(); </script> HTML; render_footer(); exit; } if ($route === 'login' || $route === 'register') { render_header($route==='login'?'تسجيل الدخول':'إنشاء حساب'); $isLogin = $route==='login'; echo '<div class="card" style="max-width:460px;margin:auto;padding:16px">'; echo '<h2>'.($isLogin?'دخول':'تسجيل').'</h2>'; echo '<form id="auth">'; if (!$isLogin) echo '<input name="username" placeholder="اسم المستخدم" style="width:100%;padding:10px;border:1px solid var(--border);border-radius:10px;background:var(--bg);color:var(--text);margin-bottom:8px">'; echo '<input name="email" placeholder="البريد الإلكتروني" style="width:100%;padding:10px;border:1px solid var(--border);border-radius:10px;background:var(--bg);color:var(--text);margin-bottom:8px">'; echo '<input type="password" name="password" placeholder="كلمة المرور" style="width:100%;padding:10px;border:1px solid var(--border);border-radius:10px;background:var(--bg);color:var(--text);margin-bottom:8px">'; echo '<input type="hidden" name="csrf" value="'.esc($_SESSION['csrf']).'">'; echo '<button class="btn primary" style="width:100%">'.($isLogin?'دخول':'تسجيل').'</button>'; echo '</form></div>'; echo <<<JS <script> document.getElementById('auth').addEventListener('submit', async (e)=>{ e.preventDefault(); const fd=new FormData(e.target); const url='?action=' + ('{$isLogin}'==='1'?'login':'register'); // hack, replaced below }); </script> JS; // نص JS صحيح (بدون hack) echo "<script> document.getElementById('auth').addEventListener('submit', async (e)=>{ e.preventDefault(); const fd=new FormData(e.target); const url='?action=".($isLogin?'login':'register')."'; const r=await fetch(url,{method:'POST',body:fd}); const d=await r.json(); if(d.ok){ location.href='?route=home'; } else if(d.pending){ alert('تم التسجيل، بانتظار موافقة الإدارة'); location.href='?route=home'; } else{ alert(d.error||'حدث خطأ'); } }); </script>"; render_footer(); exit; } if ($route === 'admin' && is_admin()) { render_header('لوحة التحكم'); echo '<div class="admin-grid">'; // بطاقة إضافة فيلم/مسلسل echo '<div class="card" style="padding:12px"><h3>إضافة فيلم/مسلسل</h3><form id="addMovie">'; echo '<select name="type" class="btn"><option value="movie">فيلم</option><option value="series">مسلسل</option></select> '; echo '<input name="title" placeholder="العنوان (عربي)" class="btn"> '; echo '<input name="title_en" placeholder="العنوان (إنجليزي)" class="btn"> '; echo '<input name="genre" placeholder="التصنيف" class="btn"> '; echo '<input name="year" type="number" placeholder="السنة" class="btn"> '; echo '<input name="duration" type="number" placeholder="المدة بالدقائق" class="btn"> '; echo '<select name="quality" class="btn"><option>480p</option><option selected>720p</option><option>1080p</option><option>4K</option></select> '; echo '<input name="language" placeholder="اللغة (ar,en...)" class="btn"> '; echo '<input name="poster" placeholder="رابط بوستر" class="btn" style="width:100%"> '; echo '<input name="backdrop" placeholder="رابط خلفية" class="btn" style="width:100%"> '; echo '<textarea name="description" placeholder="الوصف (عربي)" class="btn" style="width:100%;height:70px"></textarea>'; echo '<textarea name="description_en" placeholder="الوصف (إنجليزي)" class="btn" style="width:100%;height:70px"></textarea>'; echo '<label><input type="checkbox" name="is_featured"> مميز</label>'; echo '<input type="hidden" name="csrf" value="'.esc($_SESSION['csrf']).'">'; echo '<div><button class="btn primary">حفظ</button></div></form></div>'; // بطاقة إضافة حلقة echo '<div class="card" style="padding:12px"><h3>إضافة حلقة لمسلسل</h3><form id="addEpisode">'; echo '<input name="movie_id" type="number" placeholder="ID المسلسل" class="btn"> '; echo '<input name="season" type="number" placeholder="الموسم" class="btn"> '; echo '<input name="episode" type="number" placeholder="الحلقة" class="btn"> '; echo '<input name="title" placeholder="عنوان الحلقة (ع)" class="btn"> '; echo '<input name="title_en" placeholder="عنوان الحلقة (EN)" class="btn"> '; echo '<input name="duration" type="number" placeholder="المدة بالدقائق" class="btn"> '; echo '<input name="mp4_src" placeholder="رابط افتراضي" class="btn" style="width:100%"> '; echo '<textarea name="sources_json" placeholder=\'JSON للجودات [{"label":"1080p","src":"...","type":"video/mp4"}]\' class="btn" style="width:100%;height:80px"></textarea>'; echo '<input type="hidden" name="csrf" value="'.esc($_SESSION['csrf']).'">'; echo '<div><button class="btn primary">حفظ</button></div></form></div>'; // إدارة المستخدمين echo '<div class="card" style="padding:12px"><h3>المستخدمون</h3><div id="users"></div></div>'; // إشعار يدوي echo '<div class="card" style="padding:12px"><h3>إرسال إشعار</h3><form id="notify"><input name="user_id" type="number" placeholder="User ID (فارغ = للجميع)" class="btn"> <input name="message_ar" placeholder="رسالة بالعربية" class="btn" style="width:100%"> <input name="message_en" placeholder="EN Message" class="btn" style="width:100%"> <input name="link" placeholder="رابط (اختياري)" class="btn" style="width:100%"><input type="hidden" name="csrf" value="'.esc($_SESSION['csrf']).'"><div><button class="btn primary">إرسال</button></div></form></div>'; echo '</div>'; // grid echo <<<JS <script> // إضافة فيلم/مسلسل document.getElementById('addMovie').addEventListener('submit', async (e)=>{ e.preventDefault(); const fd=new FormData(e.target); const r=await fetch('?action=adminAddMovie',{method:'POST',body:fd}); const d=await r.json(); if(d.ok){ alert('تم إضافة العنصر ID='+d.id); } }); // إضافة حلقة document.getElementById('addEpisode').addEventListener('submit', async (e)=>{ e.preventDefault(); const fd=new FormData(e.target); const r=await fetch('?action=adminAddEpisode',{method:'POST',body:fd}); const d=await r.json(); if(d.ok){ alert('تم إضافة الحلقة ID='+d.id); } }); // تحميل قائمة المستخدمين async function loadUsers(){ const r=await fetch('?action=adminUsers'); const d=await r.json(); if(!d.ok) return; const box=document.getElementById('users'); box.innerHTML=''; const t=document.createElement('table'); t.className='table'; t.innerHTML='<tr><th>ID</th><th>اسم</th><th>بريد</th><th>الدور</th><th>الحالة</th><th>تحكم</th></tr>'; d.users.forEach(u=>{ const tr=document.createElement('tr'); tr.innerHTML='<td>'+u.id+'</td><td>'+u.username+'</td><td>'+u.email+'</td>'+ '<td>'+u.role+'</td><td>'+u.status+'</td>'+ '<td>'+ '<button class="btn" onclick="userOp('+u.id+',\\'approve\\')">قبول</button> '+ '<button class="btn" onclick="userOp('+u.id+',\\'reject\\')">رفض</button> '+ '<button class="btn" onclick="banUser('+u.id+')">حظر</button> '+ '<button class="btn" onclick="setRole('+u.id+')">تغيير الدور</button>'+ '</td>'; t.appendChild(tr); }); box.appendChild(t); } loadUsers(); async function userOp(id,op){ const fd=new URLSearchParams(); fd.set('id',id); fd.set('op',op); fd.set('csrf','{$_SESSION['csrf']}'); const r=await fetch('?action=adminUsers',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:fd.toString()}); const d=await r.json(); if(d.ok) loadUsers(); } async function banUser(id){ const reason=prompt('سبب الحظر:')||''; const until=prompt('حتى تاريخ (YYYY-MM-DD):')||''; const fd=new URLSearchParams(); fd.set('id',id); fd.set('op','ban'); fd.set('reason',reason); fd.set('until',until); fd.set('csrf','{$_SESSION['csrf']}'); const r=await fetch('?action=adminUsers',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:fd.toString()}); const d=await r.json(); if(d.ok) loadUsers(); } async function setRole(id){ const role=prompt('ادخل الدور: Admin/Editor/Moderator/User','User'); if(!role) return; const fd=new URLSearchParams(); fd.set('id',id); fd.set('op','role'); fd.set('role',role); fd.set('csrf','{$_SESSION['csrf']}'); const r=await fetch('?action=adminUsers',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:fd.toString()}); const d=await r.json(); if(d.ok) loadUsers(); } // إشعار يدوي document.getElementById('notify').addEventListener('submit', async (e)=>{ e.preventDefault(); const fd=new FormData(e.target); const r=await fetch('?action=adminNotify',{method:'POST',body:fd}); const d=await r.json(); if(d.ok) alert('تم الإرسال'); }); </script> JS; render_footer(); exit; } /* =========================[ بطاقات/مكوّنات ]========================= */ function card_movie($m){ $href = '?route=details&id='.$m['id']; $title = esc($m['title'] ?: $m['title_en']); $poster = esc($m['poster'] ?: 'https://placehold.co/400x600'); $badge = $m['type']==='series' ? 'مسلسل' : 'فيلم'; $avg = $m['rating_avg']? round($m['rating_avg'],1).' ⭐' : 'بدون تقييم'; echo '<a class="card" href="'.$href.'">'; echo '<div class="thumb"><img loading="lazy" src="'.$poster.'" alt=""></div>'; echo '<div class="meta"><div class="title">'.$title.'</div>'; echo '<div class="sub"><span class="badge">'.$badge.'</span><span>'.esc($m['year']).'</span><span>'.esc($m['quality']).'</span><span>'.$avg.'</span></div></div>'; echo '</a>'; } function player_block($mv, $ep=null){ $movie_id = (int)$mv['id']; $episode_id = $ep ? (int)$ep['id'] : 0; // مصادر الفيديو: $sources = []; if ($ep){ if (!empty($ep['sources_json'])) { $sources = json_decode($ep['sources_json'], true) ?: []; } if (!$sources && !empty($ep['mp4_src'])) $sources[] = ['label'=>$mv['quality']?:'720p','src'=>$ep['mp4_src'],'type'=>'video/mp4']; } else { // فيلم: نستخدم poster/backdrop كمثال، ويمكن تعيين مصدر محلي عبر ?action=stream&path=uploads/videos/xxx.mp4 // لإضافة مصدر افتراضي للأفلام: ضع رابطًا في حقل poster مؤقتًا أو أضف حقلًا مخصصًا لاحقًا if (!empty($mv['backdrop'])) $sources[] = ['label'=>$mv['quality']?:'1080p','src'=>$mv['backdrop'],'type'=>'video/mp4']; } // الحلقة التالية (Auto-Next) $next = null; if ($ep) $next = get_next_episode($mv['id'], $ep['season'], $ep['episode']); // قائمة الحلقات الجانبية $eps = $mv['type']==='series' ? get_series_episodes($mv['id']) : []; echo '<div class="player" id="playerWrap">'; echo '<video id="vid" preload="metadata" playsinline></video>'; echo '<div class="controls" id="ctrls"> <button id="playBtn">▶︎</button> <button id="back10">⏪ 10</button> <button id="fw10">10 ⏩</button> <button id="cinema">🎬 سينما</button> <button id="fs">⛶ ملء الشاشة</button> <label class="small">السرعة <select id="speed"><option>0.5</option><option>0.75</option><option selected>1</option><option>1.25</option><option>1.5</option><option>2</option></select></label> <label class="small">الجودة <select id="quality"></select></label> <input id="seek" type="range" min="0" max="100" value="0" step="0.01" style="flex:1"> <span class="time" id="time">00:00 / 00:00</span> </div>'; echo '</div>'; if ($mv['type']==='series'){ echo '<div class="card" style="margin-top:10px;padding:8px"><h3>الحلقات</h3><div class="episodes">'; foreach($eps as $e){ $sel = $ep && $e['id']==$ep['id'] ? 'style="outline:2px solid var(--primary)"' : ''; echo '<a class="ep-item" '.$sel.' href="?route=watch&episode_id='.$e['id'].'">'. '<div class="small">م'.$e['season'].' • ح'.$e['episode'].'</div><div>'.esc($e['title']?:$e['title_en']).'</div></a>'; } echo '</div></div>'; } // سكربت تشغيل $jsSources = json_encode($sources, JSON_UNESCAPED_UNICODE); $resumeMovie = $movie_id; $resumeEp = $episode_id; $autoNextUrl = ($next? ('?route=watch&episode_id='.$next['id']) : ''); echo <<<JS <script> const sources = $jsSources; const video = document.getElementById('vid'); const seek = document.getElementById('seek'); const time = document.getElementById('time'); const playBtn = document.getElementById('playBtn'); const qSel = document.getElementById('quality'); const speed= document.getElementById('speed'); function fmt(t){ if(!isFinite(t)) return '00:00'; const m=Math.floor(t/60), s=Math.floor(t%60); return (m<10?'0':'')+m+':' + (s<10?'0':'')+s; } function setSourceByLabel(label){ const s = sources.find(x=>x.label===label) || sources[0]; if(!s) return; const was = !video.paused; const cur = video.currentTime; video.src = s.src; video.type = s.type || 'video/mp4'; video.playbackRate = parseFloat(speed.value)||1; video.addEventListener('loadedmetadata', async ()=>{ let resumeAt = await getProgress($resumeMovie, $resumeEp); if(resumeAt>1) video.currentTime = resumeAt; if(was) video.play(); }, {once:true}); } function populateQuality(){ qSel.innerHTML = ''; sources.forEach((s,i)=>{ const opt=document.createElement('option'); opt.textContent=s.label||('Q'+(i+1)); opt.value=s.label||('Q'+(i+1)); qSel.appendChild(opt); }); } populateQuality(); if(sources.length){ setSourceByLabel(sources[0].label||''); } playBtn.onclick = ()=>{ if(video.paused){ video.play(); playBtn.textContent='⏸︎'; } else { video.pause(); playBtn.textContent='▶︎'; } }; document.getElementById('back10').onclick = ()=> video.currentTime = Math.max(0, video.currentTime-10); document.getElementById('fw10').onclick = ()=> video.currentTime = Math.min(video.duration||0, video.currentTime+10); document.getElementById('fs').onclick = ()=> { if(video.requestFullscreen) video.requestFullscreen(); }; document.getElementById('cinema').onclick = ()=> document.getElementById('playerWrap').classList.toggle('cinema'); speed.onchange = ()=> video.playbackRate = parseFloat(speed.value)||1; qSel.onchange = ()=>{ const lbl=qSel.value; const pos=video.currentTime||0; setSourceByLabel(lbl); video.currentTime=pos; }; video.addEventListener('timeupdate', ()=>{ if(video.duration){ seek.value = (video.currentTime/video.duration)*100; time.textContent = fmt(video.currentTime)+' / '+fmt(video.duration); if(Math.floor(video.currentTime)%5===0){ saveProgress($resumeMovie, $resumeEp, video.currentTime); } } }); seek.addEventListener('input', ()=>{ if(video.duration){ video.currentTime = (seek.value/100)*video.duration; } }); video.addEventListener('ended', ()=>{ // Auto-Next للحلقة التالية const nextUrl = "$autoNextUrl"; if(nextUrl){ location.href = nextUrl; } }); // ما قبل التشغيل: استئناف من الموضع السابق (async ()=>{ let resumeAt = await getProgress($resumeMovie, $resumeEp); if(resumeAt>1){ video.addEventListener('loadedmetadata',()=>{ video.currentTime=resumeAt; }, {once:true}); } })(); </script> JS; } /* =========================[ افتراضي: 404 ]========================= */ if (!in_array($route, ['home','search','details','watch','watchlist','stats','login','register','admin'])) { render_header('غير موجود'); echo '<p>الصفحة غير متاحة.</p>'; render_footer(); exit; } /* =========================[ انتهى ]========================= */ ?>
هو يعمل بشكل جيد لكن للاسف لا يمكنني كادمن من التعديل علي الافلام و المسلسلات التي اضيفها او حتي حذفها
تمام! أضفت لك “إدارة كاملة للتعديل والحذف” للأفلام والمسلسلات والحلقات، من خلال إجراءات API جديدة + واجهة داخل لوحة التحكم للتعديل/الحذف بشكل سريع.
كل ما عليك هو نسخ/لصق المقاطع التالية في نفس ملف index.php
:
ضع هذا المقطع بعد مقطعي adminAddMovie
و adminAddEpisode
مباشرةً (ما زال داخل قسم if (isset($_GET['action'])) { ... }
):
php/* ===== [NEW] إدارة المحتوى: قائمة/جلب/تحديث/حذف أفلام/مسلسلات وحلقات ===== */ if ($action === 'adminListMovies' && is_admin()) { // q, type, limit, offset $q = trim($_GET['q'] ?? ''); $type = $_GET['type'] ?? ''; $limit = max(1, min(200, (int)($_GET['limit'] ?? 50))); $offset = max(0, (int)($_GET['offset'] ?? 0)); $where = []; $args = []; if ($q !== '') { $where[] = "(title LIKE ? OR title_en LIKE ?)"; $args[] = "%$q%"; $args[] = "%$q%"; } if ($type === 'movie' || $type === 'series') { $where[] = "type = ?"; $args[] = $type; } $sql = "SELECT id,type,title,title_en,genre,year,quality,rating_avg,rating_count,created_at,updated_at FROM movies ".(count($where)?"WHERE ".implode(" AND ",$where):"")." ORDER BY id DESC LIMIT $limit OFFSET $offset"; $st = db()->prepare($sql); $st->execute($args); echo json_encode(['ok'=>true,'items'=>$st->fetchAll()],JSON_UNESCAPED_UNICODE); exit; } if ($action === 'adminGetMovie' && is_admin()) { $id = (int)($_GET['id'] ?? 0); $m = get_movie($id); if (!$m){ echo json_encode(['ok'=>false]); exit; } $eps = ($m['type']==='series') ? get_series_episodes($id) : []; echo json_encode(['ok'=>true,'movie'=>$m,'episodes'=>$eps],JSON_UNESCAPED_UNICODE); exit; } if ($action === 'adminUpdateMovie' && is_admin() && $_SERVER['REQUEST_METHOD']==='POST') { csrf_check(); $p = $_POST; $id = (int)$p['id']; $stmt = db()->prepare("UPDATE movies SET type=?, title=?, title_en=?, description=?, description_en=?, genre=?, year=?, duration=?, quality=?, language=?, poster=?, backdrop=?, is_featured=?, updated_at=? WHERE id=?"); $stmt->execute([ ($p['type']==='series'?'series':'movie'), trim($p['title']), trim($p['title_en']), trim($p['description']), trim($p['description_en']), trim($p['genre']), (int)$p['year'], (int)$p['duration'], trim($p['quality']), trim($p['language']), trim($p['poster']), trim($p['backdrop']), !empty($p['is_featured'])?1:0, now(), $id ]); log_action('adminUpdateMovie', json_encode(['id'=>$id],JSON_UNESCAPED_UNICODE)); echo json_encode(['ok'=>true]); exit; } if ($action === 'adminDeleteMovie' && is_admin() && $_SERVER['REQUEST_METHOD']==='POST') { csrf_check(); $id = (int)($_POST['id'] ?? 0); // سيحذف كل ما يرتبط به بفعل قيود Foreign Keys (ON DELETE CASCADE) $stmt = db()->prepare("DELETE FROM movies WHERE id=?"); $stmt->execute([$id]); log_action('adminDeleteMovie', json_encode(['id'=>$id],JSON_UNESCAPED_UNICODE)); echo json_encode(['ok'=>true]); exit; } if ($action === 'adminUpdateEpisode' && is_admin() && $_SERVER['REQUEST_METHOD']==='POST') { csrf_check(); $p = $_POST; $id = (int)$p['id']; $stmt = db()->prepare("UPDATE episodes SET season=?, episode=?, title=?, title_en=?, duration=?, mp4_src=?, sources_json=? WHERE id=?"); $stmt->execute([(int)$p['season'], (int)$p['episode'], trim($p['title']), trim($p['title_en']), (int)$p['duration'], trim($p['mp4_src']), $p['sources_json'] ?: '[]', $id]); log_action('adminUpdateEpisode', json_encode(['id'=>$id],JSON_UNESCAPED_UNICODE)); echo json_encode(['ok'=>true]); exit; } if ($action === 'adminDeleteEpisode' && is_admin() && $_SERVER['REQUEST_METHOD']==='POST') { csrf_check(); $id = (int)($_POST['id'] ?? 0); db()->prepare("DELETE FROM episodes WHERE id=?")->execute([$id]); log_action('adminDeleteEpisode', json_encode(['id'=>$id],JSON_UNESCAPED_UNICODE)); echo json_encode(['ok'=>true]); exit; }
هذه الإجراءات تضيف: قائمة المحتوى، جلب عنصر واحد، تعديل فيلم/مسلسل، حذف فيلم/مسلسل، تعديل حلقة، حذف حلقة.
ابحث عن الكتلة التي تبدأ بـ:
phpif ($route === 'admin' && is_admin()) { render_header('لوحة التحكم'); echo '<div class="admin-grid">'; ...
واستبدلها كلّها بالمقطع التالي:
phpif ($route === 'admin' && is_admin()) { render_header('لوحة التحكم'); $csrf = esc($_SESSION['csrf']); echo '<div class="admin-grid">'; /* بطاقة: إضافة فيلم/مسلسل (تبقى كما هي) */ echo '<div class="card" style="padding:12px"><h3>إضافة فيلم/مسلسل</h3><form id="addMovie">'; echo '<select name="type" class="btn"><option value="movie">فيلم</option><option value="series">مسلسل</option></select> '; echo '<input name="title" placeholder="العنوان (عربي)" class="btn"> '; echo '<input name="title_en" placeholder="العنوان (إنجليزي)" class="btn"> '; echo '<input name="genre" placeholder="التصنيف" class="btn"> '; echo '<input name="year" type="number" placeholder="السنة" class="btn"> '; echo '<input name="duration" type="number" placeholder="المدة بالدقائق" class="btn"> '; echo '<select name="quality" class="btn"><option>480p</option><option selected>720p</option><option>1080p</option><option>4K</option></select> '; echo '<input name="language" placeholder="اللغة (ar,en...)" class="btn"> '; echo '<input name="poster" placeholder="رابط بوستر" class="btn" style="width:100%"> '; echo '<input name="backdrop" placeholder="رابط خلفية / أو فيديو للأفلام" class="btn" style="width:100%"> '; echo '<textarea name="description" placeholder="الوصف (عربي)" class="btn" style="width:100%;height:70px"></textarea>'; echo '<textarea name="description_en" placeholder="الوصف (إنجليزي)" class="btn" style="width:100%;height:70px"></textarea>'; echo '<label><input type="checkbox" name="is_featured"> مميز</label>'; echo '<input type="hidden" name="csrf" value="'.$csrf.'">'; echo '<div><button class="btn primary">حفظ</button></div></form></div>'; /* بطاقة: إضافة حلقة (كما كانت) */ echo '<div class="card" style="padding:12px"><h3>إضافة حلقة لمسلسل</h3><form id="addEpisode">'; echo '<input name="movie_id" type="number" placeholder="ID المسلسل" class="btn"> '; echo '<input name="season" type="number" placeholder="الموسم" class="btn"> '; echo '<input name="episode" type="number" placeholder="الحلقة" class="btn"> '; echo '<input name="title" placeholder="عنوان الحلقة (ع)" class="btn"> '; echo '<input name="title_en" placeholder="عنوان الحلقة (EN)" class="btn"> '; echo '<input name="duration" type="number" placeholder="المدة بالدقائق" class="btn"> '; echo '<input name="mp4_src" placeholder="رابط افتراضي" class="btn" style="width:100%"> '; echo '<textarea name="sources_json" placeholder=\'JSON للجودات [{"label":"1080p","src":"...","type":"video/mp4"}]\' class="btn" style="width:100%;height:80px"></textarea>'; echo '<input type="hidden" name="csrf" value="'.$csrf.'">'; echo '<div><button class="btn primary">حفظ</button></div></form></div>'; /* بطاقة: إدارة المحتوى (جديد: تعديل/حذف) */ echo '<div class="card" style="padding:12px"><h3>إدارة المحتوى</h3> <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px"> <input id="f_q" class="btn" placeholder="ابحث بالعنوان"> <select id="f_type" class="btn"><option value="">الكل</option><option value="movie">أفلام</option><option value="series">مسلسلات</option></select> <button class="btn" id="btnSearch">بحث</button> </div> <div style="overflow:auto"> <table class="table" id="tblContent"> <thead><tr><th>ID</th><th>العنوان</th><th>النوع</th><th>سنة</th><th>جودة</th><th>تقييم</th><th>تحكم</th></tr></thead> <tbody></tbody> </table> </div> <div id="editBox" class="card" style="margin-top:12px; padding:10px; display:none"></div> </div>'; /* بطاقة: إدارة المستخدمين (كما كانت) */ echo '<div class="card" style="padding:12px"><h3>المستخدمون</h3><div id="users"></div></div>'; /* بطاقة: إشعار يدوي (كما كانت) */ echo '<div class="card" style="padding:12px"><h3>إرسال إشعار</h3><form id="notify"><input name="user_id" type="number" placeholder="User ID (فارغ = للجميع)" class="btn"> <input name="message_ar" placeholder="رسالة بالعربية" class="btn" style="width:100%"> <input name="message_en" placeholder="EN Message" class="btn" style="width:100%"> <input name="link" placeholder="رابط (اختياري)" class="btn" style="width:100%"><input type="hidden" name="csrf" value="'.$csrf.'"><div><button class="btn primary">إرسال</button></div></form></div>'; echo '</div>'; // grid echo <<<JS <script> /* ===== إضافة فيلم/مسلسل (كما كانت) ===== */ document.getElementById('addMovie').addEventListener('submit', async (e)=>{ e.preventDefault(); const fd=new FormData(e.target); const r=await fetch('?action=adminAddMovie',{method:'POST',body:fd}); const d=await r.json(); if(d.ok){ alert('تم إضافة العنصر ID='+d.id); loadContent(); } }); /* ===== إضافة حلقة (كما كانت) ===== */ document.getElementById('addEpisode').addEventListener('submit', async (e)=>{ e.preventDefault(); const fd=new FormData(e.target); const r=await fetch('?action=adminAddEpisode',{method:'POST',body:fd}); const d=await r.json(); if(d.ok){ alert('تم إضافة الحلقة ID='+d.id); if(window._editingId) editMovie(window._editingId); } }); /* ===== إدارة المحتوى: تحميل القائمة ===== */ const qEl=document.getElementById('f_q'); const tEl=document.getElementById('f_type'); document.getElementById('btnSearch').onclick = ()=> loadContent(); async function loadContent(){ const p=new URLSearchParams(); if(qEl.value) p.set('q',qEl.value); if(tEl.value) p.set('type',tEl.value); const r=await fetch('?action=adminListMovies&'+p.toString()); const d=await r.json(); const tb=document.querySelector('#tblContent tbody'); tb.innerHTML=''; if(!d.ok) return; d.items.forEach(m=>{ const tr=document.createElement('tr'); tr.innerHTML = '<td>'+m.id+'</td><td>'+ (m.title||m.title_en||'') +'</td><td>'+m.type+'</td><td>'+ (m.year||'') +'</td><td>'+ (m.quality||'') +'</td><td>'+ (m.rating_avg? m.rating_avg.toFixed(1):'-') +'</td>'+ '<td><button class="btn" onclick="editMovie('+m.id+')">تعديل</button> <button class="btn" onclick="delMovie('+m.id+')">حذف</button></td>'; tb.appendChild(tr); }); } loadContent(); /* ===== فتح محرر عنصر ===== */ window._editingId = null; async function editMovie(id){ window._editingId = id; const r=await fetch('?action=adminGetMovie&id='+id); const d=await r.json(); if(!d.ok) return; const m=d.movie, eps=d.episodes||[]; const box=document.getElementById('editBox'); box.style.display='block'; box.innerHTML = renderMovieForm(m) + (m.type==='series'? renderEpisodes(eps, m.id) : ''); attachMovieFormHandlers(); if(m.type==='series') attachEpisodeHandlers(); } window.editMovie = editMovie; /* ===== حذف عنصر ===== */ async function delMovie(id){ if(!confirm('حذف العنصر وجميع متعلقاته؟')) return; const r=await fetch('?action=adminDeleteMovie',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:'csrf={$csrf}&id='+id}); const d=await r.json(); if(d.ok){ alert('تم الحذف'); loadContent(); document.getElementById('editBox').style.display='none'; } } window.delMovie = delMovie; /* ===== قوالب ===== */ function renderMovieForm(m){ return \` <h4>تعديل: \${m.title||m.title_en} (ID: \${m.id})</h4> <form id="editMovieForm"> <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px"> <input name="id" type="hidden" value="\${m.id}"> <input name="csrf" type="hidden" value="{$csrf}"> <label class="small">النوع <select name="type" class="btn"> <option value="movie"\${m.type==='movie'?' selected':''}>فيلم</option> <option value="series"\${m.type==='series'?' selected':''}>مسلسل</option> </select> </label> <input name="title" class="btn" placeholder="العنوان (ع)" value="\${m.title||''}"> <input name="title_en" class="btn" placeholder="العنوان (EN)" value="\${m.title_en||''}"> <input name="genre" class="btn" placeholder="التصنيف" value="\${m.genre||''}"> <input name="year" type="number" class="btn" placeholder="السنة" value="\${m.year||''}"> <input name="duration" type="number" class="btn" placeholder="المدة بالدقائق" value="\${m.duration||0}"> <input name="quality" class="btn" placeholder="الجودة" value="\${m.quality||''}"> <input name="language" class="btn" placeholder="اللغة" value="\${m.language||''}"> <input name="poster" class="btn" placeholder="بوستر URL" value="\${m.poster||''}" style="grid-column:1/-1"> <input name="backdrop" class="btn" placeholder="خلفية / مصدر فيديو للأفلام" value="\${m.backdrop||''}" style="grid-column:1/-1"> <textarea name="description" class="btn" style="grid-column:1/-1;height:70px" placeholder="الوصف (ع)">\${m.description||''}</textarea> <textarea name="description_en" class="btn" style="grid-column:1/-1;height:70px" placeholder="الوصف (EN)">\${m.description_en||''}</textarea> <label style="grid-column:1/-1"><input type="checkbox" name="is_featured"\${m.is_featured? ' checked':''}> مميز</label> </div> <div style="margin-top:8px"><button class="btn primary">حفظ التغييرات</button></div> </form>\`; } function renderEpisodes(eps, movieId){ let rows = eps.map(e => \` <tr> <td>\${e.id}</td> <td><input class="btn" name="season" value="\${e.season}" style="width:70px"></td> <td><input class="btn" name="episode" value="\${e.episode}" style="width:70px"></td> <td><input class="btn" name="title" value="\${e.title||''}" style="min-width:180px"></td> <td><input class="btn" name="title_en" value="\${e.title_en||''}" style="min-width:180px"></td> <td><input class="btn" name="duration" value="\${e.duration||0}" style="width:90px"></td> <td><input class="btn" name="mp4_src" value="\${e.mp4_src||''}" style="min-width:240px"></td> <td><textarea class="btn" name="sources_json" style="min-width:240px;height:48px">\${e.sources_json||'[]'}</textarea></td> <td> <button class="btn" data-epid="\${e.id}" data-op="save">حفظ</button> <button class="btn" data-epid="\${e.id}" data-op="delete">حذف</button> </td> </tr>\`).join(''); return \` <h4>حلقات المسلسل</h4> <div style="overflow:auto"> <table class="table" id="tblEps"> <thead><tr><th>ID</th><th>م</th><th>ح</th><th>عنوان (ع)</th><th>عنوان (EN)</th><th>مدة</th><th>mp4</th><th>جودات JSON</th><th>تحكم</th></tr></thead> <tbody>\${rows}</tbody> </table></div> <div class="small">لإضافة حلقة جديدة سريعًا استخدم بطاقة "إضافة حلقة" بالأعلى (أدخل ID=\${movieId}) ثم أعد تحميل التحرير.</div>\`; } /* ===== تعامل مع حفظ الفيلم ===== */ function attachMovieFormHandlers(){ const f = document.getElementById('editMovieForm'); f.addEventListener('submit', async (e)=>{ e.preventDefault(); const fd=new FormData(f); const r=await fetch('?action=adminUpdateMovie',{method:'POST',body:fd}); const d=await r.json(); if(d.ok){ alert('تم الحفظ'); loadContent(); } }); } /* ===== تعامل مع حفظ/حذف الحلقات ===== */ function attachEpisodeHandlers(){ document.querySelectorAll('#tblEps button').forEach(btn=>{ btn.addEventListener('click', async ()=>{ const id = btn.getAttribute('data-epid'); const tr = btn.closest('tr'); if(btn.getAttribute('data-op')==='save'){ const fd=new URLSearchParams(); fd.set('csrf','{$csrf}'); fd.set('id', id); fd.set('season', tr.querySelector('[name=season]').value); fd.set('episode', tr.querySelector('[name=episode]').value); fd.set('title', tr.querySelector('[name=title]').value); fd.set('title_en', tr.querySelector('[name=title_en]').value); fd.set('duration', tr.querySelector('[name=duration]').value); fd.set('mp4_src', tr.querySelector('[name=mp4_src]').value); fd.set('sources_json', tr.querySelector('[name=sources_json]').value||'[]'); const r=await fetch('?action=adminUpdateEpisode',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:fd.toString()}); const d=await r.json(); if(d.ok){ alert('تم حفظ الحلقة'); } } else { if(!confirm('حذف هذه الحلقة؟')) return; const r=await fetch('?action=adminDeleteEpisode',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:'csrf={$csrf}&id='+id}); const d=await r.json(); if(d.ok){ alert('تم الحذف'); editMovie(window._editingId); } } }); }); } /* ===== إدارة المستخدمين (كما كانت) ===== */ async function loadUsers(){ const r=await fetch('?action=adminUsers'); const d=await r.json(); if(!d.ok) return; const box=document.getElementById('users'); box.innerHTML=''; const t=document.createElement('table'); t.className='table'; t.innerHTML='<tr><th>ID</th><th>اسم</th><th>بريد</th><th>الدور</th><th>الحالة</th><th>تحكم</th></tr>'; d.users.forEach(u=>{ const tr=document.createElement('tr'); tr.innerHTML='<td>'+u.id+'</td><td>'+u.username+'</td><td>'+u.email+'</td>'+ '<td>'+u.role+'</td><td>'+u.status+'</td>'+ '<td>'+ '<button class="btn" onclick="userOp('+u.id+',\\'approve\\')">قبول</button> '+ '<button class="btn" onclick="userOp('+u.id+',\\'reject\\')">رفض</button> '+ '<button class="btn" onclick="banUser('+u.id+')">حظر</button> '+ '<button class="btn" onclick="setRole('+u.id+')">تغيير الدور</button>'+ '</td>'; t.appendChild(tr); }); box.appendChild(t); } loadUsers(); async function userOp(id,op){ const fd=new URLSearchParams(); fd.set('id',id); fd.set('op',op); fd.set('csrf','{$csrf}'); const r=await fetch('?action=adminUsers',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:fd.toString()}); const d=await r.json(); if(d.ok) loadUsers(); } async function banUser(id){ const reason=prompt('سبب الحظر:')||''; const until=prompt('حتى تاريخ (YYYY-MM-DD):')||''; const fd=new URLSearchParams(); fd.set('id',id); fd.set('op','ban'); fd.set('reason',reason); fd.set('until',until); fd.set('csrf','{$csrf}'); const r=await fetch('?action=adminUsers',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:fd.toString()}); const d=await r.json(); if(d.ok) loadUsers(); } async function setRole(id){ const role=prompt('ادخل الدور: Admin/Editor/Moderator/User','User'); if(!role) return; const fd=new URLSearchParams(); fd.set('id',id); fd.set('op','role'); fd.set('role',role); fd.set('csrf','{$csrf}'); const r=await fetch('?action=adminUsers',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:fd.toString()}); const d=await r.json(); if(d.ok) loadUsers(); } /* ===== إشعار يدوي (كما كانت) ===== */ document.getElementById('notify').addEventListener('submit', async (e)=>{ e.preventDefault(); const fd=new FormData(e.target); const r=await fetch('?action=adminNotify',{method:'POST',body:fd}); const d=await r.json(); if(d.ok) alert('تم الإرسال'); }); </script> JS; render_footer(); exit; }
PRAGMA foreign_keys = ON;
) وبالتالي سيحذف تلقائيًا كل ما يتعلق بالعنصر (حلقات، تقييمات، تعليقات، قائمات… إلخ).Admin/Editor/Moderator
(نفس شرط is_admin()
).إذا رغبت، أقدر أرسل لك الملف كاملًا مدموجًا بهذه التعديلات في رد واحد.
قم بتحسين مشغل الفيديو و بطاقة الافلام والمسلسلات ليبدو اكثر احترافية و أرسل لي الملف كاملًا مدموجًا بهذه التعديلات في رد واحد
اكمل الكود رجاءا
php<?php /** * index.php — منصة بث احترافية (ملف واحد شامل) * PHP (vanilla) + SQLite (افتراضي) / MySQL (اختياري) + HTML/CSS/JS * النسخة تتضمن: * - إنشاء تلقائي للمجلدات والجداول (أول تشغيل) + عينات movies.json * - Router داخلي (?route=...) و API (?action=...) * - أفلام/مسلسلات/حلقات + إدارة كاملة (إضافة/تعديل/حذف) من لوحة التحكم * - مشغل فيديو احترافي مُحسَّن: تحكمات مخصصة، سرعات، جودة متعددة، ملء شاشة، وضع سينما، PiP، شريط تقدم مع Buffer، حفظ واستئناف، Auto-Next * - إشعارات داخلية + متابعة المسلسلات + قائمة مشاهدة + تقييمات وتعليقات * - بحث متقدم + إحصاءات المستخدم + ترشيحات مبسطة (قابلة للتوسعة) * - أمان: PDO + Prepared, CSRF, password_hash, HTML escape, Lazy Loading, GZIP */ /* =========================[ الإعدادات العامة ]========================= */ // فعّل MySQL (true) أو استخدم SQLite (false) const USE_MYSQL = false; const MYSQL_DSN = 'mysql:host=localhost;dbname=streamapp;charset=utf8mb4'; const MYSQL_USER = 'root'; const MYSQL_PASS = 'root'; // اسم ملف SQLite const SQLITE_FILE = __DIR__ . '/db/app.db'; // مسارات أساسية const DIR_DB = __DIR__ . '/db'; const DIR_MOVIES = __DIR__ . '/movies'; const DIR_UPLOADS = __DIR__ . '/uploads'; const DIR_VIDEOS = __DIR__ . '/uploads/videos'; // للفيديوهات المحلية (اختياري) const DIR_POSTERS = __DIR__ . '/uploads/posters'; const DIR_BACKS = __DIR__ . '/uploads/backdrops'; // ضبط المنطقة الزمنية date_default_timezone_set('UTC'); // تفعيل GZIP إن وُجد if (extension_loaded('zlib') && !ini_get('zlib.output_compression')) { @ob_start('ob_gzhandler'); } /* =========================[ الجلسات والأمان ]========================= */ session_start(); // CSRF if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(16)); } function csrf_field() { echo '<input type="hidden" name="csrf" value="'.esc($_SESSION['csrf']).'">'; } function csrf_check() { if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!isset($_POST['csrf']) || !hash_equals($_SESSION['csrf'], $_POST['csrf'])) { http_response_code(403); exit('CSRF token mismatch.'); } } } /* =========================[ أدوات مساعدة ]========================= */ function esc($s){ return htmlspecialchars($s ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function now(){ return date('Y-m-d H:i:s'); } function is_logged(){ return !empty($_SESSION['user']); } function user(){ return $_SESSION['user'] ?? null; } function is_admin(){ $u = user(); return $u && in_array($u['role'], ['Admin','Editor','Moderator']); } function is_role($roles){ $u=user(); return $u && in_array($u['role'], (array)$roles); } function device_name(){ $ua = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown'; if (stripos($ua,'Android')!==false) return 'Android'; if (stripos($ua,'iPhone')!==false) return 'iPhone'; if (stripos($ua,'iPad')!==false) return 'iPad'; if (stripos($ua,'Windows')!==false) return 'Windows'; if (stripos($ua,'Mac OS')!==false) return 'macOS'; if (stripos($ua,'Linux')!==false) return 'Linux'; return 'جهاز'; } /* =========================[ قاعدة البيانات ]========================= */ function db(){ static $pdo; if ($pdo) return $pdo; if (USE_MYSQL) { $pdo = new PDO(MYSQL_DSN, MYSQL_USER, MYSQL_PASS, [ PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC, ]); } else { $pdo = new PDO('sqlite:'.SQLITE_FILE, null, null, [ PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC, ]); $pdo->exec('PRAGMA foreign_keys = ON;'); $pdo->exec('PRAGMA journal_mode = WAL;'); } return $pdo; } function ensure_dirs_and_db(){ // مجلدات @is_dir(DIR_DB) || @mkdir(DIR_DB, 0775, true); @is_dir(DIR_MOVIES) || @mkdir(DIR_MOVIES, 0775, true); @is_dir(DIR_UPLOADS) || @mkdir(DIR_UPLOADS, 0775, true); @is_dir(DIR_VIDEOS) || @mkdir(DIR_VIDEOS, 0775, true); @is_dir(DIR_POSTERS) || @mkdir(DIR_POSTERS, 0775, true); @is_dir(DIR_BACKS) || @mkdir(DIR_BACKS, 0775, true); // قاعدة البيانات + الجداول $pdo = db(); $pdo->exec("CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, email TEXT UNIQUE, password_hash TEXT, role TEXT DEFAULT 'User', status TEXT DEFAULT 'pending', -- pending/approved/rejected/banned ban_reason TEXT, banned_until TEXT, created_at TEXT )"); $pdo->exec("CREATE TABLE IF NOT EXISTS movies ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT CHECK(type IN ('movie','series')) NOT NULL, title TEXT, title_en TEXT, description TEXT, description_en TEXT, genre TEXT, year INTEGER, duration INTEGER, -- بالدقائق للأفلام، افتراضي quality TEXT, -- 480p/720p/1080p/4K language TEXT, poster TEXT, backdrop TEXT, is_featured INTEGER DEFAULT 0, rating_avg REAL DEFAULT 0, rating_count INTEGER DEFAULT 0, created_at TEXT, updated_at TEXT )"); $pdo->exec("CREATE TABLE IF NOT EXISTS episodes ( id INTEGER PRIMARY KEY AUTOINCREMENT, movie_id INTEGER, season INTEGER, episode INTEGER, title TEXT, title_en TEXT, duration INTEGER, mp4_src TEXT, sources_json TEXT, created_at TEXT, FOREIGN KEY(movie_id) REFERENCES movies(id) ON DELETE CASCADE )"); $pdo->exec("CREATE TABLE IF NOT EXISTS ratings ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, movie_id INTEGER, rating INTEGER CHECK(rating BETWEEN 1 AND 5), created_at TEXT, updated_at TEXT, UNIQUE(user_id, movie_id), FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(movie_id) REFERENCES movies(id) ON DELETE CASCADE )"); $pdo->exec("CREATE TABLE IF NOT EXISTS comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, movie_id INTEGER, comment TEXT, created_at TEXT, updated_at TEXT, is_deleted INTEGER DEFAULT 0, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(movie_id) REFERENCES movies(id) ON DELETE CASCADE )"); $pdo->exec("CREATE TABLE IF NOT EXISTS watchlist ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, movie_id INTEGER, created_at TEXT, UNIQUE(user_id, movie_id), FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(movie_id) REFERENCES movies(id) ON DELETE CASCADE )"); $pdo->exec("CREATE TABLE IF NOT EXISTS watch_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, movie_id INTEGER, episode_id INTEGER, position REAL DEFAULT 0, device TEXT, last_watched_at TEXT, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(movie_id) REFERENCES movies(id) ON DELETE CASCADE, FOREIGN KEY(episode_id) REFERENCES episodes(id) ON DELETE CASCADE )"); $pdo->exec("CREATE TABLE IF NOT EXISTS notifications ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, message_ar TEXT, message_en TEXT, link TEXT, is_read INTEGER DEFAULT 0, created_at TEXT, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE )"); $pdo->exec("CREATE TABLE IF NOT EXISTS follows ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, movie_id INTEGER, -- للسلاسل فقط UNIQUE(user_id, movie_id), FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(movie_id) REFERENCES movies(id) ON DELETE CASCADE )"); $pdo->exec("CREATE TABLE IF NOT EXISTS activity_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, action TEXT, details TEXT, created_at TEXT, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE SET NULL )"); // ملف JSON بعينات $sampleFile = DIR_MOVIES . '/movies.json'; if (!file_exists($sampleFile)) { $samples = [ [ "type"=>"movie", "title"=>"الفيلم التجريبي", "title_en"=>"Sample Movie", "description"=>"وصف عربي للفيلم التجريبي.", "description_en"=>"English description of sample movie.", "genre"=>"دراما", "year"=>2024, "duration"=>110, "quality"=>"1080p", "language"=>"ar", "poster"=>"", "backdrop"=>"" ], [ "type"=>"series", "title"=>"المسلسل التجريبي", "title_en"=>"Sample Series", "description"=>"سلسلة تجريبية بموسم واحد وثلاث حلقات.", "description_en"=>"A sample series with one season and three episodes.", "genre"=>"إثارة", "year"=>2025, "duration"=>0, "quality"=>"720p", "language"=>"ar", "poster"=>"", "backdrop"=>"" ] ]; file_put_contents($sampleFile, json_encode($samples, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT)); } } ensure_dirs_and_db(); /* =========================[ تسجيل نشاط ]========================= */ function log_action($action, $details){ $u = user(); $stmt = db()->prepare("INSERT INTO activity_log(user_id, action, details, created_at) VALUES (?,?,?,?)"); $stmt->execute([$u['id']??null, $action, $details, now()]); } /* =========================[ إدخالات أولية ]========================= */ function maybe_seed(){ $pdo = db(); $cnt = (int)$pdo->query("SELECT COUNT(*) FROM movies")->fetchColumn(); if ($cnt===0){ // فيلم $pdo->prepare("INSERT INTO movies(type,title,title_en,description,description_en,genre,year,duration,quality,language,poster,backdrop,is_featured,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)")->execute( ['movie','الفيلم التجريبي','Sample Movie','وصف عربي للفيلم التجريبي.','English description.','دراما',2024,110,'1080p','ar','','',1,now(),now()] ); $movieId = (int)$pdo->lastInsertId(); // مسلسل $pdo->prepare("INSERT INTO movies(type,title,title_en,description,description_en,genre,year,duration,quality,language,poster,backdrop,is_featured,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)")->execute( ['series','المسلسل التجريبي','Sample Series','سلسلة تجريبية بموسم واحد وثلاث حلقات.','Sample series.','إثارة',2025,0,'720p','ar','','',1,now(),now()] ); $seriesId = (int)$pdo->lastInsertId(); // حلقات $ep = $pdo->prepare("INSERT INTO episodes(movie_id,season,episode,title,title_en,duration,mp4_src,sources_json,created_at) VALUES(?,?,?,?,?,?,?,?,?)"); for($i=1;$i<=3;$i++){ $sources = [ ["label"=>"720p","src"=>"https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4","type"=>"video/mp4"], ["label"=>"480p","src"=>"https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/480/Big_Buck_Bunny_480_10s_1MB.mp4","type"=>"video/mp4"] ]; $ep->execute([$seriesId,1,$i,"حلقة $i","Episode $i",10,$sources[0]['src'],json_encode($sources,JSON_UNESCAPED_UNICODE),now()]); } } } maybe_seed(); /* =========================[ إشعارات ]========================= */ function notify($user_id, $ar, $en, $link=null){ $stmt = db()->prepare("INSERT INTO notifications(user_id, message_ar, message_en, link, is_read, created_at) VALUES (?,?,?,?,0,?)"); $stmt->execute([$user_id, $ar, $en, $link, now()]); } function notify_all($ar,$en,$link=null){ notify(null, $ar,$en,$link); } /* =========================[ العمليات (API) ]========================= */ if (isset($_GET['action'])) { header('X-Content-Type-Options: nosniff'); header('Content-Type: application/json; charset=utf-8'); $action = $_GET['action']; /* ---------- auth ---------- */ if ($action === 'login' && $_SERVER['REQUEST_METHOD']==='POST') { csrf_check(); $email = trim($_POST['email']??''); $pass = $_POST['password']??''; $stmt = db()->prepare("SELECT * FROM users WHERE email = ?"); $stmt->execute([$email]); $u = $stmt->fetch(); if ($u && password_verify($pass, $u['password_hash'])) { if ($u['status']==='approved') { $_SESSION['user'] = $u; echo json_encode(['ok'=>true]); exit; } else { echo json_encode(['ok'=>false,'error'=>'حسابك غير مُفعّل بعد.']); exit; } } echo json_encode(['ok'=>false,'error'=>'بيانات الدخول غير صحيحة']); exit; } if ($action === 'register' && $_SERVER['REQUEST_METHOD']==='POST') { csrf_check(); $username = trim($_POST['username']??''); $email = trim($_POST['email']??''); $pass = $_POST['password']??''; if (!$username || !$email || !$pass) { echo json_encode(['ok'=>false,'error'=>'الرجاء إدخال جميع الحقول']); exit; } $hash = password_hash($pass, PASSWORD_DEFAULT); try { // أول حساب يصبح Admin تلقائيًا $cnt = (int)db()->query("SELECT COUNT(*) FROM users")->fetchColumn(); $role = $cnt===0 ? 'Admin' : 'User'; $status = $cnt===0 ? 'approved' : 'pending'; $stmt = db()->prepare("INSERT INTO users(username,email,password_hash,role,status,created_at) VALUES(?,?,?,?,?,?)"); $stmt->execute([$username,$email,$hash,$role,$status,now()]); $id = (int)db()->lastInsertId(); notify_all("تسجيل مستخدم جديد: $username","New user registered: $username","?route=admin&tab=users"); echo json_encode(['ok'=>true,'pending'=>$status!=='approved']); exit; } catch(Exception $e){ echo json_encode(['ok'=>false,'error'=>'البريد أو الاسم مستخدم مسبقًا']); exit; } } if ($action === 'logout' && $_SERVER['REQUEST_METHOD']==='POST') { csrf_check(); unset($_SESSION['user']); echo json_encode(['ok'=>true]); exit; } /* ---------- القوائم/التفاعل ---------- */ if ($action === 'toggleWatchlist' && is_logged()) { csrf_check(); $movie_id = (int)($_POST['movie_id']??0); $pdo = db(); $stmt = $pdo->prepare("SELECT id FROM watchlist WHERE user_id=? AND movie_id=?"); $stmt->execute([user()['id'],$movie_id]); $r = $stmt->fetch(); if ($r){ $pdo->prepare("DELETE FROM watchlist WHERE id=?")->execute([$r['id']]); echo json_encode(['ok'=>true,'in'=>false]); exit; } else { $pdo->prepare("INSERT INTO watchlist(user_id,movie_id,created_at) VALUES(?,?,?)")->execute([user()['id'],$movie_id,now()]); echo json_encode(['ok'=>true,'in'=>true]); exit; } } if ($action === 'rate' && is_logged()) { csrf_check(); $movie_id = (int)($_POST['movie_id']??0); $rating = max(1,min(5,(int)($_POST['rating']??0))); $pdo = db(); $exists = $pdo->prepare("SELECT id FROM ratings WHERE user_id=? AND movie_id=?"); $exists->execute([user()['id'],$movie_id]); if ($exists->fetch()){ $pdo->prepare("UPDATE ratings SET rating=?, updated_at=? WHERE user_id=? AND movie_id=?") ->execute([$rating, now(), user()['id'],$movie_id]); } else { $pdo->prepare("INSERT INTO ratings(user_id,movie_id,rating,created_at,updated_at) VALUES(?,?,?,?,?)") ->execute([user()['id'],$movie_id,$rating,now(),now()]); } $row = $pdo->query("SELECT AVG(rating) as avg, COUNT(*) as cnt FROM ratings WHERE movie_id=$movie_id")->fetch(); $pdo->prepare("UPDATE movies SET rating_avg=?, rating_count=?, updated_at=? WHERE id=?") ->execute([$row['avg'],$row['cnt'],now(),$movie_id]); echo json_encode(['ok'=>true,'avg'=>round($row['avg'],2),'count'=>(int)$row['cnt']]); exit; } if ($action === 'comment' && is_logged()) { csrf_check(); $movie_id = (int)($_POST['movie_id']??0); $text = trim($_POST['text']??''); if (!$text){ echo json_encode(['ok'=>false,'error'=>'اكتب تعليقًا.']); exit; } db()->prepare("INSERT INTO comments(user_id,movie_id,comment,created_at,updated_at) VALUES(?,?,?,?,?)") ->execute([user()['id'],$movie_id,$text,now(),now()]); echo json_encode(['ok'=>true]); exit; } if ($action === 'commentEdit' && is_logged()) { csrf_check(); $id = (int)($_POST['id']??0); $text = trim($_POST['text']??''); $c = db()->prepare("SELECT * FROM comments WHERE id=?"); $c->execute([$id]); $row=$c->fetch(); if (!$row) { echo json_encode(['ok'=>false]); exit; } if ($row['user_id']!=user()['id'] && !is_admin()) { echo json_encode(['ok'=>false]); exit; } db()->prepare("UPDATE comments SET comment=?, updated_at=? WHERE id=?")->execute([$text,now(),$id]); echo json_encode(['ok'=>true]); exit; } if ($action === 'commentDelete' && is_logged()) { csrf_check(); $id = (int)($_POST['id']??0); $c = db()->prepare("SELECT * FROM comments WHERE id=?"); $c->execute([$id]); $row=$c->fetch(); if (!$row) { echo json_encode(['ok'=>false]); exit; } if ($row['user_id']!=user()['id'] && !is_admin()) { echo json_encode(['ok'=>false]); exit; } db()->prepare("UPDATE comments SET is_deleted=1, updated_at=? WHERE id=?")->execute([now(),$id]); echo json_encode(['ok'=>true]); exit; } if ($action === 'follow' && is_logged()) { csrf_check(); $movie_id = (int)($_POST['movie_id']??0); $pdo = db(); $ex = $pdo->prepare("SELECT id FROM follows WHERE user_id=? AND movie_id=?"); $ex->execute([user()['id'],$movie_id]); if ($ex->fetch()){ $pdo->prepare("DELETE FROM follows WHERE user_id=? AND movie_id=?")->execute([user()['id'],$movie_id]); echo json_encode(['ok'=>true,'following'=>false]); exit; } else { $pdo->prepare("INSERT INTO follows(user_id,movie_id) VALUES(?,?)")->execute([user()['id'],$movie_id]); echo json_encode(['ok'=>true,'following'=>true]); exit; } } if ($action === 'saveProgress' && is_logged()) { $movie_id = (int)($_POST['movie_id']??0); $episode_id = (int)($_POST['episode_id']??0); $position = (float)($_POST['position']??0); $pdo = db(); $row = $pdo->prepare("SELECT id FROM watch_history WHERE user_id=? AND movie_id=? AND episode_id ".($episode_id?'= ?':'IS NULL')); $row->execute($episode_id ? [user()['id'],$movie_id,$episode_id] : [user()['id'],$movie_id]); if ($row->fetch()){ $pdo->prepare("UPDATE watch_history SET position=?, device=?, last_watched_at=? WHERE user_id=? AND movie_id=? AND ".($episode_id?'episode_id=?':'episode_id IS NULL')) ->execute($episode_id ? [$position, device_name(), now(), user()['id'],$movie_id,$episode_id] : [$position, device_name(), now(), user()['id'],$movie_id]); } else { $pdo->prepare("INSERT INTO watch_history(user_id,movie_id,episode_id,position,device,last_watched_at) VALUES (?,?,?,?,?,?)") ->execute([user()['id'],$movie_id, $episode_id?:null, $position, device_name(), now()]); } echo json_encode(['ok'=>true]); exit; } if ($action === 'getProgress' && is_logged()) { $movie_id = (int)($_GET['movie_id']??0); $episode_id = (int)($_GET['episode_id']??0); $pdo = db(); $stmt = $pdo->prepare("SELECT position FROM watch_history WHERE user_id=? AND movie_id=? AND (episode_id ".($episode_id?'= ?':'IS NULL').") ORDER BY last_watched_at DESC LIMIT 1"); $stmt->execute($episode_id ? [user()['id'],$movie_id,$episode_id] : [user()['id'],$movie_id]); $pos = $stmt->fetchColumn(); echo json_encode(['ok'=>true,'position'=>floatval($pos)]); exit; } if ($action === 'notifications') { $uid = user()['id'] ?? null; if ($uid){ $stmt = db()->prepare("SELECT * FROM notifications WHERE (user_id IS NULL OR user_id=?) ORDER BY id DESC LIMIT 20"); $stmt->execute([$uid]); } else { $stmt = db()->query("SELECT * FROM notifications WHERE user_id IS NULL ORDER BY id DESC LIMIT 10"); } echo json_encode(['ok'=>true,'items'=>$stmt->fetchAll()]); exit; } if ($action === 'markRead' && is_logged()) { $id = (int)($_POST['id']??0); db()->prepare("UPDATE notifications SET is_read=1 WHERE id=?")->execute([$id]); echo json_encode(['ok'=>true]); exit; } if ($action === 'searchAdvanced') { $pdo = db(); $where = []; $args = []; if (!empty($_GET['type'])) { $where[] = "type = ?"; $args[] = $_GET['type']==='series'?'series':'movie'; } if (!empty($_GET['genre'])){ $where[] = "genre LIKE ?"; $args[] = '%'.$_GET['genre'].'%'; } if (!empty($_GET['year'])) { $where[] = "year = ?"; $args[] = (int)$_GET['year']; } if (!empty($_GET['rating'])){ $where[] = "rating_avg >= ?"; $args[] = (float)$_GET['rating']; } if (!empty($_GET['quality'])){ $where[] = "quality = ?"; $args[] = $_GET['quality']; } if (!empty($_GET['language'])){ $where[] = "language = ?"; $args[] = $_GET['language']; } if (!empty($_GET['q'])){ $where[] = "(title LIKE ? OR title_en LIKE ?)"; $args[] = '%'.$_GET['q'].'%'; $args[] = '%'.$_GET['q'].'%'; } $order = "created_at DESC"; if (!empty($_GET['sort'])){ $sort = $_GET['sort']; $order = match($sort){ 'rating' => "rating_avg DESC", 'year' => "year DESC", 'alpha' => "title COLLATE NOCASE ASC", default => "created_at DESC" }; } $sql = "SELECT * FROM movies ".(count($where)?"WHERE ".implode(" AND ",$where):"")." ORDER BY $order LIMIT 100"; $stmt = $pdo->prepare($sql); $stmt->execute($args); $rows = $stmt->fetchAll(); header('Cache-Control: no-store'); echo json_encode(['ok'=>true,'results'=>$rows], JSON_UNESCAPED_UNICODE); exit; } /* ---------- إدارة: إضافة ---------- */ if ($action === 'adminAddMovie' && is_admin()) { csrf_check(); $p = $_POST; $stmt = db()->prepare("INSERT INTO movies(type,title,title_en,description,description_en,genre,year,duration,quality,language,poster,backdrop,is_featured,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); $stmt->execute([ $p['type'], trim($p['title']), trim($p['title_en']), trim($p['description']), trim($p['description_en']), trim($p['genre']), (int)$p['year'], (int)$p['duration'], $p['quality'],$p['language'], trim($p['poster']), trim($p['backdrop']), !empty($p['is_featured'])?1:0, now(), now() ]); $id = (int)db()->lastInsertId(); notify_all("تم إضافة ".($p['type']==='series'?'مسلسل':'فيلم')." جديد: ".$p['title'], "New ".($p['type']==='series'?'series':'movie')." added: ".$p['title'], "?route=details&id=$id"); log_action('adminAddMovie', json_encode(['id'=>$id,'title'=>$p['title']],JSON_UNESCAPED_UNICODE)); echo json_encode(['ok'=>true,'id'=>$id]); exit; } if ($action === 'adminAddEpisode' && is_admin()) { csrf_check(); $p = $_POST; $sources = $p['sources_json'] ?: '[]'; $stmt = db()->prepare("INSERT INTO episodes(movie_id,season,episode,title,title_en,duration,mp4_src,sources_json,created_at) VALUES(?,?,?,?,?,?,?,?,?)"); $stmt->execute([(int)$p['movie_id'],(int)$p['season'],(int)$p['episode'], trim($p['title']), trim($p['title_en']), (int)$p['duration'], trim($p['mp4_src']), $sources, now()]); $id = (int)db()->lastInsertId(); // إشعار للمتابعين $title = db()->query("SELECT title FROM movies WHERE id=".(int)$p['movie_id'])->fetchColumn(); $followers = db()->query("SELECT user_id FROM follows WHERE movie_id=".(int)$p['movie_id'])->fetchAll(PDO::FETCH_COLUMN); foreach($followers as $uid){ notify($uid, "تمت إضافة حلقة جديدة لمسلسل $title", "New episode added to $title", "?route=watch&episode_id=$id"); } log_action('adminAddEpisode', json_encode(['episode_id'=>$id,'movie_id'=>(int)$p['movie_id']],JSON_UNESCAPED_UNICODE)); echo json_encode(['ok'=>true,'id'=>$id]); exit; } /* ===== [NEW] إدارة المحتوى: قائمة/جلب/تحديث/حذف أفلام/مسلسلات وحلقات ===== */ if ($action === 'adminListMovies' && is_admin()) { $q = trim($_GET['q'] ?? ''); $type = $_GET['type'] ?? ''; $limit = max(1, min(200, (int)($_GET['limit'] ?? 50))); $offset = max(0, (int)($_GET['offset'] ?? 0)); $where = []; $args=[]; if ($q !== '') { $where[] = "(title LIKE ? OR title_en LIKE ?)"; $args[] = "%$q%"; $args[] = "%$q%"; } if ($type === 'movie' || $type === 'series') { $where[] = "type = ?"; $args[] = $type; } $sql = "SELECT id,type,title,title_en,genre,year,quality,rating_avg,rating_count,created_at,updated_at FROM movies ".(count($where)?"WHERE ".implode(" AND ",$where):"")." ORDER BY id DESC LIMIT $limit OFFSET $offset"; $st = db()->prepare($sql); $st->execute($args); echo json_encode(['ok'=>true,'items'=>$st->fetchAll()],JSON_UNESCAPED_UNICODE); exit; } if ($action === 'adminGetMovie' && is_admin()) { $id = (int)($_GET['id'] ?? 0); $m = get_movie($id); if (!$m){ echo json_encode(['ok'=>false]); exit; } $eps = ($m['type']==='series') ? get_series_episodes($id) : []; echo json_encode(['ok'=>true,'movie'=>$m,'episodes'=>$eps],JSON_UNESCAPED_UNICODE); exit; } if ($action === 'adminUpdateMovie' && is_admin() && $_SERVER['REQUEST_METHOD']==='POST') { csrf_check(); $p = $_POST; $id = (int)$p['id']; $stmt = db()->prepare("UPDATE movies SET type=?, title=?, title_en=?, description=?, description_en=?, genre=?, year=?, duration=?, quality=?, language=?, poster=?, backdrop=?, is_featured=?, updated_at=? WHERE id=?"); $stmt->execute([ ($p['type']==='series'?'series':'movie'), trim($p['title']), trim($p['title_en']), trim($p['description']), trim($p['description_en']), trim($p['genre']), (int)$p['year'], (int)$p['duration'], trim($p['quality']), trim($p['language']), trim($p['poster']), trim($p['backdrop']), !empty($p['is_featured'])?1:0, now(), $id ]); log_action('adminUpdateMovie', json_encode(['id'=>$id],JSON_UNESCAPED_UNICODE)); echo json_encode(['ok'=>true]); exit; } if ($action === 'adminDeleteMovie' && is_admin() && $_SERVER['REQUEST_METHOD']==='POST') { csrf_check(); $id = (int)($_POST['id'] ?? 0); db()->prepare("DELETE FROM movies WHERE id=?")->execute([$id]); log_action('adminDeleteMovie', json_encode(['id'=>$id],JSON_UNESCAPED_UNICODE)); echo json_encode(['ok'=>true]); exit; } if ($action === 'adminUpdateEpisode' && is_admin() && $_SERVER['REQUEST_METHOD']==='POST') { csrf_check(); $p = $_POST; $id = (int)$p['id']; $stmt = db()->prepare("UPDATE episodes SET season=?, episode=?, title=?, title_en=?, duration=?, mp4_src=?, sources_json=? WHERE id=?"); $stmt->execute([(int)$p['season'], (int)$p['episode'], trim($p['title']), trim($p['title_en']), (int)$p['duration'], trim($p['mp4_src']), $p['sources_json'] ?: '[]', $id]); log_action('adminUpdateEpisode', json_encode(['id'=>$id],JSON_UNESCAPED_UNICODE)); echo json_encode(['ok'=>true]); exit; } if ($action === 'adminDeleteEpisode' && is_admin() && $_SERVER['REQUEST_METHOD']==='POST') { csrf_check(); $id = (int)($_POST['id'] ?? 0); db()->prepare("DELETE FROM episodes WHERE id=?")->execute([$id]); log_action('adminDeleteEpisode', json_encode(['id'=>$id],JSON_UNESCAPED_UNICODE)); echo json_encode(['ok'=>true]); exit; } /* ---------- البث المحلي مع Range ---------- */ if ($action === 'stream') { $path = $_GET['path'] ?? ''; $clean = realpath(__DIR__ . '/' . $path); $base = realpath(DIR_VIDEOS); if (!$clean || !str_starts_with($clean, $base)) { http_response_code(403); exit('Forbidden'); } if (!is_file($clean)) { http_response_code(404); exit('Not found'); } $size = filesize($clean); $start = 0; $end = $size - 1; header('Content-Type: video/mp4'); header('Accept-Ranges: bytes'); if (isset($_SERVER['HTTP_RANGE']) && preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $m)) { $start = intval($m[1]); $end = ($m[2] !== '') ? intval($m[2]) : $end; header("HTTP/1.1 206 Partial Content"); header("Content-Range: bytes $start-$end/$size"); } $length = $end - $start + 1; header("Content-Length: $length"); $fp = fopen($clean, 'rb'); fseek($fp, $start); $chunk = 8192; while(!feof($fp) && $length > 0){ $read = ($length > $chunk) ? $chunk : $length; echo fread($fp, $read); $length -= $read; @ob_flush(); flush(); } fclose($fp); exit; } echo json_encode(['ok'=>false,'error'=>'Unknown action']); exit; } /* =========================[ استعلامات عرض ]========================= */ function get_featured(){ return db()->query("SELECT * FROM movies WHERE is_featured=1 ORDER BY created_at DESC LIMIT 12")->fetchAll(); } function get_latest($type=null){ $sql = "SELECT * FROM movies ".($type?"WHERE type='".($type==='series'?'series':'movie')."'":"")." ORDER BY created_at DESC LIMIT 18"; return db()->query($sql)->fetchAll(); } function get_movie($id){ $stmt = db()->prepare("SELECT * FROM movies WHERE id=?"); $stmt->execute([$id]); return $stmt->fetch(); } function get_series_episodes($movie_id){ $stmt = db()->prepare("SELECT * FROM episodes WHERE movie_id=? ORDER BY season ASC, episode ASC"); $stmt->execute([$movie_id]); return $stmt->fetchAll(); } function get_episode($id){ $stmt = db()->prepare("SELECT * FROM episodes WHERE id=?"); $stmt->execute([$id]); return $stmt->fetch(); } function get_next_episode($movie_id,$season,$episode){ $stmt = db()->prepare("SELECT * FROM episodes WHERE movie_id=? AND (season>? OR (season=? AND episode>?)) ORDER BY season ASC, episode ASC LIMIT 1"); $stmt->execute([$movie_id,$season,$season,$episode]); return $stmt->fetch(); } /* =========================[ القالب العام (HTML/CSS/JS) ]========================= */ function render_header($title=''){ $u = user(); $rtl = 'dir="rtl" lang="ar"'; $csrf = esc($_SESSION['csrf']); echo <<<HTML <!doctype html> <html $rtl> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>سينماكس - {$title}</title> <meta name="description" content="منصة بث احترافية للأفلام والمسلسلات"> <style> :root{ --bg: #0b0e13; --bg-soft:#101521; --text:#E6E9EF; --muted:#A9B1BD; --primary:#5b8cff; --accent:#00d4aa; --card:#121828; --card-2:#0f1421; --border:#1f2633; --star:#ffcc00; --danger:#ef4444; --success:#22c55e; --warning:#f59e0b; } :root.light{ --bg:#f7fafc; --bg-soft:#ffffff; --text:#0b0e13; --muted:#4b5563; --card:#ffffff; --card-2:#f8fafc; --border:#e5e7eb; --primary:#3b82f6; --accent:#10b981; } *{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--text);font:16px/1.6 system-ui,-apple-system,Segoe UI,Roboto} a{color:inherit;text-decoration:none} img{max-width:100%;height:auto;display:block} .container{max-width:1200px;margin:auto;padding:20px} .nav{position:sticky;top:0;z-index:50;background:var(--bg-soft);border-bottom:1px solid var(--border);backdrop-filter:saturate(180%) blur(10px)} .nav .inner{display:flex;align-items:center;gap:14px;justify-content:space-between;padding:12px 20px} .brand{display:flex;align-items:center;gap:10px;font-weight:800} .brand .logo{width:28px;height:28px;border-radius:8px;background:linear-gradient(135deg,var(--primary),var(--accent));box-shadow:0 6px 18px rgba(0,0,0,.35)} .search{flex:1;max-width:520px;position:relative} .search input{width:100%;padding:11px 12px 11px 40px;border-radius:12px;border:1px solid var(--border);background:var(--bg);color:var(--text)} .search:before{content:"🔎";position:absolute;left:10px;top:8px;opacity:.6} .top-actions{display:flex;align-items:center;gap:10px} .btn{display:inline-flex;align-items:center;gap:8px;padding:9px 12px;border:1px solid var(--border);background:var(--card);border-radius:10px;color:var(--text);cursor:pointer} .btn.primary{background:linear-gradient(135deg,var(--primary),var(--accent));border:0;color:white} .btn.ghost{background:transparent} .btn.danger{border-color:#7f1d1d;background:rgba(239,68,68,.08);color:#fecaca} .badge{display:inline-block;padding:2px 8px;border-radius:999px;background:var(--border);color:var(--muted);font-size:12px} .grid{display:grid;grid-template-columns:repeat(2,1fr);gap:14px} @media(min-width:600px){.grid{grid-template-columns:repeat(3,1fr)}} @media(min-width:900px){.grid{grid-template-columns:repeat(6,1fr)}} /* ===== بطاقة محسّنة ===== */ .card{background:var(--card);border:1px solid var(--border);border-radius:16px;overflow:hidden;position:relative;transition:.25s transform,.25s box-shadow} .card.hoverable:hover{transform:translateY(-4px);box-shadow:0 10px 30px rgba(0,0,0,.35)} .card .thumb{position:relative;aspect-ratio:2/3;background:#1b2233} .card .thumb img{width:100%;height:100%;object-fit:cover} .card .overlay{position:absolute;inset:0;background:linear-gradient(180deg,transparent 55%,rgba(0,0,0,.75));display:flex;align-items:flex-end;padding:10px} .ribbon{position:absolute;top:8px;right:8px;background:linear-gradient(135deg,var(--primary),var(--accent));color:#fff;font-size:12px;border-radius:8px;padding:4px 8px} .tag-type{position:absolute;top:8px;left:8px;background:rgba(0,0,0,.55);border:1px solid rgba(255,255,255,.2);backdrop-filter:blur(4px);color:#fff;font-size:12px;border-radius:8px;padding:4px 8px} .card .meta{padding:10px} .card .title{font-weight:800;display:flex;justify-content:space-between;gap:6px} .card .title small{color:var(--muted);font-weight:400} .card .sub{color:var(--muted);font-size:13px;display:flex;gap:8px;align-items:center;flex-wrap:wrap} .rate{display:flex;align-items:center;gap:6px} .rate .stars{color:var(--star);font-size:13px} .wl{position:absolute;bottom:10px;right:10px;background:rgba(0,0,0,.55);color:#fff;border:1px solid rgba(255,255,255,.25);backdrop-filter:blur(3px);border-radius:999px;padding:6px 10px} /* ===== بطل ===== */ .header-hero{position:relative;border-radius:16px;overflow:hidden;background:linear-gradient(120deg,rgba(91,140,255,.25),transparent 40%), linear-gradient(0deg,rgba(0,0,0,.35),rgba(0,0,0,.35)) center/cover no-repeat} .hero-inner{padding:28px} /* ===== اللاعب المُحسّن ===== */ .player{background:#000;border-radius:14px;overflow:hidden;border:1px solid var(--border);position:relative} .player video{width:100%;display:block;background:#000} .controls-wrap{position:absolute;inset:auto 0 0 0;background:linear-gradient(0deg,rgba(0,0,0,.6),transparent);padding:10px} .controls{display:flex;gap:8px;align-items:center;flex-wrap:wrap} .control-btn,.control-select{background:rgba(255,255,255,.10);border:0;color:#fff;padding:8px;border-radius:10px;cursor:pointer} .control-range{appearance:none;width:100%;height:6px;background:rgba(255,255,255,.12);border-radius:6px;position:relative} .buffers{position:absolute;left:0;right:0;bottom:42px;height:6px;margin:0 10px;border-radius:6px;pointer-events:none} .bar-bg{position:absolute;inset:0;background:rgba(255,255,255,.12);border-radius:6px} .bar-buffer{position:absolute;left:0;top:0;height:100%;background:rgba(255,255,255,.35);border-radius:6px} .bar-played{position:absolute;left:0;top:0;height:100%;background:linear-gradient(90deg,var(--primary),var(--accent));border-radius:6px} .time{color:#ddd;font-size:12px;margin-inline-start:auto;margin-inline-end:8px} .cinema{background:#0b0e13;} .player.pinned .controls-wrap{opacity:0;transition:.25s} .player:hover .controls-wrap{opacity:1} /* جداول وإداري */ .table{width:100%;border-collapse:collapse} .table th,.table td{border-bottom:1px solid var(--border);padding:8px;text-align:start} .admin-grid{display:grid;grid-template-columns:1fr;gap:12px} @media(min-width:900px){.admin-grid{grid-template-columns:1fr 1fr}} .kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:10px} .kpi{padding:14px;border:1px solid var(--border);border-radius:12px;background:var(--card)} .toast{position:fixed;bottom:16px;inset-inline:16px;background:var(--bg-soft);border:1px solid var(--border);color:var(--text);padding:12px;border-radius:10px;display:none} .bell{position:relative} .bell .dot{position:absolute;top:0;right:0;width:8px;height:8px;background:#f43f5e;border-radius:50%} .menu{position:absolute;right:0;margin-top:8px;background:var(--card);border:1px solid var(--border);border-radius:12px;min-width:280px;display:none} .menu ul{list-style:none;margin:0;padding:6px} .menu li{padding:8px;border-radius:8px} .menu li:hover{background:var(--bg)} .small{font-size:12px;color:var(--muted)} .tag{padding:2px 6px;border-radius:6px;background:var(--border);font-size:12px} footer{padding:20px;color:var(--muted)} </style> </head> <body> <nav class="nav"> <div class="inner"> <div class="brand"> <div class="logo"></div> <a href="?route=home"><span>سينماكس</span></a> <span class="badge">منصة بث</span> </div> <div class="search"> <form action="" method="get"> <input type="hidden" name="route" value="search"> <input name="q" placeholder="ابحث عن فيلم أو مسلسل..." value=""> </form> </div> <div class="top-actions"> <button class="btn" id="themeBtn" title="وضع ليلي/نهاري">🌙/☀️</button> <div class="bell"> <button class="btn" id="bellBtn">🔔 الإشعارات</button> <span class="dot" id="bellDot" style="display:none;"></span> <div class="menu" id="bellMenu"><ul id="notifList"></ul></div> </div> <a class="btn" href="?route=watchlist">قائمتي</a> <a class="btn" href="?route=stats">إحصاءاتي</a> HTML; if ($u){ echo '<span class="badge">مرحبًا، '.esc($u['username']).'</span>'; if (is_admin()) echo ' <a class="btn primary" href="?route=admin">لوحة التحكم</a>'; echo ' <form id="logoutForm" method="post" action="?action=logout" style="display:inline">'.csrf_input().'<button class="btn">خروج</button></form>'; } else { echo '<a class="btn" href="?route=login">دخول</a> <a class="btn primary" href="?route=register">تسجيل</a>'; } echo <<<HTML </div> </div> </nav> <div class="container"> HTML; } function csrf_input(){ return '<input type="hidden" name="csrf" value="'.esc($_SESSION['csrf']).'">'; } function render_footer(){ $csrf = esc($_SESSION['csrf']); echo <<<HTML </div> <footer class="container small">© <script>document.write(new Date().getFullYear())</script> سينماكس — تجربة بث راقية</footer> <div class="toast" id="toast"></div> <script> // وضع ليلي/نهاري const root = document.documentElement; function applyTheme(){ const t = localStorage.getItem('theme') || 'dark'; if (t==='light') root.classList.add('light'); else root.classList.remove('light'); } applyTheme(); document.getElementById('themeBtn').onclick = () => { const cur = localStorage.getItem('theme') || 'dark'; localStorage.setItem('theme', cur==='dark'?'light':'dark'); applyTheme(); }; // إشعارات const bellBtn = document.getElementById('bellBtn'); const bellMenu = document.getElementById('bellMenu'); const bellDot = document.getElementById('bellDot'); function fetchNotifs(initial=false){ fetch('?action=notifications').then(r=>r.json()).then(d=>{ if(!d.ok) return; const ul = document.getElementById('notifList'); ul.innerHTML=''; let unread = 0; d.items.forEach(n=>{ const li=document.createElement('li'); const msg = n.message_ar || n.message_en || ''; li.innerHTML = '<div>'+msg+'</div><div class="small">'+(n.created_at||'')+'</div>'; if(n.link){ li.innerHTML += '<div><a class="tag" href="'+n.link+'">فتح</a></div>'; } if(n.is_read==0){ unread++; } li.onclick = () => { if(n.id){ fetch('?action=markRead',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'id='+encodeURIComponent(n.id)}) } if(n.link) location.href = n.link; }; ul.appendChild(li); }); bellDot.style.display = unread>0 ? 'block' : 'none'; if (!initial && unread>0) showToast('لديك إشعارات جديدة'); }); } bellBtn?.addEventListener('click',()=>{ bellMenu.style.display = bellMenu.style.display==='block'?'none':'block'; if(bellMenu.style.display==='block') fetchNotifs(true); }); setInterval(fetchNotifs, 30000); fetchNotifs(true); // خروج document.getElementById('logoutForm')?.addEventListener('submit', async (e)=>{ e.preventDefault(); const fd=new FormData(e.target); const res = await fetch('?action=logout',{method:'POST',body:fd}); const d=await res.json(); if(d.ok) location.href='?route=home'; }); // أدوات عامة function showToast(msg){ const t=document.getElementById('toast'); t.textContent=msg; t.style.display='block'; setTimeout(()=>t.style.display='none',2500); } // تقييم نجوم function sendRating(movieId, val){ fetch('?action=rate',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:'csrf={$csrf}&movie_id='+movieId+'&rating='+val}) .then(r=>r.json()).then(d=>{ if(d.ok){ showToast('تم حفظ تقييمك'); const el=document.getElementById('avg_'+movieId); if(el) el.textContent=d.avg+' ⭐ ('+d.count+')'; } }); } window.sendRating = sendRating; // قائمة المشاهدة function toggleWatch(movieId, btn){ fetch('?action=toggleWatchlist',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:'csrf={$csrf}&movie_id='+movieId}) .then(r=>r.json()).then(d=>{ if(d.ok){ btn.textContent = d.in ? 'إزالة من قائمتي' : 'إضافة إلى قائمتي'; showToast(d.in?'أضيف إلى قائمتك':'أزيل من قائمتك'); } }); } window.toggleWatch = toggleWatch; // متابعة مسلسل function toggleFollow(movieId, btn){ fetch('?action=follow',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:'csrf={$csrf}&movie_id='+movieId}) .then(r=>r.json()).then(d=>{ if(d.ok){ btn.textContent = d.following ? 'إلغاء المتابعة' : 'متابعة المسلسل'; showToast(d.following?'بدأت المتابعة':'تم الإلغاء'); } }); } window.toggleFollow = toggleFollow; // تقدم المشاهدة async function saveProgress(movieId, episodeId, pos){ try{ await fetch('?action=saveProgress',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:'movie_id='+movieId+'&episode_id='+(episodeId||0)+'&position='+pos}); }catch(e){} } window.saveProgress = saveProgress; async function getProgress(movieId, episodeId){ try{ const r = await fetch('?action=getProgress&movie_id='+movieId+'&episode_id='+(episodeId||0)); const d = await r.json(); if(d.ok) return d.position||0; }catch(e){} return 0; } window.getProgress = getProgress; </script> </body></html> HTML; } /* =========================[ بطاقات/مكوّنات ]========================= */ function card_movie($m){ $href = '?route=details&id='.$m['id']; $title = esc($m['title'] ?: $m['title_en']); $poster = esc($m['poster'] ?: 'https://placehold.co/400x600?text=Poster'); $badgeType = $m['type']==='series' ? 'مسلسل' : 'فيلم'; $avg = $m['rating_avg']? round($m['rating_avg'],1) : 0; $stars = $avg ? str_repeat('★', (int)round($avg)) : '☆'; echo '<a class="card hoverable" href="'.$href.'" title="'.$title.'">'; echo ' <div class="thumb">'; echo ' <img loading="lazy" src="'.$poster.'" alt="'.$title.'">'; echo ' <div class="tag-type">'.$badgeType.'</div>'; echo ' <div class="ribbon">'.esc($m['quality'] ?: '').'</div>'; echo ' <button class="wl" onclick="event.preventDefault(); toggleWatch('.$m['id'].', this)">'.'♡ قائمتي'.'</button>'; echo ' <div class="overlay"><div>'; echo ' <div class="title">'.$title.' <small>'.esc($m['year']).'</small></div>'; echo ' <div class="sub">'; echo ' <span class="rate"><span class="stars">'.$stars.'</span><span id="avg_'.$m['id'].'">'.($avg?$avg.' ⭐ ('.$m['rating_count'].')':'لا تقييم بعد').'</span></span>'; echo ' <span class="badge">'.esc($m['genre']).'</span>'; echo ' </div>'; echo ' </div></div>'; echo ' </div>'; echo '</a>'; } function player_block($mv, $ep=null){ $movie_id = (int)$mv['id']; $episode_id = $ep ? (int)$ep['id'] : 0; // مصادر الفيديو: $sources = []; if ($ep){ if (!empty($ep['sources_json'])) { $sources = json_decode($ep['sources_json'], true) ?: []; } if (!$sources && !empty($ep['mp4_src'])) $sources[] = ['label'=>$mv['quality']?:'720p','src'=>$ep['mp4_src'],'type'=>'video/mp4']; } else { // للأفلام: يمكن استخدام ?action=stream لملف محلي أو رابط مباشر خارجي ضمن backdrop كمصدر مؤقت if (!empty($mv['backdrop'])) $sources[] = ['label'=>$mv['quality']?:'1080p','src'=>$mv['backdrop'],'type'=>'video/mp4']; } // الحلقة التالية (Auto-Next) $next = null; if ($ep) $next = get_next_episode($mv['id'], $ep['season'], $ep['episode']); // قائمة الحلقات الجانبية $eps = $mv['type']==='series' ? get_series_episodes($mv['id']) : []; echo '<div class="player" id="playerWrap">'; echo ' <video id="vid" preload="metadata" playsinline></video>'; // شريط buffer/played مستقل لتقدم أنيق echo ' <div class="buffers" id="bars"><div class="bar-bg"></div><div class="bar-buffer" id="barBuffer"></div><div class="bar-played" id="barPlayed"></div></div>'; echo ' <div class="controls-wrap"><div class="controls">'; echo ' <button class="control-btn" id="playBtn" title="تشغيل/إيقاف">▶︎</button>'; echo ' <button class="control-btn" id="back10" title="ترجيع 10ث">⏪ 10</button>'; echo ' <button class="control-btn" id="fw10" title="تقديم 10ث">10 ⏩</button>'; echo ' <button class="control-btn" id="mute" title="كتم/إلغاء">🔈</button>'; echo ' <input class="control-range" id="vol" type="range" min="0" max="1" step="0.01" value="1" style="max-width:120px">'; echo ' <label class="small">السرعة <select id="speed" class="control-select"><option>0.5</option><option>0.75</option><option selected>1</option><option>1.25</option><option>1.5</option><option>2</option></select></label>'; echo ' <label class="small">الجودة <select id="quality" class="control-select"></select></label>'; echo ' <button class="control-btn" id="cinema" title="وضع سينما">🎬</button>'; echo ' <button class="control-btn" id="pip" title="نافذة عائمة PiP">🗔</button>'; echo ' <button class="control-btn" id="fs" title="ملء الشاشة">⛶</button>'; echo ' <span class="time" id="time">00:00 / 00:00</span>'; echo ' <input id="seek" class="control-range" type="range" min="0" max="100" value="0" step="0.01" style="flex:1">'; echo ' </div></div>'; echo '</div>'; if ($mv['type']==='series'){ echo '<div class="card" style="margin-top:10px;padding:8px"><h3>الحلقات</h3><div class="episodes" style="display:flex;gap:10px;overflow:auto;padding:8px">'; foreach($eps as $e){ $sel = $ep && $e['id']==$ep['id'] ? 'style="outline:2px solid var(--primary)"' : ''; echo '<a class="ep-item" '.$sel.' href="?route=watch&episode_id='.$e['id'].'">'. '<div class="small">م'.$e['season'].' • ح'.$e['episode'].'</div><div>'.esc($e['title']?:$e['title_en']).'</div></a>'; } echo '</div></div>'; } // سكربت تشغيل $jsSources = json_encode($sources, JSON_UNESCAPED_UNICODE); $resumeMovie = $movie_id; $resumeEp = $episode_id; $autoNextUrl = ($next? ('?route=watch&episode_id='.$next['id']) : ''); echo <<<JS <script> /* ===== مشغّل مُحسّن ===== */ const sources = $jsSources; const video = document.getElementById('vid'); const seek = document.getElementById('seek'); const time = document.getElementById('time'); const playBtn = document.getElementById('playBtn'); const qSel = document.getElementById('quality'); const speed= document.getElementById('speed'); const fsBtn= document.getElementById('fs'); const cinemaBtn= document.getElementById('cinema'); const pipBtn = document.getElementById('pip'); const muteBtn= document.getElementById('mute'); const volSlider=document.getElementById('vol'); const barBuffer=document.getElementById('barBuffer'); const barPlayed=document.getElementById('barPlayed'); const playerWrap=document.getElementById('playerWrap'); function fmt(t){ if(!isFinite(t)) return '00:00'; const m=Math.floor(t/60), s=Math.floor(t%60); return (m<10?'0':'')+m+':' + (s<10?'0':'')+s; } function setSourceByLabel(label){ const s = sources.find(x=>x.label===label) || sources[0]; if(!s) return; const wasPlaying = !video.paused && !video.ended; const cur = video.currentTime||0; video.src = s.src; video.type = s.type || 'video/mp4'; video.playbackRate = parseFloat(speed.value)||1; video.addEventListener('loadedmetadata', async ()=>{ let resumeAt = await getProgress($resumeMovie, $resumeEp); // أعط الأولوية للحفظ اللحظي if(cur>1) video.currentTime = cur; else if(resumeAt>1) video.currentTime = resumeAt; if(wasPlaying) video.play(); }, {once:true}); } function populateQuality(){ qSel.innerHTML = ''; sources.forEach((s,i)=>{ const opt=document.createElement('option'); opt.textContent=s.label||('Q'+(i+1)); opt.value=s.label||('Q'+(i+1)); qSel.appendChild(opt); }); } populateQuality(); if(sources.length){ setSourceByLabel(sources[0].label||''); } function updateBars(){ if(!video.duration) return; // played const played = (video.currentTime/video.duration)*100; barPlayed.style.width = played+'%'; // buffered: خذ آخر نطاق try{ let b=0; for(let i=0;i<video.buffered.length;i++){ if(video.buffered.end(i)>b) b=video.buffered.end(i); } const bufPct = Math.min(100, (b/video.duration)*100); barBuffer.style.width = bufPct+'%'; }catch(e){} } playBtn.onclick = ()=>{ if(video.paused){ video.play(); } else { video.pause(); } }; video.addEventListener('play', ()=>{ playBtn.textContent='⏸︎'; }); video.addEventListener('pause', ()=>{ playBtn.textContent='▶︎'; }); document.getElementById('back10').onclick = ()=> video.currentTime = Math.max(0, video.currentTime-10); document.getElementById('fw10').onclick = ()=> video.currentTime = Math.min(video.duration||0, video.currentTime+10); fsBtn.onclick = ()=> { if(document.fullscreenElement){ document.exitFullscreen(); } else { playerWrap.requestFullscreen?.(); } }; cinemaBtn.onclick = ()=> document.getElementById('playerWrap').classList.toggle('cinema'); pipBtn.onclick = async ()=> { if (document.pictureInPictureElement) { await document.exitPictureInPicture(); } else { try{ await video.requestPictureInPicture(); }catch(e){} } }; muteBtn.onclick = ()=> { video.muted = !video.muted; muteBtn.textContent = video.muted?'🔇':'🔈'; }; volSlider.oninput = ()=> { video.volume = parseFloat(volSlider.value||1); video.muted = video.volume===0; muteBtn.textContent = video.muted?'🔇':'🔈'; }; speed.onchange = ()=> video.playbackRate = parseFloat(speed.value)||1; qSel.onchange = ()=>{ const lbl=qSel.value; const pos=video.currentTime||0; setSourceByLabel(lbl); setTimeout(()=>{ try{ video.currentTime=pos; }catch(e){} }, 50); }; video.addEventListener('timeupdate', ()=>{ if(video.duration){ seek.value = (video.currentTime/video.duration)*100; time.textContent = fmt(video.currentTime)+' / '+fmt(video.duration); updateBars(); if(Math.floor(video.currentTime)%5===0){ saveProgress($resumeMovie, $resumeEp, video.currentTime); } } }); video.addEventListener('progress', updateBars); seek.addEventListener('input', ()=>{ if(video.duration){ video.currentTime = (seek.value/100)*video.duration; updateBars(); } }); video.addEventListener('ended', ()=>{ const nextUrl = "$autoNextUrl"; if(nextUrl){ location.href = nextUrl; } }); // لوحات مفاتيح document.addEventListener('keydown', (e)=>{ if(!document.body.contains(video)) return; if(['INPUT','TEXTAREA'].includes(document.activeElement.tagName)) return; if(e.code==='Space'){ e.preventDefault(); playBtn.click(); } if(e.key==='f'){ fsBtn.click(); } if(e.key==='m'){ muteBtn.click(); } if(e.key==='ArrowLeft'){ e.preventDefault(); document.getElementById('back10').click(); } if(e.key==='ArrowRight'){ e.preventDefault(); document.getElementById('fw10').click(); } }); // استئناف تلقائي عند التحميل الأول (async ()=>{ let resumeAt = await getProgress($resumeMovie, $resumeEp); if(resumeAt>1){ video.addEventListener('loadedmetadata',()=>{ video.currentTime=resumeAt; updateBars(); }, {once:true}); } })(); </script> JS; } /* =========================[ الصفحات ]========================= */ $route = $_GET['route'] ?? 'home'; if ($route === 'home') { render_header('الرئيسية'); $featured = get_featured(); echo '<div class="header-hero"><div class="hero-inner"><h2>مرحبًا بك في <b>سينماكس</b> 🍿</h2><p>شاهد أحدث الأفلام والمسلسلات بجودة عالية مع تجربة مشاهدة راقية وذكية.</p></div></div>'; echo '<div class="section-title" style="display:flex;align-items:center;justify-content:space-between;margin:24px 0 12px"><h3>مميز لدينا</h3> <a class="btn ghost" href="?route=search">استعراض الكل →</a></div>'; echo '<div class="grid">'; foreach($featured as $m){ card_movie($m); } echo '</div>'; echo '<div class="section-title" style="display:flex;align-items:center;justify-content:space-between;margin:24px 0 12px"><h3>أحدث الأفلام</h3></div><div class="grid">'; foreach(get_latest('movie') as $m){ card_movie($m); } echo '</div>'; echo '<div class="section-title" style="display:flex;align-items:center;justify-content:space-between;margin:24px 0 12px"><h3>أحدث المسلسلات</h3></div><div class="grid">'; foreach(get_latest('series') as $m){ card_movie($m); } echo '</div>'; render_footer(); exit; } if ($route === 'search') { render_header('البحث المتقدم'); $q = trim($_GET['q'] ?? ''); echo <<<HTML <form method="get" class="card" style="padding:12px;margin-bottom:12px"> <input type="hidden" name="route" value="search"> <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px"> <input name="q" placeholder="كلمة مفتاحية" value="{$q}"> <select name="type"><option value="">الكل</option><option value="movie">أفلام</option><option value="series">مسلسلات</option></select> <input name="genre" placeholder="التصنيف (دراما، إثارة...)"> <input name="year" type="number" placeholder="السنة"> <input name="rating" type="number" step="0.1" placeholder="التقييم الأدنى"> <select name="quality"><option value="">الجودة</option><option>480p</option><option>720p</option><option>1080p</option><option>4K</option></select> <input name="language" placeholder="اللغة (ar,en,...)"> <select name="sort"><option value="">الأحدث</option><option value="rating">الأعلى تقييمًا</option><option value="year">الأحدث (سنة)</option><option value="alpha">أبجديًا</option></select> </div> <div style="margin-top:8px"><button class="btn primary">بحث</button></div> </form> <div id="results" class="grid"></div> <script> const params = new URLSearchParams(location.search); fetch('?action=searchAdvanced&'+params.toString()).then(r=>r.json()).then(d=>{ if(!d.ok) return; const g=document.getElementById('results'); g.innerHTML=''; d.results.forEach(m=>{ const el=document.createElement('a'); el.href='?route=details&id='+m.id; el.className='card hoverable'; const poster = m.poster||'https://placehold.co/400x600?text=Poster'; const title = m.title||m.title_en||''; el.innerHTML = '<div class="thumb">'+ '<img loading="lazy" src="'+poster+'" alt="">'+ '<div class="tag-type">'+(m.type==='series'?'مسلسل':'فيلم')+'</div>'+ '<div class="ribbon">'+(m.quality||'')+'</div>'+ '<div class="overlay"><div>'+ '<div class="title">'+title+' <small>'+(m.year||'')+'</small></div>'+ '<div class="sub"><span>'+ (m.rating_avg? (Number(m.rating_avg).toFixed(1)+' ⭐ ('+m.rating_count+')') : 'لا تقييم بعد') +'</span> <span class="badge">'+(m.genre||'')+'</span></div>'+ '</div></div>'+ '</div>'; g.appendChild(el); }); }); </script> HTML; render_footer(); exit; } if ($route === 'details') { $id = (int)($_GET['id'] ?? 0); $m = get_movie($id); if (!$m){ http_response_code(404); render_header('غير موجود'); echo '<p>العنصر غير موجود.</p>'; render_footer(); exit; } render_header(esc($m['title'])); echo '<div class="card" style="display:grid;grid-template-columns:220px 1fr;gap:14px;padding:14px">'; echo '<div><img loading="lazy" src="'.esc($m['poster'] ?: 'https://placehold.co/400x600').'" alt=""></div>'; echo '<div>'; echo '<h2 style="margin:0 0 6px">'.esc($m['title'] ?: $m['title_en']).' <span class="badge">'.esc($m['type']).'</span></h2>'; echo '<div class="small">'.esc($m['genre']).' • '.esc($m['year']).' • '.esc($m['quality']).'</div>'; echo '<p>'.esc($m['description'] ?: $m['description_en']).'</p>'; $avg = $m['rating_avg']? round($m['rating_avg'],2).' ⭐ ('.$m['rating_count'].')' : 'لا تقييم بعد'; echo '<div id="avg_'.$m['id'].'" class="badge">'.$avg.'</div><br>'; if (is_logged()){ echo '<button class="btn" onclick="toggleWatch('.$m['id'].',this)">إضافة إلى قائمتي</button> '; if ($m['type']==='series'){ echo '<button class="btn" onclick="toggleFollow('.$m['id'].',this)">متابعة المسلسل</button> '; } } else { echo '<a class="btn" href="?route=login">سجّل الدخول لإدارة قائمتك</a> '; } echo '<div style="margin-top:10px">قيّم العمل: <span class="stars" style="cursor:pointer">'; for($i=1;$i<=5;$i++) echo '<i onclick="sendRating('.$m['id'].','.$i.')">★</i>'; echo '</span></div>'; if ($m['type']==='movie'){ echo '<div style="margin-top:12px"><a class="btn primary" href="?route=watch&movie_id='.$m['id'].'">▶ مشاهدة الآن</a></div>'; } else { $eps = get_series_episodes($m['id']); $grouped = []; foreach($eps as $e){ $grouped[$e['season']][] = $e; } foreach($grouped as $season=>$list){ echo '<h3>الموسم '.$season.'</h3><div class="episodes" style="display:flex;gap:10px;overflow:auto;padding:8px">'; foreach($list as $e){ echo '<a class="ep-item" href="?route=watch&episode_id='.$e['id'].'"><div class="small">الحلقة '.$e['episode'].'</div>'. '<div>'.esc($e['title'] ?: $e['title_en']).'</div></a>'; } echo '</div>'; } } echo '</div></div>'; // التعليقات $comments = db()->query("SELECT c.*, u.username FROM comments c JOIN users u ON u.id=c.user_id WHERE c.movie_id=".$m['id']." AND c.is_deleted=0 ORDER BY c.id DESC")->fetchAll(); echo '<div class="card" style="padding:12px;margin-top:12px"><h3>التعليقات</h3>'; if (is_logged()){ echo '<form id="cform"><textarea name="text" rows="3" style="width:100%;padding:8px;border-radius:8px;background:var(--bg);color:var(--text);border:1px solid var(--border)" placeholder="اكتب تعليقك..."></textarea>'; echo '<input type="hidden" name="movie_id" value="'.$m['id'].'">'.csrf_input(); echo '<div><button class="btn primary">إرسال</button></div></form>'; echo <<<JS <script> document.getElementById('cform').addEventListener('submit', async (e)=>{ e.preventDefault(); const fd=new FormData(e.target); const r=await fetch('?action=comment',{method:'POST',body:fd}); const d=await r.json(); if(d.ok) location.reload(); else alert(d.error||'خطأ'); }); </script> JS; } else { echo '<p class="small">سجّل الدخول لتستطيع التعليق.</p>'; } foreach($comments as $c){ echo '<div class="comment" style="border-bottom:1px dashed var(--border);padding:10px 0"><div><b>'.esc($c['username']).'</b> <span class="small">'.esc($c['created_at']).'</span></div><div>'.esc($c['comment']).'</div>'; if (is_logged() && (user()['id']==$c['user_id'] || is_admin())){ echo '<div class="small"><a href="#" onclick="editC('.$c['id'].',\''.esc($c['comment']).'\');return false;">تعديل</a> • <a href="#" onclick="delC('.$c['id'].');return false;">حذف</a></div>'; } echo '</div>'; } echo <<<JS <script> function editC(id, old){ const nv = prompt('حرّر تعليقك:', old); if(nv==null) return; fetch('?action=commentEdit',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:'csrf={$_SESSION['csrf']}&id='+id+'&text='+encodeURIComponent(nv)}).then(r=>r.json()).then(d=>{ if(d.ok) location.reload(); }); } function delC(id){ if(!confirm('حذف التعليق؟')) return; fetch('?action=commentDelete',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:'csrf={$_SESSION['csrf']}&id='+id}).then(r=>r.json()).then(d=>{ if(d.ok) location.reload(); }); } </script> JS; echo '</div>'; render_footer(); exit; } if ($route === 'watch') { $movie_id = isset($_GET['movie_id']) ? (int)$_GET['movie_id'] : null; $episode_id = isset($_GET['episode_id']) ? (int)$_GET['episode_id'] : null; if ($episode_id){ $ep = get_episode($episode_id); $mv = get_movie($ep['movie_id'] ?? 0); if (!$ep || !$mv){ http_response_code(404); render_header('غير موجود'); echo '<p>الحلقة غير موجودة.</p>'; render_footer(); exit; } render_header('مشاهدة - '.esc($mv['title']).' / '.esc($ep['title'])); player_block($mv, $ep); render_footer(); exit; } else { $mv = get_movie($movie_id); if (!$mv){ http_response_code(404); render_header('غير موجود'); echo '<p>غير موجود.</p>'; render_footer(); exit; } render_header('مشاهدة - '.esc($mv['title'])); player_block($mv, null); render_footer(); exit; } } if ($route === 'watchlist' && is_logged()) { render_header('قائمتي'); $stmt = db()->prepare("SELECT m.* FROM watchlist w JOIN movies m ON m.id=w.movie_id WHERE w.user_id=? ORDER BY w.id DESC"); $stmt->execute([user()['id']]); echo '<div class="grid">'; foreach($stmt->fetchAll() as $m) card_movie($m); echo '</div>'; render_footer(); exit; } if ($route === 'stats' && is_logged()) { render_header('إحصاءاتي'); $uid = user()['id']; $rows = db()->prepare("SELECT h.*, m.title, m.type, e.season, e.episode FROM watch_history h JOIN movies m ON m.id=h.movie_id LEFT JOIN episodes e ON e.id=h.episode_id WHERE h.user_id=? ORDER BY h.last_watched_at DESC LIMIT 200"); $rows->execute([$uid]); $hist=$rows->fetchAll(); $hours = 0; foreach($hist as $h){ $hours += ($h['position'] ?? 0) / 3600; } $genres = db()->prepare("SELECT genre, COUNT(*) c FROM movies m JOIN watch_history h ON h.movie_id=m.id WHERE h.user_id=? GROUP BY genre ORDER BY c DESC"); $genres->execute([$uid]); $g=$genres->fetchAll(); echo '<div class="kpis">'; echo '<div class="kpi"><div class="small">ساعات المشاهدة</div><div style="font-size:26px">'.round($hours,1).'</div></div>'; echo '<div class="kpi"><div class="small">عناصر في قائمتي</div><div style="font-size:26px">'.(int)db()->query("SELECT COUNT(*) FROM watchlist WHERE user_id=".$uid)->fetchColumn().'</div></div>'; echo '<div class="kpi"><div class="small">تقييماتي</div><div style="font-size:26px">'.(int)db()->query("SELECT COUNT(*) FROM ratings WHERE user_id=".$uid)->fetchColumn().'</div></div>'; echo '<div class="kpi"><div class="small">تعليقاتي</div><div style="font-size:26px">'.(int)db()->query("SELECT COUNT(*) FROM comments WHERE user_id=".$uid." AND is_deleted=0")->fetchColumn().'</div></div>'; echo '</div>'; echo '<h3>سجل المشاهدة</h3><table class="table"><tr><th>العنصر</th><th>الموضع</th><th>الجهاز</th><th>آخر مشاهدة</th></tr>'; foreach($hist as $h){ $nm = esc($h['title']).($h['type']==='series'?' • م'.$h['season'].'ح'.$h['episode']:''); echo '<tr><td>'.$nm.'</td><td>'.gmdate('H:i:s', (int)$h['position']).'</td><td>'.esc($h['device']).'</td><td>'.esc($h['last_watched_at']).'</td></tr>'; } echo '</table>'; echo '<h3>التصنيفات المفضلة</h3><div style="display:flex;gap:8px;flex-wrap:wrap">'; foreach($g as $row) echo '<span class="tag">'.esc($row['genre']).' ('.$row['c'].')</span>'; echo '</div>'; // رسم بسيط على Canvas $daily = db()->prepare("SELECT substr(last_watched_at,1,10) d, COUNT(*) c FROM watch_history WHERE user_id=? GROUP BY d ORDER BY d ASC"); $daily->execute([$uid]); $labels=[]; $vals=[]; foreach($daily->fetchAll() as $r){ $labels[]=$r['d']; $vals[]=(int)$r['c']; } $labelsJS = json_encode($labels); $valsJS = json_encode($vals); echo <<<HTML <canvas id="chart" width="600" height="240" style="border:1px solid var(--border);border-radius:8px;background:var(--card)"></canvas> <script> (function(){ const labels = $labelsJS; const vals = $valsJS; const c = document.getElementById('chart').getContext('2d'); const w=c.canvas.width, h=c.canvas.height, pad=30; const max = Math.max(1, ...vals); c.clearRect(0,0,w,h); c.strokeStyle='#888'; c.beginPath(); c.moveTo(pad,10); c.lineTo(pad,h-pad); c.lineTo(w-10,h-pad); c.stroke(); c.strokeStyle='#5b8cff'; c.beginPath(); vals.forEach((v,i)=>{ const x = pad + (i/(Math.max(1,vals.length-1)))*(w-pad-20); const y = (h-pad) - (v/max)*(h-pad-20); if(i===0) c.moveTo(x,y); else c.lineTo(x,y); }); c.stroke(); c.fillStyle='#5b8cff'; vals.forEach((v,i)=>{ const x = pad + (i/(Math.max(1,vals.length-1)))*(w-pad-20); const y = (h-pad) - (v/max)*(h-pad-20); c.beginPath(); c.arc(x,y,2.5,0,Math.PI*2); c.fill(); }); })(); </script> HTML; render_footer(); exit; } if ($route === 'login' || $route === 'register') { render_header($route==='login'?'تسجيل الدخول':'إنشاء حساب'); $isLogin = $route==='login'; echo '<div class="card" style="max-width:460px;margin:auto;padding:16px">'; echo '<h2>'.($isLogin?'دخول':'تسجيل').'</h2>'; echo '<form id="auth">'; if (!$isLogin) echo '<input name="username" placeholder="اسم المستخدم" style="width:100%;padding:10px;border:1px solid var(--border);border-radius:10px;background:var(--bg);color:var(--text);margin-bottom:8px">'; echo '<input name="email" placeholder="البريد الإلكتروني" style="width:100%;padding:10px;border:1px solid var(--border);border-radius:10px;background:var(--bg);color:var(--text);margin-bottom:8px">'; echo '<input type="password" name="password" placeholder="كلمة المرور" style="width:100%;padding:10px;border:1px solid var(--border);border-radius:10px;background:var(--bg);color:var(--text);margin-bottom:8px">'; echo csrf_input(); echo '<button class="btn primary" style="width:100%">'.($isLogin?'دخول':'تسجيل').'</button>'; echo '</form></div>'; echo "<script> document.getElementById('auth').addEventListener('submit', async (e)=>{ e.preventDefault(); const fd=new FormData(e.target); const url='?action=".($isLogin?'login':'register')."'; const r=await fetch(url,{method:'POST',body:fd}); const d=await r.json(); if(d.ok){ location.href='?route=home'; } else if(d.pending){ alert('تم التسجيل، بانتظار موافقة الإدارة'); location.href='?route=home'; } else{ alert(d.error||'حدث خطأ'); } }); </script>"; render_footer(); exit; } /* =========================[ لوحة التحكم (بإدارة كاملة) ]========================= */ if ($route === 'admin' && is_admin()) { render_header('لوحة التحكم'); $csrf = esc($_SESSION['csrf']); echo '<div class="admin-grid">'; /* إضافة فيلم/مسلسل */ echo '<div class="card" style="padding:12px"><h3>إضافة فيلم/مسلسل</h3><form id="addMovie">'; echo '<select name="type" class="btn"><option value="movie">فيلم</option><option value="series">مسلسل</option></select> '; echo '<input name="title" placeholder="العنوان (عربي)" class="btn"> '; echo '<input name="title_en" placeholder="العنوان (إنجليزي)" class="btn"> '; echo '<input name="genre" placeholder="التصنيف" class="btn"> '; echo '<input name="year" type="number" placeholder="السنة" class="btn"> '; echo '<input name="duration" type="number" placeholder="المدة بالدقائق" class="btn"> '; echo '<select name="quality" class="btn"><option>480p</option><option selected>720p</option><option>1080p</option><option>4K</option></select> '; echo '<input name="language" placeholder="اللغة (ar,en...)" class="btn"> '; echo '<input name="poster" placeholder="رابط بوستر" class="btn" style="width:100%"> '; echo '<input name="backdrop" placeholder="رابط خلفية / أو فيديو للأفلام" class="btn" style="width:100%"> '; echo '<textarea name="description" placeholder="الوصف (عربي)" class="btn" style="width:100%;height:70px"></textarea>'; echo '<textarea name="description_en" placeholder="الوصف (إنجليزي)" class="btn" style="width:100%;height:70px"></textarea>'; echo '<label><input type="checkbox" name="is_featured"> مميز</label>'; echo csrf_input(); echo '<div><button class="btn primary">حفظ</button></div></form></div>'; /* إضافة حلقة */ echo '<div class="card" style="padding:12px"><h3>إضافة حلقة لمسلسل</h3><form id="addEpisode">'; echo '<input name="movie_id" type="number" placeholder="ID المسلسل" class="btn"> '; echo '<input name="season" type="number" placeholder="الموسم" class="btn"> '; echo '<input name="episode" type="number" placeholder="الحلقة" class="btn"> '; echo '<input name="title" placeholder="عنوان الحلقة (ع)" class="btn"> '; echo '<input name="title_en" placeholder="عنوان الحلقة (EN)" class="btn"> '; echo '<input name="duration" type="number" placeholder="المدة بالدقائق" class="btn"> '; echo '<input name="mp4_src" placeholder="رابط افتراضي" class="btn" style="width:100%"> '; echo '<textarea name="sources_json" placeholder=\'JSON للجودات [{"label":"1080p","src":"...","type":"video/mp4"}]\' class="btn" style="width:100%;height:80px"></textarea>'; echo csrf_input(); echo '<div><button class="btn primary">حفظ</button></div></form></div>'; /* إدارة المحتوى */ echo '<div class="card" style="padding:12px"><h3>إدارة المحتوى</h3> <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px"> <input id="f_q" class="btn" placeholder="ابحث بالعنوان"> <select id="f_type" class="btn"><option value="">الكل</option><option value="movie">أفلام</option><option value="series">مسلسلات</option></select> <button class="btn" id="btnSearch">بحث</button> </div> <div style="overflow:auto"> <table class="table" id="tblContent"> <thead><tr><th>ID</th><th>العنوان</th><th>النوع</th><th>سنة</th><th>جودة</th><th>تقييم</th><th>تحكم</th></tr></thead> <tbody></tbody> </table> </div> <div id="editBox" class="card" style="margin-top:12px; padding:10px; display:none"></div> </div>'; /* إدارة المستخدمين */ echo '<div class="card" style="padding:12px"><h3>المستخدمون</h3><div id="users"></div></div>'; /* إشعار يدوي */ echo '<div class="card" style="padding:12px"><h3>إرسال إشعار</h3><form id="notify"><input name="user_id" type="number" placeholder="User ID (فارغ = للجميع)" class="btn"> <input name="message_ar" placeholder="رسالة بالعربية" class="btn" style="width:100%"> <input name="message_en" placeholder="EN Message" class="btn" style="width:100%"> <input name="link" placeholder="رابط (اختياري)" class="btn" style="width:100%">'. csrf_input().'<div><button class="btn primary">إرسال</button></div></form></div>'; echo '</div>'; // grid echo <<<JS <script> /* ===== إضافة فيلم/مسلسل ===== */ document.getElementById('addMovie').addEventListener('submit', async (e)=>{ e.preventDefault(); const fd=new FormData(e.target); const r=await fetch('?action=adminAddMovie',{method:'POST',body:fd}); const d=await r.json(); if(d.ok){ alert('تم إضافة العنصر ID='+d.id); loadContent(); } }); /* ===== إضافة حلقة ===== */ document.getElementById('addEpisode').addEventListener('submit', async (e)=>{ e.preventDefault(); const fd=new FormData(e.target); const r=await fetch('?action=adminAddEpisode',{method:'POST',body:fd}); const d=await r.json(); if(d.ok){ alert('تم إضافة الحلقة ID='+d.id); if(window._editingId) editMovie(window._editingId); } }); /* ===== إدارة المحتوى: تحميل القائمة ===== */ const qEl=document.getElementById('f_q'); const tEl=document.getElementById('f_type'); document.getElementById('btnSearch').onclick = ()=> loadContent(); async function loadContent(){ const p=new URLSearchParams(); if(qEl.value) p.set('q',qEl.value); if(tEl.value) p.set('type',tEl.value); const r=await fetch('?action=adminListMovies&'+p.toString()); const d=await r.json(); const tb=document.querySelector('#tblContent tbody'); tb.innerHTML=''; if(!d.ok) return; d.items.forEach(m=>{ const tr=document.createElement('tr'); tr.innerHTML = '<td>'+m.id+'</td><td>'+ (m.title||m.title_en||'') +'</td><td>'+m.type+'</td><td>'+ (m.year||'') +'</td><td>'+ (m.quality||'') +'</td><td>'+ (m.rating_avg? Number(m.rating_avg).toFixed(1):'-') +'</td>'+ '<td><button class="btn" onclick="editMovie('+m.id+')">تعديل</button> <button class="btn danger" onclick="delMovie('+m.id+')">حذف</button></td>'; tb.appendChild(tr); }); } loadContent(); /* ===== محرر عنصر ===== */ window._editingId = null; async function editMovie(id){ window._editingId = id; const r=await fetch('?action=adminGetMovie&id='+id); const d=await r.json(); if(!d.ok) return; const m=d.movie, eps=d.episodes||[]; const box=document.getElementById('editBox'); box.style.display='block'; box.innerHTML = renderMovieForm(m) + (m.type==='series'? renderEpisodes(eps, m.id) : ''); attachMovieFormHandlers(); if(m.type==='series') attachEpisodeHandlers(); } window.editMovie = editMovie; /* ===== حذف عنصر ===== */ async function delMovie(id){ if(!confirm('حذف العنصر وجميع متعلقاته؟')) return; const r=await fetch('?action=adminDeleteMovie',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:'csrf={$csrf}&id='+id}); const d=await r.json(); if(d.ok){ alert('تم الحذف'); loadContent(); document.getElementById('editBox').style.display='none'; } } window.delMovie = delMovie; /* ===== قوالب ===== */ function renderMovieForm(m){ return \` <h4>تعديل: \${m.title||m.title_en} (ID: \${m.id})</h4> <form id="editMovieForm"> <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px"> <input name="id" type="hidden" value="\${m.id}"> <input name="csrf" type="hidden" value="{$csrf}"> <label class="small">النوع <select name="type" class="btn"> <option value="movie"\${m.type==='movie'?' selected':''}>فيلم</option> <option value="series"\${m.type==='series'?' selected':''}>مسلسل</option> </select> </label> <input name="title" class="btn" placeholder="العنوان (ع)" value="\${m.title||''}"> <input name="title_en" class="btn" placeholder="العنوان (EN)" value="\${m.title_en||''}"> <input name="genre" class="btn" placeholder="التصنيف" value="\${m.genre||''}"> <input name="year" type="number" class="btn" placeholder="السنة" value="\${m.year||''}"> <input name="duration" type="number" class="btn" placeholder="المدة بالدقائق" value="\${m.duration||0}"> <input name="quality" class="btn" placeholder="الجودة" value="\${m.quality||''}"> <input name="language" class="btn" placeholder="اللغة" value="\${m.language||''}"> <input name="poster" class="btn" placeholder="بوستر URL" value="\${m.poster||''}" style="grid-column:1/-1"> <input name="backdrop" class="btn" placeholder="خلفية / مصدر فيديو للأفلام" value="\${m.backdrop||''}" style="grid-column:1/-1"> <textarea name="description" class="btn" style="grid-column:1/-1;height:70px" placeholder="الوصف (ع)">\${m.description||''}</textarea> <textarea name="description_en" class="btn" style="grid-column:1/-1;height:70px" placeholder="الوصف (EN)">\${m.description_en||''}</textarea> <label style="grid-column:1/-1"><input type="checkbox" name="is_featured"\${m.is_featured? ' checked':''}> مميز</label> </div> <div style="margin-top:8px"><button class="btn primary">حفظ التغييرات</button></div> </form>\`; } function renderEpisodes(eps, movieId){ let rows = eps.map(e => \` <tr> <td>\${e.id}</td> <td><input class="btn" name="season" value="\${e.season}" style="width:70px"></td> <td><input class="btn" name="episode" value="\${e.episode}" style="width:70px"></td> <td><input class="btn" name="title" value="\${e.title||''}" style="min-width:180px"></td> <td><input class="btn" name="title_en" value="\${e.title_en||''}" style="min-width:180px"></td> <td><input class="btn" name="duration" value="\${e.duration||0}" style="width:90px"></td> <td><input class="btn" name="mp4_src" value="\${e.mp4_src||''}" style="min-width:240px"></td> <td><textarea class="btn" name="sources_json" style="min-width:240px;height:48px">\${e.sources_json||'[]'}</textarea></td> <td> <button class="btn" data-epid="\${e.id}" data-op="save">حفظ</button> <button class="btn danger" data-epid="\${e.id}" data-op="delete">حذف</button> </td> </tr>\`).join(''); return \` <h4>حلقات المسلسل</h4> <div style="overflow:auto"> <table class="table" id="tblEps"> <thead><tr><th>ID</th><th>م</th><th>ح</th><th>عنوان (ع)</th><th>عنوان (EN)</th><th>مدة</th><th>mp4</th><th>جودات JSON</th><th>تحكم</th></tr></thead> <tbody>\${rows}</tbody> </table></div> <div class="small">لإضافة حلقة جديدة سريعًا استخدم بطاقة "إضافة حلقة" بالأعلى (أدخل ID=\${movieId}) ثم أعد تحميل التحرير.</div>\`; } /* ===== حفظ الفيلم ===== */ function attachMovieFormHandlers(){ const f = document.getElementById('editMovieForm'); f.addEventListener('submit', async (e)=>{ e.preventDefault(); const fd=new FormData(f); const r=await fetch('?action=adminUpdateMovie',{method:'POST',body:fd}); const d=await r.json(); if(d.ok){ alert('تم الحفظ'); loadContent(); } }); } /* ===== حفظ/حذف الحلقات ===== */ function attachEpisodeHandlers(){ document.querySelectorAll('#tblEps button').forEach(btn=>{ btn.addEventListener('click', async ()=>{ const id = btn.getAttribute('data-epid'); const tr = btn.closest('tr'); if(btn.getAttribute('data-op')==='save'){ const fd=new URLSearchParams(); fd.set('csrf','{$csrf}'); fd.set('id', id); fd.set('season', tr.querySelector('[name=season]').value); fd.set('episode', tr.querySelector('[name=episode]').value); fd.set('title', tr.querySelector('[name=title]').value); fd.set('title_en', tr.querySelector('[name=title_en]').value); fd.set('duration', tr.querySelector('[name=duration]').value); fd.set('mp4_src', tr.querySelector('[name=mp4_src]').value); fd.set('sources_json', tr.querySelector('[name=sources_json]').value||'[]'); const r=await fetch('?action=adminUpdateEpisode',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:fd.toString()}); const d=await r.json(); if(d.ok){ alert('تم حفظ الحلقة'); } } else { if(!confirm('حذف هذه الحلقة؟')) return; const r=await fetch('?action=adminDeleteEpisode',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'csrf={$csrf}&id='+id}); const d=await r.json(); if(d.ok){ alert('تم الحذف'); editMovie(window._editingId); } } }); }); } /* ===== إدارة المستخدمين ===== */ async function loadUsers(){ const r=await fetch('?action=adminUsers'); const d=await r.json(); if(!d.ok) return; const box=document.getElementById('users'); box.innerHTML=''; const t=document.createElement('table'); t.className='table'; t.innerHTML='<tr><th>ID</th><th>اسم</th><th>بريد</th><th>الدور</th><th>الحالة</th><th>تحكم</th></tr>'; d.users.forEach(u=>{ const tr=document.createElement('tr'); tr.innerHTML='<td>'+u.id+'</td><td>'+u.username+'</td><td>'+u.email+'</td>'+ '<td>'+u.role+'</td><td>'+u.status+'</td>'+ '<td>'+ '<button class="btn" onclick="userOp('+u.id+',\\'approve\\')">قبول</button> '+ '<button class="btn" onclick="userOp('+u.id+',\\'reject\\')">رفض</button> '+ '<button class="btn danger" onclick="banUser('+u.id+')">حظر</button> '+ '<button class="btn" onclick="setRole('+u.id+')">تغيير الدور</button>'+ '</td>'; t.appendChild(tr); }); box.appendChild(t); } loadUsers(); async function userOp(id,op){ const fd=new URLSearchParams(); fd.set('id',id); fd.set('op',op); fd.set('csrf','{$csrf}'); const r=await fetch('?action=adminUsers',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:fd.toString()}); const d=await r.json(); if(d.ok) loadUsers(); } async function banUser(id){ const reason=prompt('سبب الحظر:')||''; const until=prompt('حتى تاريخ (YYYY-MM-DD):')||''; const fd=new URLSearchParams(); fd.set('id',id); fd.set('op','ban'); fd.set('reason',reason); fd.set('until',until); fd.set('csrf','{$csrf}'); const r=await fetch('?action=adminUsers',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:fd.toString()}); const d=await r.json(); if(d.ok) loadUsers(); } async function setRole(id){ const role=prompt('ادخل الدور: Admin/Editor/Moderator/User','User'); if(!role) return; const fd=new URLSearchParams(); fd.set('id',id); fd.set('op','role'); fd.set('role',role); fd.set('csrf','{$csrf}'); const r=await fetch('?action=adminUsers',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:fd.toString()}); const d=await r.json(); if(d.ok) loadUsers(); } /* ===== إشعار يدوي ===== */ document.getElementById('notify').addEventListener('submit', async (e)=>{ e.preventDefault(); const fd=new FormData(e.target); const r=await fetch('?action=adminNotify',{method:'POST',body:fd}); const d=await r.json(); if(d.ok) alert('تم الإرسال'); }); </script> JS; render_footer(); exit; } /* =========================[ افتراضي: 404 ]========================= */ render_header('غير موجود'); echo '<p>الصفحة غير متاحة.</p>'; render_footer(); exit; /* =========================[ انتهى ]========================= */ ?>