اكمل الكود رجاء
作成日: 2025年9月13日
作成日: 2025年9月13日
اكمل الكود رجاء
ممكن توضّح لي أي كود تقصد؟ 👩💻
هل عندك مقطع من الكود وتريدني أكمله لك، أم تودّ أن أكتب كود جديد من الصفر (وفي أي لغة: بايثون، جافاسكريبت، جافا… إلخ)؟
لو أرسلت لي الجزء الموجود من الكود أو وصفت المطلوب بالضبط، أقدر أكمله لك خطوة بخطوة.
المطلوب: إنشاء منصة بث احترافية شبيهة بنتفليكس لعرض الأفلام والمسلسلات، بملف واحد index.php يحتوي على كل HTML/CSS/JS/PHP، مع:
1️⃣ البنية الأساسية:
2️⃣ دعم الأفلام والمسلسلات:
3️⃣ تعدد اللغات (AR/EN):
4️⃣ المشغل الاحترافي:
5️⃣ الإشعارات الداخلية:
6️⃣ وضع ليلي/نهاري:
7️⃣ نظام التقييم والتعليقات:
8️⃣ البحث المتقدم والفلاتر:
9️⃣ إدارة الإعلانات:
🔟 قائمة المشاهدة Watchlist:
1️⃣1️⃣ الاستمرار في المشاهدة متعدد الأجهزة:
1️⃣2️⃣ إحصائيات المستخدم:
1️⃣3️⃣ نظام ترشيحات ذكي:
1️⃣4️⃣ إدارة المستخدمين:
1️⃣5️⃣ المراقبة والتحليل:
1️⃣6️⃣ الأمان والأداء:
1️⃣7️⃣ التهيئة الأولية:
اكمل الكود رجاء :
<?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=st->execute([rows=rows){
echo '<h2>'.e(t('continue_watching')).'</h2><div class="grid mt-2">';
foreach(m){
GLOBALS['LANG']==='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=rec as m){
title=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(pdo,(int)m['id']).'</div></div></a>';
}
echo '</div>';
// Trending (most watched last week)
trend=trend as m){
title=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();
_GET['q']??'');
echo '<div class="p-3 cinema"><h2>'.e(t('search')).': '.e(q!==''){
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");
q.'%'; like,like,rows=rows as m){
title=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(); ?>
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();
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");
u['id']]); st->fetchAll(PDO::FETCH_ASSOC);
echo '<div class="p-3 cinema"><h2>'.e(t('watchlist')).'</h2><div class="grid mt-2">';
foreach(m){
GLOBALS['LANG']==='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();
_GET['id']??0);
pdo->prepare("SELECT * FROM movies WHERE id=?"); id]); st->fetch(PDO::FETCH_ASSOC);
if(!title=m['title_ar']:desc =m['description_ar']:avg=avg_rating(id);
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
m['type']==='series'){
pdo->prepare("SELECT * FROM episodes WHERE movie_id=? ORDER BY season,episode LIMIT 1"); id]); ep->fetch(PDO::FETCH_ASSOC);
if(first){ episode_id=(int)default_src=qualities=first['qualities_json']?:'{}'; }
else { default_src=''; qualities='{}'; }
} else {
default_src=m['backdrop'] && str_ends_with(m['backdrop'],'.mp4') ? qualities='{}';
}
// try resume
resumePos=0;
if(u){ wh=user_watch_in(u['id'],episode_id); 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(m['type'])."</span> <span class='badge'>⭐ m['genres'])." • ".e((string)m['quality'])."</div>";
echo "<p class='mt-2'>".e(inWatchlist=false; if(u){ st=st->execute([id]); 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<=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>';
}
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");
id]); cm->fetchAll(PDO::FETCH_ASSOC);
foreach(c){
echo '<div class="comment"><div><b>'.e(c['created_at'].'</span></div><div>'.e(u && (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>';
<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; // iftext// 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 ?>
تمام! كملت لك الملف من آخر سطر توقّف عنده (داخل page_details
في سكربت المشغّل)، وأضفت:
action=savePosition
) + استئناف تلقائي.action=rate
).page_play
تدعم Auto-Next للحلقة التالية.action=nextEp
لإحضار الحلقة التالية.انسخ هذا الملحق وألصقه مباشرة بعد آخر سطر في ملفك (حيث توقف عند // if
):
phpif(!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}¤t_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}¤t_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
تسمح بإدارة المحتوى والحلقات والإشعارات وإعدادات الموقع (اسم/شعار).page_play
وعند انتهاء الفيديو.uploads/sample720.mp4
و uploads/sample1080.mp4
أو حدّث روابط الجودة من لوحة التحكم.لو تحب، أقدر أجمع لك الملف النهائي كاملًا في رسالة واحدة (نسخة كاملة من index.php
) أو أضيف أجزاء إضافية مثل إدارة الإعلانات بتخزين كامل من اللوحة.
قم بذلك
تمام — هذه نسخة مكتملة وجاهزة من ملف واحد 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=``; 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}¤t_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}¤t_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/
، وحدث روابط الجودة من لوحة التحكم (أو استخدم العينات).?action=stream&file=...
للمحتوى داخل uploads/
.