diff --git a/README.md b/README.md index 4d26cb1..28fd0ce 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,8 @@ Styled to match the existing `gate.php` / `index.php` dark theme exactly. │ └── layout.php ← Shared HTML head, topbar, footer, pagination ├── admin/ │ ├── index.php ← Video list with search + pagination -│ ├── add.php ← Add new video with multi-format sources -│ ├── edit.php ← Edit video metadata + manage sources +│ ├── add.php ← Add new video with uploaded or external URL sources +│ ├── edit.php ← Edit video metadata + manage local/remote sources │ ├── settings.php ← Site settings, per-page count, password │ ├── login.php ← Admin login │ ├── logout.php ← Session destroy + redirect @@ -63,12 +63,21 @@ Styled to match the existing `gate.php` / `index.php` dark theme exactly. 1. Go to `admin/add.php` 2. Fill in title, description, duration, sort order 3. Upload a thumbnail (JPG/PNG/WebP) -4. Upload one or more video files — each can be a different format or quality: +4. Add one or more video sources. Each source can be either: + - an uploaded local video file, stored under `media/videos/` + - an external video URL, stored in SQLite and played directly by URL +5. For uploaded files, supported formats include: - **MP4** (H.264) — widest compatibility - **WebM** (VP9) — smaller size, modern browsers - **OGV** — Firefox fallback (legacy) -5. Set quality labels (1080p, 720p, 480p, etc.) -6. Save — Video.js will auto-select the best format the browser supports +6. Set quality labels (1080p, 720p, 480p, etc.) +7. Save — Video.js will use the stored source location and auto-select the best format the browser supports + +External URLs must use `http://` or `https://`. Uploaded files and remote URLs can be mixed on the same video. + +### Public Search + +The full catalogue page includes a public search for published videos only. Search matches video title, description, and slug; draft videos remain hidden. ### Catalogue Per-Page @@ -92,6 +101,7 @@ Use the **Sort Order** field — lower numbers appear first. Default is 0. - Custom skin matching the site's dark purple/green theme - Playback rate controls: 0.5×, 1×, 1.25×, 1.5×, 2× - Multiple source fallback — browser picks best format automatically +- Local uploaded sources and remote URL sources - Thumbnail posters - Keyboard accessible diff --git a/admin/add.php b/admin/add.php index 936b82d..2f402b3 100644 --- a/admin/add.php +++ b/admin/add.php @@ -45,42 +45,76 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $video_id = (int)$db->lastInsertId(); - // Handle video source files + // Handle local upload and remote URL sources + $source_types = $_POST['source_type'] ?? []; $source_labels = $_POST['source_label'] ?? []; $source_quality = $_POST['source_quality'] ?? []; $source_order = $_POST['source_sort'] ?? []; + $source_urls = $_POST['source_url'] ?? []; $source_files = $_FILES['source_file'] ?? []; $videos_dir = MEDIA_DIR . 'videos/'; if (!is_dir($videos_dir)) mkdir($videos_dir, 0755, true); $inserted_sources = 0; + $moved_files = []; $allowed_video = ['mp4','webm','ogv','ogg','mov','mkv','avi']; - if (!empty($source_files['tmp_name'])) { - foreach ($source_files['tmp_name'] as $i => $tmp) { - if (empty($tmp) || !is_uploaded_file($tmp)) continue; - $orig_name = $source_files['name'][$i]; - $ext = strtolower(pathinfo($orig_name, PATHINFO_EXTENSION)); - if (!in_array($ext, $allowed_video)) continue; + $source_count = max( + count($source_types), + count($source_labels), + count($source_urls), + isset($source_files['tmp_name']) && is_array($source_files['tmp_name']) ? count($source_files['tmp_name']) : 0 + ); - $fname = $slug . '_' . ($i+1) . '_' . time() . '.' . $ext; - if (move_uploaded_file($tmp, $videos_dir . $fname)) { - $label = trim($source_labels[$i] ?? ''); - $quality = trim($source_quality[$i] ?? ''); - $mime = mime_from_ext($fname); - $sort = (int)($source_order[$i] ?? $i); - $db->prepare(" - INSERT INTO video_sources (video_id, label, file_path, mime_type, quality, sort_order) - VALUES (?,?,?,?,?,?) - ")->execute([$video_id, $label, 'videos/' . $fname, $mime, $quality, $sort]); - $inserted_sources++; + for ($i = 0; $i < $source_count; $i++) { + $type = ($source_types[$i] ?? 'local') === 'remote' ? 'remote' : 'local'; + $label = trim($source_labels[$i] ?? ''); + $quality = trim($source_quality[$i] ?? ''); + $sort = (int)($source_order[$i] ?? $i); + + if ($type === 'remote') { + $url = normalize_media_url($source_urls[$i] ?? ''); + if ($url === '') { + if (trim($source_urls[$i] ?? '') !== '') $error = 'External video URLs must start with http:// or https://.'; + continue; } + + $db->prepare(" + INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order) + VALUES (?,?,?,?,?,?,?,?) + ")->execute([$video_id, $label, 'remote', '', $url, mime_from_ext($url), $quality, $sort]); + $inserted_sources++; + continue; + } + + $tmp = $source_files['tmp_name'][$i] ?? ''; + if (empty($tmp) || !is_uploaded_file($tmp)) continue; + $orig_name = $source_files['name'][$i] ?? ''; + $ext = strtolower(pathinfo($orig_name, PATHINFO_EXTENSION)); + if (!in_array($ext, $allowed_video, true)) continue; + + $fname = $slug . '_' . ($i+1) . '_' . time() . '.' . $ext; + if (move_uploaded_file($tmp, $videos_dir . $fname)) { + $moved_files[] = $videos_dir . $fname; + $db->prepare(" + INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order) + VALUES (?,?,?,?,?,?,?,?) + ")->execute([$video_id, $label, 'local', 'videos/' . $fname, '', mime_from_ext($fname), $quality, $sort]); + $inserted_sources++; } } + if ($error) { + foreach ($moved_files as $moved) { + if (file_exists($moved)) unlink($moved); + } + if ($thumbnail && file_exists(MEDIA_DIR . $thumbnail)) unlink(MEDIA_DIR . $thumbnail); + $db->prepare("DELETE FROM videos WHERE id=?")->execute([$video_id]); + } else { header('Location: edit.php?id=' . $video_id . '&saved=1'); exit; + } } } } @@ -94,11 +128,13 @@ render_head('Add Video — Admin', ' .source-row { background:var(--surface-2); border:1px solid var(--border); border-radius:var(--r); padding:1rem; - display:grid; grid-template-columns:1fr 1fr 1fr auto; + display:grid; grid-template-columns:140px minmax(220px,1.5fr) 1fr 1fr 80px auto; gap:.75rem; align-items:end; } - @media(max-width:700px) { .source-row { grid-template-columns:1fr 1fr; } } + @media(max-width:900px) { .source-row { grid-template-columns:1fr 1fr; } } + @media(max-width:560px) { .source-row { grid-template-columns:1fr; } } .add-source-btn { align-self:flex-start; } + .source-location-panel[hidden] { display:none; } .section-divider { border:none; border-top:1px solid var(--border); margin:1.5rem 0 .5rem; @@ -221,21 +257,32 @@ render_head('Add Video — Admin', '
Video Sources (Multiple Formats)
+Video Sources
- Upload the same video in multiple formats/qualities. The browser will automatically pick the best one it can play. - Accepted: MP4, WebM, OGV, MOV, MKV, AVI. + Upload a video file or link to an external video URL. The database stores where each source lives, and the player uses that location at playback time. + Uploaded files: MP4, WebM, OGV, MOV, MKV, AVI. Remote URLs must use http:// or https://.