Files
mediaplayer/catalogue.php
T
Ty Clifford 86d1fedcb3 - 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.
2026-06-25 04:16:34 -04:00

209 lines
9.6 KiB
PHP

<?php
require_once __DIR__ . '/includes/db.php';
require_once __DIR__ . '/includes/layout.php';
$route_segments = public_route_segments();
$search = public_search_query_from_request();
if (url_rewrite_enabled()) {
$route_head = $route_segments[0] ?? '';
$target = null;
if (isset($_GET['q']) && $route_head !== 'search') {
$target = public_search_url($search);
} elseif (!isset($_GET['q']) && !in_array($route_head, ['all', 'search'], true)) {
$target = public_catalogue_url();
}
if ($target !== null) {
$redirect_page = max(1, (int)($_GET['page'] ?? 1));
if ($redirect_page > 1) $target = media_append_query($target, ['page' => (string)$redirect_page]);
header('Location: ' . $target, true, 302);
exit;
}
}
$per_page = (int)setting('catalogue_per_page', '10');
$page = max(1, (int)($_GET['page'] ?? 1));
$paged = get_videos_paginated($page, $per_page, true, $search);
$show_video_views = setting_bool('public_show_video_views', true);
$show_total_views = setting_bool('public_show_total_views', true);
$show_page_stats = setting_bool('public_show_page_stats', true);
$total_public_views = total_video_views(true);
$public_stats = $show_page_stats ? public_page_stats() : [
'videos' => (int)$paged['total'],
'sources' => 0,
'views' => $total_public_views,
];
render_head('Videos — TyClifford.com', '
.cat-grid {
display:grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap:1rem;
}
@media(min-width:600px) { .cat-grid { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); } }
@media(min-width:1000px) { .cat-grid { grid-template-columns: repeat(4, 1fr); } }
.cat-thumb {
background:var(--surface-2); border:1px solid var(--border);
border-radius:calc(var(--r)+2px); overflow:hidden; text-decoration:none;
color:var(--text); display:flex; flex-direction:column;
transition:border-color .2s, transform .15s, box-shadow .2s;
position:relative; cursor:pointer;
}
.cat-thumb:hover { border-color:var(--purple); transform:translateY(-2px); box-shadow:0 6px 24px rgba(176,96,255,.18); }
.cat-thumb-img { position:relative; width:100%; padding-top:56.25%; background:#000; overflow:hidden; }
.cat-thumb-img img { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; transition:transform .3s; }
.cat-thumb:hover .cat-thumb-img img { transform:scale(1.04); }
.cat-thumb-img .no-thumb {
position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
font-family:"Share Tech Mono",monospace; font-size:.6rem; color:var(--text-3); letter-spacing:.1em;
background:var(--surface);
}
.cat-thumb-dur {
position:absolute; bottom:4px; right:6px;
background:rgba(0,0,0,.75); backdrop-filter:blur(4px);
font-family:"Share Tech Mono",monospace; font-size:.55rem;
color:#fff; padding:.15em .45em; border-radius:3px;
}
.play-overlay {
position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
background:rgba(0,0,0,.35); opacity:0; transition:opacity .2s;
}
.cat-thumb:hover .play-overlay { opacity:1; }
.cat-thumb-body { padding:.65rem .75rem; display:flex; flex-direction:column; gap:.25rem; flex:1; }
.cat-thumb-title { font-size:.85rem; font-weight:600; color:var(--text); line-height:1.35; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.cat-thumb-desc { font-size:.75rem; color:var(--text-3); line-height:1.5; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.cat-thumb-meta { font-family:"Share Tech Mono",monospace; font-size:.6rem; color:var(--text-3); letter-spacing:.06em; margin-top:auto; padding-top:.4rem; }
.page-header { display:flex; flex-direction:column; gap:.5rem; }
.page-header-row { display:flex; align-items:center; justify-content:space-between; gap:1rem; flex-wrap:wrap; }
.public-search { display:flex; gap:.5rem; width:100%; max-width:460px; }
.public-search input { flex:1; min-width:0; }
@media(max-width:560px) { .public-search { max-width:none; } }
.stats-panel {
background:var(--surface); border:1px solid var(--border);
border-radius:calc(var(--r)+2px); padding:1rem 1.1rem;
display:grid; grid-template-columns:repeat(auto-fit,minmax(130px,1fr)); gap:.75rem;
}
.stats-item { display:flex; flex-direction:column; gap:.25rem; }
.stats-label { font-family:"Share Tech Mono",monospace; font-size:.6rem; letter-spacing:.12em; text-transform:uppercase; color:var(--text-3); }
.stats-value { font-family:"Share Tech Mono",monospace; font-size:1rem; color:var(--text); }
.cat-footer { display:flex; align-items:center; justify-content:space-between; gap:1rem; flex-wrap:wrap; margin-top:.5rem; }
.cat-count { font-family:"Share Tech Mono",monospace; font-size:.65rem; color:var(--text-3); letter-spacing:.06em; }
.empty-state { padding:4rem 1rem; text-align:center; display:flex; flex-direction:column; align-items:center; gap:1rem; }
.empty-state svg { color:var(--text-3); opacity:.35; }
.empty-state p { font-family:"Share Tech Mono",monospace; font-size:.75rem; color:var(--text-3); letter-spacing:.1em; }
');
?>
<main class="page" role="main">
<?php render_topbar('catalogue'); ?>
<div class="page-header">
<div class="page-header-row">
<div>
<p class="eyebrow eyebrow-purple" style="margin-bottom:.3rem">Library</p>
<h1 style="font-size:1.1rem;font-weight:700;color:var(--text);">Video Catalogue</h1>
</div>
<?php
$header_parts = [
$paged['total'] . ' ' . ($search ? 'result' : 'video') . ($paged['total'] != 1 ? 's' : ''),
];
if ($show_total_views) $header_parts[] = view_count_label($total_public_views) . ' total';
?>
<span class="cat-count">
<?= h(implode(' · ', $header_parts)) ?>
</span>
</div>
<form class="public-search" method="get" action="<?= h(public_catalogue_url()) ?>" role="search">
<input class="form-input" type="search" name="q" value="<?= h($search) ?>" placeholder="Search public videos…">
<button class="btn btn-secondary btn-sm" type="submit">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
Search
</button>
<?php if ($search): ?>
<a href="<?= h(public_catalogue_url()) ?>" class="btn btn-secondary btn-sm">Clear</a>
<?php endif; ?>
</form>
</div>
<?php if ($show_page_stats): ?>
<section class="stats-panel" aria-label="Public catalogue statistics">
<div class="stats-item">
<span class="stats-label">Public Videos</span>
<strong class="stats-value"><?= h(format_count((int)$public_stats['videos'])) ?></strong>
</div>
<?php if ($show_total_views): ?>
<div class="stats-item">
<span class="stats-label">Total Views</span>
<strong class="stats-value"><?= h(format_count((int)$public_stats['views'])) ?></strong>
</div>
<?php endif; ?>
<div class="stats-item">
<span class="stats-label">Sources</span>
<strong class="stats-value"><?= h(format_count((int)$public_stats['sources'])) ?></strong>
</div>
</section>
<?php endif; ?>
<?php if (empty($paged['videos'])): ?>
<div class="card">
<div class="empty-state">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
<p><?= $search ? 'no public videos matched your search' : 'no videos in the catalogue yet' ?></p>
</div>
</div>
<?php else: ?>
<div class="cat-grid">
<?php foreach ($paged['videos'] as $v):
$vslug = $v['slug'];
$vtitle = $v['title'];
$vdesc = $v['description'];
$vthumb = $v['thumbnail'];
$vdur = (int)$v['duration'];
$created = substr($v['created_at'], 0, 10);
$meta_parts = [$created];
if ($vdur) $meta_parts[] = format_duration($vdur);
if ($show_video_views) $meta_parts[] = view_count_label((int)$v['view_count']);
?>
<a class="cat-thumb" href="<?= h(public_video_url($vslug)) ?>" title="<?= h($vtitle) ?>">
<div class="cat-thumb-img">
<?php if ($vthumb): ?>
<img src="<?= h(public_media_url($vthumb)) ?>" alt="<?= h($vtitle) ?>" loading="lazy">
<?php else: ?>
<div class="no-thumb">NO THUMBNAIL</div>
<?php endif; ?>
<?php if ($vdur): ?>
<span class="cat-thumb-dur"><?= format_duration($vdur) ?></span>
<?php endif; ?>
<div class="play-overlay">
<svg width="36" height="36" viewBox="0 0 24 24" fill="white"><circle cx="12" cy="12" r="12" fill="rgba(0,0,0,.4)"/><polygon points="10,8 18,12 10,16" fill="white"/></svg>
</div>
</div>
<div class="cat-thumb-body">
<span class="cat-thumb-title"><?= h($vtitle) ?></span>
<?php if ($vdesc): ?>
<span class="cat-thumb-desc"><?= h($vdesc) ?></span>
<?php endif; ?>
<span class="cat-thumb-meta"><?= h(implode(' · ', $meta_parts)) ?></span>
</div>
</a>
<?php endforeach; ?>
</div>
<?php if ($paged['total_pages'] > 1): ?>
<div class="cat-footer">
<span class="cat-count">page <?= $paged['page'] ?> of <?= $paged['total_pages'] ?></span>
<?php render_pagination($paged['page'], $paged['total_pages'], public_catalogue_page_url_pattern($search)); ?>
</div>
<?php endif; ?>
<?php endif; ?>
<?php render_footer(); ?>
</main>
</body>
</html>