archive-ccc8458/video/lib/hash.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
<?php
declare(strict_types=1);
require_once __DIR__ . '/config.php';
function generate_video_hash(): string {
// 10 hex chars = 40 bits of randomness. Plenty unguessable for a personal host.
return bin2hex(random_bytes(5));
}
function is_valid_hash(string $h): bool {
return (bool) preg_match('/^[a-f0-9]{10}$/', $h);
}
function find_video_file(string $hash): ?string {
if (!is_valid_hash($hash)) return null;
foreach (ALLOWED_TYPES as $_ => $ext) {
$p = VIDEO_DIR . '/' . $hash . '.' . $ext;
if (is_file($p)) return $p;
}
return null;
}
function find_video_url(string $hash): ?string {
if (!is_valid_hash($hash)) return null;
foreach (ALLOWED_TYPES as $_ => $ext) {
if (is_file(VIDEO_DIR . '/' . $hash . '.' . $ext)) {
return VIDEO_URL . '/' . $hash . '.' . $ext;
}
}
return null;
}
function find_thumb_file(string $hash): ?string {
if (!is_valid_hash($hash)) return null;
foreach (ALLOWED_THUMB_TYPES as $_ => $ext) {
$p = VIDEO_DIR . '/' . $hash . '.' . $ext;
if (is_file($p)) return $p;
}
return null;
}
function find_thumb_url(string $hash): ?string {
if (!is_valid_hash($hash)) return null;
foreach (ALLOWED_THUMB_TYPES as $_ => $ext) {
if (is_file(VIDEO_DIR . '/' . $hash . '.' . $ext)) {
return VIDEO_URL . '/' . $hash . '.' . $ext;
}
}
return null;
}
// --- Unlisted marker: a zero-byte "<hash>.unlisted" sidecar in the videos dir.
// Unlisted videos stay reachable by direct link but are hidden from the public grid.
function unlisted_marker_path(string $hash): ?string {
if (!is_valid_hash($hash)) return null;
return VIDEO_DIR . '/' . $hash . '.unlisted';
}
function is_unlisted(string $hash): bool {
$p = unlisted_marker_path($hash);
return $p !== null && is_file($p);
}
function set_unlisted(string $hash, bool $unlisted): void {
$p = unlisted_marker_path($hash);
if ($p === null) return;
if ($unlisted) {
@file_put_contents($p, '');
@chmod($p, 0644);
} elseif (is_file($p)) {
@unlink($p);
}
}
/**
* Scan the videos directory and return one entry per video, newest first.
* Absolute URLs (prefixed with SITE_BASE) so the result is usable as-is by
* both the JSON API and server-rendered pages. Pass false to omit unlisted
* videos (used by the public grid).
*/
function list_videos(bool $includeUnlisted = true): array {
$items = [];
if (is_dir(VIDEO_DIR)) {
foreach (scandir(VIDEO_DIR) ?: [] as $name) {
if ($name === '.' || $name === '..') continue;
if (!preg_match('/^([a-f0-9]{10})\.(mp4|webm|mov|mkv)$/', $name, $m)) continue;
$hash = $m[1];
$unlisted = is_unlisted($hash);
if (!$includeUnlisted && $unlisted) continue;
$path = VIDEO_DIR . '/' . $name;
$thumb = find_thumb_url($hash);
$items[] = [
'hash' => $hash,
'filename' => $name,
'size' => filesize($path) ?: 0,
'uploaded' => filemtime($path) ?: 0,
'url' => SITE_BASE . '/video/' . $hash,
'embed' => SITE_BASE . '/video/' . $hash . '/embed',
'file' => SITE_BASE . VIDEO_URL . '/' . $name,
'thumbnail' => $thumb !== null ? SITE_BASE . $thumb : null,
'unlisted' => $unlisted,
];
}
}
usort($items, fn($a, $b) => $b['uploaded'] <=> $a['uploaded']);
return $items;
}
/**
* Offset-paginated view over list_videos(). $count <= 0 returns everything from
* $offset onward (back-compat for clients that don't paginate). Returns the page
* slice plus the grand total so callers can render page controls.
*/
function paginate_videos(bool $includeUnlisted, int $offset, int $count): array {
$all = list_videos($includeUnlisted);
$total = count($all);
$offset = max(0, $offset);
$slice = $count > 0
? array_slice($all, $offset, $count)
: array_slice($all, $offset);
return [
'videos' => $slice,
'total' => $total,
'offset' => $offset,
'count' => $count,
];
}