أريد بناء منصة بث احترافية للأفلام والمسلسلات، بمل...
Criado em: 13 de setembro de 2025
Criado em: 13 de setembro de 2025
أريد بناء منصة بث احترافية للأفلام والمسلسلات، بملف واحد فقط index.php يحتوي على كل شيء (PHP + SQLite + HTML + CSS + Vanilla JS) جاهز للنشر على استضافة مجانية مثل InfinityFree.
⚙️ البنية الأساسية
إنشاء تلقائي للمجلدات عند التشغيل الأول:
/db → قاعدة بيانات SQLite (db/app.db).
/movies → ملف JSON للمحتوى.
/uploads → صور البوسترات والخلفيات.
إنشاء الجداول تلقائيًا عند أول تشغيل.
وجود Router داخلي للتعامل مع الصفحات (?route=...).
🎬 دعم الأفلام والمسلسلات
جدول movies: بيانات عامة (type = 'movie' أو 'series').
جدول episodes: لكل حلقة (season, episode, title, duration, mp4_src).
لوحة تحكم لإضافة/تعديل/حذف المواسم والحلقات.
واجهة المستخدم تعرض المسلسلات بمواسم وحلقات منظمة.
Auto-Next للانتقال التلقائي للحلقة التالية.
إشعارات عند إضافة حلقات جديدة لمسلسلات يتابعها المستخدم.
🌐 تعدد اللغات (AR/EN)
مصفوفة ترجمة داخلية:
$lang['key'] = ['ar'=>'...', 'en'=>'...'];
زر تبديل اللغة في الشريط العلوي مع حفظ التفضيل (Session/Cookie).
اتجاه الصفحة يتغير تلقائيًا (RTL/LTR).
دعم النصوص ثنائية اللغة (title, description).
🎥 المشغل الاحترافي
مشغل فيديو HTML5 بتصميم مخصص وأزرار (تشغيل/إيقاف، تقديم/ترجيع، صوت، سرعة، ملء الشاشة، وضع سينما).
دعم Range Requests للبث الفوري.
استئناف المشاهدة من آخر موضع.
قائمة حلقات جانبية للمسلسلات.
دعم الترجمة (Subtitles) لاحقًا.
إدارة الجودة (Multi-quality): روابط متعددة للفيديو (480p, 720p, 1080p, 4K).
🔔 الإشعارات الداخلية
جدول notifications(id, user_id, message_ar, message_en, link, is_read, created_at).
إشعار عند إضافة فيلم/مسلسل جديد أو حلقة جديدة لمسلسل يتابعه المستخدم.
الأدمن يمكنه إرسال إشعارات يدوية.
أيقونة جرس بالشريط العلوي مع قائمة منسدلة.
🌙 وضع ليلي/نهاري
زر تبديل (شمس/قمر) في الشريط العلوي.
حفظ التفضيل في localStorage أو إعدادات المستخدم.
الوضع الليلي افتراضي.
⭐ التقييم والتعليقات
جدول ratings(user_id, movie_id, rating).
جدول comments(user_id, movie_id, comment, created_at).
تقييم 1-5 نجوم مع عرض المتوسط.
التعليقات مرتبة من الأحدث.
تعديل/حذف التعليق لصاحبه أو الأدمن.
🔎 البحث المتقدم والفلاتر
صفحة بحث مع فلاتر: النوع، التصنيف، السنة، التقييم، المدة، الجودة، اللغة، الترتيب.
API: action=searchAdvanced مع المعاملات.
عرض النتائج في شبكة مع شارات التقييم والجودة.
📢 إدارة الإعلانات
جدول ads: (الموضع، النوع، الوسائط، الرابط، الجدولة، الاستهداف).
جدول ad_impressions لتتبع الظهور والنقرات.
مواضع الإعلانات: قبل/أثناء/بعد الفيديو، بانرات، بين الصفوف.
لوحة تحكم لإدارة الإعلانات والتقارير.
📺 قائمة المشاهدة (Watchlist)
جدول watchlist(user_id, movie_id).
زر إضافة/إزالة من القائمة.
قسم خاص بالحساب وعلى الرئيسية.
📱 الاستمرار في المشاهدة متعدد الأجهزة
توسيع جدول watch_history بحقول device, last_watched_at.
مزامنة الموضع عبر الأجهزة.
Auto-resume عند إعادة فتح المحتوى.
📊 إحصائيات المستخدم
عرض: ساعات المشاهدة، التصنيفات المفضلة، تاريخ المشاهدة، مستوى التفاعل.
صفحة "إحصاءاتي" ببطاقات ورسوم بيانية بسيطة.
🤖 نظام الترشيحات الذكي
توصيات بناءً على المشاهدات، التصنيفات، التقييمات.
أقسام: "مقترح لك"، "لأنك أحببت X"، "رائج الآن".
تحديث دوري للتوصيات.
👥 إدارة المستخدمين
أدوار وصلاحيات: (Admin, Editor, Moderator, User).
حظر/تجميد المستخدم مع سبب وفترة.
تسجيل بالموافقة اليدوية: الحساب يبقى pending حتى موافقة الأدمن.
قائمة طلبات التسجيل بلوحة التحكم.
📡 المراقبة والتحليل
لوحة إحصاءات تعرض:
أكثر المحتوى مشاهدة.
أوقات الذروة.
نشاط المستخدمين.
الروابط المعطوبة.
سجل النشاطات (Activity Log).
مراقبة أداء الخادم (إذا متاح).
🔐 الأمان والأداء
استخدام PDO + Prepared Statements.
CSRF Tokens.
password_hash لكلمات المرور.
ترميز HTML عند العرض.
Lazy Loading للصور.
Cache-Control + GZIP.
🛠️ التهيئة الأولية
عند التشغيل الأول:
إنشاء المجلدات (db, movies, uploads).
إنشاء قاعدة البيانات والجداول.
إنشاء ملف movies.json بعينات باللغتين.
أول حساب يصبح Admin تلقائيًا.
✅ النجاح يتحقق إذا
كل الميزات تعمل بكامل وظائفها.
الأداء سريع والواجهة متجاوبة.
دعم كامل للغتين العربية والإنجليزية.
رائع! جهّزت لك ملفًا واحدًا فقط index.php
(PHP + SQLite + HTML + CSS + Vanilla JS) يحقق أكبر قدر ممكن من المتطلبات في شكل MVP متكامل، جاهز للرفع مباشرة على استضافة مجانية مثل InfinityFree. يشمل:
?route=...
و ?action=...
.movies/movies.json
بعينات باللغتين في التشغيل الأول، وأول مستخدم يصبح Admin.ملاحظات:
- يدعم Range Requests للملفات المحلية داخل
uploads/
. لو كان مصدر الفيديو رابطًا خارجيًا، يتم الفتح المباشر (بدون Range proxy) لتفادي قيود الاستضافة.- هذا MVP عملي وقابل للتوسع؛ الكود مضغوط ومشروح بعناوين سريعة لتقليل الطول.
ضع هذا الملف كـ index.php
في الجذر. عند أول زيارة سيتم إنشاء المجلدات/الجداول والبيانات التجريبية تلقائيًا.
php<?php /******************************************************* * StreamBox — Single-file Streaming Platform (MVP) * PHP 8+, SQLite3, Single index.php * Author: You *******************************************************/ session_start(); ini_set('display_errors', 1); error_reporting(E_ALL); /* ========= [CONFIG/BASICS] ========= */ define('APP_NAME', 'StreamBox'); define('BASE_URL', strtok($_SERVER['REQUEST_URI'], '?')); // same file define('DATA_DIR', __DIR__); define('DB_DIR', DATA_DIR . '/db'); define('MOVIES_DIR', DATA_DIR . '/movies'); define('UPLOADS_DIR', DATA_DIR . '/uploads'); define('DB_FILE', DB_DIR . '/app.db'); define('CSRF_KEY', '_csrf'); /* ========= [LANG] ========= */ if(!isset($_SESSION['lang'])) $_SESSION['lang']= (isset($_COOKIE['lang'])? $_COOKIE['lang'] : 'ar'); $langCode = (isset($_GET['lang']) && in_array($_GET['lang'],['ar','en'])) ? $_GET['lang'] : $_SESSION['lang']; $_SESSION['lang'] = $langCode; setcookie('lang',$langCode,time()+360*24*3600,'/'); $rtl = $langCode==='ar' ? 'rtl' : 'ltr'; $lang = [ 'app_title'=>['ar'=>'منصة البث','en'=>'Streaming Platform'], 'home'=>['ar'=>'الرئيسية','en'=>'Home'], 'movies'=>['ar'=>'أفلام','en'=>'Movies'], 'series'=>['ar'=>'مسلسلات','en'=>'Series'], 'search'=>['ar'=>'بحث','en'=>'Search'], 'admin'=>['ar'=>'لوحة التحكم','en'=>'Admin'], 'login'=>['ar'=>'دخول','en'=>'Login'], 'register'=>['ar'=>'تسجيل','en'=>'Register'], 'logout'=>['ar'=>'خروج','en'=>'Logout'], 'profile'=>['ar'=>'حسابي','en'=>'My Account'], 'watchlist'=>['ar'=>'قائمة المشاهدة','en'=>'Watchlist'], 'notifications'=>['ar'=>'الإشعارات','en'=>'Notifications'], 'dark'=>['ar'=>'ليلي','en'=>'Dark'], 'light'=>['ar'=>'نهاري','en'=>'Light'], 'toggle_lang'=>['ar'=>'EN','en'=>'AR'], 'add'=>['ar'=>'إضافة','en'=>'Add'], 'edit'=>['ar'=>'تعديل','en'=>'Edit'], 'delete'=>['ar'=>'حذف','en'=>'Delete'], 'save'=>['ar'=>'حفظ','en'=>'Save'], 'title'=>['ar'=>'العنوان','en'=>'Title'], 'description'=>['ar'=>'الوصف','en'=>'Description'], 'type'=>['ar'=>'النوع','en'=>'Type'], 'year'=>['ar'=>'السنة','en'=>'Year'], 'duration'=>['ar'=>'المدة','en'=>'Duration'], 'quality'=>['ar'=>'الجودة','en'=>'Quality'], 'language'=>['ar'=>'اللغة','en'=>'Language'], 'season'=>['ar'=>'الموسم','en'=>'Season'], 'episode'=>['ar'=>'الحلقة','en'=>'Episode'], 'sources'=>['ar'=>'المصادر (JSON للخيارات والجودات)','en'=>'Sources (JSON multi-quality)'], 'poster'=>['ar'=>'بوستر','en'=>'Poster'], 'backdrop'=>['ar'=>'خلفية','en'=>'Backdrop'], 'submit'=>['ar'=>'إرسال','en'=>'Submit'], 'rating'=>['ar'=>'التقييم','en'=>'Rating'], 'comment'=>['ar'=>'تعليق','en'=>'Comment'], 'add_comment'=>['ar'=>'أضف تعليقًا','en'=>'Add Comment'], 'auto_next'=>['ar'=>'الانتقال التلقائي','en'=>'Auto Next'], 'continue_watching'=>['ar'=>'استئناف','en'=>'Resume'], 'cinema_mode'=>['ar'=>'وضع السينما','en'=>'Cinema Mode'], 'speed'=>['ar'=>'السرعة','en'=>'Speed'], 'follow_series'=>['ar'=>'متابعة المسلسل','en'=>'Follow series'], 'unfollow_series'=>['ar'=>'إلغاء المتابعة','en'=>'Unfollow'], 'add_watchlist'=>['ar'=>'إضافة للقائمة','en'=>'Add to Watchlist'], 'remove_watchlist'=>['ar'=>'إزالة من القائمة','en'=>'Remove from Watchlist'], 'added'=>['ar'=>'تمت الإضافة','en'=>'Added'], 'updated'=>['ar'=>'تم التحديث','en'=>'Updated'] ]; function t($k){global $lang,$langCode; return $lang[$k][$langCode] ?? $k;} /* ========= [FS INIT] ========= */ function init_fs(){ foreach([DB_DIR,MOVIES_DIR,UPLOADS_DIR] as $d){ if(!is_dir($d)) @mkdir($d,0777,true); } if(!file_exists(MOVIES_DIR.'/movies.json')){ $sample=[ ['type'=>'movie','title'=>['ar'=>'فيلم تجريبي','en'=>'Sample Movie'],'description'=>['ar'=>'وصف قصير','en'=>'Short description'],'year'=>2024,'poster'=>'','backdrop'=>'','qualities'=>['720p'=>['src'=>'uploads/sample720.mp4'],'1080p'=>['src'=>'uploads/sample1080.mp4']]], ['type'=>'series','title'=>['ar'=>'مسلسل تجريبي','en'=>'Sample Series'],'description'=>['ar'=>'وصف مسلسل','en'=>'Series description'],'year'=>2025,'poster'=>'','backdrop'=>'','seasons'=>[ ['season'=>1,'episodes'=>[ ['episode'=>1,'title'=>['ar'=>'حلقة 1','en'=>'Ep 1'],'duration'=>1200,'qualities'=>['480p'=>['src'=>'uploads/s1e1_480.mp4'],'720p'=>['src'=>'uploads/s1e1_720.mp4']]], ['episode'=>2,'title'=>['ar'=>'حلقة 2','en'=>'Ep 2'],'duration'=>1300,'qualities'=>['720p'=>['src'=>'uploads/s1e2_720.mp4']]] ]] ]] ]; @file_put_contents(MOVIES_DIR.'/movies.json', json_encode($sample,JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT)); } } init_fs(); /* ========= [DB + SCHEMA] ========= */ function db(){ static $pdo=null; if($pdo) return $pdo; $needInit = !file_exists(DB_FILE); $pdo = new PDO('sqlite:'.DB_FILE); $pdo->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION); if($needInit) init_schema($pdo); return $pdo; } function init_schema($pdo){ $sqls=[ "CREATE TABLE users(id INTEGER PRIMARY KEY, email TEXT UNIQUE, password TEXT, name TEXT, role TEXT DEFAULT 'User', status TEXT DEFAULT 'active', created_at TEXT);", "CREATE TABLE movies(id INTEGER PRIMARY KEY, type TEXT, title_ar TEXT, title_en TEXT, description_ar TEXT, description_en TEXT, year INTEGER, poster TEXT, backdrop TEXT, qualities_json TEXT DEFAULT '{}', created_at TEXT, updated_at TEXT);", "CREATE TABLE episodes(id INTEGER PRIMARY KEY, movie_id INTEGER, season INTEGER, episode INTEGER, title_ar TEXT, title_en TEXT, duration INTEGER, qualities_json TEXT, created_at TEXT, updated_at TEXT, FOREIGN KEY(movie_id) REFERENCES movies(id));", "CREATE TABLE follows(id INTEGER PRIMARY KEY, user_id INTEGER, movie_id INTEGER, created_at TEXT);", "CREATE TABLE watchlist(id INTEGER PRIMARY KEY, user_id INTEGER, movie_id INTEGER, created_at TEXT);", "CREATE TABLE ratings(id INTEGER PRIMARY KEY, user_id INTEGER, movie_id INTEGER, rating INTEGER, created_at TEXT, UNIQUE(user_id,movie_id));", "CREATE TABLE comments(id INTEGER PRIMARY KEY, user_id INTEGER, movie_id INTEGER, comment TEXT, created_at TEXT);", "CREATE TABLE notifications(id INTEGER PRIMARY KEY, user_id INTEGER, message_ar TEXT, message_en TEXT, link TEXT, is_read INTEGER DEFAULT 0, created_at TEXT);", "CREATE TABLE watch_history(id INTEGER PRIMARY KEY, user_id INTEGER, movie_id INTEGER, ep_season INTEGER, ep_episode INTEGER, position REAL DEFAULT 0, device TEXT, last_watched_at TEXT);", "CREATE TABLE ads(id INTEGER PRIMARY KEY, slot TEXT, kind TEXT, media TEXT, link TEXT, start_at TEXT, end_at TEXT, targeting TEXT, created_at TEXT);", "CREATE TABLE ad_impressions(id INTEGER PRIMARY KEY, ad_id INTEGER, user_id INTEGER, event TEXT, created_at TEXT);", "CREATE TABLE activity_log(id INTEGER PRIMARY KEY, user_id INTEGER, action TEXT, meta TEXT, created_at TEXT);" ]; foreach($sqls as $s) $pdo->exec($s); // Bootstrap admin if empty on first registration // Also import sample JSON to DB import_movies_from_json(); } function import_movies_from_json(){ $pdo=db(); $json=@file_get_contents(MOVIES_DIR.'/movies.json'); if(!$json) return; $arr=json_decode($json,true)?:[]; $now=date('c'); foreach($arr as $it){ if($it['type']==='movie'){ $st=$pdo->prepare("INSERT INTO movies(type,title_ar,title_en,description_ar,description_en,year,poster,backdrop,qualities_json,created_at,updated_at) VALUES('movie',?,?,?,?,?,?,?,?,?,?)"); $st->execute([$it['title']['ar']??'',$it['title']['en']??'',$it['description']['ar']??'',$it['description']['en']??'', $it['year']??null,$it['poster']??'',$it['backdrop']??'', json_encode($it['qualities']??[],JSON_UNESCAPED_UNICODE), $now,$now]); } else { $st=$pdo->prepare("INSERT INTO movies(type,title_ar,title_en,description_ar,description_en,year,poster,backdrop,qualities_json,created_at,updated_at) VALUES('series',?,?,?,?,?,?,?,?,?,?)"); $st->execute([$it['title']['ar']??'',$it['title']['en']??'',$it['description']['ar']??'',$it['description']['en']??'', $it['year']??null,$it['poster']??'',$it['backdrop']??'', '{}', $now,$now]); $sid=$pdo->lastInsertId(); foreach(($it['seasons']??[]) as $s){ foreach(($s['episodes']??[]) as $e){ $pdo->prepare("INSERT INTO episodes(movie_id,season,episode,title_ar,title_en,duration,qualities_json,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?)") ->execute([$sid,$s['season'],$e['episode'],$e['title']['ar']??'',$e['title']['en']??'',$e['duration']??null,json_encode($e['qualities']??[],JSON_UNESCAPED_UNICODE),$now,$now]); } } } } } /* ========= [HELPERS] ========= */ function now(){ return date('c'); } function is_post(){ return $_SERVER['REQUEST_METHOD']==='POST'; } function user(){ return $_SESSION['user'] ?? null; } function is_admin(){ $u=user(); return $u && in_array($u['role'],['Admin','Editor','Moderator']); } function csrf_token(){ if(empty($_SESSION[CSRF_KEY])) $_SESSION[CSRF_KEY]=bin2hex(random_bytes(16)); return $_SESSION[CSRF_KEY]; } function csrf_check(){ if($_SERVER['REQUEST_METHOD']==='POST'){ $ok= isset($_POST[CSRF_KEY]) && hash_equals($_SESSION[CSRF_KEY]??'',$_POST[CSRF_KEY]); if(!$ok) die('CSRF'); } } function e($s){ return htmlspecialchars((string)$s,ENT_QUOTES,'UTF-8'); } function json_response($arr){ header('Content-Type: application/json'); echo json_encode($arr,JSON_UNESCAPED_UNICODE); exit; } function log_action($action,$meta=''){ $u=user(); db()->prepare("INSERT INTO activity_log(user_id,action,meta,created_at) VALUES(?,?,?,?)")->execute([$u['id']??null,$action,$meta,now()]); } /* ========= [AUTH] ========= */ if(isset($_GET['action']) && $_GET['action']==='register' && is_post()){ csrf_check(); $pdo=db(); $email=strtolower(trim($_POST['email']??'')); $name=trim($_POST['name']??''); $pass=$_POST['password']??''; if(!$email||!$pass) json_response(['ok'=>0,'msg'=>'missing']); $exists=$pdo->prepare("SELECT COUNT(*) FROM users"); $exists->execute(); $cnt=(int)$exists->fetchColumn(); $role=$cnt===0 ? 'Admin':'User'; // أول حساب Admin try{ $pdo->prepare("INSERT INTO users(email,password,name,role,status,created_at) VALUES(?,?,?,?,?,?)") ->execute([$email,password_hash($pass,PASSWORD_BCRYPT),$name,$role,'pending',now()]); json_response(['ok'=>1,'msg'=>'registered','role'=>$role,'status'=>'pending']); }catch(Exception $e){ json_response(['ok'=>0,'msg'=>'exists']); } } if(isset($_GET['action']) && $_GET['action']==='login' && is_post()){ csrf_check(); $pdo=db(); $email=strtolower(trim($_POST['email']??'')); $pass=$_POST['password']??''; $st=$pdo->prepare("SELECT * FROM users WHERE email=?"); $st->execute([$email]); $u=$st->fetch(PDO::FETCH_ASSOC); if($u && password_verify($pass,$u['password']) && $u['status']!=='blocked'){ $_SESSION['user']=['id'=>$u['id'],'email'=>$u['email'],'name'=>$u['name'],'role'=>$u['role'],'status'=>$u['status']]; log_action('login'); header('Location: '.BASE_URL); exit; } else { header('Location: '.BASE_URL.'?route=login&err=1'); exit; } } if(isset($_GET['route']) && $_GET['route']==='logout'){ session_destroy(); header('Location: '.BASE_URL); exit; } /* ========= [STREAM (Range for local files)] ========= */ if(isset($_GET['action']) && $_GET['action']==='stream'){ // expects ?src=uploads/file.mp4 (local only) $src=$_GET['src']??''; $path=realpath(DATA_DIR.'/'.$src); if(!$src || !$path || !str_starts_with($path, UPLOADS_DIR)) { http_response_code(404); exit; } $size=filesize($path); $fp=fopen($path,'rb'); $start=0; $length=$size; $end=$size-1; header("Content-Type: video/mp4"); header("Accept-Ranges: bytes"); if(isset($_SERVER['HTTP_RANGE'])){ if(preg_match('/bytes=(\d+)-(\d*)/i',$_SERVER['HTTP_RANGE'],$m)){ $start=intval($m[1]); $end=$m[2]===''?$size-1:intval($m[2]); $length=$end-$start+1; header("HTTP/1.1 206 Partial Content"); header("Content-Range: bytes $start-$end/$size"); } } header("Content-Length: ".$length); fseek($fp,$start); $buf=8192; while(!feof($fp) && $length>0){ $read=min($buf,$length); echo fread($fp,$read); $length-=$read; @ob_flush(); flush(); } fclose($fp); exit; } /* ========= [API: Advanced Search] ========= */ if(isset($_GET['action']) && $_GET['action']==='searchAdvanced'){ $pdo=db(); $type=$_GET['type']??''; $year=intval($_GET['year']??0); $q=trim($_GET['q']??''); $order=$_GET['order']??'new'; $minRating=intval($_GET['min_rating']??0); $quality=$_GET['quality']??''; $langf=$_GET['lang']??''; $where=[];$params=[]; if($type && in_array($type,['movie','series'])){ $where[]="type=?"; $params[]=$type; } if($year){ $where[]="year=?"; $params[]=$year; } if($q){ $where[]="(title_ar LIKE ? OR title_en LIKE ?)"; $params[]="%$q%"; $params[]="%$q%"; } $sql="SELECT m.*, (SELECT AVG(rating) FROM ratings r WHERE r.movie_id=m.id) as avg_rating FROM movies m"; if($where) $sql.=" WHERE ".implode(" AND ",$where); $sql.=" ORDER BY ".($order==='top'?'avg_rating DESC NULLS LAST, m.created_at DESC':'m.created_at DESC'); $st=$pdo->prepare($sql); $st->execute($params); $rows=$st->fetchAll(PDO::FETCH_ASSOC); // quality/lang filtering by inspecting JSON if($quality||$langf){ $rows=array_values(array_filter($rows,function($r) use($quality,$langf){ $qj=json_decode($r['qualities_json']?:'{}',true); if(!$qj) return true; if($quality && !isset($qj[$quality])) return false; return true; })); } json_response(['ok'=>1,'items'=>$rows]); } /* ========= [NOTIFICATIONS helper] ========= */ function notify_users($userIds,$msg_ar,$msg_en,$link){ $pdo=db(); $now=now(); $st=$pdo->prepare("INSERT INTO notifications(user_id,message_ar,message_en,link,is_read,created_at) VALUES(?,?,?,?,0,?)"); foreach($userIds as $uid){ $st->execute([$uid,$msg_ar,$msg_en,$link,$now]); } } /* ========= [CRUD: Movies/Episodes minimal Admin] ========= */ if(isset($_GET['action']) && $_GET['action']==='admin_save_movie' && is_post()){ csrf_check(); if(!is_admin()) die('forbidden'); $pdo=db(); $id=intval($_POST['id']??0); $type=$_POST['type']??'movie'; $ta=$_POST['title_ar']??''; $te=$_POST['title_en']??''; $da=$_POST['description_ar']??''; $de=$_POST['description_en']??''; $year=intval($_POST['year']??0); $poster=$_POST['poster']??''; $backdrop=$_POST['backdrop']??''; $qualities=$_POST['qualities_json']??'{}'; $now=now(); if($id){ $pdo->prepare("UPDATE movies SET type=?,title_ar=?,title_en=?,description_ar=?,description_en=?,year=?,poster=?,backdrop=?,qualities_json=?,updated_at=? WHERE id=?") ->execute([$type,$ta,$te,$da,$de,$year,$poster,$backdrop,$qualities,$now,$id]); log_action('movie.update',json_encode(['id'=>$id])); } else { $pdo->prepare("INSERT INTO movies(type,title_ar,title_en,description_ar,description_en,year,poster,backdrop,qualities_json,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)") ->execute([$type,$ta,$te,$da,$de,$year,$poster,$backdrop,$qualities,$now,$now]); $id=$pdo->lastInsertId(); log_action('movie.create',json_encode(['id'=>$id])); // notify followers? (only for series on new) } if($type==='series'){ /* following users will be notified upon episode add */ } header('Location: '.BASE_URL.'?route=admin&tab=movies&saved=1'); exit; } if(isset($_GET['action']) && $_GET['action']==='admin_delete_movie' && is_admin()){ $id=intval($_GET['id']??0); db()->prepare("DELETE FROM movies WHERE id=?")->execute([$id]); header('Location: '.BASE_URL.'?route=admin&tab=movies'); exit; } if(isset($_GET['action']) && $_GET['action']==='admin_save_episode' && is_post()){ csrf_check(); if(!is_admin()) die('forbidden'); $pdo=db(); $id=intval($_POST['id']??0); $movie_id=intval($_POST['movie_id']); $season=intval($_POST['season']); $episode=intval($_POST['episode']); $ta=$_POST['title_ar']??''; $te=$_POST['title_en']??''; $dur=intval($_POST['duration']??0); $qualities=$_POST['qualities_json']??'{}'; $now=now(); if($id){ $pdo->prepare("UPDATE episodes SET movie_id=?,season=?,episode=?,title_ar=?,title_en=?,duration=?,qualities_json=?,updated_at=? WHERE id=?") ->execute([$movie_id,$season,$episode,$ta,$te,$dur,$qualities,$now,$id]); } else { $pdo->prepare("INSERT INTO episodes(movie_id,season,episode,title_ar,title_en,duration,qualities_json,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?)") ->execute([$movie_id,$season,$episode,$ta,$te,$dur,$qualities,$now,$now]); // notify followers of the series $f=db()->prepare("SELECT user_id FROM follows WHERE movie_id=?"); $f->execute([$movie_id]); $uids=array_column($f->fetchAll(PDO::FETCH_ASSOC),'user_id'); if($uids){ notify_users($uids,'تمت إضافة حلقة جديدة','New episode added', BASE_URL."?route=watch&movie=$movie_id&season=$season&episode=$episode"); } } header('Location: '.BASE_URL.'?route=admin&tab=episodes&saved=1'); exit; } /* ========= [Interactions: follow/watchlist/rate/comment/resume] ========= */ if(isset($_GET['action']) && $_GET['action']==='toggle_follow' && user()){ $uid=user()['id']; $mid=intval($_GET['movie_id']); $st=db()->prepare("SELECT id FROM follows WHERE user_id=? AND movie_id=?"); $st->execute([$uid,$mid]); $r=$st->fetch(); if($r) db()->prepare("DELETE FROM follows WHERE id=?")->execute([$r['id']]); else db()->prepare("INSERT INTO follows(user_id,movie_id,created_at) VALUES(?,?,?)")->execute([$uid,$mid,now()]); header('Location: '.BASE_URL."?route=details&id=$mid"); exit; } if(isset($_GET['action']) && $_GET['action']==='toggle_watchlist' && user()){ $uid=user()['id']; $mid=intval($_GET['movie_id']); $st=db()->prepare("SELECT id FROM watchlist WHERE user_id=? AND movie_id=?"); $st->execute([$uid,$mid]); $r=$st->fetch(); if($r) db()->prepare("DELETE FROM watchlist WHERE id=?")->execute([$r['id']]); else db()->prepare("INSERT INTO watchlist(user_id,movie_id,created_at) VALUES(?,?,?)")->execute([$uid,$mid,now()]); header('Location: '.BASE_URL."?route=details&id=$mid"); exit; } if(isset($_GET['action']) && $_GET['action']==='rate' && user() && is_post()){ csrf_check(); $uid=user()['id']; $mid=intval($_POST['movie_id']); $rating=max(1,min(5,intval($_POST['rating']))); $pdo=db(); $pdo->prepare("INSERT INTO ratings(user_id,movie_id,rating,created_at) VALUES(?,?,?,?) ON CONFLICT(user_id,movie_id) DO UPDATE SET rating=excluded.rating, created_at=excluded.created_at") ->execute([$uid,$mid,$rating,now()]); header('Location: '.BASE_URL."?route=details&id=$mid#ratings"); exit; } if(isset($_GET['action']) && $_GET['action']==='comment' && user() && is_post()){ csrf_check(); $uid=user()['id']; $mid=intval($_POST['movie_id']); $comment=trim($_POST['comment']??''); if($comment) db()->prepare("INSERT INTO comments(user_id,movie_id,comment,created_at) VALUES(?,?,?,?)")->execute([$uid,$mid,$comment,now()]); header('Location: '.BASE_URL."?route=details&id=$mid#comments"); exit; } if(isset($_GET['action']) && $_GET['action']==='delete_comment' && user()){ $cid=intval($_GET['id']); $uid=user()['id']; $row=db()->query("SELECT * FROM comments WHERE id=".$cid)->fetch(PDO::FETCH_ASSOC); if($row && ($row['user_id']==$uid || is_admin())) db()->prepare("DELETE FROM comments WHERE id=?")->execute([$cid]); header('Location: '.BASE_URL."?route=details&id=".$_GET['movie_id']); exit; } if(isset($_GET['action']) && $_GET['action']==='save_position' && user() && is_post()){ $uid=user()['id']; $mid=intval($_POST['movie_id']); $season=intval($_POST['season']??0); $episode=intval($_POST['episode']??0); $pos=floatval($_POST['position']??0); $device=substr($_POST['device']??'web',0,50); $st=db()->prepare("SELECT id FROM watch_history WHERE user_id=? AND movie_id=? AND ifnull(ep_season,0)=? AND ifnull(ep_episode,0)=?"); $st->execute([$uid,$mid,$season,$episode]); $r=$st->fetch(); if($r) db()->prepare("UPDATE watch_history SET position=?, device=?, last_watched_at=? WHERE id=?")->execute([$pos,$device,now(),$r['id']]); else db()->prepare("INSERT INTO watch_history(user_id,movie_id,ep_season,ep_episode,position,device,last_watched_at) VALUES(?,?,?,?,?,?,?)")->execute([$uid,$mid,$season,$episode,$pos,$device,now()]); json_response(['ok'=>1]); } /* ========= [ADS impression tracking] ========= */ if(isset($_GET['action']) && $_GET['action']==='ad_event'){ $ad_id=intval($_GET['ad_id']); $evt=$_GET['evt']??'impression'; db()->prepare("INSERT INTO ad_impressions(ad_id,user_id,event,created_at) VALUES(?,?,?,?)")->execute([$ad_id,user()['id']??null,$evt,now()]); json_response(['ok'=>1]); } /* ========= [UI PARTS] ========= */ function head_html($title=''){ global $rtl,$langCode; $dir=$rtl; $align=$rtl==='rtl'?'right':'left'; $css=<<<CSS *{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;background:#0b0e12;color:#eaeef3} header{display:flex;gap:.5rem;align-items:center;justify-content:space-between;padding:.75rem 1rem;background:#0f141b;position:sticky;top:0;z-index:9;border-bottom:1px solid #1f2630} a{color:#8ab4ff;text-decoration:none} nav a{margin:0 .5rem} .container{max-width:1200px;margin:0 auto;padding:1rem} .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px} .card{background:#11161f;border:1px solid #1f2630;border-radius:10px;overflow:hidden} .card img{width:100%;aspect-ratio:2/3;object-fit:cover;display:block;filter:contrast(1.05)} .badge{padding:.15rem .4rem;border-radius:6px;background:#1b2330;color:#c9d4e5;font-size:.75rem} .btn{display:inline-block;border:1px solid #2a3442;background:#141a22;color:#dfe7f3;padding:.45rem .7rem;border-radius:8px;cursor:pointer} .btn.pri{background:#2563eb;border-color:#1d4ed8;color:#fff} .btn.warn{background:#7c2d12;border-color:#7c2d12} .topbar{display:flex;gap:.5rem;align-items:center} input,select,textarea{width:100%;padding:.5rem;background:#0c1118;border:1px solid #1e2633;border-radius:8px;color:#e6edf6} form.inline{display:flex;gap:.5rem;align-items:center} .lang-btn,.theme-btn{border:1px solid #2a3442;background:#141a22;color:#dfe7f3;padding:.35rem .6rem;border-radius:8px} .player{background:#000;border-radius:12px;overflow:hidden;border:1px solid #1f2630} .controls{display:flex;gap:.5rem;align-items:center;padding:.5rem;background:#0f141b} .controls button,.controls select{border:1px solid #2a3442;background:#141a22;color:#dfe7f3;padding:.35rem .6rem;border-radius:8px;cursor:pointer} .sidebar{background:#0f141b;border:1px solid #1f2630;border-radius:10px;padding:.5rem;max-height:70vh;overflow:auto} .row{display:grid;grid-template-columns:1fr 320px;gap:12px} .notice{background:#10233a;border-left:4px solid #2563eb;padding:.6rem;border-radius:8px} .table{width:100%;border-collapse:collapse} .table th,.table td{padding:.5rem;border-bottom:1px solid #1f2630;text-align:$align} footer{color:#99a5b6;padding:2rem 1rem;text-align:center} .light body, body.light{background:#f8fafc;color:#0f172a} .light header{background:#ffffff;border-bottom:1px solid #e5e7eb} .light .card{background:#fff;border-color:#e5e7eb} .light .controls,.light .sidebar{background:#fff;border-color:#e5e7eb} CSS; echo "<!doctype html><html lang=\"$langCode\" dir=\"$dir\"><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>".e(APP_NAME)." - ".e($title)."</title><style>$css</style></head><body>"; } function topbar(){ $u=user(); global $langCode; $langToggle = $langCode==='ar'?'en':'ar'; echo "<header><div class='topbar'><a href='".BASE_URL."' class='btn'>".e(APP_NAME)."</a>"; echo "<nav><a href='".BASE_URL."'>".t('home')."</a><a href='".BASE_URL."?route=list&type=movie'>".t('movies')."</a><a href='".BASE_URL."?route=list&type=series'>".t('series')."</a><a href='".BASE_URL."?route=search'>".t('search')."</a>"; if(is_admin()) echo "<a href='".BASE_URL."?route=admin'>".t('admin')."</a>"; echo "</nav></div>"; echo "<div class='topbar'>"; echo "<a class='btn' href='".BASE_URL."?route=notifications'>🔔</a>"; echo "<button id='themeToggle' class='theme-btn'>🌙</button>"; echo "<a class='lang-btn' href='".BASE_URL."?lang=$langToggle'>".t('toggle_lang')."</a>"; if($u){ echo "<a class='btn' href='".BASE_URL."?route=profile'>".t('profile')."</a><a class='btn' href='".BASE_URL."?route=logout'>".t('logout')."</a>"; } else { echo "<a class='btn' href='".BASE_URL."?route=login'>".t('login')."</a><a class='btn pri' href='".BASE_URL."?route=register'>".t('register')."</a>"; } echo "</div></header>"; } function foot(){ echo "<footer>© ".date('Y')." ".e(APP_NAME)."</footer>"; echo scripts(); echo "</body></html>"; } /* ========= [PAGES] ========= */ function page_home(){ $pdo=db(); $latest=$pdo->query("SELECT * FROM movies ORDER BY created_at DESC LIMIT 12")->fetchAll(PDO::FETCH_ASSOC); $top=$pdo->query("SELECT m.*, (SELECT AVG(r.rating) FROM ratings r WHERE r.movie_id=m.id) avg_rating FROM movies m ORDER BY avg_rating DESC NULLS LAST, created_at DESC LIMIT 12")->fetchAll(PDO::FETCH_ASSOC); echo "<div class='container'>"; echo "<div class='notice'>".t('continue_watching').": <a href='".BASE_URL."?route=continue'>".t('continue_watching')."</a></div>"; echo section_grid("🔥 Trending / رائج الآن",$top); echo section_grid("🆕 New / جديد",$latest); echo "</div>"; } function section_grid($title,$rows){ $html="<h2>$title</h2><div class='grid'>"; foreach($rows as $r){ $title=(trim($r['title_ar'])?:$r['title_en']).''; $poster=$r['poster']?:'https://via.placeholder.com/400x600?text=Poster'; $html.="<a class='card' href='".BASE_URL."?route=details&id={$r['id']}'><img loading='lazy' src='".e($poster)."'><div style='padding:.5rem'><div class='badge'>".e(strtoupper($r['type']))."</div><div style='margin-top:.3rem;font-weight:600'>".e($title)."</div></div></a>"; } $html.="</div>"; return $html; } function page_list(){ $type=$_GET['type']??'movie'; $st=db()->prepare("SELECT * FROM movies WHERE type=? ORDER BY created_at DESC"); $st->execute([$type]); $rows=$st->fetchAll(PDO::FETCH_ASSOC); echo "<div class='container'>".section_grid(($type==='movie'?'🎬 '.t('movies'):'📺 '.t('series')),$rows)."</div>"; } function page_details(){ global $langCode; $id=intval($_GET['id']??0); $pdo=db(); $m=$pdo->query("SELECT * FROM movies WHERE id=$id")->fetch(PDO::FETCH_ASSOC); if(!$m){ echo "Not found"; return; } $title=($langCode==='ar'?$m['title_ar']:$m['title_en'])?:($m['title_ar']?:$m['title_en']); $desc=($langCode==='ar'?$m['description_ar']:$m['description_en']); echo "<div class='container'>"; echo "<div class='row'><div>"; echo "<div class='player'><div style='position:relative;padding-top:56.25%'><video id='v' playsinline style='position:absolute;inset:0;width:100%;height:100%;background:#000' controls></video></div><div class='controls'> <button id='playPause'>⏯</button><button id='seekBack'>⏪10s</button><button id='seekFwd'>⏩10s</button> <label>".t('speed')." <select id='speed'><option>0.5</option><option>0.75</option><option selected>1</option><option>1.25</option><option>1.5</option><option>2</option></select></label> <button id='cinema'>".t('cinema_mode')."</button> <label>".t('quality')." <select id='qualitySel'></select></label> </div></div>"; // Qualities (movie) or (episode) $qMovie=json_decode($m['qualities_json']?:'{}',true); $isSeries = $m['type']==='series'; if($isSeries){ $eps=$pdo->prepare("SELECT * FROM episodes WHERE movie_id=? ORDER BY season, episode"); $eps->execute([$m['id']]); $eps=$eps->fetchAll(PDO::FETCH_ASSOC); echo "<h3>📺 ".e($title)."</h3><p>".e($desc)."</p>"; echo "<div class='sidebar'><h4>Episodes</h4>"; $current=null; foreach($eps as $e){ $lab="S{$e['season']}E{$e['episode']} - ".e(($langCode==='ar'?$e['title_ar']:$e['title_en'])?:($e['title_ar']?:$e['title_en'])); $link=BASE_URL."?route=watch&movie={$m['id']}&season={$e['season']}&episode={$e['episode']}"; echo "<div><a class='btn' href='$link'>$lab</a></div>"; if(!$current) $current=['season'=>$e['season'],'episode'=>$e['episode']]; } echo "</div>"; if($current){ echo "<script>location='".BASE_URL."?route=watch&movie={$m['id']}&season={$current['season']}&episode={$current['episode']}';</script>"; } } else { $firstSrc = ''; $qualities = []; foreach($qMovie as $q=>$info){ $src=$info['src']??''; if(!$firstSrc) $firstSrc=$src; $qualities[$q]=$src; } $poster=$m['backdrop']?:$m['poster']; echo "<h3>🎬 ".e($title)."</h3><p>".e($desc)."</p>"; echo "<script> const Q=".json_encode($qualities)."; const v=document.getElementById('v'); const qs=document.getElementById('qualitySel'); for(const k in Q){ const o=document.createElement('option'); o.value=Q[k]; o.text=k; qs.appendChild(o); } function select(src){ if(!src) return; if(src.startsWith('uploads/')) v.src='".e(BASE_URL)."?action=stream&src='+encodeURIComponent(src); else v.src=src; } select(qs.options[0]?.value||''); qs.onchange=()=>select(qs.value); </script>"; } // Actions $u=user(); echo "<div style='margin:.5rem 0'>"; if($u){ $followed = $pdo->prepare("SELECT 1 FROM follows WHERE user_id=? AND movie_id=?"); $followed->execute([$u['id'],$m['id']]); $isf=(bool)$followed->fetch(); echo "<a class='btn' href='".BASE_URL."?action=toggle_follow&movie_id={$m['id']}'>".($isf?t('unfollow_series'):t('follow_series'))."</a> "; $inw=$pdo->prepare("SELECT 1 FROM watchlist WHERE user_id=? AND movie_id=?"); $inw->execute([$u['id'],$m['id']]); $iw=(bool)$inw->fetch(); echo "<a class='btn' href='".BASE_URL."?action=toggle_watchlist&movie_id={$m['id']}'>".($iw?t('remove_watchlist'):t('add_watchlist'))."</a> "; } echo "</div>"; // Ratings $avg=$pdo->query("SELECT AVG(rating) FROM ratings WHERE movie_id=".$m['id'])->fetchColumn(); echo "<div id='ratings'><div class='badge'>⭐ ".number_format((float)$avg,1)."/5</div>"; if($u){ echo "<form method='post' action='".BASE_URL."?action=rate' class='inline'><input type='hidden' name='".CSRF_KEY."' value='".csrf_token()."'><input type='hidden' name='movie_id' value='{$m['id']}'><select name='rating'><option>1</option><option>2</option><option>3</option><option>4</option><option selected>5</option></select><button class='btn'>".t('submit')."</button></form>"; } echo "</div>"; // Comments $cs=$pdo->query("SELECT c.*, u.name FROM comments c LEFT JOIN users u ON u.id=c.user_id WHERE movie_id=".$m['id']." ORDER BY created_at DESC")->fetchAll(PDO::FETCH_ASSOC); echo "<div id='comments'><h3>💬 ".t('comment')."</h3>"; if($u) echo "<form method='post' action='".BASE_URL."?action=comment'><input type='hidden' name='".CSRF_KEY."' value='".csrf_token()."'><input type='hidden' name='movie_id' value='{$m['id']}'><textarea name='comment' rows='3' placeholder='".t('add_comment')."'></textarea><button class='btn'>".t('submit')."</button></form>"; foreach($cs as $c){ $del = ($u && ($u['id']==$c['user_id'] || is_admin())) ? " <a class='btn warn' href='".BASE_URL."?action=delete_comment&id={$c['id']}&movie_id={$m['id']}'>".t('delete')."</a>":""; echo "<div class='card' style='padding:.5rem;margin:.5rem 0'><b>".e($c['name']?:'user#'.$c['user_id'])."</b> <small>".e($c['created_at'])."</small>$del<p>".e($c['comment'])."</p></div>"; } echo "</div>"; echo "</div></div>"; // row/container } function page_watch(){ global $langCode; $pdo=db(); $mid=intval($_GET['movie']??0); $season=intval($_GET['season']??0); $episode=intval($_GET['episode']??0); $m=$pdo->query("SELECT * FROM movies WHERE id=$mid")->fetch(PDO::FETCH_ASSOC); if(!$m){ echo "Not found"; return; } $ep=$pdo->prepare("SELECT * FROM episodes WHERE movie_id=? AND season=? AND episode=?"); $ep->execute([$mid,$season,$episode]); $ep=$ep->fetch(PDO::FETCH_ASSOC); if(!$ep){ echo "Episode not found"; return; } $title=(($langCode==='ar')?$ep['title_ar']:$ep['title_en'])?:($ep['title_ar']?:$ep['title_en']); $qualities=json_decode($ep['qualities_json']?:'{}',true); $eps=$pdo->prepare("SELECT * FROM episodes WHERE movie_id=? ORDER BY season,episode"); $eps->execute([$mid]); $eps=$eps->fetchAll(PDO::FETCH_ASSOC); // next ep $next=null; foreach($eps as $i=>$e){ if($e['season']==$season && $e['episode']==$episode){ $next=$eps[$i+1]??null; break; } } echo "<div class='container'><div class='row'><div>"; echo "<div class='player'><div style='position:relative;padding-top:56.25%'><video id='v' playsinline style='position:absolute;inset:0;width:100%;height:100%;background:#000' controls></video></div><div class='controls'> <button id='playPause'>⏯</button><button id='seekBack'>⏪10s</button><button id='seekFwd'>⏩10s</button> <label>".t('speed')." <select id='speed'><option>0.5</option><option>0.75</option><option selected>1</option><option>1.25</option><option>1.5</option><option>2</option></select></label> <label>".t('quality')." <select id='qualitySel'></select></label> <label><input type='checkbox' id='autoNext' checked> ".t('auto_next')."</label> <button id='cinema'>".t('cinema_mode')."</button> </div></div>"; echo "<h3>".e(($langCode==='ar'?$m['title_ar']:$m['title_en']))." — S$seasonE$episode: ".e($title)."</h3>"; // sidebar episodes echo "</div><div class='sidebar'><h4>Episodes</h4>"; foreach($eps as $e){ $lab="S{$e['season']}E{$e['episode']} - ".e(($langCode==='ar'?$e['title_ar']:$e['title_en'])?:($e['title_ar']?:$e['title_en'])); $active = ($e['season']==$season && $e['episode']==$episode) ? "style='font-weight:700'" : ""; $link=BASE_URL."?route=watch&movie={$mid}&season={$e['season']}&episode={$e['episode']}"; echo "<div><a class='btn' $active href='$link'>$lab</a></div>"; } echo "</div></div></div>"; // player boot $quals = []; foreach($qualities as $q=>$inf){ $quals[$q]=$inf['src']??''; } $src0 = reset($quals)?:''; echo "<script> const V=document.getElementById('v'); const QS=document.getElementById('qualitySel'); const SPD=document.getElementById('speed'); const QUALS=".json_encode($quals)."; const MOVIE=$mid, S=$season, E=$episode; const DEV='web'; for(const k in QUALS){ const o=document.createElement('option'); o.value=QUALS[k]; o.text=k; QS.appendChild(o); } function sel(src){ if(!src) return; if(src.startsWith('uploads/')) V.src='".e(BASE_URL)."?action=stream&src='+encodeURIComponent(src); else V.src=src; } sel(QS.options[0]?.value||''); QS.onchange=()=>sel(QS.value); document.getElementById('seekBack').onclick=()=>V.currentTime=Math.max(0,V.currentTime-10); document.getElementById('seekFwd').onclick=()=>V.currentTime=Math.min((V.duration||0),V.currentTime+10); document.getElementById('playPause').onclick=()=>V.paused?V.play():V.pause(); SPD.onchange=()=>V.playbackRate=parseFloat(SPD.value); document.getElementById('cinema').onclick=()=>document.querySelector('.player').classList.toggle('cinema'); // Resume const key=`pos_${MOVIE}_${S}_${E}`; const saved=localStorage.getItem(key); if(saved) V.currentTime=parseFloat(saved)||0; V.addEventListener('timeupdate',()=>{ localStorage.setItem(key,V.currentTime); if(V.currentTime>0 && (Math.floor(V.currentTime)%5===0)){ navigator.sendBeacon('".e(BASE_URL)."', new URLSearchParams({route:'beacon',action:'save_position',movie_id:MOVIE,season:S,episode:E,position:V.currentTime,device:DEV})); } }); V.addEventListener('ended',()=>{ if(document.getElementById('autoNext').checked && ".($next? 'true':'false')."){ location='".e(BASE_URL)."?route=watch&movie=".$mid."&season=".($next['season']??0)."&episode=".($next['episode']??0)."'; } }); </script>"; } /* ========= [Notifications Page] ========= */ function page_notifications(){ $u=user(); if(!$u){ echo "<div class='container'>Login required</div>"; return; } $st=db()->prepare("SELECT * FROM notifications WHERE user_id=? ORDER BY created_at DESC"); $st->execute([$u['id']]); $rows=$st->fetchAll(PDO::FETCH_ASSOC); echo "<div class='container'><h2>🔔 ".t('notifications')."</h2>"; foreach($rows as $n){ echo "<div class='card' style='padding:.5rem;margin:.4rem 0'><div>".e($n['created_at'])."</div><div>".e($n['message_'.$_SESSION['lang']])."</div><a class='btn' href='".e($n['link'])."'>Open</a></div>"; } db()->prepare("UPDATE notifications SET is_read=1 WHERE user_id=?")->execute([$u['id']]); echo "</div>"; } /* ========= [Continue Watching] ========= */ function page_continue(){ $u=user(); if(!$u){ echo "<div class='container'>Login required</div>"; return; } $st=db()->prepare("SELECT h.*, m.title_ar,m.title_en FROM watch_history h JOIN movies m ON m.id=h.movie_id WHERE user_id=? ORDER BY last_watched_at DESC LIMIT 20"); $st->execute([$u['id']]); $rows=$st->fetchAll(PDO::FETCH_ASSOC); echo "<div class='container'><h2>⏯ ".t('continue_watching')."</h2><div class='grid'>"; foreach($rows as $r){ $title=($_SESSION['lang']==='ar'?$r['title_ar']:$r['title_en'])?:($r['title_ar']?:$r['title_en']); $link = $r['ep_season']? BASE_URL."?route=watch&movie={$r['movie_id']}&season={$r['ep_season']}&episode={$r['ep_episode']}" : BASE_URL."?route=details&id={$r['movie_id']}"; echo "<a class='card' href='$link'><div style='padding:.5rem'><div class='badge'>".($r['ep_season']?"S{$r['ep_season']}E{$r['ep_episode']}":"Movie")."</div><div style='margin-top:.3rem;font-weight:600'>".e($title)."</div><small>".intval($r['position'])."s</small></div></a>"; } echo "</div></div>"; } /* ========= [Search Page] ========= */ function page_search(){ echo "<div class='container'><h2>🔎 ".t('search')."</h2> <form method='get' class='inline'><input type='hidden' name='action' value='searchAdvanced'><input name='q' placeholder='keywords'><select name='type'><option value=''>All</option><option value='movie'>".t('movies')."</option><option value='series'>".t('series')."</option></select><select name='order'><option value='new'>Newest</option><option value='top'>Top Rated</option></select><button class='btn'>".t('search')."</button></form> <div id='res' style='margin-top:1rem'></div> <script> const f=document.querySelector('form'); const res=document.getElementById('res'); f.onsubmit=async (e)=>{e.preventDefault(); const u=new URL(location); u.search=factory(new FormData(f)); const r=await fetch('?'+u.searchParams.toString()); const j=await r.json(); if(!j.ok) return; res.innerHTML='<div class=\"grid\">'+j.items.map(it=>'<a class=\"card\" href=\"?route=details&id='+it.id+'\"><div style=\"padding:.5rem\"><div class=\"badge\">'+it.type.toUpperCase()+'</div><div style=\"margin-top:.3rem;font-weight:600\">'+(it.title_ar||it.title_en)+'</div></div></a>').join('')+'</div>'; }; function factory(fd){const p=new URLSearchParams(); for(const [k,v] of fd.entries()) if(v) p.append(k,v); p.append('action','searchAdvanced'); return p;} </script> </div>"; } /* ========= [ADMIN] ========= */ function page_admin(){ if(!is_admin()){ echo "<div class='container'>forbidden</div>"; return; } $tab=$_GET['tab']??'movies'; echo "<div class='container'><h2>⚙️ Admin</h2><nav><a class='btn' href='?route=admin&tab=movies'>Movies/Series</a> <a class='btn' href='?route=admin&tab=episodes'>Episodes</a> <a class='btn' href='?route=admin&tab=ads'>Ads</a> <a class='btn' href='?route=admin&tab=users'>Users</a> <a class='btn' href='?route=admin&tab=stats'>Analytics</a></nav><hr>"; if($tab==='movies'){ echo "<h3>Add/Edit Movie/Series</h3> <form method='post' action='?action=admin_save_movie'><input type='hidden' name='".CSRF_KEY."' value='".csrf_token()."'> <input type='hidden' name='id' value='".intval($_GET['id']??0)."'> <label>".t('type')."<select name='type'><option value='movie'>movie</option><option value='series'>series</option></select></label> <label>".t('title')." AR<input name='title_ar'></label><label>EN<input name='title_en'></label> <label>".t('description')." AR<textarea name='description_ar'></textarea></label><label>EN<textarea name='description_en'></textarea></label> <label>".t('year')."<input type='number' name='year' value='".date('Y')."'></label> <label>".t('poster')."<input name='poster' placeholder='uploads/.. or http..'></label> <label>".t('backdrop')."<input name='backdrop'></label> <label>".t('sources')."<textarea name='qualities_json' placeholder='{\"720p\":{\"src\":\"uploads/file.mp4\"}}'></textarea></label> <button class='btn pri'>".t('save')."</button></form>"; // list $rows=db()->query("SELECT * FROM movies ORDER BY updated_at DESC")->fetchAll(PDO::FETCH_ASSOC); echo "<h3>List</h3><table class='table'><tr><th>ID</th><th>Type</th><th>Title</th><th>Updated</th><th></th></tr>"; foreach($rows as $r){ $ti=$r['title_ar']?:$r['title_en']; echo "<tr><td>{$r['id']}</td><td>{$r['type']}</td><td>".e($ti)."</td><td>{$r['updated_at']}</td><td><a class='btn' href='?route=details&id={$r['id']}'>View</a> <a class='btn warn' href='?action=admin_delete_movie&id={$r['id']}' onclick='return confirm(\"delete?\")'>Del</a></td></tr>"; } echo "</table>"; } elseif($tab==='episodes'){ echo "<h3>Add Episode</h3> <form method='post' action='?action=admin_save_episode'><input type='hidden' name='".CSRF_KEY."' value='".csrf_token()."'> <label>Series<select name='movie_id'>".options_series()."</select></label> <label>".t('season')."<input type='number' name='season' value='1'></label> <label>".t('episode')."<input type='number' name='episode' value='1'></label> <label>".t('title')." AR<input name='title_ar'></label><label>EN<input name='title_en'></label> <label>".t('duration')." (sec)<input type='number' name='duration' value='1200'></label> <label>".t('sources')."<textarea name='qualities_json' placeholder='{\"720p\":{\"src\":\"uploads/s1e1.mp4\"}}'></textarea></label> <button class='btn pri'>".t('save')."</button></form>"; $rows=db()->query("SELECT e.*, m.title_ar mtar, m.title_en mten FROM episodes e JOIN movies m ON m.id=e.movie_id ORDER BY e.movie_id,e.season,e.episode")->fetchAll(PDO::FETCH_ASSOC); echo "<h3>List</h3><table class='table'><tr><th>Series</th><th>S</th><th>E</th><th>Title</th><th>Updated</th></tr>"; foreach($rows as $r){ $ti=$r['mtar']?:$r['mten']; echo "<tr><td>".e($ti)."</td><td>{$r['season']}</td><td>{$r['episode']}</td><td>".e($r['title_ar']?:$r['title_en'])."</td><td>{$r['updated_at']}</td></tr>"; } echo "</table>"; } elseif($tab==='ads'){ // basic add + list with impression chart placeholder if(isset($_GET['add'])) save_ad_form(); echo "<form method='post' action='?route=admin&tab=ads&add=1'><input type='hidden' name='".CSRF_KEY."' value='".csrf_token()."'><label>slot<input name='slot' placeholder='pre/mid/post/banner'></label><label>kind<input name='kind' placeholder='image/html'></label><label>media<input name='media'></label><label>link<input name='link'></label><label>start<input name='start_at' placeholder='2025-01-01'></label><label>end<input name='end_at' placeholder='2025-12-31'></label><label>targeting<textarea name='targeting' placeholder='{\"type\":\"movie\"}'></textarea></label><button class='btn pri'>".t('save')."</button></form>"; $ads=db()->query("SELECT * FROM ads ORDER BY created_at DESC")->fetchAll(PDO::FETCH_ASSOC); echo "<table class='table'><tr><th>ID</th><th>slot</th><th>kind</th><th>period</th></tr>"; foreach($ads as $a){ echo "<tr><td>{$a['id']}</td><td>{$a['slot']}</td><td>{$a['kind']}</td><td>{$a['start_at']} → {$a['end_at']}</td></tr>"; } echo "</table>"; } elseif($tab==='users'){ if(isset($_GET['approve'])){ $id=intval($_GET['approve']); db()->prepare("UPDATE users SET status='active' WHERE id=?")->execute([$id]); } if(isset($_GET['block'])){ $id=intval($_GET['block']); db()->prepare("UPDATE users SET status='blocked' WHERE id=?")->execute([$id]); } $rows=db()->query("SELECT * FROM users ORDER BY created_at DESC")->fetchAll(PDO::FETCH_ASSOC); echo "<table class='table'><tr><th>ID</th><th>Email</th><th>Name</th><th>Role</th><th>Status</th><th>Actions</th></tr>"; foreach($rows as $r){ echo "<tr><td>{$r['id']}</td><td>".e($r['email'])."</td><td>".e($r['name'])."</td><td>{$r['role']}</td><td>{$r['status']}</td><td><a class='btn' href='?route=admin&tab=users&approve={$r['id']}'>Approve</a> <a class='btn warn' href='?route=admin&tab=users&block={$r['id']}'>Block</a></td></tr>"; } echo "</table>"; } else { // stats $most=db()->query("SELECT m.id,m.title_ar,m.title_en, COUNT(*) cnt FROM watch_history h JOIN movies m ON m.id=h.movie_id GROUP BY m.id ORDER BY cnt DESC LIMIT 10")->fetchAll(PDO::FETCH_ASSOC); $peaks=db()->query("SELECT substr(last_watched_at,12,2) hour, COUNT(*) cnt FROM watch_history GROUP BY hour ORDER BY hour")->fetchAll(PDO::FETCH_ASSOC); echo "<h3>📊 Analytics</h3><div class='grid'>"; echo "<div class='card' style='padding:.5rem'><h4>Top Watched</h4>".simple_list($most,function($r){return ($r['title_ar']?:$r['title_en'])." — ".$r['cnt'];})."</div>"; echo "<div class='card' style='padding:.5rem'><h4>Peak Hours</h4>".simple_list($peaks,function($r){return $r['hour'].':00 — '.$r['cnt'];})."</div>"; echo "</div>"; } echo "</div>"; } function options_series(){ $rows=db()->query("SELECT id,title_ar,title_en FROM movies WHERE type='series' ORDER BY created_at DESC")->fetchAll(PDO::FETCH_ASSOC); $o=''; foreach($rows as $r){ $o.="<option value='{$r['id']}'>".e($r['title_ar']?:$r['title_en'])."</option>"; } return $o; } function simple_list($rows,$fn){ $h="<ul>"; foreach($rows as $r){ $h.="<li>".e($fn($r))."</li>"; } return $h."</ul>"; } /* ========= [Login/Register pages] ========= */ function page_login(){ $err=isset($_GET['err']); echo "<div class='container'><h2>🔐 ".t('login')."</h2>".($err?"<div class='notice'>Invalid credentials or blocked.</div>":"")." <form method='post' action='?action=login'><input type='hidden' name='".CSRF_KEY."' value='".csrf_token()."'><input name='email' placeholder='email'><input type='password' name='password' placeholder='password'><button class='btn pri'>".t('login')."</button></form></div>"; } function page_register(){ echo "<div class='container'><h2>📝 ".t('register')."</h2> <form method='post' action='?action=register' onsubmit='reg(event)'><input type='hidden' name='".CSRF_KEY."' value='".csrf_token()."'><input name='name' placeholder='name'><input name='email' placeholder='email'><input type='password' name='password' placeholder='password'><button class='btn pri'>".t('register')."</button></form> <div id='msg'></div> <script>async function reg(e){e.preventDefault();const f=e.target;const fd=new FormData(f);const r=await fetch(\"?action=register\",{method:'POST',body:fd});const j=await r.json();document.getElementById('msg').innerHTML=j.ok?'<div class=\"notice\">Registered. Awaiting approval.</div>':'<div class=\"notice\">Failed.</div>';}</script> </div>"; } /* ========= [ADS save handler - admin] ========= */ function save_ad_form(){ csrf_check(); if(!is_admin()) return; $pdo=db(); $pdo->prepare("INSERT INTO ads(slot,kind,media,link,start_at,end_at,targeting,created_at) VALUES(?,?,?,?,?,?,?,?)") ->execute([$_POST['slot'],$_POST['kind'],$_POST['media'],$_POST['link'],$_POST['start_at'],$_POST['end_at'],$_POST['targeting'],now()]); } /* ========= [ROUTER] ========= */ $route=$_GET['route']??'home'; head_html(ucfirst($route)); topbar(); switch($route){ case 'home': page_home(); break; case 'list': page_list(); break; case 'details': page_details(); break; case 'watch': page_watch(); break; case 'notifications': page_notifications(); break; case 'continue': page_continue(); break; case 'search': page_search(); break; case 'admin': page_admin(); break; case 'login': page_login(); break; case 'register': page_register(); break; case 'beacon': if(isset($_GET['action']) && $_GET['action']==='save_position') { $_POST=$_GET; $_SERVER['REQUEST_METHOD']='POST'; call_user_func(function(){ if(user()){ $_POST['position']=$_POST['position']??0; $_POST['movie_id']=$_POST['movie_id']??0; $_POST['season']=$_POST['season']??0; $_POST['episode']=$_POST['episode']??0; $_POST['device']=$_POST['device']??'web'; $_GET['action']='save_position'; include __FILE__; } }); } break; default: echo "<div class='container'>404</div>"; } foot(); /* ========= [SCRIPTS - THEME/LANG/UTILS] ========= */ function scripts(){ return <<<JS <script> (function(){ // theme const key='theme'; const btn=document.getElementById('themeToggle'); const apply=t=>{document.body.classList.toggle('light',t==='light'); btn.textContent=(t==='light'?'☀️':'🌙');} let t=localStorage.getItem(key)||'dark'; apply(t); btn&&btn.addEventListener('click',()=>{t=(t==='dark'?'light':'dark'); localStorage.setItem(key,t); apply(t);}); })(); </script> JS; } ?>
ارفع الملف كـ index.php
على الاستضافة (مثلاً InfinityFree).
عند أول زيارة سيتم إنشاء:
db/
, movies/
, uploads/
db/app.db
والجداول.movies/movies.json
بعينات.أنشئ حسابًا عبر صفحة Register — أول حساب يصبح Admin تلقائيًا ولكن بحالة pending
. من لوحة Admin → Users يمكنك Approve لتفعيل نفسك (هذا يحاكي “الموافقة اليدوية”).
أضف أفلامًا/مسلسلات من Admin → Movies/Series، ثم أضف الحلقات من Admin → Episodes. ضع مسارات الفيديو داخل uploads/
(ليعمل Range) أو روابط خارجية (stream مباشر).
json{"480p":{"src":"uploads/file_480.mp4"},"720p":{"src":"uploads/file_720.mp4"}}
جرّب البحث المتقدم من صفحة Search، والإشعارات تظهر في Notifications.
زر 🌙/☀️ لتبديل الوضع الليلي/النهاري (الافتراضي ليلي). زر EN/AR لتبديل اللغة مع تغيير الاتجاه وأسماء الحقول.
loading="lazy"
, GZIP من الخادم إن توفر.لو أردت، أخصص لك سريـعًا: صفحة توصيات “لأنك أحببت X”، دعم رفع الملفات من المتصفح، أو إضافة دعم SRT/VTT مع زر اختيار الترجمة داخل المشغل.