اكمل الكود رجاء

Créé le : 13 septembre 2025

Répondu en utilisant GPT-5 par Chat01

Question

اكمل الكود رجاء

Réponse

ممكن توضّح لي أي كود تقصد؟ 👩‍💻
هل عندك مقطع من الكود وتريدني أكمله لك، أم تودّ أن أكتب كود جديد من الصفر (وفي أي لغة: بايثون، جافاسكريبت، جافا… إلخ)؟

لو أرسلت لي الجزء الموجود من الكود أو وصفت المطلوب بالضبط، أقدر أكمله لك خطوة بخطوة.

Question

المطلوب: إنشاء منصة بث احترافية شبيهة بنتفليكس لعرض الأفلام والمسلسلات، بملف واحد index.php يحتوي على كل HTML/CSS/JS/PHP، مع:

1️⃣ البنية الأساسية:

  • إنشاء تلقائي للمجلدات:
    /db → قاعدة بيانات SQLite (db/app.db)
    /movies → ملف JSON للمحتوى
    /uploads → صور البوسترات والخلفيات
  • إنشاء تلقائي للجداول عند أول تشغيل.
  • Router داخلي للتعامل مع الصفحات (?route=...).

2️⃣ دعم الأفلام والمسلسلات:

  • جدول movies: بيانات عامة (type='movie' أو 'series').
  • جدول episodes: لكل حلقة (season, episode, title, duration, mp4_src).
  • لوحة تحكم لإضافة/تعديل/حذف المواسم والحلقات.
  • واجهة المستخدم تعرض المسلسلات بمواسم وحلقات منظمة.
  • Auto-Next للحلقة التالية.
  • إشعارات عند إضافة حلقات جديدة لمسلسلات يتابعها المستخدم.

3️⃣ تعدد اللغات (AR/EN):

  • مصفوفة ترجمة داخلية $lang['key'] = ['ar'=>'...','en'=>'...'].
  • زر تبديل اللغة في الشريط العلوي، يحفظ التفضيل في session/cookie.
  • اتجاه الصفحة (RTL/LTR) يتغير تلقائيًا.
  • النصوص في الأفلام/المسلسلات (title, description) تدعم لغتين.

4️⃣ المشغل الاحترافي:

  • HTML5 Video مع تصميم مخصص وأزرار تحكم (تشغيل/إيقاف، تقديم/ترجيع، صوت، سرعة، ملء الشاشة، وضع سينما).
  • دعم Range Requests للبث الفوري.
  • استئناف المشاهدة من آخر موضع.
  • قائمة حلقات جانبية للمسلسلات.
  • دعم الترجمة (Subtitles) لاحقًا.
  • إدارة الجودة (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 مع المعاملات اللازمة.
  • عرض النتائج في شبكة مع شارات التقييم والجودة.

9️⃣ إدارة الإعلانات:

  • جدول ads لإدارة الإعلانات (الموضع، النوع، الوسائط، الرابط، الجدولة، الاستهداف).
  • جدول ad_impressions لتتبع مرات الظهور والنقرات.
  • مواضع الإعلانات: قبل/أثناء/بعد الفيديو، بانرات، بين الصفوف.
  • لوحة تحكم لإضافة/تعديل/حذف الإعلانات وعرض التقارير.

🔟 قائمة المشاهدة 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 تلقائيًا.
  • تمكين الادمن من اضافة المحتوي و التعديل علبه او حذفه
  • يمكن للادمن ان يضع شعار مخصص للموقع عن طريق رفعه او تغيير اسم الموقع
    ✅ النجاح يتحقق إذا:
  • كل الميزات تعمل بكامل وظائفها.
  • الأداء سريع، والواجهة متجاوبة، والمحتوى منظم.
  • دعم كامل للغتين العربية والإنجليزية.
    قم بانشاء الكود علي خطوات لانه طويل و في الاخير قم بجمعه في ملف واحد كما طلبت

Question

اكمل الكود رجاء :

<?php /****************************** * Netflix-like Mini Platform - Single File index.php * Author: ChatGPT (GPT-5 Thinking) * PHP >= 8.0, SQLite (PDO), no external deps ******************************/ declare(strict_types=1); if (session_status() === PHP_SESSION_NONE) session_start(); /* -------- CONFIG / PATHS -------- */ $BASE_DIR = __DIR__; $DB_DIR = $BASE_DIR . '/db'; $DB_FILE = $DB_DIR . '/app.db'; $MOVIES_DIR = $BASE_DIR . '/movies'; $UPLOADS = $BASE_DIR . '/uploads'; $SITE_CACHE_SECONDS = 60; // simple cache headers for static-like parts /* -------- BASIC OUTPUT SECURITY -------- */ function e(string $s): string { return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } /* -------- LANGUAGE / I18N -------- */ if (isset($_GET['lang']) && in_array($_GET['lang'], ['ar','en'])) { $_SESSION['lang'] = $_GET['lang']; setcookie('lang', $_GET['lang'], time()+60*60*24*365, '/'); header("Location: " . strtok($_SERVER["REQUEST_URI"],'?') . (isset($_GET['route'])?'?route='.e($_GET['route']):'')); exit; } $LANG = $_SESSION['lang'] ?? ($_COOKIE['lang'] ?? 'ar'); // default AR $dir = $LANG === 'ar' ? 'rtl' : 'ltr'; $T = [ 'site_name' => ['ar'=>'سينِماتيكس','en'=>'Cinematix'], 'home' => ['ar'=>'الرئيسية','en'=>'Home'], 'movies' => ['ar'=>'أفلام','en'=>'Movies'], 'series' => ['ar'=>'مسلسلات','en'=>'Series'], 'search' => ['ar'=>'بحث','en'=>'Search'], 'login' => ['ar'=>'تسجيل الدخول','en'=>'Login'], 'register' => ['ar'=>'إنشاء حساب','en'=>'Register'], 'logout' => ['ar'=>'تسجيل الخروج','en'=>'Logout'], 'dashboard' => ['ar'=>'لوحة التحكم','en'=>'Dashboard'], 'language' => ['ar'=>'اللغة','en'=>'Language'], 'theme' => ['ar'=>'الوضع','en'=>'Theme'], 'dark' => ['ar'=>'ليلي','en'=>'Dark'], 'light' => ['ar'=>'نهاري','en'=>'Light'], 'notifications' => ['ar'=>'الإشعارات','en'=>'Notifications'], 'mark_all_read' => ['ar'=>'تعيين الكل كمقروء','en'=>'Mark all read'], 'watchlist' => ['ar'=>'قائمة المشاهدة','en'=>'Watchlist'], 'continue_watching' => ['ar'=>'استمر في المشاهدة','en'=>'Continue Watching'], 'recommended' => ['ar'=>'مقترح لك','en'=>'Recommended for You'], 'trending' => ['ar'=>'رائج الآن','en'=>'Trending'], 'because_you_liked' => ['ar'=>'لأنك أحببت','en'=>'Because you liked'], 'details' => ['ar'=>'التفاصيل','en'=>'Details'], 'play' => ['ar'=>'تشغيل','en'=>'Play'], 'seasons' => ['ar'=>'المواسم','en'=>'Seasons'], 'episodes' => ['ar'=>'الحلقات','en'=>'Episodes'], 'add' => ['ar'=>'إضافة','en'=>'Add'], 'edit' => ['ar'=>'تعديل','en'=>'Edit'], 'delete' => ['ar'=>'حذف','en'=>'Delete'], 'save' => ['ar'=>'حفظ','en'=>'Save'], 'cancel' => ['ar'=>'إلغاء','en'=>'Cancel'], 'rating' => ['ar'=>'التقييم','en'=>'Rating'], 'your_rating' => ['ar'=>'تقييمك','en'=>'Your rating'], 'comments' => ['ar'=>'التعليقات','en'=>'Comments'], 'add_comment' => ['ar'=>'أضف تعليقاً','en'=>'Add comment'], 'search_advanced' => ['ar'=>'بحث متقدم','en'=>'Advanced search'], 'quality' => ['ar'=>'الجودة','en'=>'Quality'], 'year' => ['ar'=>'السنة','en'=>'Year'], 'genre' => ['ar'=>'التصنيف','en'=>'Genre'], 'language_label' => ['ar'=>'اللغة','en'=>'Language'], 'duration' => ['ar'=>'المدة','en'=>'Duration'], 'order' => ['ar'=>'الترتيب','en'=>'Order'], 'submit' => ['ar'=>'تنفيذ','en'=>'Apply'], 'add_to_watchlist' => ['ar'=>'أضف إلى المشاهدة','en'=>'Add to Watchlist'], 'remove_from_watchlist' => ['ar'=>'إزالة من المشاهدة','en'=>'Remove from Watchlist'], 'admin_settings' => ['ar'=>'إعدادات الموقع','en'=>'Site Settings'], 'site_logo' => ['ar'=>'شعار الموقع','en'=>'Site Logo'], 'site_title' => ['ar'=>'اسم الموقع','en'=>'Site Title'], 'pending_users' => ['ar'=>'طلبات التسجيل','en'=>'Pending Signups'], 'approve' => ['ar'=>'موافقة','en'=>'Approve'], 'reject' => ['ar'=>'رفض','en'=>'Reject'], 'send_notification' => ['ar'=>'إرسال إشعار','en'=>'Send Notification'], 'ads' => ['ar'=>'الإعلانات','en'=>'Ads'], 'reports' => ['ar'=>'التقارير','en'=>'Reports'], 'stats' => ['ar'=>'الإحصاءات','en'=>'Statistics'], ]; function t(string $key): string { global $T,$LANG; return $T[$key][$LANG] ?? $key; } /* -------- CSRF -------- */ if (empty($_SESSION['csrf'])) $_SESSION['csrf']=bin2hex(random_bytes(16)); function csrf_field(): string { return '<input type="hidden" name="csrf" value="'.e($_SESSION['csrf']).'">'; } function check_csrf(): void { if ($_SERVER['REQUEST_METHOD']==='POST') { $ok = isset($_POST['csrf']) && hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf']); if (!$ok) { http_response_code(403); exit('CSRF'); } } } /* -------- INIT DIRS/DB -------- */ foreach ([$DB_DIR,$MOVIES_DIR,$UPLOADS] as $d) if(!is_dir($d)) @mkdir($d,0775,true); $pdo = new PDO('sqlite:'.$DB_FILE); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $pdo->exec("PRAGMA foreign_keys = ON;"); $pdo->exec("CREATE TABLE IF NOT EXISTS users( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE, pass TEXT, name TEXT, role TEXT DEFAULT 'User', status TEXT DEFAULT 'pending', created_at TEXT DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS settings( k TEXT PRIMARY KEY, v TEXT )"); $pdo->exec("CREATE TABLE IF NOT EXISTS movies( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT CHECK(type IN ('movie','series')) NOT NULL, title_ar TEXT, title_en TEXT, description_ar TEXT, description_en TEXT, genres TEXT, year INTEGER, lang TEXT, quality TEXT, poster TEXT, backdrop TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS episodes( id INTEGER PRIMARY KEY AUTOINCREMENT, movie_id INTEGER REFERENCES movies(id) ON DELETE CASCADE, season INTEGER, episode INTEGER, title_ar TEXT, title_en TEXT, duration INTEGER, mp4_src TEXT, qualities_json TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS ratings( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, movie_id INTEGER REFERENCES movies(id) ON DELETE CASCADE, rating INTEGER CHECK(rating>=1 AND rating<=5), UNIQUE(user_id,movie_id) )"); $pdo->exec("CREATE TABLE IF NOT EXISTS comments( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, movie_id INTEGER REFERENCES movies(id) ON DELETE CASCADE, comment TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS watchlist( user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, movie_id INTEGER REFERENCES movies(id) ON DELETE CASCADE, PRIMARY KEY(user_id,movie_id) )"); $pdo->exec("CREATE TABLE IF NOT EXISTS watch_history( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, movie_id INTEGER REFERENCES movies(id) ON DELETE CASCADE, episode_id INTEGER REFERENCES episodes(id) ON DELETE CASCADE, position_seconds INTEGER DEFAULT 0, duration_seconds INTEGER DEFAULT 0, device TEXT, last_watched_at TEXT DEFAULT CURRENT_TIMESTAMP )"); $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 DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS ads( id INTEGER PRIMARY KEY AUTOINCREMENT, position TEXT, kind TEXT, media TEXT, target_url TEXT, start_at TEXT, end_at TEXT, targeting TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS ad_impressions( id INTEGER PRIMARY KEY AUTOINCREMENT, ad_id INTEGER REFERENCES ads(id) ON DELETE CASCADE, user_id INTEGER, event TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS activity_log( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, action TEXT, meta TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP )"); /* -------- SEED movies.json / sample -------- */ $MOVIES_JSON = $MOVIES_DIR.'/movies.json'; if (!file_exists($MOVIES_JSON)) { $seed = [ ['type'=>'movie','title_ar'=>'فيلم تجريبي','title_en'=>'Sample Movie', 'description_ar'=>'وصف بسيط للفيلم','description_en'=>'Simple sample movie', 'genres'=>'Action,Drama','year'=>2023,'lang'=>'ar/en','quality'=>'1080p', 'poster'=>'','backdrop'=>''], ['type'=>'series','title_ar'=>'مسلسل تجريبي','title_en'=>'Sample Series', 'description_ar'=>'وصف بسيط للمسلسل','description_en'=>'Simple sample series', 'genres'=>'Drama','year'=>2024,'lang'=>'ar/en','quality'=>'720p', 'poster'=>'','backdrop'=>''] ]; file_put_contents($MOVIES_JSON, json_encode($seed, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT)); // Insert to DB if empty $count = (int)$pdo->query("SELECT COUNT(*) FROM movies")->fetchColumn(); if ($count===0){ $stmt=$pdo->prepare("INSERT INTO movies(type,title_ar,title_en,description_ar,description_en,genres,year,lang,quality,poster,backdrop) VALUES(?,?,?,?,?,?,?,?,?,?,?)"); foreach($seed as $m){ $stmt->execute([$m['type'],$m['title_ar'],$m['title_en'],$m['description_ar'],$m['description_en'],$m['genres'],$m['year'],$m['lang'],$m['quality'],$m['poster'],$m['backdrop']]); } // Add sample episodes for series $sid = (int)$pdo->query("SELECT id FROM movies WHERE type='series' LIMIT 1")->fetchColumn(); if ($sid){ $ep=$pdo->prepare("INSERT INTO episodes(movie_id,season,episode,title_ar,title_en,duration,mp4_src,qualities_json) VALUES(?,?,?,?,?,?,?,?)"); $qualities=json_encode(['720p'=>'/uploads/sample720.mp4','1080p'=>'/uploads/sample1080.mp4']); for($i=1;$i<=3;$i++){ $ep->execute([$sid,1,$i,"حلقة $i","Episode $i",1500,"/uploads/sample1080.mp4",$qualities]); } } } } /* -------- SIMPLE AUTH -------- */ function current_user() { return $_SESSION['user'] ?? null; } function require_login() { if (!current_user()) { header('Location:?route=login'); exit; } } function is_admin_like($u): bool { return in_array($u['role']??'', ['Admin','Editor','Moderator']); } /* -------- HELPERS -------- */ function json_out($data){ header('Content-Type: application/json'); echo json_encode($data, JSON_UNESCAPED_UNICODE); exit; } function set_flash($k,$v){ $_SESSION['flash'][$k]=$v; } function get_flash($k){ $v=$_SESSION['flash'][$k]??null; unset($_SESSION['flash'][$k]); return $v; } function log_activity(PDO $pdo, $uid, $action, $meta=''){ $st=$pdo->prepare("INSERT INTO activity_log(user_id,action,meta) VALUES(?,?,?)"); $st->execute([$uid,$action,$meta]); } /* -------- ACTIONS (AJAX/API) -------- */ $action = $_GET['action'] ?? ''; if ($action==='searchAdvanced') { // API search: type, genre, year, rating, duration, quality, lang, order $q = $_GET; $sql = "SELECT m.*, COALESCE(AVG(r.rating),0) as avg_rating FROM movies m LEFT JOIN ratings r ON r.movie_id=m.id WHERE 1=1"; $args=[]; if (!empty($q['type']) && in_array($q['type'],['movie','series'])) { $sql.=" AND m.type=?"; $args[]=$q['type']; } if (!empty($q['genre'])) { $sql.=" AND m.genres LIKE ?"; $args[]='%'.$q['genre'].'%'; } if (!empty($q['year'])) { $sql.=" AND m.year=?"; $args[]=(int)$q['year']; } if (!empty($q['quality'])) { $sql.=" AND m.quality LIKE ?"; $args[]='%'.$q['quality'].'%'; } if (!empty($q['lang'])) { $sql.=" AND m.lang LIKE ?"; $args[]='%'.$q['lang'].'%'; } $sql.=" GROUP BY m.id "; $order = $q['order'] ?? 'new'; $sql.= match($order){ 'rating' => " ORDER BY avg_rating DESC", 'year_asc' => " ORDER BY m.year ASC", 'year_desc' => " ORDER BY m.year DESC", default => " ORDER BY m.created_at DESC" }; $st=$pdo->prepare($sql); $st->execute($args); $res=$st->fetchAll(PDO::FETCH_ASSOC); json_out(['ok'=>true,'results'=>$res]); } if ($action==='toggleWatchlist') { require_login(); check_csrf(); $uid=current_user()['id']; $mid=(int)($_POST['movie_id']??0); $exists=(int)$pdo->prepare("SELECT COUNT(*) FROM watchlist WHERE user_id=? AND movie_id=?")->execute([$uid,$mid])||0; $st=$pdo->prepare("SELECT COUNT(*) FROM watchlist WHERE user_id=? AND movie_id=?"); $st->execute([$uid,$mid]); $exists=(int)$st->fetchColumn(); if ($exists){ $pdo->prepare("DELETE FROM watchlist WHERE user_id=? AND movie_id=?")->execute([$uid,$mid]); json_out(['ok'=>true,'in'=>false]); } else { $pdo->prepare("INSERT INTO watchlist(user_id,movie_id) VALUES(?,?)")->execute([$uid,$mid]); json_out(['ok'=>true,'in'=>true]); } } if ($action==='rate') { require_login(); check_csrf(); $uid=current_user()['id']; $mid=(int)($_POST['movie_id']??0); $r=max(1,min(5,(int)($_POST['rating']??0))); $pdo->prepare("INSERT INTO ratings(user_id,movie_id,rating) VALUES(?,?,?) ON CONFLICT(user_id,movie_id) DO UPDATE SET rating=excluded.rating")->execute([$uid,$mid,$r]); json_out(['ok'=>true]); } if ($action==='comment') { require_login(); check_csrf(); $uid=current_user()['id']; $mid=(int)($_POST['movie_id']??0); $c=trim($_POST['comment']??''); if($c!== '') $pdo->prepare("INSERT INTO comments(user_id,movie_id,comment) VALUES(?,?,?)")->execute([$uid,$mid,$c]); json_out(['ok'=>true]); } if ($action==='deleteComment') { require_login(); check_csrf(); $uid=current_user()['id']; $cid=(int)($_POST['comment_id']??0); // allow owner or admin-like $row=$pdo->prepare("SELECT user_id FROM comments WHERE id=?"); $row->execute([$cid]); $owner=(int)$row->fetchColumn(); $is_admin=is_admin_like(current_user()); if ($owner===$uid || $is_admin){ $pdo->prepare("DELETE FROM comments WHERE id=?")->execute([$cid]); json_out(['ok'=>true]); } json_out(['ok'=>false]); } if ($action==='savePosition') { require_login(); $uid=current_user()['id']; $mid=(int)($_POST['movie_id']??0); $eid=(int)($_POST['episode_id']??0); $pos=(int)($_POST['position']??0); $dur=(int)($_POST['duration']??0); $dev=substr($_POST['device']??'web',0,32); $st=$pdo->prepare("INSERT INTO watch_history(user_id,movie_id,episode_id,position_seconds,duration_seconds,device,last_watched_at) VALUES(?,?,?,?,?,?,CURRENT_TIMESTAMP)"); // upsert-like: keep last record per (user,movie,episode,device) $find=$pdo->prepare("SELECT id FROM watch_history WHERE user_id=? AND movie_id=? AND IFNULL(episode_id,0)=? AND device=? ORDER BY last_watched_at DESC LIMIT 1"); $find->execute([$uid,$mid,$eid,$dev]); $hid=$find->fetchColumn(); if ($hid){ $pdo->prepare("UPDATE watch_history SET position_seconds=?,duration_seconds=?,last_watched_at=CURRENT_TIMESTAMP WHERE id=?")->execute([$pos,$dur,$hid]); } else { $st->execute([$uid,$mid,$eid?:null,$pos,$dur,$dev]); } json_out(['ok'=>true]); } if ($action==='notifications') { require_login(); $uid=current_user()['id']; $st=$pdo->prepare("SELECT * FROM notifications WHERE user_id IN (?,0) ORDER BY created_at DESC LIMIT 20"); $st->execute([$uid]); $rows=$st->fetchAll(PDO::FETCH_ASSOC); json_out(['ok'=>true,'items'=>$rows]); } if ($action==='markAllRead') { require_login(); $uid=current_user()['id']; $pdo->prepare("UPDATE notifications SET is_read=1 WHERE user_id IN (?,0)")->execute([$uid]); json_out(['ok'=>true]); } if ($action==='adImpress') { $ad=(int)($_POST['ad_id']??0); $uid=current_user()['id']??null; $event=$_POST['event']??'impression'; $pdo->prepare("INSERT INTO ad_impressions(ad_id,user_id,event) VALUES(?,?,?)")->execute([$ad,$uid,$event]); json_out(['ok'=>true]); } if ($action==='stream') { // SECURE RANGE STREAM ONLY FROM /uploads $file = $_GET['file'] ?? ''; $path = realpath($GLOBALS['UPLOADS'] . '/' . ltrim($file,'/')); if (!$path || !str_starts_with($path, realpath($GLOBALS['UPLOADS'])) || !is_file($path)) { http_response_code(404); exit('Not found'); } $size = filesize($path); $fp = fopen($path,'rb'); $begin = 0; $end = $size - 1; header("Content-Type: video/mp4"); header("Accept-Ranges: bytes"); if (isset($_SERVER['HTTP_RANGE'])) { if (preg_match('/bytes=(\d+)-(\d*)/i', $_SERVER['HTTP_RANGE'], $m)) { $begin = (int)$m[1]; if ($m[2] !== '') $end = (int)$m[2]; } header('HTTP/1.1 206 Partial Content'); header("Content-Range: bytes $begin-$end/$size"); } $length = $end - $begin + 1; header("Content-Length: $length"); fseek($fp, $begin); $buffer = 8192; while(!feof($fp) && ($pos=ftell($fp)) <= $end){ if ($pos + $buffer > $end+1) $buffer = $end - $pos + 1; echo fread($fp, $buffer); flush(); } fclose($fp); exit; } /* -------- ROUTER -------- */ $route = $_GET['route'] ?? 'home'; /* -------- AUTH ROUTES -------- */ if ($route==='register' && $_SERVER['REQUEST_METHOD']==='POST') { check_csrf(); $email=trim($_POST['email']??''); $name=trim($_POST['name']??''); $pass=$_POST['pass']??''; if ($email && $pass){ $hash=password_hash($pass,PASSWORD_DEFAULT); $role='User'; $status='pending'; $pdo->prepare("INSERT INTO users(email,pass,name,role,status) VALUES(?,?,?,?,?)")->execute([$email,$hash,$name,$role,$status]); // first user becomes Admin approved $cnt=(int)$pdo->query("SELECT COUNT(*) FROM users")->fetchColumn(); if ($cnt===1) $pdo->exec("UPDATE users SET role='Admin', status='approved' WHERE id=1"); set_flash('ok','registered'); header('Location:?route=login'); exit; } } if ($route==='login' && $_SERVER['REQUEST_METHOD']==='POST') { check_csrf(); $email=trim($_POST['email']??''); $pass=$_POST['pass']??''; $st=$pdo->prepare("SELECT * FROM users WHERE email=?"); $st->execute([$email]); $u=$st->fetch(PDO::FETCH_ASSOC); if ($u && password_verify($pass,$u['pass'])){ if ($u['status']!=='approved'){ set_flash('err','pending'); header('Location:?route=login'); exit; } $_SESSION['user']=$u; header('Location:?route=home'); exit; } set_flash('err','bad'); header('Location:?route=login'); exit; } if ($route==='logout'){ unset($_SESSION['user']); header('Location:?route=home'); exit; } /* -------- ADMIN ACTIONS (simple) -------- */ if ($route==='admin.approve' && $_SERVER['REQUEST_METHOD']==='POST') { require_login(); check_csrf(); if (!is_admin_like(current_user())) exit('forbidden'); $id=(int)$_POST['id']; $ok=(int)$_POST['ok']; if ($ok===1){ $pdo->prepare("UPDATE users SET status='approved' WHERE id=?")->execute([$id]); } else { $pdo->prepare("UPDATE users SET status='rejected' WHERE id=?")->execute([$id]); } header('Location:?route=admin.users'); exit; } if ($route==='admin.notify' && $_SERVER['REQUEST_METHOD']==='POST') { require_login(); check_csrf(); if (!is_admin_like(current_user())) exit('forbidden'); $uid=(int)($_POST['user_id']??0); $ma=trim($_POST['message_ar']??''); $me=trim($_POST['message_en']??''); $link=trim($_POST['link']??''); if ($uid>0){ $pdo->prepare("INSERT INTO notifications(user_id,message_ar,message_en,link) VALUES(?,?,?,?)")->execute([$uid,$ma,$me,$link]); } else { // to all: user_id=0 $pdo->prepare("INSERT INTO notifications(user_id,message_ar,message_en,link) VALUES(0,?,?,?)")->execute([$ma,$me,$link]); } header('Location:?route=admin'); exit; } if ($route==='admin.movie.save' && $_SERVER['REQUEST_METHOD']==='POST') { require_login(); check_csrf(); if (!is_admin_like(current_user())) exit('forbidden'); $id=(int)($_POST['id']??0); $cols=['type','title_ar','title_en','description_ar','description_en','genres','year','lang','quality','poster','backdrop']; $vals=[]; foreach($cols as $c){ $vals[$c]=trim($_POST[$c]??''); } if ($id){ $set=implode(',', array_map(fn($k)=>"$k=?", array_keys($vals))); $st=$pdo->prepare("UPDATE movies SET $set WHERE id=?"); $st->execute([...array_values($vals),$id]); } else { $st=$pdo->prepare("INSERT INTO movies(".implode(',',$cols).") VALUES(".str_repeat('?,',count($cols)-1)."?)"); $st->execute(array_values($vals)); $newId=(int)$pdo->lastInsertId(); // notify followers (none yet) + general $pdo->prepare("INSERT INTO notifications(user_id,message_ar,message_en,link) VALUES(0,?,?,?)") ->execute(["تمت إضافة محتوى جديد","New content added","?route=details&id=".$newId]); } header('Location:?route=admin'); exit; } if ($route==='admin.episode.save' && $_SERVER['REQUEST_METHOD']==='POST') { require_login(); check_csrf(); if (!is_admin_like(current_user())) exit('forbidden'); $id=(int)($_POST['id']??0); $movie_id=(int)$_POST['movie_id']; $season=(int)$_POST['season']; $episode=(int)$_POST['episode']; $title_ar=trim($_POST['title_ar']??''); $title_en=trim($_POST['title_en']??''); $duration=(int)$_POST['duration']; $mp4_src=trim($_POST['mp4_src']??''); $qualities_json=trim($_POST['qualities_json']??'{}'); if ($id){ $pdo->prepare("UPDATE episodes SET season=?,episode=?,title_ar=?,title_en=?,duration=?,mp4_src=?,qualities_json=? WHERE id=?") ->execute([$season,$episode,$title_ar,$title_en,$duration,$mp4_src,$qualities_json,$id]); } else { $pdo->prepare("INSERT INTO episodes(movie_id,season,episode,title_ar,title_en,duration,mp4_src,qualities_json) VALUES(?,?,?,?,?,?,?,?)") ->execute([$movie_id,$season,$episode,$title_ar,$title_en,$duration,$mp4_src,$qualities_json]); // notify followers $followers=$pdo->prepare("SELECT user_id FROM watchlist WHERE movie_id=?"); $followers->execute([$movie_id]); $uids=$followers->fetchAll(PDO::FETCH_COLUMN); foreach ($uids as $u) { $pdo->prepare("INSERT INTO notifications(user_id,message_ar,message_en,link) VALUES(?,?,?,?)") ->execute([$u,"تمت إضافة حلقة جديدة","New episode added","?route=details&id=".$movie_id]); } } header('Location:?route=admin'); exit; } if ($route==='admin.settings' && $_SERVER['REQUEST_METHOD']==='POST') { require_login(); check_csrf(); if (!is_admin_like(current_user())) exit('forbidden'); $site_title=trim($_POST['site_title']??''); $pdo->prepare("INSERT INTO settings(k,v) VALUES('site_title',?) ON CONFLICT(k) DO UPDATE SET v=excluded.v")->execute([$site_title]); if (!empty($_FILES['logo']['name']) && is_uploaded_file($_FILES['logo']['tmp_name'])) { $ext=pathinfo($_FILES['logo']['name'], PATHINFO_EXTENSION); $name='logo.'.preg_replace('/[^a-z0-9]+/i','',$ext); $dest=$GLOBALS['UPLOADS'].'/'.$name; move_uploaded_file($_FILES['logo']['tmp_name'],$dest); $pdo->prepare("INSERT INTO settings(k,v) VALUES('site_logo',?) ON CONFLICT(k) DO UPDATE SET v=excluded.v")->execute(['/uploads/'.$name]); } header('Location:?route=admin'); exit; } /* -------- FETCH SETTINGS -------- */ $SITE_TITLE = $pdo->query("SELECT v FROM settings WHERE k='site_title'")->fetchColumn() ?: t('site_name'); $SITE_LOGO = $pdo->query("SELECT v FROM settings WHERE k='site_logo'")->fetchColumn() ?: ''; /* -------- VIEW HELPERS -------- */ function avg_rating(PDO $pdo, int $movie_id): float { $st=$pdo->prepare("SELECT AVG(rating) FROM ratings WHERE movie_id=?"); $st->execute([$movie_id]); return round((float)$st->fetchColumn(),1); } function episodes_by_season(PDO $pdo, int $movie_id): array { $st=$pdo->prepare("SELECT * FROM episodes WHERE movie_id=? ORDER BY season,episode"); $st->execute([$movie_id]); $eps=$st->fetchAll(PDO::FETCH_ASSOC); $by=[]; foreach($eps as $e){ $by[$e['season']][]=$e; } return $by; } function user_watch_in(PDO $pdo, int $uid, int $mid, ?int $eid=null): array { $st=$pdo->prepare("SELECT * FROM watch_history WHERE user_id=? AND movie_id=? AND IFNULL(episode_id,0)=? ORDER BY last_watched_at DESC LIMIT 1"); $st->execute([$uid,$mid,$eid?:0]); return $st->fetch(PDO::FETCH_ASSOC) ?: []; } function ad_pick(PDO $pdo, string $position): ?array { $st=$pdo->prepare("SELECT * FROM ads WHERE position=? AND (start_at IS NULL OR start_at<=CURRENT_TIMESTAMP) AND (end_at IS NULL OR end_at>=CURRENT_TIMESTAMP) ORDER BY RANDOM() LIMIT 1"); $st->execute([$position]); return $st->fetch(PDO::FETCH_ASSOC) ?: null; } /* -------- HEADER + STYLES -------- */ function header_html() { global $LANG,$dir,$SITE_TITLE,$SITE_LOGO; header("Referrer-Policy: strict-origin-when-cross-origin"); header("X-Content-Type-Options: nosniff"); header("X-Frame-Options: SAMEORIGIN"); // Simple cache for assets-like pages header("Cache-Control: no-store"); ?> <!doctype html> <html lang="<?=e($LANG)?>" dir="<?=$dir?>"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title><?=e($SITE_TITLE)?></title> <style> :root{ --bg:#0b0e13; --fg:#f1f5f9; --muted:#94a3b8; --accent:#22d3ee; --card:#121826; --accent-2:#6366f1; --danger:#ef4444; --ok:#22c55e; } html[theme="light"]{ --bg:#f7fafc; --fg:#0b0e13; --card:#ffffff; --muted:#475569; } *{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--fg);font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,"Noto Sans",sans-serif} a{color:inherit;text-decoration:none} .topbar{display:flex;align-items:center;gap:.75rem;background:var(--card);padding:.5rem 1rem;position:sticky;top:0;z-index:1000;border-bottom:1px solid #1f2937} .topbar .brand{display:flex;align-items:center;gap:.5rem;font-weight:700} .topbar img.logo{height:28px} .topbar .grow{flex:1} .btn{background:var(--accent-2);color:#fff;border:none;border-radius:6px;padding:.5rem .8rem;cursor:pointer} .btn.secondary{background:#334155} .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:1rem} .card{background:var(--card);border-radius:10px;overflow:hidden} .card img{width:100%;height:220px;object-fit:cover;display:block;filter:saturate(1.1)} .p-2{padding:.5rem}.p-3{padding:1rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:1rem} .badge{display:inline-block;background:#0ea5e9;color:#fff;border-radius:999px;padding:.15rem .5rem;font-size:.75rem;margin-inline-end:.25rem} .row{display:flex;gap:1rem;align-items:center} input,select,textarea{background:#0f172a;color:var(--fg);border:1px solid #1f2937;border-radius:8px;padding:.5rem .6rem} input[type=file]{background:transparent;border:none} form.inline{display:inline} .lang, .theme, .bell{cursor:pointer;padding:.35rem .5rem;border-radius:8px;background:#0f172a;border:1px solid #1f2937} .dropdown{position:relative} .dropdown-menu{position:absolute;right:0;top:110%;background:var(--card);border:1px solid #1f2937;border-radius:10px;min-width:260px;display:none;max-height:360px;overflow:auto} .dropdown.open .dropdown-menu{display:block} .video-wrap{background:#000;border-radius:12px;overflow:hidden} .player-controls{display:flex;gap:.5rem;align-items:center;padding:.5rem;background:#0b0b0b} .player-controls button, .player-controls select, .player-controls input[type=range]{background:#111827;color:#fff;border:1px solid #374151;border-radius:8px;padding:.35rem .6rem} .episodes-side{background:var(--card);border:1px solid #1f2937;border-radius:12px;max-height:520px;overflow:auto} .comment{border-bottom:1px solid #1f2937;padding:.5rem 0} .star{cursor:pointer;font-size:20px;color:#eab308} .banner{background:linear-gradient(90deg,#0ea5e9,#6366f1);border-radius:12px;padding:1rem;color:#fff} .table{width:100%;border-collapse:collapse} .table th,.table td{border-bottom:1px solid #1f2937;padding:.5rem;text-align:<?=$dir==='rtl'?'right':'left'?>} .small{font-size:.9rem;color:var(--muted)} .cinema{max-width:1200px;margin:auto} @media (min-width:1000px){ .details-layout{display:grid;grid-template-columns:2fr 1fr;gap:1rem} } </style> </head> <body> <div class="topbar"> <div class="brand"> <?php global $SITE_LOGO; if($SITE_LOGO): ?> <img class="logo" src="<?=e($SITE_LOGO)?>" alt="logo"> <?php endif; ?> <a href="?route=home"><?=e($SITE_TITLE)?></a> </div> <div class="grow"> <form action="?route=search" method="get" style="display:flex;gap:.5rem"> <input type="hidden" name="route" value="search"> <input name="q" placeholder="<?=e(t('search'))?>..."> <button class="btn"><?=e(t('search'))?></button> <a class="btn secondary" href="?route=search.advanced"><?=e(t('search_advanced'))?></a> </form> </div> <div class="row"> <a class="btn secondary" href="?lang=<?=$GLOBALS['LANG']==='ar'?'en':'ar'?>"><?=e(t('language'))?>: <?=$GLOBALS['LANG']==='ar'?'AR→EN':'EN→AR'?></a> <div class="theme" id="themeToggle"><?=e(t('theme'))?> 🌙</div> <div class="dropdown" id="bell"> <div class="bell">🔔</div> <div class="dropdown-menu p-2" id="notifMenu"><div class="small"><?=e(t('notifications'))?>...</div></div> </div> <?php if($u=current_user()): ?> <a class="btn secondary" href="?route=watchlist"><?=e(t('watchlist'))?></a> <?php if(is_admin_like($u)): ?><a class="btn" href="?route=admin"><?=e(t('dashboard'))?></a><?php endif; ?> <a class="btn" href="?route=logout"><?=e(t('logout'))?></a> <?php else: ?> <a class="btn secondary" href="?route=login"><?=e(t('login'))?></a> <a class="btn" href="?route=register"><?=e(t('register'))?></a> <?php endif; ?> </div> </div> <script> // Theme (default dark) (function(){ const saved=localStorage.getItem('theme')||'dark'; document.documentElement.setAttribute('theme', saved); document.getElementById('themeToggle').onclick=()=>{ const cur=document.documentElement.getAttribute('theme')==='light'?'dark':'light'; document.documentElement.setAttribute('theme',cur); localStorage.setItem('theme',cur); }; })(); </script> <?php } function footer_html(){ ?> <script> // Notifications dropdown const bell=document.getElementById('bell'); if (bell){ bell.addEventListener('click', async (e)=>{ bell.classList.toggle('open'); const menu=document.getElementById('notifMenu'); if(menu.dataset.loaded) return; const r=await fetch('?action=notifications'); if(!r.ok) return; const data=await r.json(); menu.innerHTML=''; (data.items||[]).forEach(n=>{ const msg = <?=json_encode($GLOBALS['LANG'])?>==='ar' ? (n.message_ar||'') : (n.message_en||''); const a=document.createElement('a'); a.href=n.link||'#'; a.className='row p-2'; a.innerHTML='🔔 '+msg+' <span class="small" style="margin-<?=$GLOBALS['dir']==='rtl'?'left':'right'?>:auto">'+n.created_at+'</span>'; menu.appendChild(a); }); const mark=document.createElement('button'); mark.className='btn secondary mt-2'; mark.textContent='<?=e(t('mark_all_read'))?>'; mark.onclick=()=>fetch('?action=markAllRead').then(()=>{}); menu.appendChild(mark); menu.dataset.loaded=1; }); } // Generic helpers async function postForm(url, form){ const fd=new FormData(form); const r=await fetch(url,{method:'POST',body:fd}); return r.json(); } </script> </body></html> <?php }

/* -------- PAGE TEMPLATES -------- */
function page_home(PDO pdo){ header_html(); // Sections: Continue watching / Recommended / Trending u=current_user();
echo '<div class="p-3 cinema">';
// Continue Watching
if(u){ st=pdo>prepare("SELECTDISTINCTm.FROMwatchhistorywJOINmoviesmONm.id=w.movieidWHEREw.userid=?ORDERBYw.lastwatchedatDESCLIMIT12");pdo->prepare("SELECT DISTINCT m.* FROM watch_history w JOIN movies m ON m.id=w.movie_id WHERE w.user_id=? ORDER BY w.last_watched_at DESC LIMIT 12"); st->execute([u[id]]);u['id']]); rows=st>fetchAll(PDO::FETCHASSOC);if(st->fetchAll(PDO::FETCH_ASSOC); if(rows){
echo '<h2>'.e(t('continue_watching')).'</h2><div class="grid mt-2">';
foreach(rowsasrows as m){
title=title=GLOBALS['LANG']==='ar'?m[titlear]:m['title_ar']:m['title_en'];
echo '<a class="card" href="?route=details&id='.$m['id'].'"><img loading="lazy" src="'.e($m['poster']?:'https://picsum.photos/300/450?blur=2').'" alt=""><div class="p-2">'.e(title).'</div></a>'; } echo '</div>'; } } // Recommended (simple heuristic: top rated or same genres you rated high) rec=pdo>query("SELECTm.,IFNULL((SELECTAVG(rating)FROMratingsrWHEREr.movieid=m.id),0)asavgratingFROMmoviesmORDERBYavgratingDESC,m.createdatDESCLIMIT12")>fetchAll(PDO::FETCHASSOC);echo<h2class="mt3">.e(t(recommended)).</h2><divclass="gridmt2">;foreach(pdo->query("SELECT m.*, IFNULL((SELECT AVG(rating) FROM ratings r WHERE r.movie_id=m.id),0) as avg_rating FROM movies m ORDER BY avg_rating DESC, m.created_at DESC LIMIT 12")->fetchAll(PDO::FETCH_ASSOC); echo '<h2 class="mt-3">'.e(t('recommended')).'</h2><div class="grid mt-2">'; foreach(rec as m){ title=GLOBALS[LANG]===ar?GLOBALS['LANG']==='ar'?m['title_ar']:m['title_en']; echo '<a class="card" href="?route=details&id='.m['id'].'"><img loading="lazy" src="'.e($m['poster']?:'https://picsum.photos/300/450?blur=2').'" alt=""><div class="p-2">'.e(title).<divclass="small">.avgrating(title).'<div class="small">⭐ '.avg_rating(pdo,(int)m['id']).'</div></div></a>'; } echo '</div>'; // Trending (most watched last week) trend=pdo>query("SELECTm.,COUNT(w.id)asviewsFROMmoviesmLEFTJOINwatchhistorywONw.movieid=m.idANDw.lastwatchedat>=datetime(now,7day)GROUPBYm.idORDERBYviewsDESCLIMIT12")>fetchAll(PDO::FETCHASSOC);echo<h2class="mt3">.e(t(trending)).</h2><divclass="gridmt2">;foreach(pdo->query("SELECT m.*, COUNT(w.id) as views FROM movies m LEFT JOIN watch_history w ON w.movie_id=m.id AND w.last_watched_at>=datetime('now','-7 day') GROUP BY m.id ORDER BY views DESC LIMIT 12")->fetchAll(PDO::FETCH_ASSOC); echo '<h2 class="mt-3">'.e(t('trending')).'</h2><div class="grid mt-2">'; foreach(trend as m){ title=GLOBALS[LANG]===ar?GLOBALS['LANG']==='ar'?m['title_ar']:m['title_en']; echo '<a class="card" href="?route=details&id='.m['id'].'"><img loading="lazy" src="'.e($m['poster']?:'https://picsum.photos/300/450?blur=2').'" alt=""><div class="p-2">'.e(title).'</div></a>'; } echo '</div></div>'; footer_html(); } function page_search(PDO pdo){
header_html();
q=trim(q=trim(_GET['q']??'');
echo '<div class="p-3 cinema"><h2>'.e(t('search')).': '.e(q).</h2><divclass="gridmt2">;if(q).'</h2><div class="grid mt-2">'; if (q!==''){
st=st=pdo->prepare("SELECT * FROM movies WHERE title_ar LIKE ? OR title_en LIKE ? OR description_ar LIKE ? OR description_en LIKE ? ORDER BY created_at DESC");
like=like='%'.q.'%'; st>execute([st->execute([like,like,like,like,like]);like]); rows=st>fetchAll(PDO::FETCHASSOC);foreach(st->fetchAll(PDO::FETCH_ASSOC); foreach(rows as m){ title=GLOBALS[LANG]===ar?GLOBALS['LANG']==='ar'?m['title_ar']:m['title_en']; echo '<a class="card" href="?route=details&id='.m['id'].'"><img loading="lazy" src="'.e($m['poster']?:'https://picsum.photos/300/450?blur=2').'" alt=""><div class="p-2">'.e($title).'</div></a>';
}
}
echo '</div></div>';
footer_html();
}
function page_search_advanced(){
header_html(); ?>

<div class="p-3 cinema"> <h2><?=e(t('search_advanced'))?></h2> <form id="advForm" class="row" onsubmit="event.preventDefault(); doAdv();"> <select name="type"><option value="">--</option><option value="movie"><?=e(t('movies'))?></option><option value="series"><?=e(t('series'))?></option></select> <input name="genre" placeholder="<?=e(t('genre'))?>"> <input name="year" placeholder="<?=e(t('year'))?>" type="number"> <input name="quality" placeholder="<?=e(t('quality'))?>"> <input name="lang" placeholder="<?=e(t('language_label'))?>"> <select name="order"> <option value="new"><?=e(t('order'))?>: New</option> <option value="rating"><?=e(t('rating'))?></option> <option value="year_desc"><?=e(t('year'))?> ↓</option> <option value="year_asc"><?=e(t('year'))?> ↑</option> </select> <button class="btn"><?=e(t('submit'))?></button> </form> <div id="advResults" class="grid mt-3"></div> </div> <script> async function doAdv(){ const f=document.getElementById('advForm'); const params=new URLSearchParams(new FormData(f)).toString(); const r=await fetch('?action=searchAdvanced&'+params); const data=await r.json(); const grid=document.getElementById('advResults'); grid.innerHTML=''; (data.results||[]).forEach(m=>{ const title = <?=json_encode($GLOBALS['LANG'])?>==='ar' ? (m.title_ar||'') : (m.title_en||''); const card=document.createElement('a'); card.className='card'; card.href='?route=details&id='+m.id; card.innerHTML=`<img loading="lazy" src="${m.poster||'https://picsum.photos/300/450?blur=2'}"><div class="p-2">${title}<div class="small">⭐ ${(m.avg_rating||0).toFixed(1)} • ${m.quality||''}</div></div>`; grid.appendChild(card); }); } </script> <?php footer_html(); } function page_login(){ header_html(); $err=get_flash('err'); echo '<div class="p-3 cinema"><h2>'.e(t('login')).'</h2>'; if($err==='pending') echo '<div class="banner">حسابك بانتظار الموافقة</div>'; if($err==='bad') echo '<div class="banner" style="background:#ef4444">بيانات غير صحيحة</div>'; echo '<form method="post" action="?route=login">'.csrf_field().' <div class="mt-2"><input name="email" placeholder="Email"></div> <div class="mt-2"><input type="password" name="pass" placeholder="Password"></div> <button class="btn mt-2">'.e(t('login')).'</button> </form></div>'; footer_html(); } function page_register(){ header_html(); $ok=get_flash('ok'); if($ok==='registered') echo '<div class="p-3 cinema"><div class="banner">'.e('تم إنشاء الحساب، بانتظار الموافقة').'</div></div>'; echo '<div class="p-3 cinema"><h2>'.e(t('register')).'</h2> <form method="post" action="?route=register">'.csrf_field().' <div class="mt-2"><input name="name" placeholder="Name"></div> <div class="mt-2"><input name="email" placeholder="Email"></div> <div class="mt-2"><input type="password" name="pass" placeholder="Password"></div> <button class="btn mt-2">'.e(t('register')).'</button> </form></div>'; footer_html(); } function page_admin(PDO $pdo){ require_login(); if(!is_admin_like(current_user())){ header_html(); echo '<div class="p-3 cinema">forbidden</div>'; footer_html(); return; } header_html(); // Pending users $pend=$pdo->query("SELECT * FROM users WHERE status='pending' ORDER BY created_at")->fetchAll(PDO::FETCH_ASSOC); echo '<div class="p-3 cinema"><h2>'.e(t('dashboard')).'</h2>'; echo '<h3>'.e(t('pending_users')).'</h3><table class="table">'; echo '<tr><th>ID</th><th>Email</th><th>Name</th><th>Actions</th></tr>'; foreach($pend as $p){ echo '<tr><td>'.$p['id'].'</td><td>'.e($p['email']).'</td><td>'.e($p['name']).'</td><td> <form class="inline" method="post" action="?route=admin.approve">'.csrf_field().'<input type="hidden" name="id" value="'.$p['id'].'"><input type="hidden" name="ok" value="1"><button class="btn">'.e(t('approve')).'</button></form> <form class="inline" method="post" action="?route=admin.approve">'.csrf_field().'<input type="hidden" name="id" value="'.$p['id'].'"><input type="hidden" name="ok" value="0"><button class="btn secondary">'.e(t('reject')).'</button></form> </td></tr>'; } echo '</table>';
text
// Site settings echo '<h3 class="mt-3">'.e(t('admin_settings')).'</h3> <form method="post" action="?route=admin.settings" enctype="multipart/form-data">'.csrf_field().' <div class="row"><label>'.e(t('site_title')).'</label><input name="site_title" value="'.e($GLOBALS['SITE_TITLE']).'"></div> <div class="row mt-2"><label>'.e(t('site_logo')).'</label><input type="file" name="logo"></div> <button class="btn mt-2">'.e(t('save')).'</button> </form>'; // Add/Edit Movie echo '<h3 class="mt-3">'.e(t('movies')).' / '.e(t('series')).'</h3>'; echo '<form method="post" action="?route=admin.movie.save" class="grid">'.csrf_field().' <input name="id" placeholder="id (leave empty for new)"> <select name="type"><option value="movie">movie</option><option value="series">series</option></select> <input name="title_ar" placeholder="title_ar"><input name="title_en" placeholder="title_en"> <input name="genres" placeholder="genres"><input name="year" placeholder="year" type="number"> <input name="lang" placeholder="ar/en"><input name="quality" placeholder="1080p"> <input name="poster" placeholder="poster URL"><input name="backdrop" placeholder="backdrop URL"> <textarea name="description_ar" placeholder="description_ar"></textarea> <textarea name="description_en" placeholder="description_en"></textarea> <button class="btn">'.e(t('save')).'</button></form>'; // Add/Edit Episode echo '<h3 class="mt-3">'.e(t('episodes')).'</h3> <form method="post" action="?route=admin.episode.save" class="grid">'.csrf_field().' <input name="id" placeholder="id (edit)"> <input name="movie_id" placeholder="movie_id" required> <input name="season" placeholder="season" type="number" required> <input name="episode" placeholder="episode" type="number" required> <input name="title_ar" placeholder="title_ar"> <input name="title_en" placeholder="title_en"> <input name="duration" placeholder="duration seconds" type="number"> <input name="mp4_src" placeholder="/uploads/file.mp4"> <textarea name="qualities_json" placeholder='<?=e(json_encode(["480p"=>"/uploads/file480.mp4","720p"=>"/uploads/file720.mp4"],JSON_UNESCAPED_UNICODE))?>'></textarea> <button class="btn"><?=e(t('save'))?></button> </form>'; // Ads quick add echo '<h3 class="mt-3">'.e(t('ads')).'</h3> <form method="post" action="?route=admin.ad.save" class="grid" onsubmit="event.preventDefault(); alert('Use DB or extend POST route to save ads.');"> <input placeholder="position (pre,mid,post,banner,inline)"> <input placeholder="kind (image,video,html)"> <input placeholder="media URL"> <input placeholder="target_url"> <input placeholder="start_at (YYYY-MM-DD HH:MM:SS)"> <input placeholder="end_at (YYYY-MM-DD HH:MM:SS)"> <textarea placeholder="targeting JSON"></textarea> <button class="btn"><?=e(t('save'))?></button> </form>'; // Send notification echo '<h3 class="mt-3">'.e(t('send_notification')).'</h3> <form method="post" action="?route=admin.notify" class="grid">'.csrf_field().' <input name="user_id" placeholder="user_id (0 for all)" value="0"> <input name="link" placeholder="link e.g. ?route=details&id=1"> <textarea name="message_ar" placeholder="message_ar"></textarea> <textarea name="message_en" placeholder="message_en"></textarea> <button class="btn">'.e(t('save')).'</button> </form>'; // Reports (simplified) echo '<h3 class="mt-3">'.e(t('reports')).'</h3>'; $top=$pdo->query("SELECT m.title_ar,m.title_en, COUNT(w.id) c FROM movies m LEFT JOIN watch_history w ON w.movie_id=m.id GROUP BY m.id ORDER BY c DESC LIMIT 10")->fetchAll(PDO::FETCH_ASSOC); echo '<div class="grid mt-2">'; foreach($top as $r){ $title=$GLOBALS['LANG']==='ar'?$r['title_ar']:$r['title_en']; echo '<div class="card p-2">'.e($title).' <div class="small">Views: '.$r['c'].'</div></div>'; } echo '</div>'; echo '</div>'; footer_html();

}
function page_watchlist(PDO pdo){ require_login(); header_html(); u=current_user();
st=st=pdo->prepare("SELECT m.* FROM watchlist w JOIN movies m ON m.id=w.movie_id WHERE w.user_id=? ORDER BY m.created_at DESC");
st>execute([st->execute([u['id']]); rows=rows=st->fetchAll(PDO::FETCH_ASSOC);
echo '<div class="p-3 cinema"><h2>'.e(t('watchlist')).'</h2><div class="grid mt-2">';
foreach(rowsasrows as m){
title=title=GLOBALS['LANG']==='ar'?m[titlear]:m['title_ar']:m['title_en'];
echo '<a class="card" href="?route=details&id='.$m['id'].'"><img loading="lazy" src="'.e($m['poster']?:'https://picsum.photos/300/450?blur=2').'"><div class="p-2">'.e(title).'</div></a>'; } echo '</div></div>'; footer_html(); } function page_details(PDO pdo){
header_html();
id=(int)(id=(int)(_GET['id']??0);
st=st=pdo->prepare("SELECT * FROM movies WHERE id=?"); st>execute([st->execute([id]); m=m=st->fetch(PDO::FETCH_ASSOC);
if(!m)echo<divclass="p3cinema">notfound</div>;footerhtml();return;m){ echo '<div class="p-3 cinema">not found</div>'; footer_html(); return; } title=GLOBALS[LANG]===ar?GLOBALS['LANG']==='ar'?m['title_ar']:m[titleen];m['title_en']; desc =GLOBALS[LANG]===ar?GLOBALS['LANG']==='ar'?m['description_ar']:m[descriptionen];m['description_en']; avg=avg_rating(pdo,pdo,id);
adbanner=adpick(ad_banner = ad_pick(pdo,'banner');
echo '<div class="p-3 cinema details-layout">';
echo '<div>';
// Video + controls
// pick playable src: for movies, we use poster-less; for series, default to first episode
episodeid=null;if(episode_id = null; if(m['type']==='series'){
ep=ep=pdo->prepare("SELECT * FROM episodes WHERE movie_id=? ORDER BY season,episode LIMIT 1"); ep>execute([ep->execute([id]); first=first=ep->fetch(PDO::FETCH_ASSOC);
if(first){ episode_id=(int)first[id];first['id']; default_src=first[mp4src];first['mp4_src']; qualities=first['qualities_json']?:'{}'; } else { default_src=''; qualities='{}'; } } else { default_src=m['backdrop'] && str_ends_with(m['backdrop'],'.mp4') ? m[backdrop]:;//optionalm['backdrop'] : ''; // optional qualities='{}';
}
// try resume
u=currentuser();u=current_user(); resumePos=0;
if(u){ wh=user_watch_in(pdo,pdo,u['id'],id,id,episode_id); resumePos=(int)(resumePos=(int)(wh['position_seconds']??0); }
echo '<div class="video-wrap"><video id="player" width="100%" preload="metadata" controls playsinline></video>
<div class="player-controls">
<button id="playBtn"></button>
<button id="back10">⏪ 10s</button>
<button id="fwd10">⏩ 10s</button>
<input id="vol" type="range" min="0" max="1" step="0.01" value="1">
<select id="speed"><option>0.5</option><option>1.0</option><option>1.25</option><option>1.5</option><option>2.0</option></select>
<select id="quality"></select>
<button id="cinema">🖼</button>
<button id="fullscreen"></button>
</div></div>';
if(ad_banner){ echo '<a class="card p-2 mt-2" href="'.e(ad_banner['target_url']).'" target="_blank" onclick="fetch('?action=adImpress', {method:'POST', body:new URLSearchParams({ad_id:'.ad_banner['id'].',event:\'click\'})});"> <div class="row"><span class="badge">Ad</span> <div>'.e(ad_banner['media']).'</div></div></a>';
}
echo "<h2 class='mt-2'>".e(title)."<spanclass=badge>".e(title)." <span class='badge'>".e(m['type'])."</span> <span class='badge'>avg</span></h2>";echo"<divclass=small>".e(avg</span></h2>"; echo "<div class='small'>".e(m['genres'])." • ".e((string)m[year])."".e(m['year'])." • ".e(m['quality'])."</div>";
echo "<p class='mt-2'>".e(desc)."</p>";desc)."</p>"; inWatchlist=false; if(u){ st=pdo>prepare("SELECTCOUNT()FROMwatchlistWHEREuserid=?ANDmovieid=?");pdo->prepare("SELECT COUNT(*) FROM watchlist WHERE user_id=? AND movie_id=?"); st->execute([u[id],u['id'],id]); inWatchlist=(int)inWatchlist=(int)st->fetchColumn()>0; }
if(u){ echo '<form id="wl" method="post" action="?action=toggleWatchlist" onsubmit="event.preventDefault();postForm(this.action,this).then(d=>{btn.textContent=d.in?\'✅ '.e(t('remove_from_watchlist')).'\':\'➕ '.e(t('add_to_watchlist')).'\';});">'.csrf_field().' <input type="hidden" name="movie_id" value="'.id.'">
<button id="btn" class="btn mt-2">'.(inWatchlist?'✅ '.e(t('remove_from_watchlist')):'➕ '.e(t('add_to_watchlist'))).'</button></form>'; } // Rating if(u){
echo '<div class="mt-2">'.e(t('your_rating')).': ';
for(i=1;i=1;i<=5;i++){ echo '<span class="star" data-v="'.i.'">★</span>';
}
echo '</div>';
}
// Comments
echo '<h3 class="mt-3">'.e(t('comments')).'</h3>';
if(u){ echo '<form id="cmt" method="post" action="?action=comment" onsubmit="event.preventDefault();postForm(this.action,this).then(()=>location.reload());">'.csrf_field().' <input type="hidden" name="movie_id" value="'.id.'">
<textarea name="comment" placeholder="'.e(t('add_comment')).'"></textarea>
<button class="btn mt-2">'.e(t('submit')).'</button></form>';
}
cm=cm=pdo->prepare("SELECT c.*, u.name, u.id as uid FROM comments c LEFT JOIN users u ON u.id=c.user_id WHERE c.movie_id=? ORDER BY c.created_at DESC");
cm>execute([cm->execute([id]); comments=comments=cm->fetchAll(PDO::FETCH_ASSOC);
foreach(commentsascomments as c){
echo '<div class="comment"><div><b>'.e(c[name]??).</b><spanclass="small">.c['name']??'').'</b> <span class="small">'.c['created_at'].'</span></div><div>'.e(c[comment]).</div>;if(c['comment']).'</div>'; if(u && (u[id]==u['id']==c['uid'] || is_admin_like(u))){ echo '<form class="inline" method="post" action="?action=deleteComment" onsubmit="event.preventDefault();postForm(this.action,this).then(()=>location.reload());">'.csrf_field().'<input type="hidden" name="comment_id" value="'.c['id'].'"><button class="btn secondary mt-2">'.e(t('delete')).'</button></form>';
}
echo '</div>';
}
echo '</div>';

text
// Right side: episodes if series echo '<div>'; if($m['type']==='series'){ $by=episodes_by_season($pdo,$id); echo '<div class="episodes-side p-2">'; foreach($by as $season=>$eps){ echo '<div class="mt-2"><b>'.e(t('seasons')).' '.$season.'</b></div>'; foreach($eps as $e){ $tt=$GLOBALS['LANG']==='ar'?$e['title_ar']:$e['title_en']; echo '<a class="row p-2 card" href="?route=play&id='.$id.'&ep='.$e['id'].'">S'.$e['season'].'E'.$e['episode'].' - '.e($tt).'</a>'; } } echo '</div>'; } echo '</div>'; echo '</div>'; // layout ?>
<script> const player=document.getElementById('player'); const playBtn=document.getElementById('playBtn'); const back10=document.getElementById('back10'); const fwd10=document.getElementById('fwd10'); const vol=document.getElementById('vol'); const speed=document.getElementById('speed'); const fs=document.getElementById('fullscreen'); const cinema=document.getElementById('cinema'); const qSel=document.getElementById('quality'); const initialPos = <?= (int)$resumePos ?>; const episodeId = <?= (int)($episode_id??0) ?>; const movieId = <?= (int)$id ?>; const qualities = <?php echo $qualities; ?>; let currentSrc = <?php echo json_encode($default_src ?: (json_decode($qualities,true)['720p'] ?? '')); ?>; function setSource(src){ if(!src) return; // if

Réflexion

Considering auto-next logic

It seems like the goal is to implement auto-next, but with some complexity like handling PHP on the page. I need a primary play route, and also a router to manage home, search, login/register, and admin pages. A rating click action is needed, plus ensuring proper file path handling for streaming.

Implementing autoplay and routing

The plan is to implement autoplay for the next episode by fetching the next episode's ID via an API call. I'll add an action for “nextEp” to return the ID based on the movie and current episode. Also need to create the page_play route, which is similar to the details page but with an episode selector and auto-next logic. Finally, I’ll output only the continuation of the code, avoiding repetition of the full file.

Continuing with code snippet

The user seems to want me to continue from the setSource part in page_details. I'll craft the continuation code, while also making sure the CSRF token is handled in the rating click functionality. The token isn't directly in JS, so I’ll embed it via PHP. This will allow the rate action to safely send the CSRF token.

Completing the JS and PHP code

I’m crafting the complete JS for the setSource function to initialize the player and manage controls like play/pause, seeking, volume, and fullscreen.

Thought for 17s

Réponse

تمام! كملت لك الملف من آخر سطر توقّف عنده (داخل page_details في سكربت المشغّل)، وأضفت:

  • إكمال عناصر المشغّل (سرعة/صوت/ملء شاشة/وضع سينما/القفز 10 ثوانٍ/اختيار الجودة).
  • حفظ موضع المشاهدة دوريًا (action=savePosition) + استئناف تلقائي.
  • تبديل الجودة مع الحفاظ على الموضع.
  • إرسال التقييم عند الضغط على النجوم (action=rate).
  • صفحة تشغيل منفصلة page_play تدعم Auto-Next للحلقة التالية.
  • API صغيرة action=nextEp لإحضار الحلقة التالية.
  • إنهاء الراوتر لباقي الصفحات.

انسخ هذا الملحق وألصقه مباشرة بعد آخر سطر في ملفك (حيث توقف عند // if):

php
if(!src) return; // If file under /uploads, stream via range endpoint if (src.startsWith('/uploads/')) { currentSrc = '?action=stream&file=' + encodeURIComponent(src.replace(/^\/uploads\//,'')); } else { currentSrc = src; } player.src = currentSrc + (currentSrc.includes('?') ? '&' : '?') + 't=' + Date.now(); // bust cache for seeking metadata } function populateQualities(){ qSel.innerHTML=''; const keys = Object.keys(qualities||{}); if (!keys.length && currentSrc) { const opt=document.createElement('option'); opt.value=currentSrc; opt.textContent='Auto'; qSel.appendChild(opt); return; } keys.sort((a,b)=>{ // numeric sort: 480p < 720p < 1080p < 4K const toN=v=>/k$/i.test(v)?parseInt(v)*1000:parseInt(v); return toN(a)-toN(b); }); keys.forEach(k=>{ const opt=document.createElement('option'); opt.value = qualities[k]; opt.textContent = k; qSel.appendChild(opt); }); // pick highest by default const last = qSel.options[qSel.options.length-1]; if (last){ qSel.value = last.value; setSource(last.value); } } function savePosDebounced(){ if (!<?= (int)!!$u ?>) return; // not logged in clearTimeout(savePosDebounced._t); savePosDebounced._t = setTimeout(()=>{ const body = new URLSearchParams({ movie_id: movieId, episode_id: episodeId||0, position: Math.floor(player.currentTime||0), duration: Math.floor(player.duration||0), device: (navigator.userAgent||'web'), }); fetch('?action=savePosition', {method:'POST', body}); }, 800); } playBtn.onclick = ()=> player.paused ? player.play() : player.pause(); back10.onclick = ()=> { player.currentTime = Math.max(0, (player.currentTime||0)-10); }; fwd10.onclick = ()=> { player.currentTime = Math.min((player.duration||0), (player.currentTime||0)+10); }; vol.oninput = ()=> { player.volume = parseFloat(vol.value||'1'); }; speed.onchange = ()=> { player.playbackRate = parseFloat(speed.value||'1'); }; fs.onclick = ()=> { if (document.fullscreenElement) document.exitFullscreen(); else player.requestFullscreen().catch(()=>{}); }; cinema.onclick = ()=> { document.querySelector('.cinema')?.classList.toggle('wide'); }; qSel.onchange = ()=> { const was = player.currentTime||0; const wasPaused = player.paused; setSource(qSel.value); player.addEventListener('loadedmetadata', function onmd(){ player.removeEventListener('loadedmetadata', onmd); player.currentTime = was; if (!wasPaused) player.play().catch(()=>{}); }); }; // Init populateQualities(); if (!qSel.options.length && currentSrc) setSource(currentSrc); // Resume if (initialPos>0){ player.addEventListener('loadedmetadata', function onMeta(){ player.removeEventListener('loadedmetadata', onMeta); if (player.duration && initialPos < player.duration-3) player.currentTime = initialPos; }); } // Auto save ['timeupdate','pause','seeking','seeked','ended'].forEach(ev=>player.addEventListener(ev, savePosDebounced)); // Simple Auto-Next if we’re in a series context player.addEventListener('ended', async ()=>{ if (!episodeId) return; // not a series ep try{ const r = await fetch(`?action=nextEp&movie_id=${movieId}&current_ep=${episodeId}`); if (!r.ok) return; const j = await r.json(); if (j.ok && j.next_id){ location.href = `?route=play&id=${movieId}&ep=${j.next_id}`; } }catch(e){} }); // Stars rating <?php if($u){ ?> const csrfToken = <?= json_encode($_SESSION['csrf'] ?? '') ?>; document.querySelectorAll('.star').forEach(s=>{ s.addEventListener('click', async ()=>{ const rating = s.dataset.v; const fd=new FormData(); fd.append('csrf', csrfToken); fd.append('movie_id', movieId); fd.append('rating', rating); await fetch('?action=rate', {method:'POST', body: fd}); // quick visual feedback document.querySelectorAll('.star').forEach(st=>st.style.opacity = (parseInt(st.dataset.v)<=parseInt(rating)) ? '1':'0.3'); }); }); <?php } ?> </script> <?php footer_html(); } /* ---------- PLAY PAGE (series episode or movie direct) ---------- */ function page_play(PDO $pdo){ header_html(); $movie_id = (int)($_GET['id']??0); $ep_id = (int)($_GET['ep']??0); $m=$pdo->prepare("SELECT * FROM movies WHERE id=?"); $m->execute([$movie_id]); $movie=$m->fetch(PDO::FETCH_ASSOC); if(!$movie){ echo '<div class="p-3 cinema">not found</div>'; footer_html(); return; } // Determine video source + qualities $episode=null; $qualities='{}'; $default_src=''; if ($movie['type']==='series'){ $st=$pdo->prepare("SELECT * FROM episodes WHERE ".($ep_id?'id=?':'movie_id=?')." ORDER BY season,episode LIMIT 1"); $st->execute($ep_id?[$ep_id]:[$movie_id]); $episode=$st->fetch(PDO::FETCH_ASSOC); if($episode){ $ep_id=(int)$episode['id']; $default_src=$episode['mp4_src']??''; $qualities=$episode['qualities_json']?:'{}'; } } else { // movie file can be set in backdrop as .mp4 (per earlier note) $default_src = (str_ends_with($movie['backdrop']??'','mp4') ? $movie['backdrop'] : ''); } $u=current_user(); $resume=0; if($u){ $wh=user_watch_in($pdo,$u['id'],$movie_id,$episode?$ep_id:null); $resume=(int)($wh['position_seconds']??0); } $title = $GLOBALS['LANG']==='ar' ? ($episode['title_ar']??$movie['title_ar']) : ($episode['title_en']??$movie['title_en']); echo '<div class="p-3 cinema details-layout"><div>'; // Player echo '<div class="video-wrap"><video id="player" width="100%" preload="metadata" controls playsinline></video> <div class="player-controls"> <button id="playBtn">⏯</button> <button id="back10">⏪ 10s</button> <button id="fwd10">⏩ 10s</button> <input id="vol" type="range" min="0" max="1" step="0.01" value="1"> <select id="speed"><option>0.5</option><option selected>1.0</option><option>1.25</option><option>1.5</option><option>2.0</option></select> <select id="quality"></select> <button id="cinema">🖼</button> <button id="fullscreen">⛶</button> </div></div>'; echo "<h2 class='mt-2'>".e($title)." <span class='badge'>".e($movie['type'])."</span></h2>"; // Side list of episodes echo '</div><div>'; if($movie['type']==='series'){ $by=episodes_by_season($pdo,$movie_id); echo '<div class="episodes-side p-2">'; foreach($by as $season=>$eps){ echo '<div class="mt-2"><b>'.e(t('seasons')).' '.$season.'</b></div>'; foreach($eps as $e){ $tt=$GLOBALS['LANG']==='ar'?$e['title_ar']:$e['title_en']; $active = ($ep_id===$e['id']) ? ' style="outline:2px solid var(--accent-2)"' : ''; echo '<a class="row p-2 card"'.$active.' href="?route=play&id='.$movie_id.'&ep='.$e['id'].'">S'.$e['season'].'E'.$e['episode'].' - '.e($tt).'</a>'; } } echo '</div>'; } echo '</div></div>'; ?> <script> const player=document.getElementById('player'); const playBtn=document.getElementById('playBtn'); const back10=document.getElementById('back10'); const fwd10=document.getElementById('fwd10'); const vol=document.getElementById('vol'); const speed=document.getElementById('speed'); const fs=document.getElementById('fullscreen'); const cinema=document.getElementById('cinema'); const qSel=document.getElementById('quality'); const initialPos = <?= (int)$resume ?>; const episodeId = <?= (int)$ep_id ?>; const movieId = <?= (int)$movie_id ?>; const qualities = <?php echo $qualities; ?>; let currentSrc = <?php echo json_encode($default_src ?: (json_decode($qualities,true)['720p'] ?? '')); ?>; function setSource(src){ if(!src) return; if (src.startsWith('/uploads/')) { currentSrc = '?action=stream&file=' + encodeURIComponent(src.replace(/^\/uploads\//,'')); } else { currentSrc = src; } player.src = currentSrc + (currentSrc.includes('?') ? '&' : '?') + 't=' + Date.now(); } function populateQualities(){ qSel.innerHTML=''; const keys = Object.keys(qualities||{}); if (!keys.length && currentSrc) { const opt=document.createElement('option'); opt.value=currentSrc; opt.textContent='Auto'; qSel.appendChild(opt); } else { keys.sort((a,b)=>{ const toN=v=>/k$/i.test(v)?parseInt(v)*1000:parseInt(v); return toN(a)-toN(b); }); keys.forEach(k=>{ const opt=document.createElement('option'); opt.value = qualities[k]; opt.textContent = k; qSel.appendChild(opt); }); const last = qSel.options[qSel.options.length-1]; if (last) { qSel.value = last.value; } } setSource(qSel.value||currentSrc); } function savePosDebounced(){ if (!<?= (int)!!$u ?>) return; clearTimeout(savePosDebounced._t); savePosDebounced._t = setTimeout(()=>{ const body = new URLSearchParams({ movie_id: movieId, episode_id: episodeId||0, position: Math.floor(player.currentTime||0), duration: Math.floor(player.duration||0), device: (navigator.userAgent||'web'), }); fetch('?action=savePosition', {method:'POST', body}); }, 800); } playBtn.onclick = ()=> player.paused ? player.play() : player.pause(); back10.onclick = ()=> { player.currentTime = Math.max(0, (player.currentTime||0)-10); }; fwd10.onclick = ()=> { player.currentTime = Math.min((player.duration||0), (player.currentTime||0)+10); }; vol.oninput = ()=> { player.volume = parseFloat(vol.value||'1'); }; speed.onchange = ()=> { player.playbackRate = parseFloat(speed.value||'1'); }; fs.onclick = ()=> { if (document.fullscreenElement) document.exitFullscreen(); else player.requestFullscreen().catch(()=>{}); }; cinema.onclick = ()=> { document.querySelector('.cinema')?.classList.toggle('wide'); }; qSel.onchange = ()=> { const was = player.currentTime||0; const wasPaused = player.paused; setSource(qSel.value); player.addEventListener('loadedmetadata', function onmd(){ player.removeEventListener('loadedmetadata', onmd); player.currentTime = was; if (!wasPaused) player.play().catch(()=>{}); }); }; populateQualities(); if (initialPos>0){ player.addEventListener('loadedmetadata', function onMeta(){ player.removeEventListener('loadedmetadata', onMeta); if (player.duration && initialPos < player.duration-3) player.currentTime = initialPos; }); } ['timeupdate','pause','seeking','seeked','ended'].forEach(ev=>player.addEventListener(ev, savePosDebounced)); player.addEventListener('ended', async ()=>{ if (!episodeId) return; try{ const r = await fetch(`?action=nextEp&movie_id=${movieId}&current_ep=${episodeId}`); if (!r.ok) return; const j = await r.json(); if (j.ok && j.next_id){ location.href = `?route=play&id=${movieId}&ep=${j.next_id}`; } }catch(e){} }); </script> <?php footer_html(); } /* ---------- API: next episode ---------- */ if ($action==='nextEp') { $movie_id = (int)($_GET['movie_id']??0); $current = (int)($_GET['current_ep']??0); $st = $pdo->prepare("SELECT id FROM episodes WHERE movie_id=? AND (season,episode) > (SELECT season,episode FROM episodes WHERE id=?) ORDER BY season,episode LIMIT 1"); // SQLite doesn't support tuple comparison directly prior to 3.15; fallback: $cur = $pdo->prepare("SELECT season,episode FROM episodes WHERE id=?"); $cur->execute([$current]); $c = $cur->fetch(PDO::FETCH_ASSOC); if ($c){ $st=$pdo->prepare("SELECT id FROM episodes WHERE movie_id=? AND (season>? OR (season=? AND episode>?)) ORDER BY season,episode LIMIT 1"); $st->execute([$movie_id,(int)$c['season'],(int)$c['season'],(int)$c['episode']]); $next = $st->fetchColumn(); json_out(['ok'=>true,'next_id'=>$next? (int)$next : null]); } json_out(['ok'=>true,'next_id'=>null]); } /* ---------- ROUTER DISPATCH ---------- */ switch ($route) { case 'home': page_home($pdo); break; case 'search': page_search($pdo); break; case 'search.advanced': page_search_advanced(); break; case 'login': if($_SERVER['REQUEST_METHOD']!=='POST') page_login(); break; case 'register': if($_SERVER['REQUEST_METHOD']!=='POST') page_register(); break; case 'admin': page_admin($pdo); break; case 'watchlist': page_watchlist($pdo); break; case 'details': page_details($pdo); break; case 'play': page_play($pdo); break; default: page_home($pdo); break; }

ملاحظات سريعة

  • البث عبر ?action=stream&file=... يقتصر على مجلد /uploads كما هو مطلوب (دعم Range).
  • التقييم، التعليقات، قائمة المشاهدة، البحث المتقدم، والإشعارات كلها جاهزة.
  • صفحة admin تسمح بإدارة المحتوى والحلقات والإشعارات وإعدادات الموقع (اسم/شعار).
  • Auto-Next للحلقات مفعّل في page_play وعند انتهاء الفيديو.
  • لتفعيل ملفات فيديو تجريبية، ارفع ملفات مثل uploads/sample720.mp4 و uploads/sample1080.mp4 أو حدّث روابط الجودة من لوحة التحكم.

لو تحب، أقدر أجمع لك الملف النهائي كاملًا في رسالة واحدة (نسخة كاملة من index.php) أو أضيف أجزاء إضافية مثل إدارة الإعلانات بتخزين كامل من اللوحة.

Question

قم بذلك

Réflexion

Delivering combined file

The user asked for the full, final code inside a single PHP file. I'll include everything we've already discussed, along with the necessary updates or completion. No external dependencies are needed

Thought for a couple of seconds

Réponse

تمام — هذه نسخة مكتملة وجاهزة من ملف واحد index.php بكل الميزات المذكورة (HTML/CSS/JS/PHP وSQLite)، انسخها كما هي وضعها في جذر مشروعك:

php
<?php /****************************** * Netflix-like Mini Platform - Single File index.php * Author: ChatGPT (GPT-5 Thinking) * PHP >= 8.0, SQLite (PDO), no external deps ******************************/ declare(strict_types=1); if (session_status() === PHP_SESSION_NONE) session_start(); /* -------- CONFIG / PATHS -------- */ $BASE_DIR = __DIR__; $DB_DIR = $BASE_DIR . '/db'; $DB_FILE = $DB_DIR . '/app.db'; $MOVIES_DIR = $BASE_DIR . '/movies'; $UPLOADS = $BASE_DIR . '/uploads'; $SITE_CACHE_SECONDS = 60; // simple cache headers for static-like parts /* -------- BASIC OUTPUT SECURITY -------- */ function e(string $s): string { return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } /* -------- LANGUAGE / I18N -------- */ if (isset($_GET['lang']) && in_array($_GET['lang'], ['ar','en'])) { $_SESSION['lang'] = $_GET['lang']; setcookie('lang', $_GET['lang'], time()+60*60*24*365, '/'); header("Location: " . strtok($_SERVER["REQUEST_URI"],'?') . (isset($_GET['route'])?'?route='.e($_GET['route']):'')); exit; } $LANG = $_SESSION['lang'] ?? ($_COOKIE['lang'] ?? 'ar'); // default AR $dir = $LANG === 'ar' ? 'rtl' : 'ltr'; $T = [ 'site_name' => ['ar'=>'سينِماتيكس','en'=>'Cinematix'], 'home' => ['ar'=>'الرئيسية','en'=>'Home'], 'movies' => ['ar'=>'أفلام','en'=>'Movies'], 'series' => ['ar'=>'مسلسلات','en'=>'Series'], 'search' => ['ar'=>'بحث','en'=>'Search'], 'login' => ['ar'=>'تسجيل الدخول','en'=>'Login'], 'register' => ['ar'=>'إنشاء حساب','en'=>'Register'], 'logout' => ['ar'=>'تسجيل الخروج','en'=>'Logout'], 'dashboard' => ['ar'=>'لوحة التحكم','en'=>'Dashboard'], 'language' => ['ar'=>'اللغة','en'=>'Language'], 'theme' => ['ar'=>'الوضع','en'=>'Theme'], 'dark' => ['ar'=>'ليلي','en'=>'Dark'], 'light' => ['ar'=>'نهاري','en'=>'Light'], 'notifications' => ['ar'=>'الإشعارات','en'=>'Notifications'], 'mark_all_read' => ['ar'=>'تعيين الكل كمقروء','en'=>'Mark all read'], 'watchlist' => ['ar'=>'قائمة المشاهدة','en'=>'Watchlist'], 'continue_watching' => ['ar'=>'استمر في المشاهدة','en'=>'Continue Watching'], 'recommended' => ['ar'=>'مقترح لك','en'=>'Recommended for You'], 'trending' => ['ar'=>'رائج الآن','en'=>'Trending'], 'because_you_liked' => ['ar'=>'لأنك أحببت','en'=>'Because you liked'], 'details' => ['ar'=>'التفاصيل','en'=>'Details'], 'play' => ['ar'=>'تشغيل','en'=>'Play'], 'seasons' => ['ar'=>'المواسم','en'=>'Seasons'], 'episodes' => ['ar'=>'الحلقات','en'=>'Episodes'], 'add' => ['ar'=>'إضافة','en'=>'Add'], 'edit' => ['ar'=>'تعديل','en'=>'Edit'], 'delete' => ['ar'=>'حذف','en'=>'Delete'], 'save' => ['ar'=>'حفظ','en'=>'Save'], 'cancel' => ['ar'=>'إلغاء','en'=>'Cancel'], 'rating' => ['ar'=>'التقييم','en'=>'Rating'], 'your_rating' => ['ar'=>'تقييمك','en'=>'Your rating'], 'comments' => ['ar'=>'التعليقات','en'=>'Comments'], 'add_comment' => ['ar'=>'أضف تعليقاً','en'=>'Add comment'], 'search_advanced' => ['ar'=>'بحث متقدم','en'=>'Advanced search'], 'quality' => ['ar'=>'الجودة','en'=>'Quality'], 'year' => ['ar'=>'السنة','en'=>'Year'], 'genre' => ['ar'=>'التصنيف','en'=>'Genre'], 'language_label' => ['ar'=>'اللغة','en'=>'Language'], 'duration' => ['ar'=>'المدة','en'=>'Duration'], 'order' => ['ar'=>'الترتيب','en'=>'Order'], 'submit' => ['ar'=>'تنفيذ','en'=>'Apply'], 'add_to_watchlist' => ['ar'=>'أضف إلى المشاهدة','en'=>'Add to Watchlist'], 'remove_from_watchlist' => ['ar'=>'إزالة من المشاهدة','en'=>'Remove from Watchlist'], 'admin_settings' => ['ar'=>'إعدادات الموقع','en'=>'Site Settings'], 'site_logo' => ['ar'=>'شعار الموقع','en'=>'Site Logo'], 'site_title' => ['ar'=>'اسم الموقع','en'=>'Site Title'], 'pending_users' => ['ar'=>'طلبات التسجيل','en'=>'Pending Signups'], 'approve' => ['ar'=>'موافقة','en'=>'Approve'], 'reject' => ['ar'=>'رفض','en'=>'Reject'], 'send_notification' => ['ar'=>'إرسال إشعار','en'=>'Send Notification'], 'ads' => ['ar'=>'الإعلانات','en'=>'Ads'], 'reports' => ['ar'=>'التقارير','en'=>'Reports'], 'stats' => ['ar'=>'الإحصاءات','en'=>'Statistics'], ]; function t(string $key): string { global $T,$LANG; return $T[$key][$LANG] ?? $key; } /* -------- CSRF -------- */ if (empty($_SESSION['csrf'])) $_SESSION['csrf']=bin2hex(random_bytes(16)); function csrf_field(): string { return '<input type="hidden" name="csrf" value="'.e($_SESSION['csrf']).'">'; } function check_csrf(): void { if ($_SERVER['REQUEST_METHOD']==='POST') { $ok = isset($_POST['csrf']) && hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf']); if (!$ok) { http_response_code(403); exit('CSRF'); } } } /* -------- INIT DIRS/DB -------- */ foreach ([$DB_DIR,$MOVIES_DIR,$UPLOADS] as $d) if(!is_dir($d)) @mkdir($d,0775,true); $pdo = new PDO('sqlite:'.$DB_FILE); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $pdo->exec("PRAGMA foreign_keys = ON;"); $pdo->exec("CREATE TABLE IF NOT EXISTS users( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE, pass TEXT, name TEXT, role TEXT DEFAULT 'User', status TEXT DEFAULT 'pending', created_at TEXT DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS settings( k TEXT PRIMARY KEY, v TEXT )"); $pdo->exec("CREATE TABLE IF NOT EXISTS movies( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT CHECK(type IN ('movie','series')) NOT NULL, title_ar TEXT, title_en TEXT, description_ar TEXT, description_en TEXT, genres TEXT, year INTEGER, lang TEXT, quality TEXT, poster TEXT, backdrop TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS episodes( id INTEGER PRIMARY KEY AUTOINCREMENT, movie_id INTEGER REFERENCES movies(id) ON DELETE CASCADE, season INTEGER, episode INTEGER, title_ar TEXT, title_en TEXT, duration INTEGER, mp4_src TEXT, qualities_json TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS ratings( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, movie_id INTEGER REFERENCES movies(id) ON DELETE CASCADE, rating INTEGER CHECK(rating>=1 AND rating<=5), UNIQUE(user_id,movie_id) )"); $pdo->exec("CREATE TABLE IF NOT EXISTS comments( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, movie_id INTEGER REFERENCES movies(id) ON DELETE CASCADE, comment TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS watchlist( user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, movie_id INTEGER REFERENCES movies(id) ON DELETE CASCADE, PRIMARY KEY(user_id,movie_id) )"); $pdo->exec("CREATE TABLE IF NOT EXISTS watch_history( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, movie_id INTEGER REFERENCES movies(id) ON DELETE CASCADE, episode_id INTEGER REFERENCES episodes(id) ON DELETE CASCADE, position_seconds INTEGER DEFAULT 0, duration_seconds INTEGER DEFAULT 0, device TEXT, last_watched_at TEXT DEFAULT CURRENT_TIMESTAMP )"); $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 DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS ads( id INTEGER PRIMARY KEY AUTOINCREMENT, position TEXT, kind TEXT, media TEXT, target_url TEXT, start_at TEXT, end_at TEXT, targeting TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS ad_impressions( id INTEGER PRIMARY KEY AUTOINCREMENT, ad_id INTEGER REFERENCES ads(id) ON DELETE CASCADE, user_id INTEGER, event TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS activity_log( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, action TEXT, meta TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP )"); /* -------- SEED movies.json / sample -------- */ $MOVIES_JSON = $MOVIES_DIR.'/movies.json'; if (!file_exists($MOVIES_JSON)) { $seed = [ ['type'=>'movie','title_ar'=>'فيلم تجريبي','title_en'=>'Sample Movie', 'description_ar'=>'وصف بسيط للفيلم','description_en'=>'Simple sample movie', 'genres'=>'Action,Drama','year'=>2023,'lang'=>'ar/en','quality'=>'1080p', 'poster'=>'','backdrop'=>''], ['type'=>'series','title_ar'=>'مسلسل تجريبي','title_en'=>'Sample Series', 'description_ar'=>'وصف بسيط للمسلسل','description_en'=>'Simple sample series', 'genres'=>'Drama','year'=>2024,'lang'=>'ar/en','quality'=>'720p', 'poster'=>'','backdrop'=>''] ]; file_put_contents($MOVIES_JSON, json_encode($seed, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT)); // Insert to DB if empty $count = (int)$pdo->query("SELECT COUNT(*) FROM movies")->fetchColumn(); if ($count===0){ $stmt=$pdo->prepare("INSERT INTO movies(type,title_ar,title_en,description_ar,description_en,genres,year,lang,quality,poster,backdrop) VALUES(?,?,?,?,?,?,?,?,?,?,?)"); foreach($seed as $m){ $stmt->execute([$m['type'],$m['title_ar'],$m['title_en'],$m['description_ar'],$m['description_en'],$m['genres'],$m['year'],$m['lang'],$m['quality'],$m['poster'],$m['backdrop']]); } // Add sample episodes for series $sid = (int)$pdo->query("SELECT id FROM movies WHERE type='series' LIMIT 1")->fetchColumn(); if ($sid){ $ep=$pdo->prepare("INSERT INTO episodes(movie_id,season,episode,title_ar,title_en,duration,mp4_src,qualities_json) VALUES(?,?,?,?,?,?,?,?)"); $qualities=json_encode(['720p'=>'/uploads/sample720.mp4','1080p'=>'/uploads/sample1080.mp4']); for($i=1;$i<=3;$i++){ $ep->execute([$sid,1,$i,"حلقة $i","Episode $i",1500,"/uploads/sample1080.mp4",$qualities]); } } } } /* -------- SIMPLE AUTH -------- */ function current_user() { return $_SESSION['user'] ?? null; } function require_login() { if (!current_user()) { header('Location:?route=login'); exit; } } function is_admin_like($u): bool { return in_array($u['role']??'', ['Admin','Editor','Moderator']); } /* -------- HELPERS -------- */ function json_out($data){ header('Content-Type: application/json'); echo json_encode($data, JSON_UNESCAPED_UNICODE); exit; } function set_flash($k,$v){ $_SESSION['flash'][$k]=$v; } function get_flash($k){ $v=$_SESSION['flash'][$k]??null; unset($_SESSION['flash'][$k]); return $v; } function log_activity(PDO $pdo, $uid, $action, $meta=''){ $st=$pdo->prepare("INSERT INTO activity_log(user_id,action,meta) VALUES(?,?,?)"); $st->execute([$uid,$action,$meta]); } /* -------- ACTIONS (AJAX/API) -------- */ $action = $_GET['action'] ?? ''; if ($action==='searchAdvanced') { // API search: type, genre, year, rating, duration, quality, lang, order $q = $_GET; $sql = "SELECT m.*, COALESCE(AVG(r.rating),0) as avg_rating FROM movies m LEFT JOIN ratings r ON r.movie_id=m.id WHERE 1=1"; $args=[]; if (!empty($q['type']) && in_array($q['type'],['movie','series'])) { $sql.=" AND m.type=?"; $args[]=$q['type']; } if (!empty($q['genre'])) { $sql.=" AND m.genres LIKE ?"; $args[]='%'.$q['genre'].'%'; } if (!empty($q['year'])) { $sql.=" AND m.year=?"; $args[]=(int)$q['year']; } if (!empty($q['quality'])) { $sql.=" AND m.quality LIKE ?"; $args[]='%'.$q['quality'].'%'; } if (!empty($q['lang'])) { $sql.=" AND m.lang LIKE ?"; $args[]='%'.$q['lang'].'%'; } $sql.=" GROUP BY m.id "; $order = $q['order'] ?? 'new'; $sql.= match($order){ 'rating' => " ORDER BY avg_rating DESC", 'year_asc' => " ORDER BY m.year ASC", 'year_desc' => " ORDER BY m.year DESC", default => " ORDER BY m.created_at DESC" }; $st=$pdo->prepare($sql); $st->execute($args); $res=$st->fetchAll(PDO::FETCH_ASSOC); json_out(['ok'=>true,'results'=>$res]); } if ($action==='toggleWatchlist') { require_login(); check_csrf(); $uid=current_user()['id']; $mid=(int)($_POST['movie_id']??0); $exists=(int)$pdo->prepare("SELECT COUNT(*) FROM watchlist WHERE user_id=? AND movie_id=?")->execute([$uid,$mid])||0; $st=$pdo->prepare("SELECT COUNT(*) FROM watchlist WHERE user_id=? AND movie_id=?"); $st->execute([$uid,$mid]); $exists=(int)$st->fetchColumn(); if ($exists){ $pdo->prepare("DELETE FROM watchlist WHERE user_id=? AND movie_id=?")->execute([$uid,$mid]); json_out(['ok'=>true,'in'=>false]); } else { $pdo->prepare("INSERT INTO watchlist(user_id,movie_id) VALUES(?,?)")->execute([$uid,$mid]); json_out(['ok'=>true,'in'=>true]); } } if ($action==='rate') { require_login(); check_csrf(); $uid=current_user()['id']; $mid=(int)($_POST['movie_id']??0); $r=max(1,min(5,(int)($_POST['rating']??0))); $pdo->prepare("INSERT INTO ratings(user_id,movie_id,rating) VALUES(?,?,?) ON CONFLICT(user_id,movie_id) DO UPDATE SET rating=excluded.rating")->execute([$uid,$mid,$r]); json_out(['ok'=>true]); } if ($action==='comment') { require_login(); check_csrf(); $uid=current_user()['id']; $mid=(int)($_POST['movie_id']??0); $c=trim($_POST['comment']??''); if($c!== '') $pdo->prepare("INSERT INTO comments(user_id,movie_id,comment) VALUES(?,?,?)")->execute([$uid,$mid,$c]); json_out(['ok'=>true]); } if ($action==='deleteComment') { require_login(); check_csrf(); $uid=current_user()['id']; $cid=(int)($_POST['comment_id']??0); // allow owner or admin-like $row=$pdo->prepare("SELECT user_id FROM comments WHERE id=?"); $row->execute([$cid]); $owner=(int)$row->fetchColumn(); $is_admin=is_admin_like(current_user()); if ($owner===$uid || $is_admin){ $pdo->prepare("DELETE FROM comments WHERE id=?")->execute([$cid]); json_out(['ok'=>true]); } json_out(['ok'=>false]); } if ($action==='savePosition') { require_login(); $uid=current_user()['id']; $mid=(int)($_POST['movie_id']??0); $eid=(int)($_POST['episode_id']??0); $pos=(int)($_POST['position']??0); $dur=(int)($_POST['duration']??0); $dev=substr($_POST['device']??'web',0,32); $st=$pdo->prepare("INSERT INTO watch_history(user_id,movie_id,episode_id,position_seconds,duration_seconds,device,last_watched_at) VALUES(?,?,?,?,?,?,CURRENT_TIMESTAMP)"); // upsert-like: keep last record per (user,movie,episode,device) $find=$pdo->prepare("SELECT id FROM watch_history WHERE user_id=? AND movie_id=? AND IFNULL(episode_id,0)=? AND device=? ORDER BY last_watched_at DESC LIMIT 1"); $find->execute([$uid,$mid,$eid,$dev]); $hid=$find->fetchColumn(); if ($hid){ $pdo->prepare("UPDATE watch_history SET position_seconds=?,duration_seconds=?,last_watched_at=CURRENT_TIMESTAMP WHERE id=?")->execute([$pos,$dur,$hid]); } else { $st->execute([$uid,$mid,$eid?:null,$pos,$dur,$dev]); } json_out(['ok'=>true]); } if ($action==='notifications') { require_login(); $uid=current_user()['id']; $st=$pdo->prepare("SELECT * FROM notifications WHERE user_id IN (?,0) ORDER BY created_at DESC LIMIT 20"); $st->execute([$uid]); $rows=$st->fetchAll(PDO::FETCH_ASSOC); json_out(['ok'=>true,'items'=>$rows]); } if ($action==='markAllRead') { require_login(); $uid=current_user()['id']; $pdo->prepare("UPDATE notifications SET is_read=1 WHERE user_id IN (?,0)")->execute([$uid]); json_out(['ok'=>true]); } if ($action==='adImpress') { $ad=(int)($_POST['ad_id']??0); $uid=current_user()['id']??null; $event=$_POST['event']??'impression'; $pdo->prepare("INSERT INTO ad_impressions(ad_id,user_id,event) VALUES(?,?,?)")->execute([$ad,$uid,$event]); json_out(['ok'=>true]); } if ($action==='stream') { // SECURE RANGE STREAM ONLY FROM /uploads $file = $_GET['file'] ?? ''; $path = realpath($GLOBALS['UPLOADS'] . '/' . ltrim($file,'/')); if (!$path || !str_starts_with($path, realpath($GLOBALS['UPLOADS'])) || !is_file($path)) { http_response_code(404); exit('Not found'); } $size = filesize($path); $fp = fopen($path,'rb'); $begin = 0; $end = $size - 1; header("Content-Type: video/mp4"); header("Accept-Ranges: bytes"); if (isset($_SERVER['HTTP_RANGE'])) { if (preg_match('/bytes=(\d+)-(\d*)/i', $_SERVER['HTTP_RANGE'], $m)) { $begin = (int)$m[1]; if ($m[2] !== '') $end = (int)$m[2]; } header('HTTP/1.1 206 Partial Content'); header("Content-Range: bytes $begin-$end/$size"); } $length = $end - $begin + 1; header("Content-Length: $length"); fseek($fp, $begin); $buffer = 8192; while(!feof($fp) && ($pos=ftell($fp)) <= $end){ if ($pos + $buffer > $end+1) $buffer = $end - $pos + 1; echo fread($fp, $buffer); flush(); } fclose($fp); exit; } /* ---------- API: next episode ---------- */ if ($action==='nextEp') { $movie_id = (int)($_GET['movie_id']??0); $current = (int)($_GET['current_ep']??0); $cur = $pdo->prepare("SELECT season,episode FROM episodes WHERE id=?"); $cur->execute([$current]); $c = $cur->fetch(PDO::FETCH_ASSOC); if ($c){ $st=$pdo->prepare("SELECT id FROM episodes WHERE movie_id=? AND (season>? OR (season=? AND episode>?)) ORDER BY season,episode LIMIT 1"); $st->execute([$movie_id,(int)$c['season'],(int)$c['season'],(int)$c['episode']]); $next = $st->fetchColumn(); json_out(['ok'=>true,'next_id'=>$next? (int)$next : null]); } json_out(['ok'=>true,'next_id'=>null]); } /* -------- ROUTER -------- */ $route = $_GET['route'] ?? 'home'; /* -------- AUTH ROUTES -------- */ if ($route==='register' && $_SERVER['REQUEST_METHOD']==='POST') { check_csrf(); $email=trim($_POST['email']??''); $name=trim($_POST['name']??''); $pass=$_POST['pass']??''; if ($email && $pass){ $hash=password_hash($pass,PASSWORD_DEFAULT); $role='User'; $status='pending'; $pdo->prepare("INSERT INTO users(email,pass,name,role,status) VALUES(?,?,?,?,?)")->execute([$email,$hash,$name,$role,$status]); // first user becomes Admin approved $cnt=(int)$pdo->query("SELECT COUNT(*) FROM users")->fetchColumn(); if ($cnt===1) $pdo->exec("UPDATE users SET role='Admin', status='approved' WHERE id=1"); set_flash('ok','registered'); header('Location:?route=login'); exit; } } if ($route==='login' && $_SERVER['REQUEST_METHOD']==='POST') { check_csrf(); $email=trim($_POST['email']??''); $pass=$_POST['pass']??''; $st=$pdo->prepare("SELECT * FROM users WHERE email=?"); $st->execute([$email]); $u=$st->fetch(PDO::FETCH_ASSOC); if ($u && password_verify($pass,$u['pass'])){ if ($u['status']!=='approved'){ set_flash('err','pending'); header('Location:?route=login'); exit; } $_SESSION['user']=$u; header('Location:?route=home'); exit; } set_flash('err','bad'); header('Location:?route=login'); exit; } if ($route==='logout'){ unset($_SESSION['user']); header('Location:?route=home'); exit; } /* -------- ADMIN ACTIONS (simple) -------- */ if ($route==='admin.approve' && $_SERVER['REQUEST_METHOD']==='POST') { require_login(); check_csrf(); if (!is_admin_like(current_user())) exit('forbidden'); $id=(int)$_POST['id']; $ok=(int)$_POST['ok']; if ($ok===1){ $pdo->prepare("UPDATE users SET status='approved' WHERE id=?")->execute([$id]); } else { $pdo->prepare("UPDATE users SET status='rejected' WHERE id=?")->execute([$id]); } header('Location:?route=admin.users'); exit; } if ($route==='admin.notify' && $_SERVER['REQUEST_METHOD']==='POST') { require_login(); check_csrf(); if (!is_admin_like(current_user())) exit('forbidden'); $uid=(int)($_POST['user_id']??0); $ma=trim($_POST['message_ar']??''); $me=trim($_POST['message_en']??''); $link=trim($_POST['link']??''); if ($uid>0){ $pdo->prepare("INSERT INTO notifications(user_id,message_ar,message_en,link) VALUES(?,?,?,?)")->execute([$uid,$ma,$me,$link]); } else { // to all: user_id=0 $pdo->prepare("INSERT INTO notifications(user_id,message_ar,message_en,link) VALUES(0,?,?,?)")->execute([$ma,$me,$link]); } header('Location:?route=admin'); exit; } if ($route==='admin.movie.save' && $_SERVER['REQUEST_METHOD']==='POST') { require_login(); check_csrf(); if (!is_admin_like(current_user())) exit('forbidden'); $id=(int)($_POST['id']??0); $cols=['type','title_ar','title_en','description_ar','description_en','genres','year','lang','quality','poster','backdrop']; $vals=[]; foreach($cols as $c){ $vals[$c]=trim($_POST[$c]??''); } if ($id){ $set=implode(',', array_map(fn($k)=>"$k=?", array_keys($vals))); $st=$pdo->prepare("UPDATE movies SET $set WHERE id=?"); $st->execute([...array_values($vals),$id]); } else { $st=$pdo->prepare("INSERT INTO movies(".implode(',',$cols).") VALUES(".str_repeat('?,',count($cols)-1)."?)"); $st->execute(array_values($vals)); $newId=(int)$pdo->lastInsertId(); // notify followers (none yet) + general $pdo->prepare("INSERT INTO notifications(user_id,message_ar,message_en,link) VALUES(0,?,?,?)") ->execute(["تمت إضافة محتوى جديد","New content added","?route=details&id=".$newId]); } header('Location:?route=admin'); exit; } if ($route==='admin.episode.save' && $_SERVER['REQUEST_METHOD']==='POST') { require_login(); check_csrf(); if (!is_admin_like(current_user())) exit('forbidden'); $id=(int)($_POST['id']??0); $movie_id=(int)$_POST['movie_id']; $season=(int)$_POST['season']; $episode=(int)$_POST['episode']; $title_ar=trim($_POST['title_ar']??''); $title_en=trim($_POST['title_en']??''); $duration=(int)$_POST['duration']; $mp4_src=trim($_POST['mp4_src']??''); $qualities_json=trim($_POST['qualities_json']??'{}'); if ($id){ $pdo->prepare("UPDATE episodes SET season=?,episode=?,title_ar=?,title_en=?,duration=?,mp4_src=?,qualities_json=? WHERE id=?") ->execute([$season,$episode,$title_ar,$title_en,$duration,$mp4_src,$qualities_json,$id]); } else { $pdo->prepare("INSERT INTO episodes(movie_id,season,episode,title_ar,title_en,duration,mp4_src,qualities_json) VALUES(?,?,?,?,?,?,?,?)") ->execute([$movie_id,$season,$episode,$title_ar,$title_en,$duration,$mp4_src,$qualities_json]); // notify followers $followers=$pdo->prepare("SELECT user_id FROM watchlist WHERE movie_id=?"); $followers->execute([$movie_id]); $uids=$followers->fetchAll(PDO::FETCH_COLUMN); foreach ($uids as $u) { $pdo->prepare("INSERT INTO notifications(user_id,message_ar,message_en,link) VALUES(?,?,?,?)") ->execute([$u,"تمت إضافة حلقة جديدة","New episode added","?route=details&id=".$movie_id]); } } header('Location:?route=admin'); exit; } if ($route==='admin.settings' && $_SERVER['REQUEST_METHOD']==='POST') { require_login(); check_csrf(); if (!is_admin_like(current_user())) exit('forbidden'); $site_title=trim($_POST['site_title']??''); $pdo->prepare("INSERT INTO settings(k,v) VALUES('site_title',?) ON CONFLICT(k) DO UPDATE SET v=excluded.v")->execute([$site_title]); if (!empty($_FILES['logo']['name']) && is_uploaded_file($_FILES['logo']['tmp_name'])) { $ext=pathinfo($_FILES['logo']['name'], PATHINFO_EXTENSION); $name='logo.'.preg_replace('/[^a-z0-9]+/i','',$ext); $dest=$GLOBALS['UPLOADS'].'/'.$name; move_uploaded_file($_FILES['logo']['tmp_name'],$dest); $pdo->prepare("INSERT INTO settings(k,v) VALUES('site_logo',?) ON CONFLICT(k) DO UPDATE SET v=excluded.v")->execute(['/uploads/'.$name]); } header('Location:?route=admin'); exit; } /* -------- FETCH SETTINGS -------- */ $SITE_TITLE = $pdo->query("SELECT v FROM settings WHERE k='site_title'")->fetchColumn() ?: t('site_name'); $SITE_LOGO = $pdo->query("SELECT v FROM settings WHERE k='site_logo'")->fetchColumn() ?: ''; /* -------- VIEW HELPERS -------- */ function avg_rating(PDO $pdo, int $movie_id): float { $st=$pdo->prepare("SELECT AVG(rating) FROM ratings WHERE movie_id=?"); $st->execute([$movie_id]); return round((float)$st->fetchColumn(),1); } function episodes_by_season(PDO $pdo, int $movie_id): array { $st=$pdo->prepare("SELECT * FROM episodes WHERE movie_id=? ORDER BY season,episode"); $st->execute([$movie_id]); $eps=$st->fetchAll(PDO::FETCH_ASSOC); $by=[]; foreach($eps as $e){ $by[$e['season']][]=$e; } return $by; } function user_watch_in(PDO $pdo, int $uid, int $mid, ?int $eid=null): array { $st=$pdo->prepare("SELECT * FROM watch_history WHERE user_id=? AND movie_id=? AND IFNULL(episode_id,0)=? ORDER BY last_watched_at DESC LIMIT 1"); $st->execute([$uid,$mid,$eid?:0]); return $st->fetch(PDO::FETCH_ASSOC) ?: []; } function ad_pick(PDO $pdo, string $position): ?array { $st=$pdo->prepare("SELECT * FROM ads WHERE position=? AND (start_at IS NULL OR start_at<=CURRENT_TIMESTAMP) AND (end_at IS NULL OR end_at>=CURRENT_TIMESTAMP) ORDER BY RANDOM() LIMIT 1"); $st->execute([$position]); return $st->fetch(PDO::FETCH_ASSOC) ?: null; } /* -------- HEADER + STYLES -------- */ function header_html() { global $LANG,$dir,$SITE_TITLE,$SITE_LOGO; header("Referrer-Policy: strict-origin-when-cross-origin"); header("X-Content-Type-Options: nosniff"); header("X-Frame-Options: SAMEORIGIN"); // Simple cache for assets-like pages header("Cache-Control: no-store"); ?> <!doctype html> <html lang="<?=e($LANG)?>" dir="<?=$dir?>"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title><?=e($SITE_TITLE)?></title> <style> :root{ --bg:#0b0e13; --fg:#f1f5f9; --muted:#94a3b8; --accent:#22d3ee; --card:#121826; --accent-2:#6366f1; --danger:#ef4444; --ok:#22c55e; } html[theme="light"]{ --bg:#f7fafc; --fg:#0b0e13; --card:#ffffff; --muted:#475569; } *{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--fg);font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,"Noto Sans",sans-serif} a{color:inherit;text-decoration:none} .topbar{display:flex;align-items:center;gap:.75rem;background:var(--card);padding:.5rem 1rem;position:sticky;top:0;z-index:1000;border-bottom:1px solid #1f2937} .topbar .brand{display:flex;align-items:center;gap:.5rem;font-weight:700} .topbar img.logo{height:28px} .topbar .grow{flex:1} .btn{background:var(--accent-2);color:#fff;border:none;border-radius:6px;padding:.5rem .8rem;cursor:pointer} .btn.secondary{background:#334155} .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:1rem} .card{background:var(--card);border-radius:10px;overflow:hidden} .card img{width:100%;height:220px;object-fit:cover;display:block;filter:saturate(1.1)} .p-2{padding:.5rem}.p-3{padding:1rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:1rem} .badge{display:inline-block;background:#0ea5e9;color:#fff;border-radius:999px;padding:.15rem .5rem;font-size:.75rem;margin-inline-end:.25rem} .row{display:flex;gap:1rem;align-items:center} input,select,textarea{background:#0f172a;color:var(--fg);border:1px solid #1f2937;border-radius:8px;padding:.5rem .6rem} input[type=file]{background:transparent;border:none} form.inline{display:inline} .lang, .theme, .bell{cursor:pointer;padding:.35rem .5rem;border-radius:8px;background:#0f172a;border:1px solid #1f2937} .dropdown{position:relative} .dropdown-menu{position:absolute;right:0;top:110%;background:var(--card);border:1px solid #1f2937;border-radius:10px;min-width:260px;display:none;max-height:360px;overflow:auto} .dropdown.open .dropdown-menu{display:block} .video-wrap{background:#000;border-radius:12px;overflow:hidden} .player-controls{display:flex;gap:.5rem;align-items:center;padding:.5rem;background:#0b0b0b} .player-controls button, .player-controls select, .player-controls input[type=range]{background:#111827;color:#fff;border:1px solid #374151;border-radius:8px;padding:.35rem .6rem} .episodes-side{background:var(--card);border:1px solid #1f2937;border-radius:12px;max-height:520px;overflow:auto} .comment{border-bottom:1px solid #1f2937;padding:.5rem 0} .star{cursor:pointer;font-size:20px;color:#eab308} .banner{background:linear-gradient(90deg,#0ea5e9,#6366f1);border-radius:12px;padding:1rem;color:#fff} .table{width:100%;border-collapse:collapse} .table th,.table td{border-bottom:1px solid #1f2937;padding:.5rem;text-align:<?=$dir==='rtl'?'right':'left'?>} .small{font-size:.9rem;color:var(--muted)} .cinema{max-width:1200px;margin:auto} @media (min-width:1000px){ .details-layout{display:grid;grid-template-columns:2fr 1fr;gap:1rem} } </style> </head> <body> <div class="topbar"> <div class="brand"> <?php global $SITE_LOGO; if($SITE_LOGO): ?> <img class="logo" src="<?=e($SITE_LOGO)?>" alt="logo"> <?php endif; ?> <a href="?route=home"><?=e($SITE_TITLE)?></a> </div> <div class="grow"> <form action="?route=search" method="get" style="display:flex;gap:.5rem"> <input type="hidden" name="route" value="search"> <input name="q" placeholder="<?=e(t('search'))?>..."> <button class="btn"><?=e(t('search'))?></button> <a class="btn secondary" href="?route=search.advanced"><?=e(t('search_advanced'))?></a> </form> </div> <div class="row"> <a class="btn secondary" href="?lang=<?=$GLOBALS['LANG']==='ar'?'en':'ar'?>"><?=e(t('language'))?>: <?=$GLOBALS['LANG']==='ar'?'AR→EN':'EN→AR'?></a> <div class="theme" id="themeToggle"><?=e(t('theme'))?> 🌙</div> <div class="dropdown" id="bell"> <div class="bell">🔔</div> <div class="dropdown-menu p-2" id="notifMenu"><div class="small"><?=e(t('notifications'))?>...</div></div> </div> <?php if($u=current_user()): ?> <a class="btn secondary" href="?route=watchlist"><?=e(t('watchlist'))?></a> <?php if(is_admin_like($u)): ?><a class="btn" href="?route=admin"><?=e(t('dashboard'))?></a><?php endif; ?> <a class="btn" href="?route=logout"><?=e(t('logout'))?></a> <?php else: ?> <a class="btn secondary" href="?route=login"><?=e(t('login'))?></a> <a class="btn" href="?route=register"><?=e(t('register'))?></a> <?php endif; ?> </div> </div> <script> // Theme (default dark) (function(){ const saved=localStorage.getItem('theme')||'dark'; document.documentElement.setAttribute('theme', saved); document.getElementById('themeToggle').onclick=()=>{ const cur=document.documentElement.getAttribute('theme')==='light'?'dark':'light'; document.documentElement.setAttribute('theme',cur); localStorage.setItem('theme',cur); }; })(); </script> <?php } function footer_html(){ ?> <script> // Notifications dropdown const bell=document.getElementById('bell'); if (bell){ bell.addEventListener('click', async (e)=>{ bell.classList.toggle('open'); const menu=document.getElementById('notifMenu'); if(menu.dataset.loaded) return; const r=await fetch('?action=notifications'); if(!r.ok) return; const data=await r.json(); menu.innerHTML=''; (data.items||[]).forEach(n=>{ const msg = <?=json_encode($GLOBALS['LANG'])?>==='ar' ? (n.message_ar||'') : (n.message_en||''); const a=document.createElement('a'); a.href=n.link||'#'; a.className='row p-2'; a.innerHTML='🔔 '+msg+' <span class="small" style="margin-<?=$GLOBALS['dir']==='rtl'?'left':'right'?>:auto">'+n.created_at+'</span>'; menu.appendChild(a); }); const mark=document.createElement('button'); mark.className='btn secondary mt-2'; mark.textContent='<?=e(t('mark_all_read'))?>'; mark.onclick=()=>fetch('?action=markAllRead').then(()=>{}); menu.appendChild(mark); menu.dataset.loaded=1; }); } // Generic helpers async function postForm(url, form){ const fd=new FormData(form); const r=await fetch(url,{method:'POST',body:fd}); return r.json(); } </script> </body></html> <?php } /* -------- PAGE TEMPLATES -------- */ function page_home(PDO $pdo){ header_html(); // Sections: Continue watching / Recommended / Trending $u=current_user(); echo '<div class="p-3 cinema">'; // Continue Watching if($u){ $st=$pdo->prepare("SELECT DISTINCT m.* FROM watch_history w JOIN movies m ON m.id=w.movie_id WHERE w.user_id=? ORDER BY w.last_watched_at DESC LIMIT 12"); $st->execute([$u['id']]); $rows=$st->fetchAll(PDO::FETCH_ASSOC); if($rows){ echo '<h2>'.e(t('continue_watching')).'</h2><div class="grid mt-2">'; foreach($rows as $m){ $title=$GLOBALS['LANG']==='ar'?$m['title_ar']:$m['title_en']; echo '<a class="card" href="?route=details&id='.$m['id'].'"><img loading="lazy" src="'.e($m['poster']?:'https://picsum.photos/300/450?blur=2').'" alt=""><div class="p-2">'.e($title).'</div></a>'; } echo '</div>'; } } // Recommended $rec=$pdo->query("SELECT m.*, IFNULL((SELECT AVG(rating) FROM ratings r WHERE r.movie_id=m.id),0) as avg_rating FROM movies m ORDER BY avg_rating DESC, m.created_at DESC LIMIT 12")->fetchAll(PDO::FETCH_ASSOC); echo '<h2 class="mt-3">'.e(t('recommended')).'</h2><div class="grid mt-2">'; foreach($rec as $m){ $title=$GLOBALS['LANG']==='ar'?$m['title_ar']:$m['title_en']; echo '<a class="card" href="?route=details&id='.$m['id'].'"><img loading="lazy" src="'.e($m['poster']?:'https://picsum.photos/300/450?blur=2').'" alt=""><div class="p-2">'.e($title).'<div class="small">⭐ '.avg_rating($pdo,(int)$m['id']).'</div></div></a>'; } echo '</div>'; // Trending (most watched last week) $trend=$pdo->query("SELECT m.*, COUNT(w.id) as views FROM movies m LEFT JOIN watch_history w ON w.movie_id=m.id AND w.last_watched_at>=datetime('now','-7 day') GROUP BY m.id ORDER BY views DESC LIMIT 12")->fetchAll(PDO::FETCH_ASSOC); echo '<h2 class="mt-3">'.e(t('trending')).'</h2><div class="grid mt-2">'; foreach($trend as $m){ $title=$GLOBALS['LANG']==='ar'?$m['title_ar']:$m['title_en']; echo '<a class="card" href="?route=details&id='.$m['id'].'"><img loading="lazy" src="'.e($m['poster']?:'https://picsum.photos/300/450?blur=2').'" alt=""><div class="p-2">'.e($title).'</div></a>'; } echo '</div></div>'; footer_html(); } function page_search(PDO $pdo){ header_html(); $q=trim($_GET['q']??''); echo '<div class="p-3 cinema"><h2>'.e(t('search')).': '.e($q).'</h2><div class="grid mt-2">'; if ($q!==''){ $st=$pdo->prepare("SELECT * FROM movies WHERE title_ar LIKE ? OR title_en LIKE ? OR description_ar LIKE ? OR description_en LIKE ? ORDER BY created_at DESC"); $like='%'.$q.'%'; $st->execute([$like,$like,$like,$like]); $rows=$st->fetchAll(PDO::FETCH_ASSOC); foreach($rows as $m){ $title=$GLOBALS['LANG']==='ar'?$m['title_ar']:$m['title_en']; echo '<a class="card" href="?route=details&id='.$m['id'].'"><img loading="lazy" src="'.e($m['poster']?:'https://picsum.photos/300/450?blur=2').'" alt=""><div class="p-2">'.e($title).'</div></a>'; } } echo '</div></div>'; footer_html(); } function page_search_advanced(){ header_html(); ?> <div class="p-3 cinema"> <h2><?=e(t('search_advanced'))?></h2> <form id="advForm" class="row" onsubmit="event.preventDefault(); doAdv();"> <select name="type"><option value="">--</option><option value="movie"><?=e(t('movies'))?></option><option value="series"><?=e(t('series'))?></option></select> <input name="genre" placeholder="<?=e(t('genre'))?>"> <input name="year" placeholder="<?=e(t('year'))?>" type="number"> <input name="quality" placeholder="<?=e(t('quality'))?>"> <input name="lang" placeholder="<?=e(t('language_label'))?>"> <select name="order"> <option value="new"><?=e(t('order'))?>: New</option> <option value="rating"><?=e(t('rating'))?></option> <option value="year_desc"><?=e(t('year'))?></option> <option value="year_asc"><?=e(t('year'))?></option> </select> <button class="btn"><?=e(t('submit'))?></button> </form> <div id="advResults" class="grid mt-3"></div> </div> <script> async function doAdv(){ const f=document.getElementById('advForm'); const params=new URLSearchParams(new FormData(f)).toString(); const r=await fetch('?action=searchAdvanced&'+params); const data=await r.json(); const grid=document.getElementById('advResults'); grid.innerHTML=''; (data.results||[]).forEach(m=>{ const title = <?=json_encode($GLOBALS['LANG'])?>==='ar' ? (m.title_ar||'') : (m.title_en||''); const card=document.createElement('a'); card.className='card'; card.href='?route=details&id='+m.id; card.innerHTML=`<img loading="lazy" src="${m.poster||'https://picsum.photos/300/450?blur=2'}"><div class="p-2">${title}<div class="small">⭐ ${(m.avg_rating||0).toFixed(1)} • ${m.quality||''}</div></div>`; grid.appendChild(card); }); } </script> <?php footer_html(); } function page_login(){ header_html(); $err=get_flash('err'); echo '<div class="p-3 cinema"><h2>'.e(t('login')).'</h2>'; if($err==='pending') echo '<div class="banner">حسابك بانتظار الموافقة</div>'; if($err==='bad') echo '<div class="banner" style="background:#ef4444">بيانات غير صحيحة</div>'; echo '<form method="post" action="?route=login">'.csrf_field().' <div class="mt-2"><input name="email" placeholder="Email"></div> <div class="mt-2"><input type="password" name="pass" placeholder="Password"></div> <button class="btn mt-2">'.e(t('login')).'</button> </form></div>'; footer_html(); } function page_register(){ header_html(); $ok=get_flash('ok'); if($ok==='registered') echo '<div class="p-3 cinema"><div class="banner">'.e('تم إنشاء الحساب، بانتظار الموافقة').'</div></div>'; echo '<div class="p-3 cinema"><h2>'.e(t('register')).'</h2> <form method="post" action="?route=register">'.csrf_field().' <div class="mt-2"><input name="name" placeholder="Name"></div> <div class="mt-2"><input name="email" placeholder="Email"></div> <div class="mt-2"><input type="password" name="pass" placeholder="Password"></div> <button class="btn mt-2">'.e(t('register')).'</button> </form></div>'; footer_html(); } function page_admin(PDO $pdo){ require_login(); if(!is_admin_like(current_user())){ header_html(); echo '<div class="p-3 cinema">forbidden</div>'; footer_html(); return; } header_html(); // Pending users $pend=$pdo->query("SELECT * FROM users WHERE status='pending' ORDER BY created_at")->fetchAll(PDO::FETCH_ASSOC); echo '<div class="p-3 cinema"><h2>'.e(t('dashboard')).'</h2>'; echo '<h3>'.e(t('pending_users')).'</h3><table class="table">'; echo '<tr><th>ID</th><th>Email</th><th>Name</th><th>Actions</th></tr>'; foreach($pend as $p){ echo '<tr><td>'.$p['id'].'</td><td>'.e($p['email']).'</td><td>'.e($p['name']).'</td><td> <form class="inline" method="post" action="?route=admin.approve">'.csrf_field().'<input type="hidden" name="id" value="'.$p['id'].'"><input type="hidden" name="ok" value="1"><button class="btn">'.e(t('approve')).'</button></form> <form class="inline" method="post" action="?route=admin.approve">'.csrf_field().'<input type="hidden" name="id" value="'.$p['id'].'"><input type="hidden" name="ok" value="0"><button class="btn secondary">'.e(t('reject')).'</button></form> </td></tr>'; } echo '</table>'; // Site settings echo '<h3 class="mt-3">'.e(t('admin_settings')).'</h3> <form method="post" action="?route=admin.settings" enctype="multipart/form-data">'.csrf_field().' <div class="row"><label>'.e(t('site_title')).'</label><input name="site_title" value="'.e($GLOBALS['SITE_TITLE']).'"></div> <div class="row mt-2"><label>'.e(t('site_logo')).'</label><input type="file" name="logo"></div> <button class="btn mt-2">'.e(t('save')).'</button> </form>'; // Add/Edit Movie echo '<h3 class="mt-3">'.e(t('movies')).' / '.e(t('series')).'</h3>'; echo '<form method="post" action="?route=admin.movie.save" class="grid">'.csrf_field().' <input name="id" placeholder="id (leave empty for new)"> <select name="type"><option value="movie">movie</option><option value="series">series</option></select> <input name="title_ar" placeholder="title_ar"><input name="title_en" placeholder="title_en"> <input name="genres" placeholder="genres"><input name="year" placeholder="year" type="number"> <input name="lang" placeholder="ar/en"><input name="quality" placeholder="1080p"> <input name="poster" placeholder="poster URL"><input name="backdrop" placeholder="backdrop URL (.mp4 ok)"> <textarea name="description_ar" placeholder="description_ar"></textarea> <textarea name="description_en" placeholder="description_en"></textarea> <button class="btn">'.e(t('save')).'</button></form>'; // Add/Edit Episode echo '<h3 class="mt-3">'.e(t('episodes')).'</h3> <form method="post" action="?route=admin.episode.save" class="grid">'.csrf_field().' <input name="id" placeholder="id (edit)"> <input name="movie_id" placeholder="movie_id" required> <input name="season" placeholder="season" type="number" required> <input name="episode" placeholder="episode" type="number" required> <input name="title_ar" placeholder="title_ar"> <input name="title_en" placeholder="title_en"> <input name="duration" placeholder="duration seconds" type="number"> <input name="mp4_src" placeholder="/uploads/file.mp4"> <textarea name="qualities_json" placeholder='<?=e(json_encode(["480p"=>"/uploads/file480.mp4","720p"=>"/uploads/file720.mp4"],JSON_UNESCAPED_UNICODE))?>'></textarea> <button class="btn"><?=e(t('save'))?></button> </form>'; // Ads quick add (placeholder form) echo '<h3 class="mt-3">'.e(t('ads')).'</h3> <form method="post" action="?route=admin.ad.save" class="grid" onsubmit="event.preventDefault(); alert(\'Use DB or extend POST route to save ads.\');"> <input placeholder="position (pre,mid,post,banner,inline)"> <input placeholder="kind (image,video,html)"> <input placeholder="media URL"> <input placeholder="target_url"> <input placeholder="start_at (YYYY-MM-DD HH:MM:SS)"> <input placeholder="end_at (YYYY-MM-DD HH:MM:SS)"> <textarea placeholder="targeting JSON"></textarea> <button class="btn"><?=e(t('save'))?></button> </form>'; // Send notification echo '<h3 class="mt-3">'.e(t('send_notification')).'</h3> <form method="post" action="?route=admin.notify" class="grid">'.csrf_field().' <input name="user_id" placeholder="user_id (0 for all)" value="0"> <input name="link" placeholder="link e.g. ?route=details&id=1"> <textarea name="message_ar" placeholder="message_ar"></textarea> <textarea name="message_en" placeholder="message_en"></textarea> <button class="btn">'.e(t('save')).'</button> </form>'; // Reports (simplified) echo '<h3 class="mt-3">'.e(t('reports')).'</h3>'; $top=$pdo->query("SELECT m.title_ar,m.title_en, COUNT(w.id) c FROM movies m LEFT JOIN watch_history w ON w.movie_id=m.id GROUP BY m.id ORDER BY c DESC LIMIT 10")->fetchAll(PDO::FETCH_ASSOC); echo '<div class="grid mt-2">'; foreach($top as $r){ $title=$GLOBALS['LANG']==='ar'?$r['title_ar']:$r['title_en']; echo '<div class="card p-2">'.e($title).' <div class="small">Views: '.$r['c'].'</div></div>'; } echo '</div>'; echo '</div>'; footer_html(); } function page_watchlist(PDO $pdo){ require_login(); header_html(); $u=current_user(); $st=$pdo->prepare("SELECT m.* FROM watchlist w JOIN movies m ON m.id=w.movie_id WHERE w.user_id=? ORDER BY m.created_at DESC"); $st->execute([$u['id']]); $rows=$st->fetchAll(PDO::FETCH_ASSOC); echo '<div class="p-3 cinema"><h2>'.e(t('watchlist')).'</h2><div class="grid mt-2">'; foreach($rows as $m){ $title=$GLOBALS['LANG']==='ar'?$m['title_ar']:$m['title_en']; echo '<a class="card" href="?route=details&id='.$m['id'].'"><img loading="lazy" src="'.e($m['poster']?:'https://picsum.photos/300/450?blur=2').'"><div class="p-2">'.e($title).'</div></a>'; } echo '</div></div>'; footer_html(); } function page_details(PDO $pdo){ header_html(); $id=(int)($_GET['id']??0); $st=$pdo->prepare("SELECT * FROM movies WHERE id=?"); $st->execute([$id]); $m=$st->fetch(PDO::FETCH_ASSOC); if(!$m){ echo '<div class="p-3 cinema">not found</div>'; footer_html(); return; } $title=$GLOBALS['LANG']==='ar'?$m['title_ar']:$m['title_en']; $desc =$GLOBALS['LANG']==='ar'?$m['description_ar']:$m['description_en']; $avg=avg_rating($pdo,$id); $ad_banner = ad_pick($pdo,'banner'); echo '<div class="p-3 cinema details-layout">'; echo '<div>'; // Video + controls $episode_id = null; if($m['type']==='series'){ $ep=$pdo->prepare("SELECT * FROM episodes WHERE movie_id=? ORDER BY season,episode LIMIT 1"); $ep->execute([$id]); $first=$ep->fetch(PDO::FETCH_ASSOC); if($first){ $episode_id=(int)$first['id']; $default_src=$first['mp4_src']; $qualities=$first['qualities_json']?:'{}'; } else { $default_src=''; $qualities='{}'; } } else { $default_src=$m['backdrop'] && str_ends_with($m['backdrop'],'.mp4') ? $m['backdrop'] : ''; // optional $qualities='{}'; } // try resume $u=current_user(); $resumePos=0; if($u){ $wh=user_watch_in($pdo,$u['id'],$id,$episode_id); $resumePos=(int)($wh['position_seconds']??0); } echo '<div class="video-wrap"><video id="player" width="100%" preload="metadata" controls playsinline></video> <div class="player-controls"> <button id="playBtn"></button> <button id="back10">⏪ 10s</button> <button id="fwd10">⏩ 10s</button> <input id="vol" type="range" min="0" max="1" step="0.01" value="1"> <select id="speed"><option>0.5</option><option>1.0</option><option>1.25</option><option>1.5</option><option>2.0</option></select> <select id="quality"></select> <button id="cinema">🖼</button> <button id="fullscreen"></button> </div></div>'; if($ad_banner){ echo '<a class="card p-2 mt-2" href="'.e($ad_banner['target_url']).'" target="_blank" onclick="fetch(\'?action=adImpress\', {method:\'POST\', body:new URLSearchParams({ad_id:'.$ad_banner['id'].',event:\'click\'})});"> <div class="row"><span class="badge">Ad</span> <div>'.e($ad_banner['media']).'</div></div></a>'; } echo "<h2 class='mt-2'>".e($title)." <span class='badge'>".e($m['type'])."</span> <span class='badge'>⭐ $avg</span></h2>"; echo "<div class='small'>".e($m['genres'])." • ".e((string)$m['year'])." • ".e($m['quality'])."</div>"; echo "<p class='mt-2'>".e($desc)."</p>"; $inWatchlist=false; if($u){ $st=$pdo->prepare("SELECT COUNT(*) FROM watchlist WHERE user_id=? AND movie_id=?"); $st->execute([$u['id'],$id]); $inWatchlist=(int)$st->fetchColumn()>0; } if($u){ echo '<form id="wl" method="post" action="?action=toggleWatchlist" onsubmit="event.preventDefault();postForm(this.action,this).then(d=>{btn.textContent=d.in?\'✅ '.e(t('remove_from_watchlist')).'\':\'➕ '.e(t('add_to_watchlist')).'\';});">'.csrf_field().' <input type="hidden" name="movie_id" value="'.$id.'"> <button id="btn" class="btn mt-2">'.($inWatchlist?'✅ '.e(t('remove_from_watchlist')):'➕ '.e(t('add_to_watchlist'))).'</button></form>'; } // Rating if($u){ echo '<div class="mt-2">'.e(t('your_rating')).': '; for($i=1;$i<=5;$i++){ echo '<span class="star" data-v="'.$i.'"></span>'; } echo '</div>'; } // Comments echo '<h3 class="mt-3">'.e(t('comments')).'</h3>'; if($u){ echo '<form id="cmt" method="post" action="?action=comment" onsubmit="event.preventDefault();postForm(this.action,this).then(()=>location.reload());">'.csrf_field().' <input type="hidden" name="movie_id" value="'.$id.'"> <textarea name="comment" placeholder="'.e(t('add_comment')).'"></textarea> <button class="btn mt-2">'.e(t('submit')).'</button></form>'; } $cm=$pdo->prepare("SELECT c.*, u.name, u.id as uid FROM comments c LEFT JOIN users u ON u.id=c.user_id WHERE c.movie_id=? ORDER BY c.created_at DESC"); $cm->execute([$id]); $comments=$cm->fetchAll(PDO::FETCH_ASSOC); foreach($comments as $c){ echo '<div class="comment"><div><b>'.e($c['name']??'').'</b> <span class="small">'.$c['created_at'].'</span></div><div>'.e($c['comment']).'</div>'; if($u && ($u['id']==$c['uid'] || is_admin_like($u))){ echo '<form class="inline" method="post" action="?action=deleteComment" onsubmit="event.preventDefault();postForm(this.action,this).then(()=>location.reload());">'.csrf_field().'<input type="hidden" name="comment_id" value="'.$c['id'].'"><button class="btn secondary mt-2">'.e(t('delete')).'</button></form>'; } echo '</div>'; } echo '</div>'; // Right side: episodes if series echo '<div>'; if($m['type']==='series'){ $by=episodes_by_season($pdo,$id); echo '<div class="episodes-side p-2">'; foreach($by as $season=>$eps){ echo '<div class="mt-2"><b>'.e(t('seasons')).' '.$season.'</b></div>'; foreach($eps as $e){ $tt=$GLOBALS['LANG']==='ar'?$e['title_ar']:$e['title_en']; echo '<a class="row p-2 card" href="?route=play&id='.$id.'&ep='.$e['id'].'">S'.$e['season'].'E'.$e['episode'].' - '.e($tt).'</a>'; } } echo '</div>'; } echo '</div>'; echo '</div>'; // layout ?> <script> const player=document.getElementById('player'); const playBtn=document.getElementById('playBtn'); const back10=document.getElementById('back10'); const fwd10=document.getElementById('fwd10'); const vol=document.getElementById('vol'); const speed=document.getElementById('speed'); const fs=document.getElementById('fullscreen'); const cinema=document.getElementById('cinema'); const qSel=document.getElementById('quality'); const initialPos = <?= (int)$resumePos ?>; const episodeId = <?= (int)($episode_id??0) ?>; const movieId = <?= (int)$id ?>; const qualities = <?php echo $qualities; ?>; let currentSrc = <?php echo json_encode($default_src ?: (json_decode($qualities,true)['720p'] ?? '')); ?>; function setSource(src){ if(!src) return; // If file under /uploads, stream via range endpoint if (src.startsWith('/uploads/')) { currentSrc = '?action=stream&file=' + encodeURIComponent(src.replace(/^\/uploads\//,'')); } else { currentSrc = src; } player.src = currentSrc + (currentSrc.includes('?') ? '&' : '?') + 't=' + Date.now(); // bust cache } function populateQualities(){ qSel.innerHTML=''; const keys = Object.keys(qualities||{}); if (!keys.length && currentSrc) { const opt=document.createElement('option'); opt.value=currentSrc; opt.textContent='Auto'; qSel.appendChild(opt); return; } keys.sort((a,b)=>{ const toN=v=>/k$/i.test(v)?parseInt(v)*1000:parseInt(v); return toN(a)-toN(b); }); keys.forEach(k=>{ const opt=document.createElement('option'); opt.value = qualities[k]; opt.textContent = k; qSel.appendChild(opt); }); const last = qSel.options[qSel.options.length-1]; if (last){ qSel.value = last.value; setSource(last.value); } } function savePosDebounced(){ if (!<?= (int)!!$u ?>) return; // not logged in clearTimeout(savePosDebounced._t); savePosDebounced._t = setTimeout(()=>{ const body = new URLSearchParams({ movie_id: movieId, episode_id: episodeId||0, position: Math.floor(player.currentTime||0), duration: Math.floor(player.duration||0), device: (navigator.userAgent||'web'), }); fetch('?action=savePosition', {method:'POST', body}); }, 800); } playBtn.onclick = ()=> player.paused ? player.play() : player.pause(); back10.onclick = ()=> { player.currentTime = Math.max(0, (player.currentTime||0)-10); }; fwd10.onclick = ()=> { player.currentTime = Math.min((player.duration||0), (player.currentTime||0)+10); }; vol.oninput = ()=> { player.volume = parseFloat(vol.value||'1'); }; speed.onchange = ()=> { player.playbackRate = parseFloat(speed.value||'1'); }; fs.onclick = ()=> { if (document.fullscreenElement) document.exitFullscreen(); else player.requestFullscreen().catch(()=>{}); }; cinema.onclick = ()=> { document.querySelector('.cinema')?.classList.toggle('wide'); }; qSel.onchange = ()=> { const was = player.currentTime||0; const wasPaused = player.paused; setSource(qSel.value); player.addEventListener('loadedmetadata', function onmd(){ player.removeEventListener('loadedmetadata', onmd); player.currentTime = was; if (!wasPaused) player.play().catch(()=>{}); }); }; // Init populateQualities(); if (!qSel.options.length && currentSrc) setSource(currentSrc); // Resume if (initialPos>0){ player.addEventListener('loadedmetadata', function onMeta(){ player.removeEventListener('loadedmetadata', onMeta); if (player.duration && initialPos < player.duration-3) player.currentTime = initialPos; }); } // Auto save ['timeupdate','pause','seeking','seeked','ended'].forEach(ev=>player.addEventListener(ev, savePosDebounced)); // Auto-Next for series player.addEventListener('ended', async ()=>{ if (!episodeId) return; // not a series ep try{ const r = await fetch(`?action=nextEp&movie_id=${movieId}&current_ep=${episodeId}`); if (!r.ok) return; const j = await r.json(); if (j.ok && j.next_id){ location.href = `?route=play&id=${movieId}&ep=${j.next_id}`; } }catch(e){} }); // Stars rating <?php if($u){ ?> const csrfToken = <?= json_encode($_SESSION['csrf'] ?? '') ?>; document.querySelectorAll('.star').forEach(s=>{ s.addEventListener('click', async ()=>{ const rating = s.dataset.v; const fd=new FormData(); fd.append('csrf', csrfToken); fd.append('movie_id', movieId); fd.append('rating', rating); await fetch('?action=rate', {method:'POST', body: fd}); document.querySelectorAll('.star').forEach(st=>st.style.opacity = (parseInt(st.dataset.v)<=parseInt(rating)) ? '1':'0.3'); }); }); <?php } ?> </script> <?php footer_html(); } /* ---------- PLAY PAGE (series episode or movie direct) ---------- */ function page_play(PDO $pdo){ header_html(); $movie_id = (int)($_GET['id']??0); $ep_id = (int)($_GET['ep']??0); $m=$pdo->prepare("SELECT * FROM movies WHERE id=?"); $m->execute([$movie_id]); $movie=$m->fetch(PDO::FETCH_ASSOC); if(!$movie){ echo '<div class="p-3 cinema">not found</div>'; footer_html(); return; } // Determine video source + qualities $episode=null; $qualities='{}'; $default_src=''; if ($movie['type']==='series'){ $st=$pdo->prepare("SELECT * FROM episodes WHERE ".($ep_id?'id=?':'movie_id=?')." ORDER BY season,episode LIMIT 1"); $st->execute($ep_id?[$ep_id]:[$movie_id]); $episode=$st->fetch(PDO::FETCH_ASSOC); if($episode){ $ep_id=(int)$episode['id']; $default_src=$episode['mp4_src']??''; $qualities=$episode['qualities_json']?:'{}'; } } else { $default_src = (str_ends_with($movie['backdrop']??'','mp4') ? $movie['backdrop'] : ''); } $u=current_user(); $resume=0; if($u){ $wh=user_watch_in($pdo,$u['id'],$movie_id,$episode?$ep_id:null); $resume=(int)($wh['position_seconds']??0); } $title = $GLOBALS['LANG']==='ar' ? ($episode['title_ar']??$movie['title_ar']) : ($episode['title_en']??$movie['title_en']); echo '<div class="p-3 cinema details-layout"><div>'; // Player echo '<div class="video-wrap"><video id="player" width="100%" preload="metadata" controls playsinline></video> <div class="player-controls"> <button id="playBtn">⏯</button> <button id="back10">⏪ 10s</button> <button id="fwd10">⏩ 10s</button> <input id="vol" type="range" min="0" max="1" step="0.01" value="1"> <select id="speed"><option>0.5</option><option selected>1.0</option><option>1.25</option><option>1.5</option><option>2.0</option></select> <select id="quality"></select> <button id="cinema">🖼</button> <button id="fullscreen">⛶</button> </div></div>'; echo "<h2 class='mt-2'>".e($title)." <span class='badge'>".e($movie['type'])."</span></h2>"; // Side list of episodes echo '</div><div>'; if($movie['type']==='series'){ $by=episodes_by_season($pdo,$movie_id); echo '<div class="episodes-side p-2">'; foreach($by as $season=>$eps){ echo '<div class="mt-2"><b>'.e(t('seasons')).' '.$season.'</b></div>'; foreach($eps as $e){ $tt=$GLOBALS['LANG']==='ar'?$e['title_ar']:$e['title_en']; $active = ($ep_id===$e['id']) ? ' style="outline:2px solid var(--accent-2)"' : ''; echo '<a class="row p-2 card"'.$active.' href="?route=play&id='.$movie_id.'&ep='.$e['id'].'">S'.$e['season'].'E'.$e['episode'].' - '.e($tt).'</a>'; } } echo '</div>'; } echo '</div></div>'; ?> <script> const player=document.getElementById('player'); const playBtn=document.getElementById('playBtn'); const back10=document.getElementById('back10'); const fwd10=document.getElementById('fwd10'); const vol=document.getElementById('vol'); const speed=document.getElementById('speed'); const fs=document.getElementById('fullscreen'); const cinema=document.getElementById('cinema'); const qSel=document.getElementById('quality'); const initialPos = <?= (int)$resume ?>; const episodeId = <?= (int)$ep_id ?>; const movieId = <?= (int)$movie_id ?>; const qualities = <?php echo $qualities; ?>; let currentSrc = <?php echo json_encode($default_src ?: (json_decode($qualities,true)['720p'] ?? '')); ?>; function setSource(src){ if(!src) return; if (src.startsWith('/uploads/')) { currentSrc = '?action=stream&file=' + encodeURIComponent(src.replace(/^\/uploads\//,'')); } else { currentSrc = src; } player.src = currentSrc + (currentSrc.includes('?') ? '&' : '?') + 't=' + Date.now(); } function populateQualities(){ qSel.innerHTML=''; const keys = Object.keys(qualities||{}); if (!keys.length && currentSrc) { const opt=document.createElement('option'); opt.value=currentSrc; opt.textContent='Auto'; qSel.appendChild(opt); } else { keys.sort((a,b)=>{ const toN=v=>/k$/i.test(v)?parseInt(v)*1000:parseInt(v); return toN(a)-toN(b); }); keys.forEach(k=>{ const opt=document.createElement('option'); opt.value = qualities[k]; opt.textContent = k; qSel.appendChild(opt); }); const last = qSel.options[qSel.options.length-1]; if (last) { qSel.value = last.value; } } setSource(qSel.value||currentSrc); } function savePosDebounced(){ if (!<?= (int)!!$u ?>) return; clearTimeout(savePosDebounced._t); savePosDebounced._t = setTimeout(()=>{ const body = new URLSearchParams({ movie_id: movieId, episode_id: episodeId||0, position: Math.floor(player.currentTime||0), duration: Math.floor(player.duration||0), device: (navigator.userAgent||'web'), }); fetch('?action=savePosition', {method:'POST', body}); }, 800); } playBtn.onclick = ()=> player.paused ? player.play() : player.pause(); back10.onclick = ()=> { player.currentTime = Math.max(0, (player.currentTime||0)-10); }; fwd10.onclick = ()=> { player.currentTime = Math.min((player.duration||0), (player.currentTime||0)+10); }; vol.oninput = ()=> { player.volume = parseFloat(vol.value||'1'); }; speed.onchange = ()=> { player.playbackRate = parseFloat(speed.value||'1'); }; fs.onclick = ()=> { if (document.fullscreenElement) document.exitFullscreen(); else player.requestFullscreen().catch(()=>{}); }; cinema.onclick = ()=> { document.querySelector('.cinema')?.classList.toggle('wide'); }; qSel.onchange = ()=> { const was = player.currentTime||0; const wasPaused = player.paused; setSource(qSel.value); player.addEventListener('loadedmetadata', function onmd(){ player.removeEventListener('loadedmetadata', onmd); player.currentTime = was; if (!wasPaused) player.play().catch(()=>{}); }); }; populateQualities(); if (initialPos>0){ player.addEventListener('loadedmetadata', function onMeta(){ player.removeEventListener('loadedmetadata', onMeta); if (player.duration && initialPos < player.duration-3) player.currentTime = initialPos; }); } ['timeupdate','pause','seeking','seeked','ended'].forEach(ev=>player.addEventListener(ev, savePosDebounced)); player.addEventListener('ended', async ()=>{ if (!episodeId) return; try{ const r = await fetch(`?action=nextEp&movie_id=${movieId}&current_ep=${episodeId}`); if (!r.ok) return; const j = await r.json(); if (j.ok && j.next_id){ location.href = `?route=play&id=${movieId}&ep=${j.next_id}`; } }catch(e){} }); </script> <?php footer_html(); } /* ---------- ROUTER DISPATCH ---------- */ switch ($route) { case 'home': page_home($pdo); break; case 'search': page_search($pdo); break; case 'search.advanced': page_search_advanced(); break; case 'login': if($_SERVER['REQUEST_METHOD']!=='POST') page_login(); break; case 'register': if($_SERVER['REQUEST_METHOD']!=='POST') page_register(); break; case 'admin': page_admin($pdo); break; case 'watchlist': page_watchlist($pdo); break; case 'details': page_details($pdo); break; case 'play': page_play($pdo); break; default: page_home($pdo); break; }

نصائح تشغيل سريعة:

  • تأكد من وجود مجلدات: db, movies, uploads بصلاحيات كتابة للـ PHP.
  • ارفع ملفات فيديو تجريبية في uploads/، وحدث روابط الجودة من لوحة التحكم (أو استخدم العينات).
  • أول مستخدم يسجل يصبح Admin تلقائيًا ويُفعّل حسابه تلقائيًا.
  • المشغّل يدعم Range Requests عبر ?action=stream&file=... للمحتوى داخل uploads/.
  • يدعم العربية/الإنجليزية وتبديل RTL/LTR، الوضع الليلي افتراضي، وميزات: قائمة مشاهدة، تقييم/تعليقات، متابعة ومتابعة الموضع عبر الأجهزة، بحث متقدم، إشعارات، وإدارة بسيطة من لوحة التحكم.

Partager cette Q&R