|
Server : LiteSpeed System : Linux terra.hostitbro.com 5.14.0-611.54.3.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Thu May 7 16:31:24 EDT 2026 x86_64 User : outerorb ( 1091) PHP Version : 8.1.34 Disable Function : mail Directory : /home2/outerorb/.trash/mplanner/ | |
|
Path: /home2/outerorb/.trash/mplanner/index.php
Size: 36.97 KB
Permissions: 0644
<?php
/*
index.php - OTT Weekend Content Planner (Server-side OpenAI + PHP + SQLite)
Single-file app (PHP + HTML + CSS + JS)
- Uses OpenAI Responses API (server-side) with provided API key
- Stores users & history in SQLite (mplan_lib.sqlite)
- Endpoints:
* signup, login, logout (session)
* generate (calls OpenAI /v1/responses) -> returns strict parsed JSON recommendations
* save_search (store results in history)
* get_history (retrieve user's history)
- No local/mocked suggestions: on AI failure, shows explicit error
- All IDs/classes prefixed with mplan_lib
*/
/* ---------------- CONFIG ---------------- */
session_start();
// Put your OpenAI key here (user-provided)
define('MPLAN_LIB_OPENAI_KEY', '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');
// OpenAI endpoint
define('MPLAN_LIB_OPENAI_ENDPOINT', 'https://api.openai.com/v1/responses');
// SQLite DB file (in same directory)
define('MPLAN_LIB_DB', __DIR__ . '/mplan_lib.sqlite');
/* ---------------- DB SETUP ---------------- */
function mplan_lib_db() {
$dbfile = MPLAN_LIB_DB;
$init = !file_exists($dbfile);
$pdo = new PDO('sqlite:' . $dbfile);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
if ($init) {
$pdo->exec("PRAGMA journal_mode = WAL;");
$pdo->exec("CREATE TABLE users (
username TEXT PRIMARY KEY,
password_hash TEXT NOT NULL
);");
$pdo->exec("CREATE TABLE history (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
title TEXT NOT NULL,
created_at TEXT NOT NULL,
prefs_json TEXT NOT NULL,
results_json TEXT NOT NULL,
FOREIGN KEY(username) REFERENCES users(username)
);");
}
return $pdo;
}
/* ---------------- Helpers ---------------- */
function mplan_lib_user() {
return $_SESSION['mplan_lib_user'] ?? null;
}
function mplan_lib_json_header() {
header('Content-Type: application/json; charset=utf-8');
}
function mplan_lib_now_iso() {
return gmdate('c');
}
/* ---------------- AJAX / ACTIONS ---------------- */
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['mplan_lib_action'] ?? '';
// SIGNUP
if ($action === 'signup') {
mplan_lib_json_header();
$username = trim($_POST['mplan_lib_username'] ?? '');
$password = $_POST['mplan_lib_password'] ?? '';
$confirm = $_POST['mplan_lib_confirm'] ?? '';
if ($username === '' || $password === '' || $confirm === '') {
echo json_encode(['ok'=>false,'error'=>'All fields required']); exit;
}
if (!preg_match('/^[A-Za-z0-9._-]{3,32}$/', $username)) {
echo json_encode(['ok'=>false,'error'=>'Username invalid (3–32 chars: letters/numbers/_ . -)']); exit;
}
if ($password !== $confirm) {
echo json_encode(['ok'=>false,'error'=>'Passwords do not match']); exit;
}
$pdo = mplan_lib_db();
$stmt = $pdo->prepare('SELECT 1 FROM users WHERE username = ?');
$stmt->execute([$username]);
if ($stmt->fetch()) {
echo json_encode(['ok'=>false,'error'=>'Username already exists']); exit;
}
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)');
$stmt->execute([$username, $hash]);
$_SESSION['mplan_lib_user'] = $username;
echo json_encode(['ok'=>true,'user'=>$username]); exit;
}
// LOGIN
if ($action === 'login') {
mplan_lib_json_header();
$username = trim($_POST['mplan_lib_username'] ?? '');
$password = $_POST['mplan_lib_password'] ?? '';
if ($username === '' || $password === '') { echo json_encode(['ok'=>false,'error'=>'All fields required']); exit; }
$pdo = mplan_lib_db();
$stmt = $pdo->prepare('SELECT password_hash FROM users WHERE username = ?');
$stmt->execute([$username]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row || !password_verify($password, $row['password_hash'])) {
echo json_encode(['ok'=>false,'error'=>'Invalid credentials']); exit;
}
$_SESSION['mplan_lib_user'] = $username;
echo json_encode(['ok'=>true,'user'=>$username]); exit;
}
// LOGOUT
if ($action === 'logout') {
mplan_lib_json_header();
session_destroy();
echo json_encode(['ok'=>true]); exit;
}
// GENERATE (call OpenAI Responses API server-side)
if ($action === 'generate') {
mplan_lib_json_header();
// Accept preferences (allow generation even if not logged in)
$genre = trim($_POST['mplan_lib_genre'] ?? '');
$mood = trim($_POST['mplan_lib_mood'] ?? '');
$platform = trim($_POST['mplan_lib_platform'] ?? '');
$duration = trim($_POST['mplan_lib_duration'] ?? '');
if ($genre === '' || $mood === '' || $platform === '' || $duration === '') {
echo json_encode(['ok'=>false,'error'=>'All fields are required']); exit;
}
// Build strict prompt instructing JSON-only output
$system = "You are an OTT weekend content recommendation assistant for users in India. Return STRICT JSON only with this exact schema and nothing else:
{
\"recommendations\": [
{
\"title\": \"string\",
\"type\": \"movie\" | \"series\",
\"imdb\": \"string or N/A\",
\"year\": \"optional year number or empty string\",
\"synopsis\": \"short <= 40 words\",
\"trailer\": \"https URL or empty string\",
\"ott\": \"https URL or empty string\",
\"poster\": \"https URL or empty string\",
\"platforms\": [\"Netflix\",\"Prime Video\",\"Hotstar\",\"JioCinema\",\"Sony LIV\",\"ZEE5\",\"Apple TV+\"]
}
]
}
Rules:
- Return exactly 5 recommendation objects.
- Ensure titles are realistic/popular and likely available in India.
- If you cannot provide a valid URL for trailer or ott, return an empty string for that field and include platform names in platforms array.
- No extra commentary or text outside the JSON.";
$user = "User preferences:
Genre: {$genre}
Mood: {$mood}
Preferred platform: {$platform}
Duration: {$duration}
Return the JSON now.";
// Prepare payload according to OpenAI Responses API
$payload = [
"model" => "gpt-4o-mini",
"input" => $system . "\n\n" . $user,
"temperature" => 0.6,
"max_output_tokens" => 1200
];
// Call OpenAI via cURL
$ch = curl_init(MPLAN_LIB_OPENAI_ENDPOINT);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"Authorization: Bearer " . MPLAN_LIB_OPENAI_KEY
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$resp = curl_exec($ch);
$err = curl_error($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($err || !$resp || $http >= 400) {
// return explicit error (no fallback)
$msg = $err ?: ('OpenAI HTTP ' . $http);
echo json_encode(['ok'=>false,'error'=>'AI service unavailable: ' . $msg]); exit;
}
// Parse OpenAI response. Responses API returns 'output' array. Extract textual content.
$j = json_decode($resp, true);
$text = '';
if (isset($j['output']) && is_array($j['output'])) {
foreach ($j['output'] as $part) {
if (isset($part['content']) && is_array($part['content'])) {
foreach ($part['content'] as $c) {
// 'type' may be 'output_text' with 'text', or content may have 'text' directly
if (isset($c['text'])) $text .= $c['text'];
elseif (isset($c['type']) && $c['type'] === 'output_text' && isset($c['text'])) $text .= $c['text'];
// Some shapes may have 'content' => ['type'=>'output_text','text'=>...]
}
}
}
}
// Fallback extraction: some API responses include 'output_text' top-level
if ($text === '' && isset($j['output_text'])) {
$text = $j['output_text'];
}
if ($text === '' && isset($j['choices']) && is_array($j['choices'])) {
// older-style choices
foreach ($j['choices'] as $chc) {
if (isset($chc['text'])) $text .= $chc['text'];
elseif (isset($chc['message']['content'])) $text .= $chc['message']['content'];
}
}
if ($text === '') {
// As a last resort try to json_encode the whole response body and search for JSON object
$raw = $resp;
$m = [];
if (preg_match('/\{.*"recommendations".*\}/s', $raw, $m)) {
$text = $m[0];
}
}
if ($text === '') {
echo json_encode(['ok'=>false,'error'=>'AI returned empty or unparsable response']); exit;
}
// Attempt to parse JSON from text
$parsed = json_decode($text, true);
if (!$parsed) {
// try to extract first JSON object substring
if (preg_match('/\{[\s\S]*\}/', $text, $m)) {
$candidate = $m[0];
$parsed = json_decode($candidate, true);
}
}
if (!$parsed || !isset($parsed['recommendations']) || !is_array($parsed['recommendations'])) {
echo json_encode(['ok'=>false,'error'=>'AI returned invalid JSON structure']); exit;
}
// Normalize recommendations: ensure each has required keys
$results = [];
foreach ($parsed['recommendations'] as $rec) {
if (!isset($rec['title']) || trim($rec['title']) === '') continue;
$results[] = [
'title' => $rec['title'],
'type' => $rec['type'] ?? 'movie',
'imdb' => $rec['imdb'] ?? 'N/A',
'year' => $rec['year'] ?? '',
'synopsis' => $rec['synopsis'] ?? '',
'trailer' => $rec['trailer'] ?? '',
'ott' => $rec['ott'] ?? '',
'poster' => $rec['poster'] ?? '',
'platforms'=> is_array($rec['platforms']) ? $rec['platforms'] : []
];
}
// Ensure we have at least 1 result; otherwise error
if (count($results) < 1) {
echo json_encode(['ok'=>false,'error'=>'AI returned no valid recommendations']); exit;
}
// Return parsed recommendations to client
echo json_encode(['ok'=>true,'recommendations'=>$results]); exit;
}
// SAVE SEARCH - requires login
if ($action === 'save_search') {
mplan_lib_json_header();
$user = mplan_lib_user();
if (!$user) { echo json_encode(['ok'=>false,'error'=>'Not logged in']); exit; }
$title = trim($_POST['mplan_lib_title'] ?? '');
$prefs = json_decode($_POST['mplan_lib_prefs'] ?? 'null', true);
$results = json_decode($_POST['mplan_lib_results'] ?? 'null', true);
if ($title === '' || !is_array($prefs) || !is_array($results)) { echo json_encode(['ok'=>false,'error'=>'Invalid payload']); exit; }
$pdo = mplan_lib_db();
$id = bin2hex(random_bytes(8));
$stmt = $pdo->prepare('INSERT INTO history (id, username, title, created_at, prefs_json, results_json) VALUES (?, ?, ?, ?, ?, ?)');
$stmt->execute([$id, $user, $title, mplan_lib_now_iso(), json_encode($prefs), json_encode($results)]);
echo json_encode(['ok'=>true,'entry'=>['id'=>$id,'title'=>$title,'date'=>mplan_lib_now_iso()]]); exit;
}
// GET HISTORY - requires login
if ($action === 'get_history') {
mplan_lib_json_header();
$user = mplan_lib_user();
if (!$user) { echo json_encode(['ok'=>true,'history'=>[]]); exit; }
$pdo = mplan_lib_db();
$stmt = $pdo->prepare('SELECT id, title, created_at, prefs_json, results_json FROM history WHERE username = ? ORDER BY created_at DESC');
$stmt->execute([$user]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$hist = [];
foreach ($rows as $r) {
$hist[] = [
'id' => $r['id'],
'title' => $r['title'],
'date' => $r['created_at'],
'prefs' => json_decode($r['prefs_json'], true),
'results' => json_decode($r['results_json'], true)
];
}
echo json_encode(['ok'=>true,'history'=>$hist]); exit;
}
// Unknown action
mplan_lib_json_header();
echo json_encode(['ok'=>false,'error'=>'Unknown action']); exit;
}
/* ---------------- END ACTIONS ---------------- */
/* ---------------- PAGE STATE ---------------- */
$current_user = mplan_lib_user();
$history_items = [];
if ($current_user) {
$pdo = mplan_lib_db();
$stmt = $pdo->prepare('SELECT id, title, created_at FROM history WHERE username = ? ORDER BY created_at DESC');
$stmt->execute([$current_user]);
$history_items = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/* ---------------- HTML / UI ---------------- */
?><!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>OTT Weekend Content Planner • mplan_lib</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
:root{
--mplan_lib-bg:#071019; --mplan_lib-card:#0f1720; --mplan_lib-accent:#3ee7c8;
--mplan_lib-text:#e6eef2; --mplan_lib-sub:#9fb4c6; --mplan_lib-border:#203241;
--mplan_lib-error:#ffb4b4; --mplan_lib-good:#9ff7d7;
}
body.mplan_lib-body{margin:0;background:radial-gradient(1200px 600px at 10% -10%, #083a50 0%, #071019 60%),var(--mplan_lib-bg);color:var(--mplan_lib-text);font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}
.mplan_lib-wrap{max-width:1080px;margin:28px auto;padding:0 16px}
.mplan_lib-card{background:linear-gradient(180deg,rgba(255,255,255,0.02),rgba(255,255,255,0)),var(--mplan_lib-card);border:1px solid var(--mplan_lib-border);border-radius:12px;padding:14px;box-shadow:0 8px 28px rgba(0,0,0,.35)}
.mplan_lib-header{display:flex;gap:12px;align-items:center;margin-bottom:14px}
.mplan_lib-logo{width:48px;height:48px;border-radius:12px;background:conic-gradient(from 180deg,#52d2ff,#3ee7c8,#a78bfa,#52d2ff);box-shadow:0 6px 20px rgba(34,170,220,.12)}
.mplan_lib-title{font-weight:800;font-size:clamp(18px,3.4vw,26px)}
.mplan_lib-sub{color:var(--mplan_lib-sub);font-size:13px}
.mplan_lib-grid{display:grid;grid-template-columns:320px 1fr;gap:14px}
.mplan_lib-panel{display:flex;flex-direction:column;gap:10px}
.mplan_lib-label{font-size:12px;color:var(--mplan_lib-sub);text-transform:uppercase;letter-spacing:.08em}
.mplan_lib-select,.mplan_lib-input,.mplan_lib-button{background:#0c1116;color:var(--mplan_lib-text);border:1px solid var(--mplan_lib-border);border-radius:8px;padding:9px 10px;font-size:14px;outline:none}
.mplan_lib-select:focus,.mplan_lib-input:focus{box-shadow:0 0 0 4px rgba(62,231,200,.08);border-color:var(--mplan_lib-accent)}
.mplan_lib-button{cursor:pointer;font-weight:700;border-color:#204a5b}
.mplan_lib-button[disabled]{opacity:.6;cursor:not-allowed}
.mplan_lib-note{color:var(--mplan_lib-sub);font-size:13px}
.mplan_lib-loginTabs{display:flex;gap:8px}
.mplan_lib-tabBtn{padding:8px 10px;border-radius:8px;border:1px solid var(--mplan_lib-border);background:#0a0f14;color:var(--mplan_lib-text);cursor:pointer}
.mplan_lib-tabBtn.mplan_lib-active{border-color:var(--mplan_lib-accent)}
.mplan_lib-historyList{display:flex;flex-direction:column;gap:8px;max-height:520px;overflow:auto}
.mplan_lib-historyItem{display:flex;justify-content:space-between;align-items:center;gap:8px;border:1px solid var(--mplan_lib-border);border-radius:8px;padding:10px;background:#08121a;cursor:pointer}
.mplan_lib-historyItem:hover{border-color:var(--mplan_lib-accent)}
.mplan_lib-date{color:var(--mplan_lib-sub);font-size:12px}
.mplan_lib-results{display:grid;gap:12px}
.mplan_lib-cardItem{display:grid;grid-template-columns:92px 1fr;gap:12px;padding:12px;border-radius:8px;background:linear-gradient(180deg,rgba(255,255,255,0.01),transparent)}
.mplan_lib-poster{width:92px;height:128px;border-radius:8px;background:#08121a;object-fit:cover;border:1px solid var(--mplan_lib-border)}
.mplan_lib-itemTitle{font-weight:700;font-size:16px;display:flex;gap:8px;align-items:center}
.mplan_lib-chip{font-size:12px;border-radius:999px;padding:4px 8px;border:1px solid var(--mplan_lib-border);color:var(--mplan_lib-sub)}
.mplan_lib-imdb{color:#ffd26a;font-weight:700}
.mplan_lib-meta{font-size:13px;color:var(--mplan_lib-sub);margin-top:4px}
.mplan_lib-syn{margin-top:8px;line-height:1.45;font-size:14px;color:var(--mplan_lib-text)}
.mplan_lib-links{margin-top:10px;display:flex;flex-wrap:wrap;gap:8px}
.mplan_lib-link{font-size:13px;border-radius:8px;padding:8px 10px;border:1px solid var(--mplan_lib-border);text-decoration:none;color:var(--mplan_lib-text);background:#051219}
.mplan_lib-link:hover{border-color:var(--mplan_lib-accent)}
.mplan_lib-topbar{display:flex;justify-content:space-between;align-items:center;gap:10px}
@media (max-width:980px){.mplan_lib-grid{grid-template-columns:1fr}}
@media (max-width:680px){.mplan_lib-cardItem{grid-template-columns:72px 1fr}.mplan_lib-poster{width:72px;height:108px}}
</style>
</head>
<body class="mplan_lib-body">
<div class="mplan_lib-wrap">
<header class="mplan_lib-header">
<div class="mplan_lib-logo" aria-hidden="true"></div>
<div>
<div class="mplan_lib-title">OTT Weekend Content Planner</div>
<div class="mplan_lib-sub">Server-side OpenAI (gpt-4o-mini) • Save searches to your account • Strict JSON output</div>
</div>
</header>
<?php if (!$current_user): ?>
<!-- AUTH PANEL -->
<section class="mplan_lib-card mplan_lib-panel">
<div class="mplan_lib-topbar">
<div class="mplan_lib-chip">Create account to save your plans</div>
<div class="mplan_lib-loginTabs">
<button id="mplan_lib_tab_login" class="mplan_lib-tabBtn mplan_lib-active">Login</button>
<button id="mplan_lib_tab_signup" class="mplan_lib-tabBtn">Sign Up</button>
</div>
</div>
<div id="mplan_lib_auth_msg" class="mplan_lib-note" style="min-height:22px"></div>
<div id="mplan_lib_login_form">
<label class="mplan_lib-label">Login</label>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
<input id="mplan_lib_login_username" class="mplan_lib-input" placeholder="Username">
<input id="mplan_lib_login_password" class="mplan_lib-input" type="password" placeholder="Password">
</div>
<button id="mplan_lib_login_btn" class="mplan_lib-button" style="margin-top:8px">Login</button>
</div>
<div id="mplan_lib_signup_form" style="display:none">
<label class="mplan_lib-label">Sign Up</label>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
<input id="mplan_lib_signup_username" class="mplan_lib-input" placeholder="Choose username">
<input id="mplan_lib_signup_password" class="mplan_lib-input" type="password" placeholder="Password">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:6px">
<input id="mplan_lib_signup_confirm" class="mplan_lib-input" type="password" placeholder="Confirm password">
<div class="mplan_lib-note">3–32 chars: letters/numbers/_ . -</div>
</div>
<button id="mplan_lib_signup_btn" class="mplan_lib-button" style="margin-top:8px">Create account & continue</button>
</div>
<div class="mplan_lib-note">You may try the planner below even without an account; saving requires login.</div>
</section>
<!-- Planner (try without login) -->
<section class="mplan_lib-card mplan_lib-panel" style="margin-top:12px">
<div class="mplan_lib-topbar">
<div class="mplan_lib-chip">Try Planner (no account required)</div>
<div class="mplan_lib-note">Server (PHP) calls OpenAI. If it fails you will see an error message.</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
<div class="mplan_lib-panel">
<label class="mplan_lib-label">Genre</label>
<select id="mplan_lib_genre" class="mplan_lib-select">
<option value="">Choose…</option>
<option>Action</option><option>Thriller</option><option>Crime</option><option>Drama</option><option>Comedy</option>
<option>Romance</option><option>Sci-Fi</option><option>Fantasy</option><option>Documentary</option><option>Horror</option><option>Family</option>
</select>
<label class="mplan_lib-label">Mood</label>
<select id="mplan_lib_mood" class="mplan_lib-select">
<option value="">Choose…</option>
<option>Feel-good</option><option>Adrenaline</option><option>Thoughtful</option><option>Dark & Gritty</option>
<option>Romantic</option><option>Family Night</option><option>Mind-bending</option><option>Chill</option><option>Laugh Out Loud</option>
</select>
<label class="mplan_lib-label">Preferred OTT</label>
<select id="mplan_lib_platform" class="mplan_lib-select">
<option value="">Choose…</option>
<option>Netflix</option><option>Prime Video</option><option>Hotstar</option><option>JioCinema</option><option>Sony LIV</option><option>ZEE5</option><option>Apple TV+</option><option>Any</option>
</select>
<label class="mplan_lib-label">Duration</label>
<select id="mplan_lib_duration" class="mplan_lib-select">
<option value="">Choose…</option>
<option>Short binge (2–4h)</option><option>Medium (4–6h)</option><option>Long binge (6–12h)</option>
</select>
<div style="margin-top:8px;display:flex;align-items:center;gap:8px">
<input id="mplan_lib_save_consent" type="checkbox"><label for="mplan_lib_save_consent" class="mplan_lib-note">Ask to save results</label>
</div>
<button id="mplan_lib_generate_btn" class="mplan_lib-button" style="margin-top:10px">Generate Weekend Plan</button>
</div>
<div class="mplan_lib-panel">
<div id="mplan_lib_results_msg" style="min-height:26px"></div>
<div id="mplan_lib_results" class="mplan_lib-results"></div>
</div>
</div>
</section>
<?php else: ?>
<!-- DASHBOARD -->
<section class="mplan_lib-card mplan_lib-panel">
<div class="mplan_lib-topbar">
<div class="mplan_lib-chip">Hi, <?= htmlspecialchars($current_user) ?> — your dashboard</div>
<div style="display:flex;gap:8px;align-items:center">
<button id="mplan_lib_logout_btn" class="mplan_lib-button">Logout</button>
</div>
</div>
<div class="mplan_lib-grid" style="margin-top:8px">
<aside class="mplan_lib-panel">
<label class="mplan_lib-label">Your saved searches</label>
<div id="mplan_lib_history" class="mplan_lib-historyList">
<?php if (empty($history_items)): ?>
<div class="mplan_lib-note">No saved searches yet.</div>
<?php else: foreach ($history_items as $h): ?>
<div class="mplan_lib-historyItem" data-mplan_lib-id="<?= htmlspecialchars($h['id']) ?>">
<div>
<div><?= htmlspecialchars($h['title']) ?></div>
<div class="mplan_lib-date"><?= htmlspecialchars($h['created_at'] ?? $h['date'] ?? '') ?></div>
</div>
<div class="mplan_lib-chip">Open ▸</div>
</div>
<?php endforeach; endif; ?>
</div>
</aside>
<main class="mplan_lib-panel">
<div class="mplan_lib-chip">New Search</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div>
<label class="mplan_lib-label">Genre</label>
<select id="mplan_lib_genre" class="mplan_lib-select">
<option value="">Choose…</option>
<option>Action</option><option>Thriller</option><option>Crime</option><option>Drama</option><option>Comedy</option>
<option>Romance</option><option>Sci-Fi</option><option>Fantasy</option><option>Documentary</option><option>Horror</option><option>Family</option>
</select>
</div>
<div>
<label class="mplan_lib-label">Mood</label>
<select id="mplan_lib_mood" class="mplan_lib-select">
<option value="">Choose…</option>
<option>Feel-good</option><option>Adrenaline</option><option>Thoughtful</option><option>Dark & Gritty</option>
<option>Romantic</option><option>Family Night</option><option>Mind-bending</option><option>Chill</option><option>Laugh Out Loud</option>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:8px">
<div>
<label class="mplan_lib-label">Preferred OTT</label>
<select id="mplan_lib_platform" class="mplan_lib-select">
<option value="">Choose…</option>
<option>Netflix</option><option>Prime Video</option><option>Hotstar</option><option>JioCinema</option><option>Sony LIV</option><option>ZEE5</option><option>Apple TV+</option><option>Any</option>
</select>
</div>
<div>
<label class="mplan_lib-label">Duration</label>
<select id="mplan_lib_duration" class="mplan_lib-select">
<option value="">Choose…</option>
<option>Short binge (2–4h)</option><option>Medium (4–6h)</option><option>Long binge (6–12h)</option>
</select>
</div>
</div>
<div style="margin-top:8px;display:flex;align-items:center;gap:8px">
<input id="mplan_lib_save_consent" type="checkbox" checked><label for="mplan_lib_save_consent" class="mplan_lib-note">Save this search</label>
</div>
<button id="mplan_lib_generate_btn" class="mplan_lib-button" style="margin-top:8px">Generate Weekend Plan</button>
<div id="mplan_lib_results_msg" style="margin-top:12px;min-height:26px"></div>
<div id="mplan_lib_results" class="mplan_lib-results" style="margin-top:8px"></div>
</main>
</div>
</section>
<?php endif; ?>
<footer style="margin:18px 4px" class="mplan_lib-note">Server-side OpenAI • sqlite storage • Strict JSON parsing • All classes/IDs prefixed with mplan_lib</footer>
</div>
<script>
/* ---------------- Client-side JS: UI, AJAX, render ---------------- */
function mplan_lib_msg(text, type='note') {
const el = document.getElementById('mplan_lib_results_msg') || document.getElementById('mplan_lib_auth_msg');
if (!el) return;
el.className = type === 'error' ? 'mplan_lib-error' : (type === 'success' ? 'mplan_lib-success' : 'mplan_lib-note');
el.textContent = text;
}
function mplan_lib_trailerLink(title){ return 'https://www.youtube.com/results?search_query=' + encodeURIComponent(title + ' official trailer'); }
function mplan_lib_platformLink(platform, title){
const q = encodeURIComponent(title);
switch ((platform||'').toLowerCase()){
case 'netflix': return title ? `https://www.netflix.com/search?q=${q}` : 'https://www.netflix.com/in/';
case 'prime video': return title ? `https://www.primevideo.com/search?phrase=${q}` : 'https://www.primevideo.com/';
case 'hotstar': case 'disney+ hotstar': return title ? `https://www.hotstar.com/in/search?q=${q}` : 'https://www.hotstar.com/in/';
case 'jiocinema': return title ? `https://www.jiocinema.com/search/${q}` : 'https://www.jiocinema.com/';
case 'sony liv': case 'sonyliv': return title ? `https://www.sonyliv.com/search/${q}` : 'https://www.sonyliv.com/';
case 'zee5': return title ? `https://www.zee5.com/search?q=${q}` : 'https://www.zee5.com/';
case 'apple tv+': case 'apple tv': return title ? `https://tv.apple.com/in/search?term=${q}` : 'https://tv.apple.com/in';
default: return title ? `https://www.google.com/search?q=${encodeURIComponent(title + ' watch online')}` : 'https://www.justwatch.com/in';
}
}
function mplan_lib_buildTitle(prefs){
const d = new Date();
const label = d.toLocaleString(undefined, {year:'numeric', month:'short', day:'2-digit'});
return `${prefs.genre} • ${prefs.mood} • ${prefs.platform} • ${prefs.duration} — ${label}`;
}
function mplan_lib_renderResults(items){
const box = document.getElementById('mplan_lib_results');
if (!box) return;
box.innerHTML = '';
items.forEach(it=>{
const poster = it.poster || 'https://images.unsplash.com/photo-1489599849927-2ee91cede3ba?q=80&w=400&auto=format&fit=crop';
const imdbBadge = (it.imdb && it.imdb!=='N/A') ? `<span class="mplan_lib-chip">IMDb <span class="mplan_lib-imdb">${it.imdb}</span></span>` : '';
const type = it.type || 'movie';
const yearPart = it.year ? ` • ${it.year}` : '';
const typeBadge = `<span class="mplan_lib-chip">${type.charAt(0).toUpperCase()+type.slice(1)}${yearPart}</span>`;
const links = [];
if (it.trailer) links.push(`<a class="mplan_lib-link" href="${it.trailer}" target="_blank" rel="noopener">Watch trailer ▸</a>`);
if (it.ott) links.push(`<a class="mplan_lib-link" href="${it.ott}" target="_blank" rel="noopener">Open OTT ▸</a>`);
if (it.platforms && Array.isArray(it.platforms)) {
it.platforms.forEach(p => links.push(`<a class="mplan_lib-link" href="${mplan_lib_platformLink(p, it.title)}" target="_blank" rel="noopener">Open on ${p}</a>`));
}
const el = document.createElement('article');
el.className = 'mplan_lib-card mplan_lib-cardItem';
el.innerHTML = `
<img class="mplan_lib-poster" src="${poster}" alt="${it.title} poster" loading="lazy">
<div>
<div class="mplan_lib-itemTitle">
<span>${it.title}</span>
${imdbBadge}
${typeBadge}
</div>
<div class="mplan_lib-syn">${it.synopsis || 'Synopsis not provided.'}</div>
<div class="mplan_lib-links">${links.join('')}</div>
</div>
`;
box.appendChild(el);
});
}
/* ---------------- AJAX helper ---------------- */
async function mplan_lib_postForm(formData) {
const resp = await fetch('', { method: 'POST', body: formData });
return resp.json();
}
/* ---------------- Generate (calls server /generate) ---------------- */
async function mplan_lib_generate() {
const genre = document.getElementById('mplan_lib_genre').value.trim();
const mood = document.getElementById('mplan_lib_mood').value.trim();
const platform = document.getElementById('mplan_lib_platform').value.trim();
const duration = document.getElementById('mplan_lib_duration').value.trim();
if (!genre || !mood || !platform || !duration) { mplan_lib_msg('Please fill all fields.', 'error'); return; }
mplan_lib_msg('Generating — contacting OpenAI (server)...', 'note');
const fd = new FormData();
fd.append('mplan_lib_action', 'generate');
fd.append('mplan_lib_genre', genre);
fd.append('mplan_lib_mood', mood);
fd.append('mplan_lib_platform', platform);
fd.append('mplan_lib_duration', duration);
try {
const j = await mplan_lib_postForm(fd);
if (!j.ok) { mplan_lib_msg(j.error || 'AI service unavailable.', 'error'); document.getElementById('mplan_lib_results').innerHTML = ''; return; }
const recs = j.recommendations || [];
// ensure each rec has trailer/ott links
const items = recs.map(r=>({
title: r.title || '',
type: r.type || 'movie',
imdb: r.imdb || 'N/A',
year: r.year || '',
synopsis: r.synopsis || '',
trailer: r.trailer || mplan_lib_trailerLink(r.title || ''),
ott: r.ott || mplan_lib_platformLink(platform==='Any'?'':platform, r.title || ''),
poster: r.poster || '',
platforms: Array.isArray(r.platforms) ? r.platforms : []
}));
mplan_lib_renderResults(items);
mplan_lib_msg('AI returned results. Use "Save this search" to store it.', 'success');
// If user asked to save (checkbox) and is logged in, call save_search automatically
const wantSave = !!(document.getElementById('mplan_lib_save_consent') && document.getElementById('mplan_lib_save_consent').checked);
const isLoggedIn = <?= $current_user ? 'true' : 'false'; ?>;
if (wantSave && isLoggedIn) {
const prefs = { genre, mood, platform, duration };
const title = mplan_lib_buildTitle(prefs);
const fd2 = new FormData();
fd2.append('mplan_lib_action','save_search');
fd2.append('mplan_lib_title', title);
fd2.append('mplan_lib_prefs', JSON.stringify(prefs));
fd2.append('mplan_lib_results', JSON.stringify(items));
const saved = await mplan_lib_postForm(fd2);
if (saved.ok) {
mplan_lib_msg('Saved to your account.', 'success');
// update history UI if present
const list = document.getElementById('mplan_lib_history');
if (list) {
const div = document.createElement('div');
div.className = 'mplan_lib-historyItem';
div.dataset.mplan_libId = saved.entry.id;
div.innerHTML = `<div><div>${saved.entry.title}</div><div class="mplan_lib-date">${saved.entry.date}</div></div><div class="mplan_lib-chip">Open ▸</div>`;
list.prepend(div);
}
} else {
mplan_lib_msg('Saved failed: ' + (saved.error || 'unknown'), 'error');
}
}
} catch (err) {
console.error(err);
mplan_lib_msg('AI service unavailable. Please try again later.', 'error');
document.getElementById('mplan_lib_results').innerHTML = '';
}
}
/* ---------------- Save & History handlers are in server endpoints ---------------- */
async function mplan_lib_setupAuth() {
// Tabs
const tabLogin = document.getElementById('mplan_lib_tab_login');
const tabSignup = document.getElementById('mplan_lib_tab_signup');
const loginBox = document.getElementById('mplan_lib_login_form');
const signupBox = document.getElementById('mplan_lib_signup_form');
if (tabLogin && tabSignup) {
tabLogin.addEventListener('click', ()=>{ tabLogin.classList.add('mplan_lib-active'); tabSignup.classList.remove('mplan_lib-active'); loginBox.style.display='block'; signupBox.style.display='none'; });
tabSignup.addEventListener('click', ()=>{ tabSignup.classList.add('mplan_lib-active'); tabLogin.classList.remove('mplan_lib-active'); signupBox.style.display='block'; loginBox.style.display='none'; });
}
// Login
const loginBtn = document.getElementById('mplan_lib_login_btn');
if (loginBtn) {
loginBtn.addEventListener('click', async ()=>{
const msg = document.getElementById('mplan_lib_auth_msg');
msg.className=''; msg.textContent='';
const u = document.getElementById('mplan_lib_login_username').value.trim();
const p = document.getElementById('mplan_lib_login_password').value;
if (!u || !p) { msg.className='mplan_lib-error'; msg.textContent='Enter username and password.'; return; }
const fd = new FormData(); fd.append('mplan_lib_action','login'); fd.append('mplan_lib_username', u); fd.append('mplan_lib_password', p);
const j = await mplan_lib_postForm(fd);
if (j.ok) location.reload(); else { msg.className='mplan_lib-error'; msg.textContent = j.error || 'Login failed'; }
});
}
// Signup
const signupBtn = document.getElementById('mplan_lib_signup_btn');
if (signupBtn) {
signupBtn.addEventListener('click', async ()=>{
const msg = document.getElementById('mplan_lib_auth_msg');
msg.className=''; msg.textContent='';
const u = document.getElementById('mplan_lib_signup_username').value.trim();
const p = document.getElementById('mplan_lib_signup_password').value;
const c = document.getElementById('mplan_lib_signup_confirm').value;
if (!u || !p || !c) { msg.className='mplan_lib-error'; msg.textContent='Fill all fields.'; return; }
const fd = new FormData(); fd.append('mplan_lib_action','signup'); fd.append('mplan_lib_username', u); fd.append('mplan_lib_password', p); fd.append('mplan_lib_confirm', c);
const j = await mplan_lib_postForm(fd);
if (j.ok) location.reload(); else { msg.className='mplan_lib-error'; msg.textContent = j.error || 'Signup failed'; }
});
}
// Logout
const logoutBtn = document.getElementById('mplan_lib_logout_btn');
if (logoutBtn) {
logoutBtn.addEventListener('click', async ()=>{
const fd = new FormData(); fd.append('mplan_lib_action','logout');
await mplan_lib_postForm(fd);
location.reload();
});
}
}
function mplan_lib_historyClick() {
const list = document.getElementById('mplan_lib_history');
if (!list) return;
list.addEventListener('click', async (e) => {
const item = e.target.closest('.mplan_lib-historyItem');
if (!item) return;
const id = item.dataset.mplan_libId;
const fd = new FormData(); fd.append('mplan_lib_action','get_history');
const j = await mplan_lib_postForm(fd);
if (!j.ok) { mplan_lib_msg('Could not load history', 'error'); return; }
const found = (j.history || []).find(x => x.id === id);
if (!found) { mplan_lib_msg('Saved item not found', 'error'); return; }
mplan_lib_renderResults(found.results || []);
mplan_lib_msg('Opened saved plan: ' + found.title, 'success');
});
}
document.addEventListener('DOMContentLoaded', function(){
// wire up
const genBtn = document.getElementById('mplan_lib_generate_btn');
if (genBtn) genBtn.addEventListener('click', async ()=>{ genBtn.setAttribute('disabled','true'); await mplan_lib_generate(); genBtn.removeAttribute('disabled'); });
mplan_lib_setupAuth();
mplan_lib_historyClick();
});
</script>
</body>
</html>