' . $m[0] . ' (opens in new tab)'; }, $html); return nl2br($linked ?? $html, false); } function nice_title($s){ return trim(ucwords(str_replace(['_','-'], ' ', (string)$s))); } function lower_text($s){ return function_exists('mb_strtolower') ? mb_strtolower((string)$s, 'UTF-8') : strtolower((string)$s); } function fold_text($s){ $s = strtr((string)$s, [ 'æ'=>'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)) : '/'; } 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 shown_video_paths($config){ $videos = []; $seen = []; foreach (glob($config['uploads_dir'].'/*', GLOB_ONLYDIR) ?: [] as $dir) { // Public index shows only items explicitly enabled with the admin "Show" switch. if (is_file($dir . '/.mvlog-hidden')) continue; $stateFile = $dir . '/.movmaker-state.json'; if (!is_file($stateFile)) continue; $state = json_decode((string)file_get_contents($stateFile), true); if (!is_array($state)) continue; $name = basename((string)($state['output'] ?? '')); if ($name === '' || preg_match('/_preview\.(mp4|webm|mov|m4v)$/i', $name)) continue; $path = $config['videos_dir'] . '/' . $name; if (!is_file($path) || isset($seen[$name])) continue; $seen[$name] = true; $videos[] = $path; } return $videos; } function permalink_video_paths($config, $articleId){ $articleId = strtolower(trim((string)$articleId)); if (!preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $articleId)) return []; foreach (glob($config['uploads_dir'].'/*', GLOB_ONLYDIR) ?: [] as $dir) { if (!is_file($dir . '/.mvlog-permalink')) continue; if (read_article_id($dir) !== $articleId) continue; $stateFile = $dir . '/.movmaker-state.json'; if (!is_file($stateFile)) return []; $state = json_decode((string)file_get_contents($stateFile), true); if (!is_array($state)) return []; $name = basename((string)($state['output'] ?? '')); if ($name === '' || preg_match('/_preview\.(mp4|webm|mov|m4v)$/i', $name)) return []; $path = $config['videos_dir'] . '/' . $name; return is_file($path) ? [$path] : []; } return []; } 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 input_dirs($base){ return glob($base.'/*', GLOB_ONLYDIR) ?: []; } function read_article_id($dir){ $idFile = rtrim((string)$dir, '/').'/.mvlog-id'; if (!is_file($idFile)) return ''; $id = trim((string)file_get_contents($idFile)); return preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $id) ? $id : ''; } function input_dir_permalink($dir){ return is_file($dir . '/.mvlog-permalink'); } function article_permalink_enabled($config, $articleId){ static $cache = null; if ($cache === null) { $cache = []; foreach (input_dirs($config['uploads_dir']) as $dir) { $id = read_article_id($dir); if ($id !== '') $cache[$id] = input_dir_permalink($dir); } } $articleId = strtolower(trim((string)$articleId)); if (!preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $articleId)) return false; return !empty($cache[$articleId]); } function permalink_slug_from_title($title, $fallback = 'post'){ $slug = strtr((string)$title, [ 'æ'=>'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')) { $ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $slug); if ($ascii !== false) $slug = $ascii; } $slug = strtolower($slug); $slug = preg_replace('/[^a-z0-9]+/', '-', $slug) ?? ''; $slug = trim($slug, '-'); return $slug !== '' ? $slug : $fallback; } function permalink_title_for_dir($config, $dir, $videoCache = []){ $dataFile = rtrim((string)$dir, '/') . '/data.txt'; if (is_file($dataFile)) { foreach (file($dataFile, FILE_IGNORE_NEW_LINES) ?: [] as $line) { if (!str_contains($line, ':')) continue; [$key, $value] = array_map('trim', explode(':', $line, 2)); if (in_array(strtolower($key), ['title','name'], true) && $value !== '') return $value; } } $stateFile = rtrim((string)$dir, '/') . '/.movmaker-state.json'; $state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : []; $output = is_array($state) ? basename((string)($state['output'] ?? '')) : ''; if ($output !== '') { $path = $config['videos_dir'] . '/' . $output; if (is_file($path)) { $meta = $videoCache[$output]['metadata'] ?? video_metadata($path); $title = trim((string)($meta['title'] ?? '')); if ($title !== '') return $title; } } return nice_title(basename((string)$dir)); } function permalink_slug_map($config, $videoCache = []){ $map = []; $used = []; $dirs = input_dirs($config['uploads_dir']); sort($dirs, SORT_STRING); foreach ($dirs as $dir) { if (!input_dir_permalink($dir)) continue; $id = read_article_id($dir); if ($id === '') continue; $base = permalink_slug_from_title(permalink_title_for_dir($config, $dir, $videoCache), $id); $slug = $base; if (isset($used[$slug])) $slug = $base . '-' . substr($id, -6); $n = 2; while (isset($used[$slug])) { $slug = $base . '-' . substr($id, -6) . '-' . $n; $n++; } $used[$slug] = true; $map[$slug] = $id; } return $map; } function article_permalink_url($config, $articleId, $videoCache = [], $absolute = false){ $articleId = strtolower(trim((string)$articleId)); if (!preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $articleId)) return ''; foreach (permalink_slug_map($config, $videoCache) as $slug => $id) { if ($id === $articleId) { $path = '/post/' . rawurlencode($slug); return $absolute ? ('https://bubulescu.org' . $path) : $path; } } return ''; } function article_id_for_permalink_slug($config, $slug, $videoCache = []){ $slug = strtolower(trim((string)$slug, "/ \t\n\r\0\x0B")); if (!preg_match('/^[a-z0-9][a-z0-9-]*$/', $slug)) return ''; $map = permalink_slug_map($config, $videoCache); return $map[$slug] ?? ''; } 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); $allVideos = shown_video_paths($config); $videoCache = load_video_cache($allVideos); usort($allVideos, 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; }); $postSlug = trim((string)($_GET['post'] ?? '')); $articleId = ''; if ($postSlug !== '') { $articleId = article_id_for_permalink_slug($config, $postSlug, $videoCache); if ($articleId === '') render_404_page(); } else { $articleId = trim((string)($_GET['id'] ?? '')); if ($articleId !== '' && !preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $articleId)) { render_404_page(); } if ($articleId !== '' && article_permalink_enabled($config, $articleId)) { $canonicalPath = article_permalink_url($config, $articleId, $videoCache, false); if ($canonicalPath !== '') { header('Location: ' . $canonicalPath, true, 301); exit; } } } if ($articleId !== '') { if (!article_permalink_enabled($config, $articleId)) { render_404_page(); } // Permalink pages are allowed when Permalink is ON, even if Show is OFF. $videos = permalink_video_paths($config, $articleId); $rawKeyword = ''; $keywordType = 'general'; $keywordValue = ''; } else { $videos = array_values(array_filter($allVideos, fn($v) => empty($hiddenOutputs[basename($v)]))); $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) { if ($articleId === '') { render_blank_page(); } render_404_page(); } $activeFilterCount = ($keywordValue !== '') ? 1 : 0; $baseParams = ($rawKeyword !== '') ? ['q' => $rawKeyword] : []; $defaultOgImageUrl = 'https://bubulescu.org/assets/img/bubulescuorg.jpg'; $ogImageUrl = $defaultOgImageUrl; $ogImageAlt = 'journal of an unreliable narrator.'; $ogType = ($articleId !== '') ? 'article' : 'website'; $canonicalArticleUrl = ($articleId !== '') ? article_permalink_url($config, $articleId, $videoCache, true) : ''; $ogUrl = ($canonicalArticleUrl !== '') ? $canonicalArticleUrl : 'https://bubulescu.org/'; $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 = $firstTitle; $pageTitle = $firstTitle; } } } $headerTitles = require __DIR__ . '/header_titles.php'; $siteHeaderTitle = $headerTitles[array_rand($headerTitles)]; ?> <?=h($pageTitle)?>
×

No videos found for current filter.

'date=' . (string)($m['date'] ?? '')]); $locFilterUrl = query_url(['q' => 'location=' . (string)($m['location'] ?? '')]); ?>
1) $linkParams['page'] = $i; ?>