- 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:
+9
-2
@@ -42,6 +42,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$duration = (int)($_POST['duration'] ?? 0);
|
||||
$sort_order = (int)($_POST['sort_order'] ?? 0);
|
||||
$view_count = max(0, (int)($_POST['view_count'] ?? $video['view_count']));
|
||||
$published = isset($_POST['published']) ? 1 : 0;
|
||||
$auto_thumb = isset($_POST['auto_thumbnail']);
|
||||
|
||||
@@ -74,10 +75,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!$error) {
|
||||
$db->prepare("
|
||||
UPDATE videos SET title=?, description=?, slug=?, thumbnail=?,
|
||||
duration=?, sort_order=?, published=?,
|
||||
duration=?, sort_order=?, view_count=?, published=?,
|
||||
updated_at=datetime('now')
|
||||
WHERE id=?
|
||||
")->execute([$title, $description, $slug, $thumbnail, $duration, $sort_order, $published, $id]);
|
||||
")->execute([$title, $description, $slug, $thumbnail, $duration, $sort_order, $view_count, $published, $id]);
|
||||
|
||||
// Update existing sources sort/label/quality
|
||||
$upd_labels = $_POST['src_label'] ?? [];
|
||||
@@ -328,6 +329,12 @@ render_head('Edit Video — Admin', '
|
||||
<input class="form-input" type="number" id="sort_order" name="sort_order"
|
||||
value="<?= (int)$video['sort_order'] ?>" min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="view_count">View Count</label>
|
||||
<input class="form-input" type="number" id="view_count" name="view_count"
|
||||
value="<?= (int)$video['view_count'] ?>" min="0">
|
||||
<span class="hint">Use this to restore older local videos or override an imported external count.</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Published</label>
|
||||
<label style="display:flex;align-items:center;gap:.6rem;cursor:pointer;margin-top:.35rem;">
|
||||
|
||||
+59
-19
@@ -49,6 +49,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if ($result['not_found']) $msg .= ', ' . $result['not_found'] . ' without a readable original date';
|
||||
$msg .= '.';
|
||||
}
|
||||
|
||||
if ($action === 'restore_external_view_counts') {
|
||||
$limit = max(1, min(100, (int)($_POST['external_view_limit'] ?? 25)));
|
||||
$result = media_restore_external_view_counts($limit);
|
||||
$msg = 'Upkeeping checked ' . $result['checked'] . ' external video' . ($result['checked'] === 1 ? '' : 's') . ': ';
|
||||
$msg .= $result['restored'] . ' restored';
|
||||
if ($result['already_current']) $msg .= ', ' . $result['already_current'] . ' already current';
|
||||
if ($result['not_found']) $msg .= ', ' . $result['not_found'] . ' without a readable external count';
|
||||
$msg .= '.';
|
||||
}
|
||||
}
|
||||
|
||||
// Search + pagination
|
||||
@@ -68,7 +78,7 @@ $offset = ($page - 1) * $per_page;
|
||||
|
||||
$list_stmt = $db->prepare("
|
||||
SELECT v.id, v.title, v.slug, v.thumbnail, v.duration, v.published, v.sort_order,
|
||||
v.created_at,
|
||||
v.created_at, v.view_count,
|
||||
COUNT(vs.id) AS source_count,
|
||||
SUM(CASE WHEN vs.source_type='remote' THEN 1 ELSE 0 END) AS remote_count
|
||||
FROM videos v
|
||||
@@ -84,6 +94,8 @@ $list_stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||
$list_stmt->execute();
|
||||
$videos = $list_stmt->fetchAll();
|
||||
$youtube_timestamp_candidates = media_youtube_timestamp_candidate_count();
|
||||
$external_view_candidates = media_external_view_count_candidate_count();
|
||||
$admin_total_views = total_video_views(false);
|
||||
|
||||
$qs = fn($p) => '?' . http_build_query(array_filter(['q' => $search, 'page' => $p]));
|
||||
|
||||
@@ -143,7 +155,9 @@ render_head('Admin — TyClifford.com', '
|
||||
.rec-count { font-family:"Share Tech Mono",monospace; font-size:.63rem; color:var(--text-3); letter-spacing:.06em; }
|
||||
.section-heading { font-family:"Share Tech Mono",monospace; font-size:.65rem; letter-spacing:.15em; text-transform:uppercase; color:var(--text-3); margin-bottom:.75rem; }
|
||||
.hint { font-size:.72rem; color:var(--text-3); margin-top:.2rem; line-height:1.5; }
|
||||
.upkeep-stack { display:flex; flex-direction:column; gap:1rem; }
|
||||
.upkeep-row { display:flex; align-items:flex-end; justify-content:space-between; gap:1rem; flex-wrap:wrap; }
|
||||
.upkeep-row + .upkeep-row { border-top:1px solid var(--border); padding-top:1rem; }
|
||||
.upkeep-copy { max-width:620px; }
|
||||
.upkeep-form { display:flex; align-items:flex-end; gap:.65rem; flex-wrap:wrap; }
|
||||
.upkeep-form .form-group { min-width:120px; }
|
||||
@@ -202,24 +216,46 @@ render_head('Admin — TyClifford.com', '
|
||||
|
||||
<div class="card">
|
||||
<p class="section-heading">Upkeeping</p>
|
||||
<div class="upkeep-row">
|
||||
<div class="upkeep-copy">
|
||||
<h2 class="card-title">Correct YouTube timestamps</h2>
|
||||
<p class="hint">
|
||||
Fetches original YouTube publish dates and updates the database timestamp shown in the admin list and public catalogue.
|
||||
<?= $youtube_timestamp_candidates ?> YouTube-backed video<?= $youtube_timestamp_candidates === 1 ? '' : 's' ?> currently match this upkeep task.
|
||||
</p>
|
||||
</div>
|
||||
<form class="upkeep-form" method="post" action="index.php">
|
||||
<input type="hidden" name="action" value="correct_youtube_timestamps">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="timestamp_limit">Batch Size</label>
|
||||
<input class="form-input" type="number" id="timestamp_limit" name="timestamp_limit" value="25" min="1" max="100">
|
||||
<div class="upkeep-stack">
|
||||
<div class="upkeep-row">
|
||||
<div class="upkeep-copy">
|
||||
<h2 class="card-title">Correct YouTube timestamps</h2>
|
||||
<p class="hint">
|
||||
Fetches original YouTube publish dates and updates the database timestamp shown in the admin list and public catalogue.
|
||||
<?= $youtube_timestamp_candidates ?> YouTube-backed video<?= $youtube_timestamp_candidates === 1 ? '' : 's' ?> currently match this upkeep task.
|
||||
</p>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-sm" type="submit">
|
||||
Correct Dates
|
||||
</button>
|
||||
</form>
|
||||
<form class="upkeep-form" method="post" action="index.php">
|
||||
<input type="hidden" name="action" value="correct_youtube_timestamps">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="timestamp_limit">Batch Size</label>
|
||||
<input class="form-input" type="number" id="timestamp_limit" name="timestamp_limit" value="25" min="1" max="100">
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-sm" type="submit">
|
||||
Correct Dates
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="upkeep-row">
|
||||
<div class="upkeep-copy">
|
||||
<h2 class="card-title">Restore external view counts</h2>
|
||||
<p class="hint">
|
||||
Scrapes readable counts from supported external providers and stores the higher value between the provider and local database.
|
||||
<?= $external_view_candidates ?> external video<?= $external_view_candidates === 1 ? '' : 's' ?> can be checked.
|
||||
Current database total: <?= h(view_count_label((int)$admin_total_views)) ?>.
|
||||
</p>
|
||||
</div>
|
||||
<form class="upkeep-form" method="post" action="index.php">
|
||||
<input type="hidden" name="action" value="restore_external_view_counts">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="external_view_limit">Batch Size</label>
|
||||
<input class="form-input" type="number" id="external_view_limit" name="external_view_limit" value="25" min="1" max="100">
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-sm" type="submit">
|
||||
Restore Views
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -253,6 +289,7 @@ render_head('Admin — TyClifford.com', '
|
||||
<th class="title-cell">Title / Slug</th>
|
||||
<th>Duration</th>
|
||||
<th>Sources</th>
|
||||
<th>Views</th>
|
||||
<th>Status</th>
|
||||
<th>Date</th>
|
||||
<th class="actions-cell">Actions</th>
|
||||
@@ -260,7 +297,7 @@ render_head('Admin — TyClifford.com', '
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($videos)): ?>
|
||||
<tr><td colspan="7" style="text-align:center;padding:3rem;font-family:'Share Tech Mono',monospace;font-size:.7rem;color:var(--text-3);">
|
||||
<tr><td colspan="8" style="text-align:center;padding:3rem;font-family:'Share Tech Mono',monospace;font-size:.7rem;color:var(--text-3);">
|
||||
<?= $search ? 'No videos match "' . h($search) . '"' : 'No videos yet. <a href="add.php" style="color:var(--purple)">Add one!</a>' ?>
|
||||
</td></tr>
|
||||
<?php else: ?>
|
||||
@@ -286,6 +323,9 @@ render_head('Admin — TyClifford.com', '
|
||||
<span style="color:var(--text-3);"> · <?= (int)$v['remote_count'] ?> URL<?= (int)$v['remote_count'] != 1 ? 's' : '' ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="font-family:'Share Tech Mono',monospace;font-size:.7rem;color:var(--text-2);">
|
||||
<?= h(format_count((int)$v['view_count'])) ?>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge <?= $v['published'] ? 'badge-green' : 'badge-gray' ?>">
|
||||
<?= $v['published'] ? 'Published' : 'Draft' ?>
|
||||
|
||||
@@ -14,11 +14,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$site_sub = trim($_POST['site_sub'] ?? 'tyclifford.com / media');
|
||||
$url_mode = $_POST['url_rewrite_mode'] ?? 'pretty';
|
||||
if (!in_array($url_mode, ['pretty', 'query'], true)) $url_mode = 'pretty';
|
||||
$show_video_views = isset($_POST['public_show_video_views']) ? '1' : '0';
|
||||
$show_total_views = isset($_POST['public_show_total_views']) ? '1' : '0';
|
||||
$show_page_stats = isset($_POST['public_show_page_stats']) ? '1' : '0';
|
||||
|
||||
set_setting('catalogue_per_page', (string)$per_page);
|
||||
set_setting('site_title', $site_title);
|
||||
set_setting('site_sub', $site_sub);
|
||||
set_setting('url_rewrite_mode', $url_mode);
|
||||
set_setting('public_show_video_views', $show_video_views);
|
||||
set_setting('public_show_total_views', $show_total_views);
|
||||
set_setting('public_show_page_stats', $show_page_stats);
|
||||
$msg = 'Settings saved.';
|
||||
}
|
||||
|
||||
@@ -44,11 +50,15 @@ $per_page = setting('catalogue_per_page', '10');
|
||||
$site_title = setting('site_title', 'Ty Clifford');
|
||||
$site_sub = setting('site_sub', 'tyclifford.com / media');
|
||||
$url_rewrite_mode = url_rewrite_mode();
|
||||
$public_show_video_views = setting_bool('public_show_video_views', true);
|
||||
$public_show_total_views = setting_bool('public_show_total_views', true);
|
||||
$public_show_page_stats = setting_bool('public_show_page_stats', true);
|
||||
|
||||
// DB stats
|
||||
$db = get_db();
|
||||
$total_videos = $db->query("SELECT COUNT(*) FROM videos")->fetchColumn();
|
||||
$total_sources = $db->query("SELECT COUNT(*) FROM video_sources")->fetchColumn();
|
||||
$total_views = total_video_views(false);
|
||||
$db_size = file_exists(DB_PATH) ? round(filesize(DB_PATH)/1024, 1) : 0;
|
||||
$media_size = 0;
|
||||
if (is_dir(MEDIA_DIR)) {
|
||||
@@ -76,6 +86,11 @@ render_head('Settings — Admin', '
|
||||
.stat-tile { background:var(--surface-2); border:1px solid var(--border); border-radius:var(--r); padding:1rem; display:flex; flex-direction:column; gap:.3rem; }
|
||||
.stat-val { font-family:"Share Tech Mono",monospace; font-size:1.4rem; color:var(--text); font-weight:700; }
|
||||
.stat-label{ font-family:"Share Tech Mono",monospace; font-size:.6rem; letter-spacing:.1em; text-transform:uppercase; color:var(--text-3); }
|
||||
.option-list { display:grid; grid-template-columns:1fr; gap:.65rem; margin:0 0 1rem; }
|
||||
@media(min-width:700px) { .option-list { grid-template-columns:repeat(3,1fr); } }
|
||||
.check-option { display:flex; align-items:flex-start; gap:.55rem; background:var(--surface-2); border:1px solid var(--border); border-radius:var(--r); padding:.75rem; color:var(--text-2); font-size:.78rem; line-height:1.45; }
|
||||
.check-option input { margin-top:.15rem; accent-color:var(--purple); }
|
||||
.check-option strong { display:block; color:var(--text); font-size:.8rem; margin-bottom:.1rem; }
|
||||
');
|
||||
?>
|
||||
<main class="page" role="main">
|
||||
@@ -135,6 +150,10 @@ render_head('Settings — Admin', '
|
||||
<span class="stat-val"><?= $total_sources ?></span>
|
||||
<span class="stat-label">Sources</span>
|
||||
</div>
|
||||
<div class="stat-tile">
|
||||
<span class="stat-val"><?= h(format_count((int)$total_views)) ?></span>
|
||||
<span class="stat-label">Views</span>
|
||||
</div>
|
||||
<div class="stat-tile">
|
||||
<span class="stat-val"><?= $db_size ?> KB</span>
|
||||
<span class="stat-label">Database</span>
|
||||
@@ -177,6 +196,30 @@ render_head('Settings — Admin', '
|
||||
<span class="hint">Default routes are /v/<slug>, /all, and /search/<query>. Pretty URLs require Apache mod_rewrite and the included .htaccess.</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="form-label" style="margin-bottom:.55rem;">Public Display</p>
|
||||
<div class="option-list">
|
||||
<label class="check-option">
|
||||
<input type="checkbox" name="public_show_video_views" value="1" <?= $public_show_video_views ? 'checked' : '' ?>>
|
||||
<span>
|
||||
<strong>Video view counts</strong>
|
||||
Show each video's view count on public cards and the player.
|
||||
</span>
|
||||
</label>
|
||||
<label class="check-option">
|
||||
<input type="checkbox" name="public_show_total_views" value="1" <?= $public_show_total_views ? 'checked' : '' ?>>
|
||||
<span>
|
||||
<strong>Total view count</strong>
|
||||
Show the combined public view total.
|
||||
</span>
|
||||
</label>
|
||||
<label class="check-option">
|
||||
<input type="checkbox" name="public_show_page_stats" value="1" <?= $public_show_page_stats ? 'checked' : '' ?>>
|
||||
<span>
|
||||
<strong>Page statistics</strong>
|
||||
Show public library stats on the homepage and catalogue.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" type="submit">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17,21 17,13 7,13 7,21"/><polyline points="7,3 7,8 15,8"/></svg>
|
||||
Save Settings
|
||||
|
||||
Reference in New Issue
Block a user