diff --git a/README.md b/README.md
index cdc16c5..44c0cba 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,69 @@
-# Decrypt
-### [+] Created By HTR-TECH (@***tahmid.rayat***)
-### [+] Disclaimer :
-***Decrypter is a tool to decrypt Encrypted Bash Scripts into a Readable Format.This Tool is created for Educational Purpose only.I am not responsible for any misuse of this tool.***
+# GENZMOVIE
-
+GENZMOVIE is a PHP + MySQL movie streaming website with OPhim auto crawler, admin dashboard, ad monetization, user accounts, watchlist/history, SEO routes, and responsive UI.
-### [+] Installation
-```apt update```
+## Project Structure
-```apt install git python2 -y```
+```text
+genzmovie/
+├── config/
+├── controllers/
+├── models/
+├── views/
+├── assets/
+├── uploads/
+├── admin/
+├── crawler/
+├── api/
+├── includes/
+├── database/
+├── index.php
+└── .htaccess
+```
-```git clone https://github.com/hax0rtahm1d/decrypt```
+## Features
-```cd decrypt```
+- Netflix-style homepage with featured slider and category blocks.
+- Search + autocomplete + filter by genre/year/country/quality.
+- Movie details with metadata, episodes, and related movies.
+- Player supports iframe embed and direct MP4/HLS source.
+- Multiple server episode links.
+- User register/login/logout with dashboard (favorites + watch history).
+- Ad monetization: header/sidebar/popup/footer/video pre-roll placements.
+- Admin dashboard for crawler trigger, ad management, overview of movies/users/comments.
+- OPhim crawler imports movie metadata + episodes + links.
+- SEO: friendly route `/phim/{slug}` and XML sitemap endpoint.
+- Security basics: password hashing, prepared statements, CSRF token, output escaping.
-```python2 dec.py```
+## Installation Guide
-### Or, Use Single Command
+1. **Copy files** into web root (Apache/Nginx + PHP 8+).
+2. **Create database**:
+ ```bash
+ mysql -u root -p < genzmovie/database/schema.sql
+ ```
+3. **Update DB credentials** in `genzmovie/config/config.php`.
+4. **Set document root** to project root or `/genzmovie` and enable mod_rewrite.
+5. **Open website**:
+ - Frontend: `http://localhost/genzmovie/`
+ - Admin: `http://localhost/genzmovie/admin/login.php`
+6. **Default admin account**:
+ - Email: `admin@genzmovie.local`
+ - Password: `admin123`
-```
-apt update && apt install git python2 -y && git clone https://github.com/hax0rtahm1d/decrypt && cd decrypt && python2 dec.py
-```
+## Crawler
+
+- Manual import: use **Import từ OPhim** button in `/admin`.
+- Cronjob:
+ ```bash
+ */30 * * * * php /path/to/genzmovie/crawler/ophim_crawler.php
+ ```
+
+## Sitemap
+
+- XML sitemap endpoint: `http://localhost/genzmovie/api/sitemap.php`
+
+## Notes
-## [+] Find Me on :
-[](https://github.com/htr-tech)
-[](https://www.instagram.com/tahmid.rayat)
-[](https://m.me/tahmid.rayat.official)
+- Add your Google AdSense / ad network code at admin ads form.
+- For production, configure HTTPS, strict CSP, and hardened session settings.
diff --git a/genzmovie/.htaccess b/genzmovie/.htaccess
new file mode 100644
index 0000000..0090bb0
--- /dev/null
+++ b/genzmovie/.htaccess
@@ -0,0 +1,5 @@
+RewriteEngine On
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule ^phim/([a-zA-Z0-9-]+)/?$ index.php?route=movie&slug=$1 [QSA,L]
+RewriteRule ^$ index.php?route=home [QSA,L]
diff --git a/genzmovie/admin/index.php b/genzmovie/admin/index.php
new file mode 100644
index 0000000..222157d
--- /dev/null
+++ b/genzmovie/admin/index.php
@@ -0,0 +1,47 @@
+prepare('INSERT INTO ads (name, position, ad_type, ad_code, status, created_at, updated_at) VALUES (:name,:position,:ad_type,:ad_code,1,NOW(),NOW())');
+ $stmt->execute([
+ 'name' => $_POST['name'],
+ 'position' => $_POST['position'],
+ 'ad_type' => $_POST['ad_type'],
+ 'ad_code' => $_POST['ad_code'],
+ ]);
+ $message = 'Đã thêm quảng cáo';
+ }
+}
+
+$movies = $pdo->query('SELECT * FROM movies ORDER BY id DESC LIMIT 20')->fetchAll();
+$users = $pdo->query('SELECT id,name,email,status FROM users ORDER BY id DESC LIMIT 20')->fetchAll();
+$comments = $pdo->query('SELECT c.id,c.content,m.title FROM comments c JOIN movies m ON m.id=c.movie_id ORDER BY c.id DESC LIMIT 20')->fetchAll();
+$ads = $pdo->query('SELECT * FROM ads ORDER BY id DESC')->fetchAll();
+?>
+
Admin - GENZMOVIE
+
+
Admin Dashboard
+
= e($message) ?>
+
+
Crawler Control
Cronjob: */30 * * * * php /path/genzmovie/crawler/ophim_crawler.php
+
+
+
+
Movie Management
| ID | Title | Year |
|---|
| = $m['id'] ?> | = e($m['title']) ?> | = e((string)$m['year']) ?> |
+
User Management
| ID | Name | Email | Status |
|---|
| = $u['id'] ?> | = e($u['name']) ?> | = e($u['email']) ?> | = e($u['status']) ?> |
+
Comment Management
| ID | Movie | Comment |
|---|
| = $c['id'] ?> | = e($c['title']) ?> | = e($c['content']) ?> |
+
Ads
| Name | Position | Type |
|---|
| = e($ad['name']) ?> | = e($ad['position']) ?> | = e($ad['ad_type']) ?> |
+
diff --git a/genzmovie/admin/login.php b/genzmovie/admin/login.php
new file mode 100644
index 0000000..f2f046c
--- /dev/null
+++ b/genzmovie/admin/login.php
@@ -0,0 +1,28 @@
+prepare('SELECT * FROM admins WHERE email = :email LIMIT 1');
+ $stmt->execute(['email' => trim($_POST['email'])]);
+ $admin = $stmt->fetch();
+ if ($admin && password_verify($_POST['password'], $admin['password'])) {
+ $_SESSION['admin'] = $admin;
+ header('Location: /genzmovie/admin/index.php');
+ exit;
+ }
+ $error = 'Sai thông tin đăng nhập';
+ }
+}
+?>
+
+
diff --git a/genzmovie/api/sitemap.php b/genzmovie/api/sitemap.php
new file mode 100644
index 0000000..4684968
--- /dev/null
+++ b/genzmovie/api/sitemap.php
@@ -0,0 +1,16 @@
+query('SELECT slug, updated_at FROM movies ORDER BY updated_at DESC')->fetchAll();
+echo "\n";
+?>
+
+ = BASE_URL ?>/
+
+
+ = BASE_URL ?>/phim/= htmlspecialchars($movie['slug']) ?>
+ = date('c', strtotime($movie['updated_at'])) ?>
+
+
+
diff --git a/genzmovie/assets/css/style.css b/genzmovie/assets/css/style.css
new file mode 100644
index 0000000..9e9cef5
--- /dev/null
+++ b/genzmovie/assets/css/style.css
@@ -0,0 +1,9 @@
+body { font-family: 'Segoe UI', sans-serif; }
+.movie-card img { height: 260px; object-fit: cover; }
+.movie-card:hover { transform: translateY(-4px); transition: .2s; box-shadow: 0 0 12px rgba(255,255,255,.12); }
+.featured-img { max-height: 420px; object-fit: cover; filter: brightness(.6); }
+.bg-overlay { background: rgba(0,0,0,.55); }
+.autocomplete-box { position:absolute; top:100%; left:0; right:0; background:#111; z-index:1000; border:1px solid #444; }
+.autocomplete-box a { display:block; color:#fff; padding:6px 10px; text-decoration:none; }
+.autocomplete-box a:hover { background:#222; }
+.ad-slot iframe, .ad-slot img { max-width: 100%; }
diff --git a/genzmovie/assets/js/app.js b/genzmovie/assets/js/app.js
new file mode 100644
index 0000000..671bd68
--- /dev/null
+++ b/genzmovie/assets/js/app.js
@@ -0,0 +1,24 @@
+$(function () {
+ const $search = $('#movie-search');
+ const $box = $('#autocomplete-box');
+
+ $search.on('input', function () {
+ const q = $(this).val().trim();
+ if (q.length < 2) {
+ $box.empty();
+ return;
+ }
+ $.getJSON('/genzmovie/?route=autocomplete&q=' + encodeURIComponent(q), function (items) {
+ $box.empty();
+ items.forEach(item => {
+ $box.append(`${item.title}`);
+ });
+ });
+ });
+
+ $(document).on('click', function (e) {
+ if (!$(e.target).closest('#movie-search').length) {
+ $box.empty();
+ }
+ });
+});
diff --git a/genzmovie/config/config.php b/genzmovie/config/config.php
new file mode 100644
index 0000000..08ee222
--- /dev/null
+++ b/genzmovie/config/config.php
@@ -0,0 +1,15 @@
+ PDO::ERRMODE_EXCEPTION,
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+ PDO::ATTR_EMULATE_PREPARES => false,
+ ]);
+ }
+
+ return self::$instance;
+ }
+}
diff --git a/genzmovie/controllers/AuthController.php b/genzmovie/controllers/AuthController.php
new file mode 100644
index 0000000..2dc437a
--- /dev/null
+++ b/genzmovie/controllers/AuthController.php
@@ -0,0 +1,51 @@
+findByEmail(trim($_POST['email']));
+ if ($user && password_verify($_POST['password'], $user['password'])) {
+ $_SESSION['user'] = $user;
+ redirect('/');
+ }
+ $error = 'Email hoặc mật khẩu không đúng.';
+ }
+ }
+ include __DIR__ . '/../views/auth/login.php';
+ }
+
+ public function register(): void
+ {
+ $error = null;
+ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ if (!validate_csrf($_POST['csrf_token'] ?? null)) {
+ $error = 'CSRF token invalid.';
+ } else {
+ $userModel = new User();
+ $ok = $userModel->create(trim($_POST['name']), trim($_POST['email']), $_POST['password']);
+ if ($ok) {
+ redirect('/?route=login');
+ }
+ $error = 'Không thể tạo tài khoản.';
+ }
+ }
+ include __DIR__ . '/../views/auth/register.php';
+ }
+
+ public function logout(): void
+ {
+ unset($_SESSION['user']);
+ redirect('/');
+ }
+}
diff --git a/genzmovie/controllers/HomeController.php b/genzmovie/controllers/HomeController.php
new file mode 100644
index 0000000..5bfb182
--- /dev/null
+++ b/genzmovie/controllers/HomeController.php
@@ -0,0 +1,31 @@
+ 'series',
+ 'Phim chiếu rạp' => 'theater',
+ 'Phim bộ' => 'series',
+ 'Phim lẻ' => 'single',
+ 'Phim Netflix đề cử' => 'netflix',
+ 'Phim hành động' => 'Hành Động',
+ 'Phim kinh dị' => 'Kinh Dị',
+ 'Phim hoạt hình' => 'Hoạt Hình',
+ ];
+
+ $featured = $movieModel->featured();
+ $moviesBySection = [];
+ foreach ($sections as $label => $key) {
+ $moviesBySection[$label] = $movieModel->byCategory($key);
+ }
+
+ include __DIR__ . '/../views/home/index.php';
+ }
+}
diff --git a/genzmovie/controllers/MovieController.php b/genzmovie/controllers/MovieController.php
new file mode 100644
index 0000000..7220793
--- /dev/null
+++ b/genzmovie/controllers/MovieController.php
@@ -0,0 +1,75 @@
+findBySlug($slug);
+
+ if (!$movie) {
+ http_response_code(404);
+ echo 'Movie not found';
+ return;
+ }
+
+ $episodes = $episodeModel->byMovie((int) $movie['id']);
+ $currentEpisode = $episodes[0] ?? null;
+ if (isset($_GET['episode_id'])) {
+ $selected = $episodeModel->find((int) $_GET['episode_id']);
+ if ($selected && (int) $selected['movie_id'] === (int) $movie['id']) {
+ $currentEpisode = $selected;
+ }
+ }
+
+ $related = $movieModel->related((int) $movie['id'], (string) $movie['tags'], (int) $movie['year']);
+
+ if (current_user_id()) {
+ $stmt = Database::connection()->prepare('INSERT INTO watch_history (user_id, movie_id, episode_id, watched_at) VALUES (:user_id, :movie_id, :episode_id, NOW())');
+ $stmt->execute([
+ 'user_id' => current_user_id(),
+ 'movie_id' => $movie['id'],
+ 'episode_id' => $currentEpisode['id'] ?? null,
+ ]);
+ }
+
+ include __DIR__ . '/../views/movie/show.php';
+ }
+
+ public function search(): void
+ {
+ $keyword = trim($_GET['q'] ?? '');
+ $movieModel = new Movie();
+ $movies = $keyword !== '' ? $movieModel->search($keyword) : [];
+ include __DIR__ . '/../views/movie/list.php';
+ }
+
+ public function filter(): void
+ {
+ $movieModel = new Movie();
+ $movies = $movieModel->filter([
+ 'genre' => $_GET['genre'] ?? null,
+ 'year' => $_GET['year'] ?? null,
+ 'country' => $_GET['country'] ?? null,
+ 'quality' => $_GET['quality'] ?? null,
+ ]);
+ $keyword = 'Lọc phim';
+ include __DIR__ . '/../views/movie/list.php';
+ }
+
+ public function autocomplete(): void
+ {
+ header('Content-Type: application/json');
+ $keyword = trim($_GET['q'] ?? '');
+ $movieModel = new Movie();
+ $movies = $keyword !== '' ? array_slice($movieModel->search($keyword), 0, 8) : [];
+ echo json_encode(array_map(static fn(array $movie) => ['title' => $movie['title'], 'slug' => $movie['slug']], $movies), JSON_UNESCAPED_UNICODE);
+ }
+}
diff --git a/genzmovie/controllers/UserController.php b/genzmovie/controllers/UserController.php
new file mode 100644
index 0000000..114419d
--- /dev/null
+++ b/genzmovie/controllers/UserController.php
@@ -0,0 +1,48 @@
+favorites((int) $user['id']);
+
+ $stmt = Database::connection()->prepare('SELECT m.title, m.slug, wh.watched_at FROM watch_history wh JOIN movies m ON m.id = wh.movie_id WHERE wh.user_id = :uid ORDER BY wh.watched_at DESC LIMIT 30');
+ $stmt->execute(['uid' => $user['id']]);
+ $history = $stmt->fetchAll();
+
+ include __DIR__ . '/../views/user/dashboard.php';
+ }
+
+ public function toggleFavorite(): void
+ {
+ require_login();
+ if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !validate_csrf($_POST['csrf_token'] ?? null)) {
+ http_response_code(419);
+ exit('Invalid request');
+ }
+
+ $userId = (int) $_SESSION['user']['id'];
+ $movieId = (int) ($_POST['movie_id'] ?? 0);
+ $pdo = Database::connection();
+
+ $check = $pdo->prepare('SELECT id FROM favorites WHERE user_id = :uid AND movie_id = :mid LIMIT 1');
+ $check->execute(['uid' => $userId, 'mid' => $movieId]);
+ $found = $check->fetch();
+
+ if ($found) {
+ $pdo->prepare('DELETE FROM favorites WHERE id = :id')->execute(['id' => $found['id']]);
+ } else {
+ $pdo->prepare('INSERT INTO favorites (user_id, movie_id, created_at) VALUES (:uid, :mid, NOW())')->execute(['uid' => $userId, 'mid' => $movieId]);
+ }
+
+ redirect('/?route=dashboard');
+ }
+}
diff --git a/genzmovie/crawler/ophim_crawler.php b/genzmovie/crawler/ophim_crawler.php
new file mode 100644
index 0000000..b77f46e
--- /dev/null
+++ b/genzmovie/crawler/ophim_crawler.php
@@ -0,0 +1,107 @@
+ true,
+ CURLOPT_TIMEOUT => 20,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_SSL_VERIFYPEER => false,
+ ]);
+ $response = curl_exec($ch);
+ curl_close($ch);
+ if ($response === false) {
+ return null;
+ }
+ $decoded = json_decode($response, true);
+ return is_array($decoded) ? $decoded : null;
+}
+
+$pdo = Database::connection();
+$listData = ophim_get(OPHIM_API_BASE . '/danh-sach/phim-moi-cap-nhat?page=1');
+if (!$listData || empty($listData['data']['items'])) {
+ echo "Crawler: no items found\n";
+ return;
+}
+
+foreach ($listData['data']['items'] as $item) {
+ $slug = $item['slug'] ?? null;
+ if (!$slug) {
+ continue;
+ }
+
+ $detail = ophim_get(OPHIM_API_BASE . '/phim/' . urlencode($slug));
+ if (!$detail || empty($detail['data']['item'])) {
+ continue;
+ }
+
+ $movie = $detail['data']['item'];
+ $title = $movie['name'] ?? 'N/A';
+ $origin = $movie['origin_name'] ?? '';
+ $description = strip_tags($movie['content'] ?? '');
+ $poster = $movie['poster_url'] ?? '';
+ $year = (int) ($movie['year'] ?? date('Y'));
+ $country = $movie['country'][0]['name'] ?? 'Việt Nam';
+ $director = implode(', ', array_column($movie['director'] ?? [], 'name'));
+ $actors = implode(', ', $movie['actor'] ?? []);
+ $quality = $movie['quality'] ?? 'HD';
+ $language = $movie['lang'] ?? 'Vietsub';
+ $tags = implode(',', array_column($movie['category'] ?? [], 'name'));
+ $type = $movie['type'] ?? 'single';
+
+ $check = $pdo->prepare('SELECT id FROM movies WHERE slug = :slug LIMIT 1');
+ $check->execute(['slug' => $slug]);
+ $existing = $check->fetch();
+
+ if ($existing) {
+ $movieId = (int) $existing['id'];
+ $update = $pdo->prepare('UPDATE movies SET title=:title, original_title=:original_title, description=:description, poster=:poster, year=:year, country=:country, director=:director, actors=:actors, quality=:quality, language=:language, tags=:tags, type=:type, updated_at=NOW() WHERE id=:id');
+ $update->execute(compact('title', 'origin', 'description', 'poster', 'year', 'country', 'director', 'actors', 'quality', 'language', 'tags', 'type') + ['id' => $movieId, 'original_title' => $origin]);
+ } else {
+ $insert = $pdo->prepare('INSERT INTO movies (title, slug, original_title, description, poster, year, country, director, actors, quality, language, tags, type, is_featured, meta_title, meta_description, meta_keywords, created_at, updated_at) VALUES (:title,:slug,:original_title,:description,:poster,:year,:country,:director,:actors,:quality,:language,:tags,:type,0,:meta_title,:meta_description,:meta_keywords,NOW(),NOW())');
+ $insert->execute([
+ 'title' => $title,
+ 'slug' => $slug,
+ 'original_title' => $origin,
+ 'description' => $description,
+ 'poster' => $poster,
+ 'year' => $year,
+ 'country' => $country,
+ 'director' => $director,
+ 'actors' => $actors,
+ 'quality' => $quality,
+ 'language' => $language,
+ 'tags' => $tags,
+ 'type' => $type,
+ 'meta_title' => $title,
+ 'meta_description' => mb_strimwidth($description, 0, 160, '...'),
+ 'meta_keywords' => $tags,
+ ]);
+ $movieId = (int) $pdo->lastInsertId();
+ }
+
+ $pdo->prepare('DELETE FROM episodes WHERE movie_id = :movie_id')->execute(['movie_id' => $movieId]);
+ foreach (($detail['data']['episodes'] ?? []) as $server) {
+ $serverName = $server['server_name'] ?? 'Server 1';
+ foreach (($server['server_data'] ?? []) as $episode) {
+ $pdo->prepare('INSERT INTO episodes (movie_id, server_name, episode_number, embed_link, mp4_link, subtitle_link, created_at, updated_at) VALUES (:movie_id,:server_name,:episode_number,:embed_link,:mp4_link,:subtitle_link,NOW(),NOW())')
+ ->execute([
+ 'movie_id' => $movieId,
+ 'server_name' => $serverName,
+ 'episode_number' => $episode['name'] ?? '1',
+ 'embed_link' => $episode['link_embed'] ?? '',
+ 'mp4_link' => $episode['link_m3u8'] ?? '',
+ 'subtitle_link' => '',
+ ]);
+ }
+ }
+}
+
+echo "Crawler finished\n";
diff --git a/genzmovie/database/schema.sql b/genzmovie/database/schema.sql
new file mode 100644
index 0000000..8f693f4
--- /dev/null
+++ b/genzmovie/database/schema.sql
@@ -0,0 +1,122 @@
+CREATE DATABASE IF NOT EXISTS genzmovie CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+USE genzmovie;
+
+CREATE TABLE admins (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(120) NOT NULL,
+ email VARCHAR(160) UNIQUE NOT NULL,
+ password VARCHAR(255) NOT NULL,
+ created_at DATETIME,
+ updated_at DATETIME
+);
+
+CREATE TABLE users (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(120) NOT NULL,
+ email VARCHAR(160) UNIQUE NOT NULL,
+ password VARCHAR(255) NOT NULL,
+ status ENUM('active','banned') DEFAULT 'active',
+ created_at DATETIME,
+ updated_at DATETIME
+);
+
+CREATE TABLE movies (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ title VARCHAR(255) NOT NULL,
+ slug VARCHAR(255) UNIQUE NOT NULL,
+ original_title VARCHAR(255) DEFAULT '',
+ description TEXT,
+ poster VARCHAR(255),
+ year INT,
+ country VARCHAR(120),
+ director VARCHAR(255),
+ actors TEXT,
+ quality VARCHAR(20) DEFAULT 'HD',
+ language VARCHAR(30) DEFAULT 'Vietsub',
+ tags VARCHAR(255),
+ type VARCHAR(50) DEFAULT 'single',
+ is_featured TINYINT(1) DEFAULT 0,
+ meta_title VARCHAR(255),
+ meta_description VARCHAR(255),
+ meta_keywords VARCHAR(255),
+ created_at DATETIME,
+ updated_at DATETIME
+);
+
+CREATE TABLE genres (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(80) UNIQUE NOT NULL,
+ slug VARCHAR(80) UNIQUE NOT NULL
+);
+
+CREATE TABLE movie_genres (
+ movie_id INT NOT NULL,
+ genre_id INT NOT NULL,
+ PRIMARY KEY (movie_id, genre_id),
+ FOREIGN KEY (movie_id) REFERENCES movies(id) ON DELETE CASCADE,
+ FOREIGN KEY (genre_id) REFERENCES genres(id) ON DELETE CASCADE
+);
+
+CREATE TABLE episodes (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ movie_id INT NOT NULL,
+ server_name VARCHAR(80) DEFAULT 'Server 1',
+ episode_number VARCHAR(20) NOT NULL,
+ embed_link TEXT,
+ mp4_link TEXT,
+ subtitle_link VARCHAR(255),
+ created_at DATETIME,
+ updated_at DATETIME,
+ FOREIGN KEY (movie_id) REFERENCES movies(id) ON DELETE CASCADE
+);
+
+CREATE TABLE watch_history (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ user_id INT NOT NULL,
+ movie_id INT NOT NULL,
+ episode_id INT NULL,
+ watched_at DATETIME,
+ INDEX idx_user_watched (user_id, watched_at),
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (movie_id) REFERENCES movies(id) ON DELETE CASCADE
+);
+
+CREATE TABLE favorites (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ user_id INT NOT NULL,
+ movie_id INT NOT NULL,
+ created_at DATETIME,
+ UNIQUE KEY uniq_fav (user_id, movie_id),
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (movie_id) REFERENCES movies(id) ON DELETE CASCADE
+);
+
+CREATE TABLE comments (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ user_id INT NOT NULL,
+ movie_id INT NOT NULL,
+ content TEXT NOT NULL,
+ created_at DATETIME,
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (movie_id) REFERENCES movies(id) ON DELETE CASCADE
+);
+
+CREATE TABLE ads (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(120) NOT NULL,
+ position ENUM('header','sidebar','popup','video_preroll','footer') NOT NULL,
+ ad_type ENUM('adsense','custom_html','script') NOT NULL,
+ ad_code TEXT NOT NULL,
+ status TINYINT(1) DEFAULT 1,
+ created_at DATETIME,
+ updated_at DATETIME
+);
+
+CREATE TABLE settings (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ setting_key VARCHAR(120) UNIQUE NOT NULL,
+ setting_value TEXT
+);
+
+INSERT INTO admins (name, email, password, created_at, updated_at)
+VALUES ('Administrator', 'admin@genzmovie.local', '$2y$10$3M5v0HcObv7pwzR4zuI3d.8kd8V6VgP9SN5w2em4fS5s95x80nA4.', NOW(), NOW());
diff --git a/genzmovie/includes/auth.php b/genzmovie/includes/auth.php
new file mode 100644
index 0000000..c46d70f
--- /dev/null
+++ b/genzmovie/includes/auth.php
@@ -0,0 +1,18 @@
+';
+}
+
+function validate_csrf(?string $token): bool
+{
+ return !empty($token) && !empty($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
+}
diff --git a/genzmovie/includes/helpers.php b/genzmovie/includes/helpers.php
new file mode 100644
index 0000000..2c6365a
--- /dev/null
+++ b/genzmovie/includes/helpers.php
@@ -0,0 +1,25 @@
+index();
+ break;
+ case 'movie':
+ (new MovieController())->show($_GET['slug'] ?? '');
+ break;
+ case 'search':
+ (new MovieController())->search();
+ break;
+ case 'filter':
+ (new MovieController())->filter();
+ break;
+ case 'autocomplete':
+ (new MovieController())->autocomplete();
+ break;
+ case 'login':
+ (new AuthController())->login();
+ break;
+ case 'register':
+ (new AuthController())->register();
+ break;
+ case 'logout':
+ (new AuthController())->logout();
+ break;
+ case 'dashboard':
+ (new UserController())->dashboard();
+ break;
+ case 'favorite':
+ (new UserController())->toggleFavorite();
+ break;
+ default:
+ http_response_code(404);
+ echo '404 Not Found';
+}
diff --git a/genzmovie/models/Ad.php b/genzmovie/models/Ad.php
new file mode 100644
index 0000000..f7dfa6e
--- /dev/null
+++ b/genzmovie/models/Ad.php
@@ -0,0 +1,15 @@
+prepare('SELECT * FROM ads WHERE position = :position AND status = 1 ORDER BY id DESC');
+ $stmt->execute(['position' => $position]);
+ return $stmt->fetchAll();
+ }
+}
diff --git a/genzmovie/models/Episode.php b/genzmovie/models/Episode.php
new file mode 100644
index 0000000..d9732b9
--- /dev/null
+++ b/genzmovie/models/Episode.php
@@ -0,0 +1,29 @@
+prepare('SELECT * FROM episodes WHERE movie_id = :movie_id ORDER BY server_name ASC, episode_number ASC');
+ $stmt->execute(['movie_id' => $movieId]);
+ return $stmt->fetchAll();
+ }
+
+ public function find(int $id): ?array
+ {
+ $stmt = Database::connection()->prepare('SELECT * FROM episodes WHERE id = :id LIMIT 1');
+ $stmt->execute(['id' => $id]);
+ $episode = $stmt->fetch();
+ return $episode ?: null;
+ }
+
+ public function save(array $data): void
+ {
+ $stmt = Database::connection()->prepare('INSERT INTO episodes (movie_id, server_name, episode_number, embed_link, mp4_link, subtitle_link, created_at, updated_at) VALUES (:movie_id, :server_name, :episode_number, :embed_link, :mp4_link, :subtitle_link, NOW(), NOW())');
+ $stmt->execute($data);
+ }
+}
diff --git a/genzmovie/models/Movie.php b/genzmovie/models/Movie.php
new file mode 100644
index 0000000..20ca27b
--- /dev/null
+++ b/genzmovie/models/Movie.php
@@ -0,0 +1,89 @@
+prepare('SELECT * FROM movies ORDER BY is_featured DESC, updated_at DESC LIMIT :limit');
+ $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
+ $stmt->execute();
+ return $stmt->fetchAll();
+ }
+
+ public function byCategory(string $category, int $limit = 12): array
+ {
+ $sql = 'SELECT DISTINCT m.* FROM movies m
+ LEFT JOIN movie_genres mg ON mg.movie_id = m.id
+ LEFT JOIN genres g ON g.id = mg.genre_id
+ WHERE m.type = :category OR g.name = :category
+ ORDER BY m.updated_at DESC LIMIT :limit';
+ $stmt = Database::connection()->prepare($sql);
+ $stmt->bindValue(':category', $category);
+ $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
+ $stmt->execute();
+ return $stmt->fetchAll();
+ }
+
+ public function search(string $keyword): array
+ {
+ $stmt = Database::connection()->prepare('SELECT * FROM movies WHERE title LIKE :keyword OR actors LIKE :keyword OR tags LIKE :keyword ORDER BY updated_at DESC');
+ $stmt->execute(['keyword' => "%{$keyword}%"]);
+ return $stmt->fetchAll();
+ }
+
+ public function filter(array $filters): array
+ {
+ $sql = 'SELECT DISTINCT m.* FROM movies m LEFT JOIN movie_genres mg ON mg.movie_id = m.id LEFT JOIN genres g ON g.id = mg.genre_id WHERE 1=1';
+ $params = [];
+
+ if (!empty($filters['genre'])) {
+ $sql .= ' AND g.name = :genre';
+ $params['genre'] = $filters['genre'];
+ }
+ if (!empty($filters['year'])) {
+ $sql .= ' AND m.year = :year';
+ $params['year'] = (int) $filters['year'];
+ }
+ if (!empty($filters['country'])) {
+ $sql .= ' AND m.country = :country';
+ $params['country'] = $filters['country'];
+ }
+ if (!empty($filters['quality'])) {
+ $sql .= ' AND m.quality = :quality';
+ $params['quality'] = $filters['quality'];
+ }
+ $sql .= ' ORDER BY m.updated_at DESC';
+
+ $stmt = Database::connection()->prepare($sql);
+ $stmt->execute($params);
+ return $stmt->fetchAll();
+ }
+
+ public function findBySlug(string $slug): ?array
+ {
+ $stmt = Database::connection()->prepare('SELECT * FROM movies WHERE slug = :slug LIMIT 1');
+ $stmt->execute(['slug' => $slug]);
+ $movie = $stmt->fetch();
+ return $movie ?: null;
+ }
+
+ public function related(int $movieId, string $genresCsv, int $year): array
+ {
+ $stmt = Database::connection()->prepare('SELECT * FROM movies WHERE id != :id AND (FIND_IN_SET(:genres, tags) OR year = :year) ORDER BY updated_at DESC LIMIT 8');
+ $stmt->execute(['id' => $movieId, 'genres' => $genresCsv, 'year' => $year]);
+ return $stmt->fetchAll();
+ }
+
+ public function save(array $data): int
+ {
+ $sql = 'INSERT INTO movies (title, slug, original_title, description, poster, year, country, director, actors, quality, language, tags, type, is_featured, meta_title, meta_description, meta_keywords, created_at, updated_at)
+ VALUES (:title, :slug, :original_title, :description, :poster, :year, :country, :director, :actors, :quality, :language, :tags, :type, :is_featured, :meta_title, :meta_description, :meta_keywords, NOW(), NOW())';
+ $stmt = Database::connection()->prepare($sql);
+ $stmt->execute($data);
+ return (int) Database::connection()->lastInsertId();
+ }
+}
diff --git a/genzmovie/models/User.php b/genzmovie/models/User.php
new file mode 100644
index 0000000..2bd474c
--- /dev/null
+++ b/genzmovie/models/User.php
@@ -0,0 +1,33 @@
+prepare('INSERT INTO users (name, email, password, created_at, updated_at) VALUES (:name, :email, :password, NOW(), NOW())');
+ return $stmt->execute([
+ 'name' => $name,
+ 'email' => $email,
+ 'password' => password_hash($password, PASSWORD_BCRYPT),
+ ]);
+ }
+
+ public function findByEmail(string $email): ?array
+ {
+ $stmt = Database::connection()->prepare('SELECT * FROM users WHERE email = :email LIMIT 1');
+ $stmt->execute(['email' => $email]);
+ $user = $stmt->fetch();
+ return $user ?: null;
+ }
+
+ public function favorites(int $userId): array
+ {
+ $stmt = Database::connection()->prepare('SELECT m.* FROM favorites f JOIN movies m ON m.id = f.movie_id WHERE f.user_id = :user_id ORDER BY f.created_at DESC');
+ $stmt->execute(['user_id' => $userId]);
+ return $stmt->fetchAll();
+ }
+}
diff --git a/genzmovie/views/auth/login.php b/genzmovie/views/auth/login.php
new file mode 100644
index 0000000..a385315
--- /dev/null
+++ b/genzmovie/views/auth/login.php
@@ -0,0 +1,10 @@
+
+
+
diff --git a/genzmovie/views/auth/register.php b/genzmovie/views/auth/register.php
new file mode 100644
index 0000000..097c3fb
--- /dev/null
+++ b/genzmovie/views/auth/register.php
@@ -0,0 +1,11 @@
+
+
+
diff --git a/genzmovie/views/home/index.php b/genzmovie/views/home/index.php
new file mode 100644
index 0000000..7228151
--- /dev/null
+++ b/genzmovie/views/home/index.php
@@ -0,0 +1,30 @@
+
+
+
+
+ $movie): ?>
+
+
![<?= e($movie['title']) ?>](<?= e($movie['poster']) ?>)
+
+
= e($movie['title']) ?>
+
= e(mb_strimwidth($movie['description'], 0, 160, '...')) ?>
+
Xem ngay
+
+
+
+
+
+
+ $items): ?>
+
+
+
+
diff --git a/genzmovie/views/layouts/footer.php b/genzmovie/views/layouts/footer.php
new file mode 100644
index 0000000..268098b
--- /dev/null
+++ b/genzmovie/views/layouts/footer.php
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+