Files
mediaplayer/catalogue.php
T
Ty Clifford 2d52255b93 - 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).
2026-06-25 04:40:59 -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(video_total_views($v));
?>
<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>