- Implemented:
Added view_count database migration/defaults and shared helpers in [includes/db.php](/Users/tyemeclifford/Documents/GH/mediaplayer/includes/db.php). Public player and embed.php now increment views. Homepage and catalogue can display per-video views, total views, and public stats based on admin settings. Added Admin → Settings toggles for public view/stat display. Added Admin → Upkeeping action to restore external view counts from readable provider pages, with YouTube scraping support first. Added manual per-video view-count editing in [admin/edit.php](/Users/tyemeclifford/Documents/GH/mediaplayer/admin/edit.php) for older local uploads or corrections. Updated [README.md](/Users/tyemeclifford/Documents/GH/mediaplayer/README.md) with the new behavior.
This commit is contained in:
+239
@@ -34,6 +34,7 @@ function get_db(): PDO {
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
thumbnail TEXT NOT NULL DEFAULT '',
|
||||
duration INTEGER NOT NULL DEFAULT 0,
|
||||
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')),
|
||||
@@ -58,6 +59,7 @@ function get_db(): PDO {
|
||||
CREATE INDEX IF NOT EXISTS idx_sources_video ON video_sources(video_id);
|
||||
");
|
||||
|
||||
ensure_video_schema($pdo);
|
||||
ensure_video_source_schema($pdo);
|
||||
$pdo->exec("CREATE INDEX IF NOT EXISTS idx_sources_type ON video_sources(source_type)");
|
||||
|
||||
@@ -68,6 +70,9 @@ function get_db(): PDO {
|
||||
'site_title' => 'Ty Clifford',
|
||||
'site_sub' => 'tyclifford.com / media',
|
||||
'url_rewrite_mode' => 'pretty',
|
||||
'public_show_video_views' => '1',
|
||||
'public_show_total_views' => '1',
|
||||
'public_show_page_stats' => '1',
|
||||
];
|
||||
$ins = $pdo->prepare("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)");
|
||||
foreach ($defaults as $k => $v) $ins->execute([$k, $v]);
|
||||
@@ -75,6 +80,17 @@ function get_db(): PDO {
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
function ensure_video_schema(PDO $pdo): void {
|
||||
$cols = $pdo->query("PRAGMA table_info(videos)")->fetchAll();
|
||||
$names = array_column($cols, 'name');
|
||||
|
||||
if (!in_array('view_count', $names, true)) {
|
||||
$pdo->exec("ALTER TABLE videos ADD COLUMN view_count INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
|
||||
$pdo->exec("UPDATE videos SET view_count=0 WHERE view_count IS NULL");
|
||||
}
|
||||
|
||||
function ensure_video_source_schema(PDO $pdo): void {
|
||||
$cols = $pdo->query("PRAGMA table_info(video_sources)")->fetchAll();
|
||||
$names = array_column($cols, 'name');
|
||||
@@ -101,6 +117,51 @@ function set_setting(string $key, string $value): void {
|
||||
->execute([$key, $value]);
|
||||
}
|
||||
|
||||
function setting_bool(string $key, bool $fallback = false): bool {
|
||||
$value = strtolower(trim(setting($key, $fallback ? '1' : '0')));
|
||||
if ($value === '') return $fallback;
|
||||
return in_array($value, ['1', 'true', 'yes', 'on'], true);
|
||||
}
|
||||
|
||||
function format_count(int $value): string {
|
||||
return number_format(max(0, $value));
|
||||
}
|
||||
|
||||
function view_count_label(int $views): string {
|
||||
$views = max(0, $views);
|
||||
return format_count($views) . ' view' . ($views === 1 ? '' : 's');
|
||||
}
|
||||
|
||||
function increment_video_view_count(int $video_id): int {
|
||||
if ($video_id <= 0) return 0;
|
||||
|
||||
$db = get_db();
|
||||
$db->prepare("UPDATE videos SET view_count=view_count+1 WHERE id=?")->execute([$video_id]);
|
||||
|
||||
$stmt = $db->prepare("SELECT view_count FROM videos WHERE id=?");
|
||||
$stmt->execute([$video_id]);
|
||||
return max(0, (int)$stmt->fetchColumn());
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
function public_page_stats(): array {
|
||||
$db = get_db();
|
||||
return [
|
||||
'videos' => (int)$db->query("SELECT COUNT(*) FROM videos WHERE published=1")->fetchColumn(),
|
||||
'sources' => (int)$db->query("
|
||||
SELECT COUNT(*)
|
||||
FROM video_sources vs
|
||||
INNER JOIN videos v ON v.id = vs.video_id
|
||||
WHERE v.published=1
|
||||
")->fetchColumn(),
|
||||
'views' => total_video_views(true),
|
||||
];
|
||||
}
|
||||
|
||||
function url_rewrite_mode(): string {
|
||||
$mode = strtolower(setting('url_rewrite_mode', 'pretty'));
|
||||
return in_array($mode, ['pretty', 'query'], true) ? $mode : 'pretty';
|
||||
@@ -461,6 +522,184 @@ function media_youtube_publish_datetime(string $url): string {
|
||||
return media_youtube_publish_datetime_from_html($fetch['body']);
|
||||
}
|
||||
|
||||
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 = trim($value);
|
||||
if ($value === '') return 0;
|
||||
|
||||
if (preg_match('/([0-9]+(?:[,.][0-9]+)*)\s*([kmb])?\b/i', $value, $match)) {
|
||||
$number = $match[1];
|
||||
$unit = strtolower($match[2] ?? '');
|
||||
if ($unit === '') {
|
||||
return max(0, (int)preg_replace('/\D+/', '', $number));
|
||||
}
|
||||
|
||||
$numeric = (float)str_replace(',', '', $number);
|
||||
$multiplier = match ($unit) {
|
||||
'k' => 1000,
|
||||
'm' => 1000000,
|
||||
'b' => 1000000000,
|
||||
default => 1,
|
||||
};
|
||||
return max(0, (int)round($numeric * $multiplier));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function media_youtube_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+)/',
|
||||
'/"userInteractionCount"\s*:\s*"(\d+)"/',
|
||||
'/"userInteractionCount"\s*:\s*(\d+)/',
|
||||
];
|
||||
foreach ($variants as $variant) {
|
||||
foreach ($exact_patterns as $pattern) {
|
||||
if (preg_match($pattern, $variant, $match)) {
|
||||
return max(0, (int)$match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$text_patterns = [
|
||||
'/"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/',
|
||||
];
|
||||
foreach ($variants as $variant) {
|
||||
foreach ($text_patterns as $pattern) {
|
||||
if (preg_match($pattern, $variant, $match)) {
|
||||
$count = media_count_from_text($match[1]);
|
||||
if ($count > 0) return $count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function media_youtube_view_count(string $url): int {
|
||||
$id = media_youtube_id_from_url($url);
|
||||
if ($id === '') return 0;
|
||||
|
||||
$watch_url = 'https://www.youtube.com/watch?v=' . rawurlencode($id);
|
||||
$fetch = media_http_get($watch_url, 15, 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8');
|
||||
if ($fetch['body'] === '' || $fetch['error'] !== '') return 0;
|
||||
|
||||
return media_youtube_view_count_from_html($fetch['body']);
|
||||
}
|
||||
|
||||
function media_external_view_count_from_url(string $url): int {
|
||||
$url = normalize_media_url($url);
|
||||
if ($url === '') return 0;
|
||||
|
||||
$host = strtolower((string)(parse_url($url, PHP_URL_HOST) ?? ''));
|
||||
if (media_host_matches($host, 'youtube.com') || media_host_matches($host, 'youtu.be') || media_host_matches($host, 'youtube-nocookie.com')) {
|
||||
return media_youtube_view_count($url);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function media_external_view_urls_for_video(int $video_id): array {
|
||||
$stmt = get_db()->prepare("
|
||||
SELECT source_url
|
||||
FROM video_sources
|
||||
WHERE video_id=? AND source_type='remote' AND source_url!=''
|
||||
ORDER BY sort_order ASC, id ASC
|
||||
");
|
||||
$stmt->execute([$video_id]);
|
||||
|
||||
$urls = [];
|
||||
foreach ($stmt->fetchAll() as $source) {
|
||||
$url = normalize_media_url($source['source_url'] ?? '');
|
||||
if ($url !== '') $urls[] = $url;
|
||||
}
|
||||
return $urls;
|
||||
}
|
||||
|
||||
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->execute([$video_id]);
|
||||
$current = max(0, (int)$stmt->fetchColumn());
|
||||
|
||||
$best = 0;
|
||||
$best_url = '';
|
||||
foreach ($urls as $url) {
|
||||
$count = media_external_view_count_from_url($url);
|
||||
if ($count > $best) {
|
||||
$best = $count;
|
||||
$best_url = $url;
|
||||
}
|
||||
}
|
||||
|
||||
if ($best <= 0) {
|
||||
return ['status' => 'not_found', 'current' => $current, 'external' => 0, 'url' => ''];
|
||||
}
|
||||
|
||||
if ($best <= $current) {
|
||||
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=?")
|
||||
->execute([$best, $video_id]);
|
||||
|
||||
return ['status' => 'restored', 'current' => $current, 'external' => $best, 'url' => $best_url];
|
||||
}
|
||||
|
||||
function media_external_view_count_candidates(int $limit): array {
|
||||
$limit = max(1, min(100, $limit));
|
||||
$stmt = get_db()->prepare("
|
||||
SELECT DISTINCT v.id
|
||||
FROM videos v
|
||||
INNER JOIN video_sources vs ON vs.video_id = v.id
|
||||
WHERE vs.source_type='remote' AND vs.source_url!=''
|
||||
ORDER BY v.id DESC
|
||||
LIMIT :limit
|
||||
");
|
||||
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return array_map('intval', array_column($stmt->fetchAll(), 'id'));
|
||||
}
|
||||
|
||||
function media_external_view_count_candidate_count(): int {
|
||||
return (int)get_db()->query("
|
||||
SELECT COUNT(DISTINCT v.id)
|
||||
FROM videos v
|
||||
INNER JOIN video_sources vs ON vs.video_id = v.id
|
||||
WHERE vs.source_type='remote' AND vs.source_url!=''
|
||||
")->fetchColumn();
|
||||
}
|
||||
|
||||
function media_restore_external_view_counts(int $limit = 25): array {
|
||||
$result = [
|
||||
'checked' => 0,
|
||||
'restored' => 0,
|
||||
'already_current' => 0,
|
||||
'not_found' => 0,
|
||||
'no_external' => 0,
|
||||
];
|
||||
|
||||
foreach (media_external_view_count_candidates($limit) as $video_id) {
|
||||
$result['checked']++;
|
||||
$applied = media_restore_external_view_count_for_video($video_id);
|
||||
$status = $applied['status'] ?? 'not_found';
|
||||
if (!isset($result[$status])) $status = 'not_found';
|
||||
$result[$status]++;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
function media_youtube_source_url_for_video(int $video_id): string {
|
||||
$stmt = get_db()->prepare("
|
||||
SELECT source_url
|
||||
|
||||
Reference in New Issue
Block a user