- What changed:
Added external_view_count so provider totals from YouTube/Vimeo/etc are added to the site’s total without double-counting local plays. Updated public totals and per-video public view displays to use site views + external views. Changed Upkeeping action to Correct external view counts with a Correct Views button. Expanded provider scraping beyond YouTube with generic page/JSON-LD/oEmbed patterns for readable Vimeo, Dailymotion, and similar pages. Improved Add Video:Title no longer has to be manually entered first. External URL rows now have Fetch Info. Metadata can fill title, description, duration, source label, and external view count. Save also retries metadata extraction server-side. Added authenticated metadata endpoint: [admin/media_info.php](/Users/tyemeclifford/Documents/GH/mediaplayer/admin/media_info.php). Updated docs in [README.md](/Users/tyemeclifford/Documents/GH/mediaplayer/README.md).
This commit is contained in:
+207
-10
@@ -35,6 +35,7 @@ function get_db(): PDO {
|
||||
thumbnail TEXT NOT NULL DEFAULT '',
|
||||
duration INTEGER NOT NULL DEFAULT 0,
|
||||
view_count INTEGER NOT NULL DEFAULT 0,
|
||||
external_view_count INTEGER NOT NULL DEFAULT 0,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
published INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
@@ -87,8 +88,12 @@ function ensure_video_schema(PDO $pdo): void {
|
||||
if (!in_array('view_count', $names, true)) {
|
||||
$pdo->exec("ALTER TABLE videos ADD COLUMN view_count INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
if (!in_array('external_view_count', $names, true)) {
|
||||
$pdo->exec("ALTER TABLE videos ADD COLUMN external_view_count INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
|
||||
$pdo->exec("UPDATE videos SET view_count=0 WHERE view_count IS NULL");
|
||||
$pdo->exec("UPDATE videos SET external_view_count=0 WHERE external_view_count IS NULL");
|
||||
}
|
||||
|
||||
function ensure_video_source_schema(PDO $pdo): void {
|
||||
@@ -132,6 +137,10 @@ function view_count_label(int $views): string {
|
||||
return format_count($views) . ' view' . ($views === 1 ? '' : 's');
|
||||
}
|
||||
|
||||
function video_total_views(array $video): int {
|
||||
return max(0, (int)($video['view_count'] ?? 0)) + max(0, (int)($video['external_view_count'] ?? 0));
|
||||
}
|
||||
|
||||
function increment_video_view_count(int $video_id): int {
|
||||
if ($video_id <= 0) return 0;
|
||||
|
||||
@@ -145,7 +154,7 @@ function increment_video_view_count(int $video_id): int {
|
||||
|
||||
function total_video_views(bool $published_only = true): int {
|
||||
$where = $published_only ? 'WHERE published=1' : '';
|
||||
return max(0, (int)get_db()->query("SELECT COALESCE(SUM(view_count),0) FROM videos $where")->fetchColumn());
|
||||
return max(0, (int)get_db()->query("SELECT COALESCE(SUM(view_count + external_view_count),0) FROM videos $where")->fetchColumn());
|
||||
}
|
||||
|
||||
function public_page_stats(): array {
|
||||
@@ -524,7 +533,7 @@ function media_youtube_publish_datetime(string $url): string {
|
||||
|
||||
function media_count_from_text(string $value): int {
|
||||
$value = strtolower(trim(html_entity_decode(strip_tags($value), ENT_QUOTES | ENT_HTML5, 'UTF-8')));
|
||||
$value = str_replace(["\xc2\xa0", ' views', ' view'], [' ', '', ''], $value);
|
||||
$value = str_replace(["\xc2\xa0", ' views', ' view', ' plays', ' play'], [' ', '', '', '', ''], $value);
|
||||
$value = trim($value);
|
||||
if ($value === '') return 0;
|
||||
|
||||
@@ -548,13 +557,35 @@ function media_count_from_text(string $value): int {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function media_youtube_view_count_from_html(string $html): int {
|
||||
function media_json_decode_value(string $value): string {
|
||||
$decoded = json_decode('"' . str_replace(['\\', '"'], ['\\\\', '\"'], $value) . '"');
|
||||
return is_string($decoded) ? $decoded : $value;
|
||||
}
|
||||
|
||||
function media_pick_longest_text(string $current, string $candidate): string {
|
||||
$candidate = trim(html_entity_decode(strip_tags($candidate), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
||||
return strlen($candidate) > strlen($current) ? $candidate : $current;
|
||||
}
|
||||
|
||||
function media_view_count_from_html(string $html): int {
|
||||
$html = html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$variants = [$html, stripcslashes($html)];
|
||||
|
||||
$exact_patterns = [
|
||||
'/"viewCount"\s*:\s*"(\d+)"/',
|
||||
'/"viewCount"\s*:\s*(\d+)/',
|
||||
'/"view_count"\s*:\s*"(\d+)"/',
|
||||
'/"view_count"\s*:\s*(\d+)/',
|
||||
'/"views"\s*:\s*"(\d+)"/',
|
||||
'/"views"\s*:\s*(\d+)/',
|
||||
'/"plays"\s*:\s*"(\d+)"/',
|
||||
'/"plays"\s*:\s*(\d+)/',
|
||||
'/"play_count"\s*:\s*"(\d+)"/',
|
||||
'/"play_count"\s*:\s*(\d+)/',
|
||||
'/"stats_number_of_plays"\s*:\s*"(\d+)"/',
|
||||
'/"stats_number_of_plays"\s*:\s*(\d+)/',
|
||||
'/"views_total"\s*:\s*"(\d+)"/',
|
||||
'/"views_total"\s*:\s*(\d+)/',
|
||||
'/"userInteractionCount"\s*:\s*"(\d+)"/',
|
||||
'/"userInteractionCount"\s*:\s*(\d+)/',
|
||||
];
|
||||
@@ -570,12 +601,12 @@ function media_youtube_view_count_from_html(string $html): int {
|
||||
'/"viewCountText"\s*:\s*\{\s*"simpleText"\s*:\s*"([^"]+)"/',
|
||||
'/"shortViewCountText"\s*:\s*\{\s*"simpleText"\s*:\s*"([^"]+)"/',
|
||||
'/"views"\s*:\s*\{\s*"simpleText"\s*:\s*"([^"]+)"/',
|
||||
'/([0-9][0-9,.\s]*\s*[KMBkmb]?)\s+views\b/',
|
||||
'/([0-9][0-9,.\s]*\s*[KMBkmb]?)\s+(?:views|plays)\b/',
|
||||
];
|
||||
foreach ($variants as $variant) {
|
||||
foreach ($text_patterns as $pattern) {
|
||||
if (preg_match($pattern, $variant, $match)) {
|
||||
$count = media_count_from_text($match[1]);
|
||||
$count = media_count_from_text(media_json_decode_value($match[1]));
|
||||
if ($count > 0) return $count;
|
||||
}
|
||||
}
|
||||
@@ -584,6 +615,10 @@ function media_youtube_view_count_from_html(string $html): int {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function media_youtube_view_count_from_html(string $html): int {
|
||||
return media_view_count_from_html($html);
|
||||
}
|
||||
|
||||
function media_youtube_view_count(string $url): int {
|
||||
$id = media_youtube_id_from_url($url);
|
||||
if ($id === '') return 0;
|
||||
@@ -604,7 +639,10 @@ function media_external_view_count_from_url(string $url): int {
|
||||
return media_youtube_view_count($url);
|
||||
}
|
||||
|
||||
return 0;
|
||||
$fetch = media_http_get($url, 15, 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8');
|
||||
if ($fetch['body'] === '' || $fetch['error'] !== '') return 0;
|
||||
|
||||
return media_view_count_from_html($fetch['body']);
|
||||
}
|
||||
|
||||
function media_external_view_urls_for_video(int $video_id): array {
|
||||
@@ -628,7 +666,7 @@ function media_restore_external_view_count_for_video(int $video_id): array {
|
||||
$urls = media_external_view_urls_for_video($video_id);
|
||||
if (!$urls) return ['status' => 'no_external', 'current' => 0, 'external' => 0, 'url' => ''];
|
||||
|
||||
$stmt = get_db()->prepare("SELECT view_count FROM videos WHERE id=?");
|
||||
$stmt = get_db()->prepare("SELECT external_view_count FROM videos WHERE id=?");
|
||||
$stmt->execute([$video_id]);
|
||||
$current = max(0, (int)$stmt->fetchColumn());
|
||||
|
||||
@@ -650,10 +688,10 @@ function media_restore_external_view_count_for_video(int $video_id): array {
|
||||
return ['status' => 'already_current', 'current' => $current, 'external' => $best, 'url' => $best_url];
|
||||
}
|
||||
|
||||
get_db()->prepare("UPDATE videos SET view_count=?, updated_at=datetime('now') WHERE id=?")
|
||||
get_db()->prepare("UPDATE videos SET external_view_count=?, updated_at=datetime('now') WHERE id=?")
|
||||
->execute([$best, $video_id]);
|
||||
|
||||
return ['status' => 'restored', 'current' => $current, 'external' => $best, 'url' => $best_url];
|
||||
return ['status' => 'corrected', 'current' => $current, 'external' => $best, 'url' => $best_url];
|
||||
}
|
||||
|
||||
function media_external_view_count_candidates(int $limit): array {
|
||||
@@ -683,7 +721,7 @@ function media_external_view_count_candidate_count(): int {
|
||||
function media_restore_external_view_counts(int $limit = 25): array {
|
||||
$result = [
|
||||
'checked' => 0,
|
||||
'restored' => 0,
|
||||
'corrected' => 0,
|
||||
'already_current' => 0,
|
||||
'not_found' => 0,
|
||||
'no_external' => 0,
|
||||
@@ -700,6 +738,10 @@ function media_restore_external_view_counts(int $limit = 25): array {
|
||||
return $result;
|
||||
}
|
||||
|
||||
function media_correct_external_view_counts(int $limit = 25): array {
|
||||
return media_restore_external_view_counts($limit);
|
||||
}
|
||||
|
||||
function media_youtube_source_url_for_video(int $video_id): string {
|
||||
$stmt = get_db()->prepare("
|
||||
SELECT source_url
|
||||
@@ -817,6 +859,161 @@ function media_provider_oembed_endpoint(string $url, string $host): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
function media_iso_duration_to_seconds(string $duration): int {
|
||||
$duration = trim($duration);
|
||||
if ($duration === '') return 0;
|
||||
if (ctype_digit($duration)) return (int)$duration;
|
||||
if (!preg_match('/^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/i', $duration, $m)) return 0;
|
||||
|
||||
$days = (int)($m[1] ?? 0);
|
||||
$hours = (int)($m[2] ?? 0);
|
||||
$minutes = (int)($m[3] ?? 0);
|
||||
$seconds = (int)($m[4] ?? 0);
|
||||
return max(0, ($days * 86400) + ($hours * 3600) + ($minutes * 60) + $seconds);
|
||||
}
|
||||
|
||||
function media_html_meta_content(string $html, array $names): string {
|
||||
foreach ($names as $name) {
|
||||
$quoted = preg_quote($name, '/');
|
||||
$patterns = [
|
||||
'/<meta[^>]+(?:property|name|itemprop)=["\']' . $quoted . '["\'][^>]+content=["\']([^"\']*)["\']/i',
|
||||
'/<meta[^>]+content=["\']([^"\']*)["\'][^>]+(?:property|name|itemprop)=["\']' . $quoted . '["\']/i',
|
||||
];
|
||||
foreach ($patterns as $pattern) {
|
||||
if (preg_match($pattern, $html, $m)) {
|
||||
$value = trim(html_entity_decode($m[1], ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
||||
if ($value !== '') return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function media_html_title_tag(string $html): string {
|
||||
if (!preg_match('/<title[^>]*>(.*?)<\/title>/is', $html, $m)) return '';
|
||||
return trim(html_entity_decode(strip_tags($m[1]), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
||||
}
|
||||
|
||||
function media_apply_metadata_value(array &$metadata, string $key, $value): void {
|
||||
if (!is_scalar($value) && !is_array($value)) return;
|
||||
if (is_array($value) && $key !== 'thumbnail_url') {
|
||||
$value = $value[0] ?? '';
|
||||
if (is_array($value)) return;
|
||||
}
|
||||
|
||||
if ($key === 'thumbnail_url') {
|
||||
if (is_array($value)) $value = $value[0] ?? $value['url'] ?? $value['contentUrl'] ?? '';
|
||||
if (is_array($value)) $value = $value['url'] ?? $value['contentUrl'] ?? '';
|
||||
$url = normalize_media_url((string)$value);
|
||||
if ($url !== '' && $metadata['thumbnail_url'] === '') $metadata['thumbnail_url'] = $url;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($key === 'duration') {
|
||||
$seconds = is_numeric($value) ? (int)$value : media_iso_duration_to_seconds((string)$value);
|
||||
if ($seconds > 0 && (int)$metadata['duration'] <= 0) $metadata['duration'] = $seconds;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($key === 'view_count') {
|
||||
$count = is_numeric($value) ? (int)$value : media_count_from_text((string)$value);
|
||||
if ($count > (int)$metadata['view_count']) $metadata['view_count'] = $count;
|
||||
return;
|
||||
}
|
||||
|
||||
$text = trim(html_entity_decode(strip_tags((string)$value), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
||||
if ($text === '') return;
|
||||
|
||||
if ($key === 'description') {
|
||||
$metadata['description'] = media_pick_longest_text((string)$metadata['description'], $text);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((string)$metadata[$key] === '') $metadata[$key] = $text;
|
||||
}
|
||||
|
||||
function media_collect_json_metadata($value, array &$metadata): void {
|
||||
if (!is_array($value)) return;
|
||||
|
||||
$name = $value['name'] ?? $value['headline'] ?? $value['title'] ?? '';
|
||||
if ($name !== '') media_apply_metadata_value($metadata, 'title', $name);
|
||||
if (!empty($value['description'])) media_apply_metadata_value($metadata, 'description', $value['description']);
|
||||
if (!empty($value['thumbnailUrl'])) media_apply_metadata_value($metadata, 'thumbnail_url', $value['thumbnailUrl']);
|
||||
if (!empty($value['thumbnail'])) media_apply_metadata_value($metadata, 'thumbnail_url', $value['thumbnail']);
|
||||
if (!empty($value['duration'])) media_apply_metadata_value($metadata, 'duration', $value['duration']);
|
||||
if (!empty($value['uploadDate'])) media_apply_metadata_value($metadata, 'published_at', $value['uploadDate']);
|
||||
if (!empty($value['datePublished'])) media_apply_metadata_value($metadata, 'published_at', $value['datePublished']);
|
||||
|
||||
if (!empty($value['interactionStatistic'])) {
|
||||
$stats = isset($value['interactionStatistic'][0]) ? $value['interactionStatistic'] : [$value['interactionStatistic']];
|
||||
foreach ($stats as $stat) {
|
||||
if (!is_array($stat)) continue;
|
||||
$type = strtolower((string)($stat['interactionType']['@type'] ?? $stat['interactionType'] ?? ''));
|
||||
if ($type === '' || str_contains($type, 'watch') || str_contains($type, 'view')) {
|
||||
media_apply_metadata_value($metadata, 'view_count', $stat['userInteractionCount'] ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($value as $child) {
|
||||
if (is_array($child)) media_collect_json_metadata($child, $metadata);
|
||||
}
|
||||
}
|
||||
|
||||
function media_external_metadata(string $url): array {
|
||||
$url = normalize_media_url($url);
|
||||
$metadata = [
|
||||
'url' => $url,
|
||||
'provider' => '',
|
||||
'title' => '',
|
||||
'description' => '',
|
||||
'thumbnail_url' => '',
|
||||
'duration' => 0,
|
||||
'view_count' => 0,
|
||||
'published_at' => '',
|
||||
];
|
||||
if ($url === '') return $metadata;
|
||||
|
||||
$host = strtolower((string)(parse_url($url, PHP_URL_HOST) ?? ''));
|
||||
$provider = media_provider_embed($url);
|
||||
$metadata['provider'] = $provider['provider'] ?? '';
|
||||
|
||||
$endpoint = media_provider_oembed_endpoint($url, $host);
|
||||
if ($endpoint !== '') {
|
||||
$fetch = media_http_get($endpoint, 8, 'application/json,text/html;q=0.5,*/*;q=0.2');
|
||||
$data = $fetch['body'] !== '' && $fetch['error'] === '' ? json_decode($fetch['body'], true) : null;
|
||||
if (is_array($data)) {
|
||||
media_apply_metadata_value($metadata, 'title', $data['title'] ?? '');
|
||||
media_apply_metadata_value($metadata, 'description', $data['description'] ?? '');
|
||||
media_apply_metadata_value($metadata, 'thumbnail_url', $data['thumbnail_url'] ?? '');
|
||||
if ($metadata['provider'] === '') {
|
||||
media_apply_metadata_value($metadata, 'provider', strtolower((string)($data['provider_name'] ?? '')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$fetch = media_http_get($url, 15, 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8');
|
||||
if ($fetch['body'] === '' || $fetch['error'] !== '') return $metadata;
|
||||
|
||||
$html = $fetch['body'];
|
||||
media_apply_metadata_value($metadata, 'title', media_html_meta_content($html, ['og:title', 'twitter:title', 'title']));
|
||||
media_apply_metadata_value($metadata, 'title', media_html_title_tag($html));
|
||||
media_apply_metadata_value($metadata, 'description', media_html_meta_content($html, ['og:description', 'twitter:description', 'description']));
|
||||
media_apply_metadata_value($metadata, 'thumbnail_url', media_html_meta_content($html, ['og:image', 'twitter:image', 'thumbnailUrl', 'thumbnail']));
|
||||
media_apply_metadata_value($metadata, 'duration', media_html_meta_content($html, ['video:duration', 'duration']));
|
||||
media_apply_metadata_value($metadata, 'published_at', media_html_meta_content($html, ['article:published_time', 'datePublished', 'uploadDate']));
|
||||
media_apply_metadata_value($metadata, 'view_count', media_view_count_from_html($html));
|
||||
|
||||
if (preg_match_all('/<script[^>]+type=["\']application\/ld\+json["\'][^>]*>(.*?)<\/script>/is', $html, $matches)) {
|
||||
foreach ($matches[1] as $json) {
|
||||
$decoded = json_decode(html_entity_decode($json, ENT_QUOTES | ENT_HTML5, 'UTF-8'), true);
|
||||
if (is_array($decoded)) media_collect_json_metadata($decoded, $metadata);
|
||||
}
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
function media_oembed_thumbnail_url(string $url, string $host): string {
|
||||
$endpoint = media_provider_oembed_endpoint($url, $host);
|
||||
if ($endpoint === '') return '';
|
||||
|
||||
Reference in New Issue
Block a user