archive-ccc8458/video/lib/hash.php 4.2 KB
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,
    ];
}