diff --git a/README.md b/README.md index 1c9c083..4255b4b 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Styled to match the existing `gate.php` / `index.php` dark theme exactly. ### Adding Videos (Admin) 1. Go to `admin/add.php` -2. Fill in title, description, duration, sort order +2. Fill in title, description, duration, sort order, or paste an external URL and click **Fetch Info** to auto-fill readable metadata 3. Upload a thumbnail (JPG/PNG/WebP) 4. Add one or more video sources. Each source can be either: - an uploaded local video file, stored under `media/videos/` @@ -78,6 +78,8 @@ Styled to match the existing `gate.php` / `index.php` dark theme exactly. External URLs must use `http://` or `https://`. Uploaded files and remote URLs can be mixed on the same video. Recognized provider URLs such as YouTube, Vimeo, Dailymotion, Twitch, Facebook, TikTok, Instagram, X/Twitter, SoundCloud, Spotify, Google Drive, Wistia, Streamable, and Loom render through their embed players. +When adding an external URL, the admin form can fetch title, description, duration, thumbnail metadata, and readable provider view counts through oEmbed, page metadata, and JSON-LD. The save action repeats the metadata lookup as a fallback, so blank titles can be filled from supported external pages even if the browser autofill was not used. + If no thumbnail image is uploaded, the add/edit forms can auto-grab one from the first supported source. YouTube and Dailymotion use direct thumbnail URLs; Vimeo, Wistia, TikTok, SoundCloud, and Spotify use oEmbed when available. Local uploaded videos can generate a first-frame thumbnail when `ffmpeg` is installed on the server. ### Importing YouTube Channels @@ -88,7 +90,7 @@ Imported YouTube videos use the video's original publish date for the database ` ### View Counts and Public Statistics -Each public player load increments the video's `view_count`, including standalone embeds served through `embed.php`. View counts appear in the admin video list and can be edited per video from **Admin → Edit Video** for older local uploads or manual corrections. +Each public player load increments the video's site-local `view_count`, including standalone embeds served through `embed.php`. External provider totals are stored separately as `external_view_count`, and public totals add local and external counts together. View counts appear in the admin video list and can be edited per video from **Admin → Edit Video** for older local uploads or manual corrections. The public display is configurable in **Admin → Settings → Public Display**: @@ -96,7 +98,7 @@ The public display is configurable in **Admin → Settings → Public Display**: - show or hide the total public view count - show or hide public page statistics on the homepage and catalogue -The admin dashboard also includes **Upkeeping → Restore external view counts**. It checks existing remote-source videos in batches, scrapes readable provider counts when available, and stores the higher value between the provider count and the local database count. YouTube watch pages are supported first; unsupported providers and local-only uploads can still be corrected manually on the edit screen. +The admin dashboard also includes **Upkeeping → Correct external view counts**. It checks existing remote-source videos in batches, scrapes readable provider counts when available, and adds those provider totals to the media-site totals without double-counting local plays. YouTube has dedicated parsing, and generic metadata patterns cover readable Vimeo, Dailymotion, and similar provider pages when they expose counts. Unsupported providers and local-only uploads can still be corrected manually on the edit screen. ### Public Search diff --git a/admin/add.php b/admin/add.php index 68ce199..24aed78 100644 --- a/admin/add.php +++ b/admin/add.php @@ -14,9 +14,45 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $sort_order = (int)($_POST['sort_order'] ?? 0); $published = isset($_POST['published']) ? 1 : 0; $auto_thumb = isset($_POST['auto_thumbnail']); + $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'] ?? []; + $source_metadata = []; + $initial_external_view_count = 0; + + $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 + ); + + for ($i = 0; $i < $source_count; $i++) { + $type = ($source_types[$i] ?? 'local') === 'remote' ? 'remote' : 'local'; + if ($type !== 'remote') continue; + + $url = normalize_media_url($source_urls[$i] ?? ''); + if ($url === '') continue; + + $metadata = media_external_metadata($url); + $source_metadata[$url] = $metadata; + if ($title === '' && $metadata['title'] !== '') $title = $metadata['title']; + if ($description === '' && $metadata['description'] !== '') $description = $metadata['description']; + if ($duration <= 0 && (int)$metadata['duration'] > 0) $duration = (int)$metadata['duration']; + if ((int)$metadata['view_count'] > $initial_external_view_count) { + $initial_external_view_count = (int)$metadata['view_count']; + } + } + + $_POST['title'] = $title; + $_POST['description'] = $description; + $_POST['duration'] = (string)$duration; if (!$title) { - $error = 'Title is required.'; + $error = 'Title is required. If you use an external URL, click Fetch Info or leave the title blank and make sure the URL is readable.'; } else { $slug = make_slug($title); @@ -40,20 +76,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$error) { // Insert video $db->prepare(" - INSERT INTO videos (title, description, slug, thumbnail, duration, sort_order, published) - VALUES (?, ?, ?, ?, ?, ?, ?) - ")->execute([$title, $description, $slug, $thumbnail, $duration, $sort_order, $published]); + INSERT INTO videos (title, description, slug, thumbnail, duration, external_view_count, sort_order, published) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ")->execute([$title, $description, $slug, $thumbnail, $duration, $initial_external_view_count, $sort_order, $published]); $video_id = (int)$db->lastInsertId(); // 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); @@ -61,13 +90,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $moved_files = []; $allowed_video = ['mp4','webm','ogv','ogg','mov','mkv','avi']; - $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 - ); - for ($i = 0; $i < $source_count; $i++) { $type = ($source_types[$i] ?? 'local') === 'remote' ? 'remote' : 'local'; $label = trim($source_labels[$i] ?? ''); @@ -80,6 +102,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (trim($source_urls[$i] ?? '') !== '') $error = 'External video URLs must start with http:// or https://.'; continue; } + $metadata = $source_metadata[$url] ?? []; + if ($label === '' && !empty($metadata['provider'])) { + $label = ucwords(str_replace('-', ' ', (string)$metadata['provider'])); + } $db->prepare(" INSERT INTO video_sources (video_id, label, source_type, file_path, source_url, mime_type, quality, sort_order) @@ -144,6 +170,11 @@ render_head('Add Video — Admin', ' @media(max-width:560px) { .source-row { grid-template-columns:1fr; } } .add-source-btn { align-self:flex-start; } .source-location-panel[hidden] { display:none; } + .source-url-control { display:flex; gap:.45rem; align-items:center; } + .source-url-control input { min-width:0; } + .metadata-status { font-family:"Share Tech Mono",monospace; font-size:.6rem; color:var(--text-3); min-height:1em; } + .metadata-status.success { color:var(--green); } + .metadata-status.error { color:var(--red); } .section-divider { border:none; border-top:1px solid var(--border); margin:1.5rem 0 .5rem; @@ -232,9 +263,10 @@ render_head('Add Video — Admin', '
Video Details
- Scrapes readable counts from supported external providers and stores the higher value between the provider and local database. + Scrapes readable counts from supported providers and adds them to the media-site totals alongside local plays. = $external_view_candidates ?> external video= $external_view_candidates === 1 ? '' : 's' ?> can be checked. Current database total: = h(view_count_label((int)$admin_total_views)) ?>.