movmaker-webui/index.php
2026-05-25 22:28:14 +02:00

94 lines
5.9 KiB
PHP

<?php
$config = require __DIR__ . "/config.php";
foreach (["videos_dir", "thumbs_dir", "uploads_dir"] as $d) if (!is_dir($config[$d])) mkdir($config[$d], 0775, true);
if (!is_dir(__DIR__ . "/cache")) mkdir(__DIR__ . "/cache", 0775, true);
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, "UTF-8"); }
function nice_title($s){ return trim(ucwords(str_replace(['_','-'], ' ', $s))); }
function video_metadata($path){
$base = pathinfo($path, PATHINFO_FILENAME);
$meta = ['title' => nice_title($base), 'date' => date('Y-m-d', filemtime($path)), 'sort_date' => date('Y-m-d', filemtime($path)), 'location' => '', 'description' => ''];
if (preg_match('/^(\d{4})(\d{2})(\d{2})[_-](.+)$/', $base, $m)) {
$meta['date'] = "$m[1]-$m[2]-$m[3]";
$meta['sort_date'] = "$m[1]-$m[2]-$m[3]";
$meta['title'] = nice_title($m[4]);
}
foreach ([dirname($path).'/'.$base.'.txt', dirname($path).'/'.$base.'.data.txt'] as $sidecar) {
if (!is_file($sidecar)) continue;
foreach (file($sidecar, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
if (!str_contains($line, ':')) continue;
[$k, $v] = array_map('trim', explode(':', $line, 2));
$k = strtolower($k);
if (in_array($k, ['title','date','location','place','description'], true)) $meta[$k === 'place' ? 'location' : $k] = $v;
}
}
$ffprobe = trim((string)shell_exec('command -v ffprobe 2>/dev/null'));
if ($ffprobe !== '') {
$json = shell_exec(escapeshellarg($ffprobe).' -v quiet -print_format json -show_entries format_tags '.escapeshellarg($path).' 2>/dev/null');
$data = json_decode((string)$json, true);
$tags = $data['format']['tags'] ?? [];
$lower = [];
foreach ($tags as $k => $v) $lower[strtolower($k)] = $v;
if (!empty($lower['title'])) $meta['title'] = $lower['title'];
if (!empty($lower['date'])) {
$meta['date'] = $lower['date'];
}
if (!empty($lower['creation_time'])) {
$meta['sort_date'] = substr($lower['creation_time'], 0, 10);
if (empty($lower['date'])) $meta['date'] = $meta['sort_date'];
}
foreach (['location','place','location-eng','com.apple.quicktime.location.name','com.apple.quicktime.location.iso6709'] as $k) {
if (!empty($lower[$k])) { $meta['location'] = $lower[$k]; break; }
}
foreach (['description','synopsis'] as $k) {
if (!empty($lower[$k])) { $meta['description'] = $lower[$k]; break; }
}
}
return $meta;
}
function load_video_cache($videos){
$cacheFile = __DIR__ . '/cache/videos.json';
$cache = is_file($cacheFile) ? json_decode((string)file_get_contents($cacheFile), true) : [];
if (!is_array($cache) || ($cache['version'] ?? 0) !== 2) $cache = [];
$items = $cache['items'] ?? [];
$newItems = [];
$changed = false;
foreach ($videos as $path) {
$key = basename($path);
$mtime = filemtime($path);
$size = filesize($path);
$cached = $items[$key] ?? null;
if (!is_array($cached) || ($cached['mtime'] ?? null) !== $mtime || ($cached['size'] ?? null) !== $size) {
$cached = ['mtime' => $mtime, 'size' => $size, 'metadata' => video_metadata($path)];
$changed = true;
}
$newItems[$key] = $cached;
}
if (array_diff(array_keys($items), array_keys($newItems))) $changed = true;
if ($changed) {
file_put_contents($cacheFile, json_encode(['version' => 2, 'generated_at' => date('c'), 'items' => $newItems], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX);
}
return $newItems;
}
$videos = glob($config['videos_dir'].'/*.{mp4,webm,mov,m4v}', GLOB_BRACE) ?: [];
$videoCache = load_video_cache($videos);
usort($videos, function($a, $b) use ($videoCache) {
$am = $videoCache[basename($a)] ?? [];
$bm = $videoCache[basename($b)] ?? [];
$ad = strtotime($am['metadata']['sort_date'] ?? $am['metadata']['date'] ?? '') ?: filemtime($a);
$bd = strtotime($bm['metadata']['sort_date'] ?? $bm['metadata']['date'] ?? '') ?: filemtime($b);
return $bd <=> $ad;
});
$page=max(1,(int)($_GET['page']??1)); $per=$config['items_per_page']; $total=count($videos); $pages=max(1,(int)ceil($total/$per)); $page=min($page,$pages); $slice=array_slice($videos,($page-1)*$per,$per);
?>
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>MVLog</title><link rel="alternate" type="application/rss+xml" title="MVLog RSS" href="feed.php"><link rel="stylesheet" href="style.css"></head><body>
<header class="site-header"><a class="brand" href="index.php"><h1>MVLog</h1><p>Bubulescu.Org</p></a><a class="rss-link" href="feed.php" aria-label="RSS feed" title="RSS feed"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6.2 17.8a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM2.2 8.7v3.1a10 10 0 0 1 10 10h3.1A13.1 13.1 0 0 0 2.2 8.7Zm0-5.5v3.1a15.5 15.5 0 0 1 15.5 15.5h3.1A18.6 18.6 0 0 0 2.2 3.2Z"/></svg></a></header><main>
<section id="videos"><?php if(!$slice): ?><p>No videos yet.</p><?php endif; ?><div class="video-list">
<?php foreach($slice as $v): $name=basename($v); $url=$config['public_videos'].'/'.rawurlencode($name); $m=$videoCache[$name]['metadata'] ?? video_metadata($v); ?>
<article class="video-row"><video controls controlsList="nodownload" oncontextmenu="return false" preload="metadata" src="<?=h($url)?>"></video><div class="video-info"><h2><?=h($m['title'])?></h2><p class="meta"><span><?=h($m['date'])?></span><?php if($m['location']): ?><span><?=h($m['location'])?></span><?php endif; ?></p><?php if($m['description']): ?><p class="description"><?=h($m['description'])?></p><?php endif; ?></div></article>
<?php endforeach; ?></div><div class="pages"><?php for($i=1;$i<=$pages;$i++): ?><a class="<?=$i===$page?'active':''?>" href="?page=<?=$i?>"><?=$i?></a><?php endfor; ?></div></section>
</main><footer>Bubulescu.Org</footer></body></html>