'ae','Æ'=>'ae','ø'=>'o','Ø'=>'o','å'=>'aa','Å'=>'aa',
'ä'=>'ae','Ä'=>'ae','ö'=>'oe','Ö'=>'oe','ü'=>'ue','Ü'=>'ue','ß'=>'ss','ẞ'=>'ss',
'č'=>'c','Č'=>'c','ć'=>'c','Ć'=>'c','ž'=>'z','Ž'=>'z','š'=>'s','Š'=>'s','đ'=>'d','Đ'=>'d',
]);
if (function_exists('iconv')) {
$x = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
if ($x !== false) $s = $x;
}
$s = lower_text($s);
return preg_replace('/[^a-z0-9]+/', ' ', $s) ?? '';
}
function ci_contains($haystack, $needle){
$haystack = (string)$haystack;
$needle = (string)$needle;
if ($needle === '') return true;
if (function_exists('mb_stripos') && mb_stripos($haystack, $needle, 0, 'UTF-8') !== false) return true;
if (strpos(lower_text($haystack), lower_text($needle)) !== false) return true;
$foldHay = fold_text($haystack);
$foldNeedle = trim(fold_text($needle));
if ($foldNeedle === '') return false;
return strpos($foldHay, $foldNeedle) !== false;
}
function keep_non_empty(array $params): array { return array_filter($params, fn($v)=>$v !== '' && $v !== null); }
function query_url(array $params): string {
$params = keep_non_empty($params);
return $params ? ('?' . http_build_query($params)) : 'index.php';
}
function hidden_video_outputs($config){
$hidden = [];
foreach (glob($config['uploads_dir'].'/*', GLOB_ONLYDIR) ?: [] as $dir) {
if (!is_file($dir . '/.mvlog-hidden')) continue;
$state = is_file($dir . '/.movmaker-state.json') ? json_decode((string)file_get_contents($dir . '/.movmaker-state.json'), true) : [];
if (is_array($state) && !empty($state['output'])) $hidden[basename((string)$state['output'])] = true;
}
return $hidden;
}
function map_hidden_outputs($config){
$hidden = [];
foreach (glob($config['uploads_dir'].'/*', GLOB_ONLYDIR) ?: [] as $dir) {
if (!is_file($dir . '/.mvlog-hide-map')) continue;
$state = is_file($dir . '/.movmaker-state.json') ? json_decode((string)file_get_contents($dir . '/.movmaker-state.json'), true) : [];
if (is_array($state) && !empty($state['output'])) $hidden[basename((string)$state['output'])] = true;
}
return $hidden;
}
function public_url_path($path){
$path = str_replace('\\', '/', trim((string)$path));
if ($path === '') return '';
$parts = array_values(array_filter(explode('/', $path), static fn($p) => $p !== ''));
return implode('/', array_map('rawurlencode', $parts));
}
function video_thumb_public_url($config, $videoName){
$videoName = basename((string)$videoName);
if ($videoName === '') return '';
$info = function_exists('video_thumb_source_info') ? video_thumb_source_info($config, $videoName) : [];
$sourcePath = (string)($info['source_path'] ?? '');
if ($sourcePath !== '') {
$abs = str_starts_with($sourcePath, '/') ? $sourcePath : __DIR__ . '/' . ltrim($sourcePath, '/');
if (is_file($abs)) return public_url_path($sourcePath) . '?v=' . filemtime($abs);
}
$sourceFile = basename((string)($info['source_file'] ?? ''));
$inputDir = basename((string)($info['input_dir'] ?? ''));
if ($sourceFile !== '' && $inputDir !== '') {
$rel = 'in-dir/' . $inputDir . '/' . $sourceFile;
$abs = __DIR__ . '/' . $rel;
if (is_file($abs)) return public_url_path($rel) . '?v=' . filemtime($abs);
}
return '';
}
function video_thumb_source_info($config, $videoName){
$videoName = basename((string)$videoName);
if ($videoName === '') return [];
$base = pathinfo($videoName, PATHINFO_FILENAME);
$file = $config['videos_dir'] . '/' . $base . '_thumb.json';
if (!is_file($file)) return [];
$data = json_decode((string)file_get_contents($file), true);
return is_array($data) ? $data : [];
}
function video_article_id($config, $videoName){
$videoName = basename((string)$videoName);
if ($videoName === '') return '';
foreach (glob($config['uploads_dir'].'/*', GLOB_ONLYDIR) ?: [] as $dir) {
$stateFile = $dir . '/.movmaker-state.json';
if (!is_file($stateFile)) continue;
$state = json_decode((string)file_get_contents($stateFile), true);
if (!is_array($state)) continue;
$output = basename((string)($state['output'] ?? ''));
$previewOutput = basename((string)($state['preview_output'] ?? ''));
if ($videoName === $output || $videoName === $previewOutput) return (string)($state['article_id'] ?? '');
}
return '';
}
function article_og_image_url($articleId){
$articleId = strtolower(trim((string)$articleId));
if (!preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $articleId)) return '';
$path = __DIR__ . '/out-dir/' . $articleId . '_og.jpg';
if (!is_file($path)) return '';
return 'https://bubulescu.org/out-dir/' . rawurlencode($articleId . '_og.jpg');
}
function render_404_page(){
http_response_code(404);
include __DIR__ . '/404.php';
exit;
}
function render_blank_page(){
$headerTitles = require __DIR__ . '/header_titles.php';
$siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
$brandUrl = '/';
$ogImageUrl = 'https://bubulescu.org/assets/img/bubulescuorg.jpg';
?>
MVLog
nice_title($base),
'teaser' => '',
'date' => date('Y-m-d', filemtime($path)),
'sort_date' => date('Y-m-d', filemtime($path)),
'location' => '',
'quote_da' => '',
'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','teaser','date','location','place','description','quote_da'], true)) {
$target = ($k === 'place') ? 'location' : $k;
$meta[$target] = $target === 'quote_da' ? trim((string)$v, "\"“”") : $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((string)$k)] = $v;
if (!empty($lower['title'])) $meta['title'] = $lower['title'];
if (!empty($lower['teaser'])) $meta['teaser'] = $lower['teaser'];
if (!empty($lower['date'])) $meta['date'] = $lower['date'];
if (!empty($lower['creation_time'])) {
$meta['sort_date'] = substr((string)$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; }
}
if (!empty($lower['quote_da'])) {
$meta['quote_da'] = trim((string)$lower['quote_da'], "\"“”");
}
}
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) !== 4) $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' => 4,
'generated_at' => date('c'),
'items' => $newItems
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX);
}
return $newItems;
}
$hiddenOutputs = hidden_video_outputs($config);
$hiddenMapOutputs = map_hidden_outputs($config);
$videos = array_values(array_filter(
glob($config['videos_dir'].'/*.{mp4,webm,mov,m4v}', GLOB_BRACE) ?: [],
fn($v)=>empty($hiddenOutputs[basename($v)]) && !preg_match('/_preview\.(mp4|webm|mov|m4v)$/i', basename($v))
));
$videoCache = load_video_cache($videos);
usort($videos, function($a, $b) use ($videoCache) {
$am = $videoCache[basename($a)] ?? [];
$bm = $videoCache[basename($b)] ?? [];
$ad = strtotime((string)($am['metadata']['sort_date'] ?? $am['metadata']['date'] ?? '')) ?: filemtime($a);
$bd = strtotime((string)($bm['metadata']['sort_date'] ?? $bm['metadata']['date'] ?? '')) ?: filemtime($b);
return $bd <=> $ad;
});
$articleId = trim((string)($_GET['id'] ?? ''));
if ($articleId !== '' && (!preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $articleId) || count(array_diff_key($_GET, ['id' => true])) > 0)) {
render_404_page();
}
if ($articleId === '') {
render_blank_page();
}
if ($articleId !== '') {
$videos = array_values(array_filter($videos, function($v) use ($articleId, $config) {
return video_article_id($config, basename($v)) === $articleId;
}));
$rawKeyword = '';
$keywordType = 'general';
$keywordValue = '';
} else {
$rawKeyword = trim((string)($_GET['q'] ?? ''));
// Backward compatibility with older filter/query formats.
if ($rawKeyword === '') {
$legacyTitle = trim((string)($_GET['f_title'] ?? ''));
$legacyDate = trim((string)($_GET['f_date'] ?? ''));
$legacyLocation = trim((string)($_GET['f_location'] ?? ''));
$legacyJob = trim((string)($_GET['job'] ?? ''));
if ($legacyTitle !== '') $rawKeyword = $legacyTitle;
elseif ($legacyDate !== '') $rawKeyword = 'date=' . $legacyDate;
elseif ($legacyLocation !== '') $rawKeyword = 'location=' . $legacyLocation;
elseif ($legacyJob !== '') {
$legacy = preg_replace('/^\d{8}[_-]/', '', $legacyJob);
$rawKeyword = nice_title($legacy ?: $legacyJob);
}
}
$keywordType = 'general';
$keywordValue = $rawKeyword;
if ($rawKeyword !== '' && preg_match('/^\s*(date|location|place)\s*=\s*(.+)\s*$/i', $rawKeyword, $m)) {
$keywordType = lower_text((string)$m[1]);
if ($keywordType === 'place') $keywordType = 'location';
$keywordValue = trim((string)$m[2]);
}
$videos = array_values(array_filter($videos, function($v) use ($videoCache, $keywordType, $keywordValue) {
if ($keywordValue === '') return true;
$name = basename($v);
$m = $videoCache[$name]['metadata'] ?? video_metadata($v);
$title = (string)($m['title'] ?? '');
$teaser = (string)($m['teaser'] ?? '');
$description = (string)($m['description'] ?? '');
$quoteDa = (string)($m['quote_da'] ?? '');
$date = (string)($m['date'] ?? '');
$sortDate = (string)($m['sort_date'] ?? '');
$location = (string)($m['location'] ?? '');
if ($keywordType === 'date') {
return ci_contains($date, $keywordValue) || ci_contains($sortDate, $keywordValue);
}
if ($keywordType === 'location') {
return ci_contains($location, $keywordValue);
}
return ci_contains($title, $keywordValue) || ci_contains($teaser, $keywordValue) || ci_contains($quoteDa, $keywordValue) || ci_contains($description, $keywordValue);
}));
}
$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);
if (!$slice) {
render_404_page();
}
$activeFilterCount = ($keywordValue !== '') ? 1 : 0;
$baseParams = ($rawKeyword !== '') ? ['q' => $rawKeyword] : [];
$defaultOgImageUrl = 'https://bubulescu.org/assets/img/bubulescuorg.jpg';
$ogImageUrl = $defaultOgImageUrl;
$ogImageAlt = 'MVLog: journal of an unreliable narrator.';
$pageTitle = 'MVLog';
if ($articleId !== '') {
$customOg = article_og_image_url($articleId);
if ($customOg !== '') {
$ogImageUrl = $customOg;
}
if (!empty($slice)) {
$firstVideo = basename((string)$slice[0]);
$firstMeta = $videoCache[$firstVideo]['metadata'] ?? video_metadata($slice[0]);
$firstTitle = trim((string)($firstMeta['title'] ?? ''));
if ($firstTitle !== '') {
$ogImageAlt = 'MVLog: ' . $firstTitle;
$pageTitle = 'MVLog: ' . $firstTitle;
}
}
}
$headerTitles = require __DIR__ . '/header_titles.php';
$siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
$brandUrl = '?id=' . rawurlencode($articleId);
?>
=h($pageTitle)?>
Showing =h((string)$total)?> article(s)
0): ?> with =h((string)$activeFilterCount)?> active filter(s)
No videos found for current filter.
$articleIdForVideo]) : '';
$url = $config['public_videos'].'/'.rawurlencode($name);
$m = $videoCache[$name]['metadata'] ?? video_metadata($v);
$posterUrl = video_thumb_public_url($config, $name);
$dateFilterUrl = query_url(['q' => 'date=' . (string)($m['date'] ?? '')]);
$locFilterUrl = query_url(['q' => 'location=' . (string)($m['location'] ?? '')]);
?>
=h($m['date'])?>
=h($m['location'])?>
=h((string)$m['teaser'])?>
=h($m['date'])?>
=h($m['location'])?>
=h((string)$m['teaser'])?>
=nl2br(h($m['description']), false)?>
“=h(trim((string)$m['quote_da'], "\"“”"))?>”
1) $linkParams['page'] = $i;
?>
=$i?>