diff --git a/index.php b/index.php deleted file mode 100644 index d7c6108..0000000 --- a/index.php +++ /dev/null @@ -1,500 +0,0 @@ -'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 video_metadata($path){ - $base = pathinfo($path, PATHINFO_FILENAME); - $meta = [ - 'title' => 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 !== '') { - $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); - -$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.'; -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; - } - } -} -$headerTitles = [ - 'Multiple Voices Log', - 'MultiVerse Log', - 'Miles & Views Log', - 'Moving Vistas Log', - 'Motorcycle Voyage Log', - 'Mapped Vagabond Log', - 'Mountain Valley Log', - 'Miles Versus Logic', - 'Motorcycle Voyage Log', - 'Mapped Vagabond Log', - 'Multiple Viewpoints Log', - 'Multiple Versions of Life', - 'Motion, Velocity & Luck', - 'Miles, Views & Life', - 'Many Voices of Life', - 'Many Views of Life', - 'Miles Beyond Reality', - 'Motion, Velocity & Lunacy', - 'Maps, Villages & Legends', - 'Multi-Vision Log', - 'Mutable View Log', - 'Mistakes, Victories & Lessons', - 'Mostly Verified Legends', - 'Mostly Vague Logistics', - 'Marginally Viable Leadership', - 'Mileage Versus Luck', - 'Mechanical Violence Log', - 'Moderately Violent Leisure', - 'Mistakes Validated Later', - 'Maybe Very Lost', - 'Miles, Views & Lunch', - 'Maps, Villages & Lager', - 'Missing Various Lefts', - 'Mistakes, Views & Luck', - 'Many Very Longroads', - 'Mileage Varies Largely', - 'Motorcycling Via Luck', - 'Motorcycle Victim Lifestyle', - 'Mildly Violating Limits', - 'My Vices Last', -]; -$siteHeaderTitle = $headerTitles[array_rand($headerTitles)]; -?> - - - - - -MVL - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
-
- - × -
- -
- - -

- Showing article(s) - 0): ?> with 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'] ?? '')]); - ?> -
-
-

-

- - -

-

-
-
- - - ">" alt="Map"> - -
-
-

-

- - -

-

-

-

-
-
- -
- -
- 1) $linkParams['page'] = $i; - ?> - - -
-
-
- - - \ No newline at end of file diff --git a/new.php b/new.php deleted file mode 100644 index c6fc559..0000000 --- a/new.php +++ /dev/null @@ -1,1809 +0,0 @@ -'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 ascii_safe($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; - } - return preg_replace('/[^\x20-\x7E]/', '', $s) ?? ''; -} -function slugify($s){ $s = ascii_safe($s); $s = strtolower(trim($s)); $s = preg_replace('/[^a-z0-9]+/', '-', $s); return trim($s, '-') ?: 'movie'; } -function rrmdir($dir){ if(!is_dir($dir)) return; foreach(scandir($dir) as $f){ if($f==='.'||$f==='..') continue; $p="$dir/$f"; is_dir($p)?rrmdir($p):unlink($p);} rmdir($dir); } -function apply_thumbnail_overlay($imagePath, $title, $teaser) { - $logFile = __DIR__ . '/logs/image_magick.log'; - file_put_contents($logFile, "--- New Thumbnail Job ---\n", FILE_APPEND); - - $fontPath = __DIR__ . '/assets/fonts/IBMPlexSans-SemiBold.ttf'; - $titleFontPath = is_file(__DIR__ . '/assets/fonts/BebasNeue-Regular.ttf') - ? __DIR__ . '/assets/fonts/BebasNeue-Regular.ttf' - : $fontPath; - $teaserFontPath = is_file(__DIR__ . '/assets/fonts/NotoSans-Bold.ttf') - ? __DIR__ . '/assets/fonts/NotoSans-Bold.ttf' - : $fontPath; - if (!is_file($fontPath)) { - throw new RuntimeException('Thumbnail font file is missing.'); - } - - $imgW = 1200; - $imgH = 630; - - $normalize = static function ($text): string { - $text = trim((string)$text); - if ($text === '') return ''; - $text = preg_replace('/\s+/u', ' ', $text) ?? $text; - return trim($text); - }; - $wrapText = static function ($text, int $width) use ($normalize): string { - $text = $normalize($text); - return $text === '' ? '' : wordwrap($text, $width, "\n", false); - }; - $clampWrappedText = static function ($text, int $width, int $maxLines) use ($normalize): string { - $text = $normalize($text); - if ($text === '') return ''; - $words = preg_split('/\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY) ?: []; - if (!$words) return ''; - $candidate = implode(' ', $words); - while (true) { - $wrapped = wordwrap($candidate, $width, "\n", false); - $lineCount = substr_count($wrapped, "\n") + 1; - if ($lineCount <= $maxLines || count($words) <= 1) return $wrapped; - array_pop($words); - $candidate = implode(' ', $words) . '…'; - } - }; - $fitTitleText = static function ($text) use ($wrapText): array { - $bestText = ''; - $bestSize = 95; - $bestWidth = 28; - $bestLines = 1; - for ($size = 94; $size >= 60; $size -= 4) { - $width = max(16, min(46, (int)round(24 + (94 - $size) / 2.7))); - $candidate = $wrapText($text, $width); - $lines = substr_count($candidate, "\n") + 1; - $bestText = $candidate; - $bestSize = $size; - $bestWidth = $width; - $bestLines = $lines; - if ($lines <= 2) break; - } - return [$bestText, $bestSize, $bestWidth, $bestLines]; - }; - - [$titleText, $titleFontSize, $titleWrapWidth, $titleLines] = $fitTitleText($title); - $teaserText = $clampWrappedText($teaser, 38, 6); - $teaserLines = max(1, substr_count($teaserText, "\n") + 1); - - // Move the whole block a bit right while keeping left-aligned text. - $left = max(18, (int)round($imgW * 0.055)); - $titleY = max(24, (int)round($imgH * 0.06)); - $titleLineStep = max(68, (int)round($imgH * 0.043)); - $teaserY = max((int)round($imgH * 0.25), $titleY + ($titleLines * $titleLineStep) + max(56, (int)round($imgH * 0.03))); - - $overlayBase = tempnam(sys_get_temp_dir(), 'thumb_overlay_'); - $outputPath = dirname($imagePath) . '/' . basename($imagePath) . '.tmp.' . uniqid('', true) . '.jpg'; - if ($overlayBase === false) { - throw new RuntimeException('Failed to create temporary thumbnail overlay file.'); - } - $overlayPath = $overlayBase . '.png'; - @unlink($overlayBase); - - $cmd1 = sprintf( - '/usr/bin/convert -size %dx%d xc:none -font %s -pointsize %d -fill "#F3F4F6" -stroke black -strokewidth 2 -interline-spacing 2 -gravity northwest -annotate +%d+%d %s -font %s -pointsize 58 -fill "#EADFC9" -stroke black -strokewidth 2 -interline-spacing -18 -gravity northwest -annotate +%d+%d %s %s 2>&1', - $imgW, - $imgH, - escapeshellarg($titleFontPath), - $titleFontSize, - $left, - $titleY, - escapeshellarg($titleText), - escapeshellarg($teaserFontPath), - $left, - $teaserY, - escapeshellarg($teaserText), - escapeshellarg($overlayPath) - ); - file_put_contents($logFile, "Overlay CMD: " . $cmd1 . "\n", FILE_APPEND); - $out1 = shell_exec($cmd1); - file_put_contents($logFile, "Overlay Output: " . $out1 . "\n", FILE_APPEND); - if (!is_file($overlayPath) || filesize($overlayPath) < 100) { - @unlink($overlayPath); - @unlink($outputPath); - throw new RuntimeException('Failed to render thumbnail overlay: ' . trim((string)$out1)); - } - - $maxBytes = 600 * 1024; - $qualities = [88, 84, 80, 76, 72, 68, 64, 60, 56, 52, 48]; - $finalPath = ''; - $finalSize = 0; - $finalOut = ''; - - foreach ($qualities as $quality) { - $attemptPath = dirname($imagePath) . '/' . basename($imagePath) . '.tmp.' . uniqid('', true) . '.jpg'; - $cmd2 = sprintf( - '/usr/bin/convert -filter Lanczos -define filter:blur=0.85 %s -resize %dx%d^ -gravity center -extent %dx%d -modulate 62,76 %s -composite -strip -interlace Plane -sampling-factor 4:2:0 -quality %d %s 2>&1', - escapeshellarg($imagePath), - $imgW, - $imgH, - $imgW, - $imgH, - escapeshellarg($overlayPath), - $quality, - escapeshellarg($attemptPath) - ); - file_put_contents($logFile, "Composite CMD (q=$quality): " . $cmd2 . "\n", FILE_APPEND); - $out2 = shell_exec($cmd2); - file_put_contents($logFile, "Composite Output (q=$quality): " . $out2 . "\n", FILE_APPEND); - - if (!is_file($attemptPath) || filesize($attemptPath) < 100) { - @unlink($attemptPath); - $finalOut = trim((string)$out2); - continue; - } - - $finalPath = $attemptPath; - $finalSize = filesize($attemptPath); - $finalOut = trim((string)$out2); - if ($finalSize <= $maxBytes) break; - } - - if ($finalPath === '') { - @unlink($overlayPath); - @unlink($outputPath); - throw new RuntimeException('Failed to compose thumbnail overlay: ' . $finalOut); - } - - if ($finalSize > $maxBytes) { - file_put_contents($logFile, "Thumbnail still above size limit: " . $finalSize . " bytes\n", FILE_APPEND); - } - - if (!@rename($finalPath, $imagePath)) { - @unlink($overlayPath); - @unlink($finalPath); - throw new RuntimeException('Failed to replace thumbnail image.'); - } - - @unlink($overlayPath); - @unlink($outputPath); -} - -function input_dirs($base){ return glob($base.'/*', GLOB_ONLYDIR) ?: []; } -function safe_input_dir($base, $name){ $name = basename((string)$name); $path = realpath($base . '/' . $name); $root = realpath($base); if (!$path || !$root || !str_starts_with($path, $root . DIRECTORY_SEPARATOR) || !is_dir($path)) throw new RuntimeException('Invalid input directory.'); return $path; } -function article_id_is_valid($id){ return is_string($id) && preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $id); } -function generate_article_id(){ return gmdate('YmdHis') . bin2hex(random_bytes(8)); } -function read_article_id($dir){ - $idFile = rtrim((string)$dir, '/').'/'.ARTICLE_ID_FILE; - if (!is_file($idFile)) return ''; - $id = trim((string)file_get_contents($idFile)); - return article_id_is_valid($id) ? $id : ''; -} -function write_article_id($dir, $id){ - if (!article_id_is_valid($id)) throw new RuntimeException('Invalid article id.'); - $idFile = rtrim((string)$dir, '/').'/'.ARTICLE_ID_FILE; - $tmp = $idFile . '.tmp.' . bin2hex(random_bytes(4)); - if (file_put_contents($tmp, $id . "\n", LOCK_EX) === false) throw new RuntimeException('Cannot write article id.'); - chmod($tmp, 0664); - if (!rename($tmp, $idFile)) { @unlink($tmp); throw new RuntimeException('Cannot save article id.'); } -} -function ensure_article_id($dir, &$seenIds = null){ - $existing = read_article_id($dir); - if ($existing !== '' && (!is_array($seenIds) || empty($seenIds[$existing]))) { - if (is_array($seenIds)) $seenIds[$existing] = basename((string)$dir); - return $existing; - } - do { $id = generate_article_id(); } while (is_array($seenIds) && !empty($seenIds[$id])); - write_article_id($dir, $id); - if (is_array($seenIds)) $seenIds[$id] = basename((string)$dir); - return $id; -} -function article_index_signature($dirs){ - $names = array_map('basename', $dirs); - sort($names, SORT_STRING); - $parts = []; - foreach ($names as $name) $parts[] = $name; - return hash('sha256', implode("\n", $parts)); -} -function load_articles_index($base, $dirs = null){ - if ($dirs === null) $dirs = input_dirs($base); - $cacheFile = __DIR__ . '/cache/articles-index.json'; - $signature = article_index_signature($dirs); - $cache = is_file($cacheFile) ? json_decode((string)file_get_contents($cacheFile), true) : []; - if (is_array($cache) - && ($cache['version'] ?? 0) === 1 - && ($cache['signature'] ?? '') === $signature - && is_array($cache['by_id'] ?? null) - && is_array($cache['by_dir'] ?? null) - && !in_array('', $cache['by_dir'], true)) { - return $cache; - } - - $byId = []; - $byDir = []; - foreach ($dirs as $dir) { - $name = basename((string)$dir); - $id = read_article_id($dir); - if ($id === '' || !empty($byId[$id])) { - $id = ensure_article_id($dir, $byId); - } - if ($id !== '' && empty($byId[$id])) $byId[$id] = $name; - $byDir[$name] = $id; - } - ksort($byId, SORT_STRING); - ksort($byDir, SORT_STRING); - $out = [ - 'version' => 1, - 'generated_at' => date('c'), - 'signature' => $signature, - 'by_id' => $byId, - 'by_dir' => $byDir, - ]; - file_put_contents($cacheFile, json_encode($out, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX); - return $out; -} -function resolve_input_dir($base, $idOrDir, $articleIndex = null){ - $token = trim((string)$idOrDir); - if ($token === '') throw new RuntimeException('Invalid input directory.'); - if ($articleIndex === null) $articleIndex = load_articles_index($base); - if (article_id_is_valid($token)) { - $name = (string)($articleIndex['by_id'][$token] ?? ''); - if ($name !== '') return safe_input_dir($base, $name); - } - return safe_input_dir($base, $token); -} -function resolve_input_dir_from_request($base, $id, $dir, $articleIndex = null){ - $id = trim((string)$id); - $dir = trim((string)$dir); - if ($id !== '') { - try { return resolve_input_dir($base, $id, $articleIndex); } - catch (Throwable $e) { if ($dir !== '') return resolve_input_dir($base, $dir, $articleIndex); throw $e; } - } - return resolve_input_dir($base, $dir, $articleIndex); -} -function read_data($dir){ - $data = ['title'=>'','teaser'=>'','quote_da'=>'','date'=>'','place'=>'','description'=>'','captions'=>[],'video_audio'=>[]]; $file = $dir . '/data.txt'; - if (!is_file($file)) return $data; - $lines = file($file, FILE_IGNORE_NEW_LINES); - for ($i = 0; $i < count($lines); $i++) { - $line = trim($lines[$i]); - if ($line === '' || str_starts_with($line, '#') || !str_contains($line, ':')) continue; - [$key, $value] = array_map('trim', explode(':', $line, 2)); - $low = strtolower($key); - if ($value === '|' && in_array($low, ['description','desc','synopsis'], true)) { - $block = []; - while ($i + 1 < count($lines)) { - $next = $lines[$i + 1]; - if (trim($next) !== '' && $next[0] !== ' ' && $next[0] !== "\t") break; - $i++; - if (str_starts_with($next, ' ')) $next = substr($next, 2); - elseif (str_starts_with($next, ' ') || str_starts_with($next, "\t")) $next = substr($next, 1); - $block[] = rtrim($next); - } - $data['description'] = trim(implode("\n", $block)); - } elseif (in_array($low, ['title','name'], true)) $data['title'] = $value; - elseif (in_array($low, ['teaser','tagline','subtitle'], true)) $data['teaser'] = $value; - elseif ($low === 'quote_da') $data['quote_da'] = trim($value, "\"“”"); - elseif (in_array($low, ['date','dates','when'], true)) $data['date'] = $value; - elseif (in_array($low, ['place','location','where'], true)) $data['place'] = $value; - elseif (in_array($low, ['description','desc','synopsis'], true)) $data['description'] = $value; - elseif (str_ends_with($low, ' audio')) { $fileKey = trim(substr($key, 0, -6)); if ($fileKey !== '' && in_array(strtolower($value), ['1','yes','true','on'], true)) $data['video_audio'][$fileKey] = true; } - elseif ($key !== '') $data['captions'][$key] = $value; - } - return $data; -} -function write_data($dir, $post){ - $title = trim($post['title'] ?? ''); - $teaser = trim($post['teaser'] ?? ''); - $quote_da = trim($post['quote_da'] ?? ''); - $place = trim($post['place'] ?? ''); - $date = trim($post['date'] ?? ''); - $description = trim($post['description'] ?? ''); - if (function_exists('mb_substr')) $description = mb_substr($description, 0, 5000, 'UTF-8'); else $description = substr($description, 0, 5000); - $lines = []; - if ($title !== '') $lines[] = 'Title: ' . $title; - if ($teaser !== '') $lines[] = 'Teaser: ' . $teaser; - if ($quote_da !== '') $lines[] = 'Quote_da: ' . trim($quote_da, "\"“”"); - if ($place !== '') $lines[] = 'Location: ' . $place; - if ($date !== '') $lines[] = 'Date: ' . $date; - if ($description !== '') { - $lines[] = 'Description: |'; - $description = str_replace(["\r\n", "\r"], "\n", $description); - foreach (explode("\n", $description) as $descLine) $lines[] = ' ' . rtrim($descLine); - } - $useAudioFiles = $post['use_audio_files'] ?? []; - $useAudio = is_array($useAudioFiles) ? array_flip(array_map('basename', array_map('strval', $useAudioFiles))) : []; - foreach (array_keys($useAudio) as $fileName) if ($fileName !== '') $lines[] = $fileName . ' audio: yes'; - $captionFiles = $post['caption_files'] ?? []; - $captions = $post['captions'] ?? []; - if (is_array($captionFiles) && is_array($captions)) { - foreach ($captionFiles as $i => $fileName) { - $fileName = basename((string)$fileName); - $caption = trim((string)($captions[$i] ?? '')); - if ($fileName !== '' && $caption !== '') $lines[] = $fileName . ': ' . $caption; - } - } - file_put_contents($dir . '/data.txt', implode("\n", $lines) . "\n"); -} -function editable_file($f){ $ext = strtolower(pathinfo((string)$f, PATHINFO_EXTENSION)); return $f !== '' && $f[0] !== '.' && $f !== 'data.txt' && in_array($ext, ['jpg','jpeg','png','webp','gif','mp4','mov','m4v','avi','mkv','webm','mp3','wav','m4a','aac','ogg','flac'], true); } -function natural_file_compare($a, $b){ return strnatcasecmp((string)$a, (string)$b); } - -function is_visual_media_file($name){ - $ext = strtolower(pathinfo((string)$name, PATHINFO_EXTENSION)); - return in_array($ext, ['jpg','jpeg','png','webp','gif','mp4','mov','m4v','avi','mkv','webm'], true); -} - -function parse_date_from_filename($path) { - $filename = basename($path); - if (!preg_match('/(?:^|[^0-9])(\\d{8})[_-]?(\\d{6})(?:[^0-9]|$)/', $filename, $m)) { - return null; - } - $y = (int)substr($m[1], 0, 4); - $mo = (int)substr($m[1], 4, 2); - $d = (int)substr($m[1], 6, 2); - $hh = (int)substr($m[2], 0, 2); - $mi = (int)substr($m[2], 2, 2); - $ss = (int)substr($m[2], 4, 2); - if (!checkdate($mo, $d, $y) || $hh > 23 || $mi > 59 || $ss > 59) { - return null; - } - return $m[1] . $m[2]; -} - -function mvlog_local_timezone(){ - static $tz = null; - if ($tz instanceof DateTimeZone) return $tz; - - $name = trim((string)@file_get_contents('/etc/timezone')); - if ($name === '') $name = (string)date_default_timezone_get(); - if ($name === '') $name = 'UTC'; - - try { $tz = new DateTimeZone($name); } - catch (Throwable $e) { $tz = new DateTimeZone('UTC'); } - - return $tz; -} - -function parse_embedded_datetime_to_local($value){ - $value = trim((string)$value); - if ($value === '') return null; - - try { - $dt = new DateTimeImmutable($value); - return $dt->setTimezone(mvlog_local_timezone())->format('YmdHis'); - } catch (Throwable $e) { - return null; - } -} - -function media_file_date($path){ - $ext = strtolower(pathinfo((string)$path, PATHINFO_EXTENSION)); - - // Priority 2: Exif/embedded metadata - if (in_array($ext, ['jpg','jpeg','tif','tiff'], true) && function_exists('exif_read_data')) { - $exif = @exif_read_data($path); - foreach (['DateTimeOriginal','DateTimeDigitized','DateTime'] as $k) { - $value = (string)($exif[$k] ?? ''); - if ($value !== '' && preg_match('/^(\\d{4}):(\\d{2}):(\\d{2})(?:\\s+(\\d{2}):(\\d{2}):(\\d{2}))?/', $value, $m)) { - $date = $m[1] . $m[2] . $m[3]; - $time = isset($m[4]) ? ($m[4] . $m[5] . $m[6]) : '000000'; - return $date . $time; - } - } - } - - if (in_array($ext, ['mp4','mov','m4v','avi','mkv','webm'], true)) { - $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=creation_time:stream_tags=creation_time '.escapeshellarg($path).' 2>/dev/null'); - $data = json_decode((string)$json, true); - $candidates = []; - if (is_array($data['format']['tags'] ?? null)) $candidates[] = (string)($data['format']['tags']['creation_time'] ?? ''); - foreach (($data['streams'] ?? []) as $stream) if (is_array($stream['tags'] ?? null)) $candidates[] = (string)($stream['tags']['creation_time'] ?? ''); - foreach ($candidates as $value) { - $normalized = parse_embedded_datetime_to_local($value); - if ($normalized !== null) { - return $normalized; - } - if (preg_match('/^(\\d{4})-(\\d{2})-(\\d{2})/', $value, $m)) { - return $m[1].$m[2].$m[3].'000000'; - } - } - } - } - - // Priority 3: filename parsing - $from_filename = parse_date_from_filename($path); - if ($from_filename) { - return $from_filename; - } - - return null; -} - -function media_sort_datetime($path){ - return media_file_date($path) ?? date('YmdHis', filemtime($path)); -} - -function list_files($dir){ - $files = array_values(array_filter(scandir($dir), fn($f)=>editable_file($f) && is_file($dir.'/'.$f))); - $orderFile = $dir . '/order.json'; - - $media = []; - $other = []; - foreach ($files as $name) { - if (is_visual_media_file($name)) $media[] = $name; - else $other[] = $name; - } - - // Priority 1: explicit order file, but regenerate if file set changed. - if (is_file($orderFile)) { - $order = json_decode((string)file_get_contents($orderFile), true); - if (is_array($order)) { - $mediaSet = array_fill_keys($media, true); - $orderSet = []; - $sortedMedia = []; - - foreach ($order as $name) { - if (!is_string($name) || !is_visual_media_file($name)) continue; - if (isset($mediaSet[$name]) && !isset($orderSet[$name])) { - $orderSet[$name] = true; - $sortedMedia[] = $name; - } - } - - $sameSet = (count($orderSet) === count($mediaSet)); - if ($sameSet) { - foreach ($mediaSet as $name => $_) { - if (!isset($orderSet[$name])) { - $sameSet = false; - break; - } - } - } - - if ($sameSet) { - return array_merge($sortedMedia, $other); - } - } - } - - // Missing/invalid/stale order.json: sort by datetime and recreate order.json - usort($media, function($a, $b) use ($dir) { - $da = media_sort_datetime($dir . '/' . $a); - $db = media_sort_datetime($dir . '/' . $b); - if ($da === $db) return 0; // never fallback to filename ordering - return strcmp($da, $db); - }); - - file_put_contents($orderFile, json_encode(array_values($media), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); - @chmod($orderFile, 0664); - - return array_merge($media, $other); -} - -function media_sort_files($dir, $files){ - return list_files($dir); -} -function visual_order_files($dir){ - $all = list_files($dir); - return array_values(array_filter($all, fn($name)=>is_visual_media_file($name))); -} -function write_visual_order($dir, $order){ - $clean = []; - $seen = []; - foreach ((array)$order as $name) { - if (!is_string($name)) continue; - $name = basename($name); - if (!is_visual_media_file($name) || isset($seen[$name])) continue; - if (!is_file($dir . '/' . $name)) continue; - $seen[$name] = true; - $clean[] = $name; - } - file_put_contents($dir . '/order.json', json_encode(array_values($clean), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); - @chmod($dir . '/order.json', 0664); - return $clean; -} -function move_visual_file_order($dir, $file, $direction){ - $file = basename((string)$file); - if (!is_visual_media_file($file) || !is_file($dir . '/' . $file)) throw new RuntimeException('Invalid media file for reordering.'); - $direction = strtolower(trim((string)$direction)); - if (!in_array($direction, ['up','down'], true)) throw new RuntimeException('Invalid move direction.'); - - $order = visual_order_files($dir); - $idx = array_search($file, $order, true); - if ($idx === false) throw new RuntimeException('File not found in visual order.'); - - $newIdx = $idx; - if ($direction === 'up' && $idx > 0) { - [$order[$idx - 1], $order[$idx]] = [$order[$idx], $order[$idx - 1]]; - $newIdx = $idx - 1; - } elseif ($direction === 'down' && $idx < count($order) - 1) { - [$order[$idx], $order[$idx + 1]] = [$order[$idx + 1], $order[$idx]]; - $newIdx = $idx + 1; - } - - write_visual_order($dir, $order); - return [ - 'changed' => $newIdx !== $idx, - 'index' => $newIdx, - 'count' => count($order), - ]; -} -function input_dir_date_from_media($dir){ - $dates = []; - foreach (list_files($dir) as $file) { - $date = media_file_date($dir . '/' . $file); - if ($date !== null) $dates[] = $date; - } - if ($dates) { - sort($dates, SORT_STRING); - return $dates[0]; - } - return date('Ymd'); -} -function input_dir_date_from_text($text){ - $text = trim((string)$text); - if ($text === '') return null; - if (preg_match('/^(\d{4})[.-]?(\d{2})[.-]?(\d{2})$/', $text, $m) && checkdate((int)$m[2], (int)$m[3], (int)$m[1])) return "$m[1]$m[2]$m[3]"; - if (preg_match('/^(\d{1,2})[\/. -](\d{1,2})[\/. -](\d{4})$/', $text, $m) && checkdate((int)$m[2], (int)$m[1], (int)$m[3])) return sprintf('%04d%02d%02d', $m[3], $m[2], $m[1]); - $ts = strtotime($text); - if ($ts !== false) return date('Ymd', $ts); - if (preg_match('/^(jan|january|feb|february|mar|march|apr|april|may|jun|june|jul|july|aug|august|sep|sept|september|oct|october|nov|november|dec|december)\s+(\d{4})$/i', $text, $m)) { - $months = ['jan'=>1,'january'=>1,'feb'=>2,'february'=>2,'mar'=>3,'march'=>3,'apr'=>4,'april'=>4,'may'=>5,'jun'=>6,'june'=>6,'jul'=>7,'july'=>7,'aug'=>8,'august'=>8,'sep'=>9,'sept'=>9,'september'=>9,'oct'=>10,'october'=>10,'nov'=>11,'november'=>11,'dec'=>12,'december'=>12]; - return sprintf('%04d%02d01', (int)$m[2], $months[strtolower($m[1])]); - } - return null; -} -function unique_input_dir($base, $slug){ - $dir = $base . '/' . $slug; - if (!file_exists($dir)) return $dir; - for ($i = 2; ; $i++) { - $candidate = $base . '/' . $slug . '-' . $i; - if (!file_exists($candidate)) return $candidate; - } -} -function input_dir_signature($dir){ - $script = <<<'PY' -import hashlib, json, os, sys -root = sys.argv[1] -entries = [] -for dirpath, dirnames, filenames in os.walk(root): - dirnames[:] = [d for d in dirnames if d != '.movmaker-lock'] - for name in filenames: - if name in {'.movmaker-state.json', '.movmaker-enabled', '.movmaker-preview', '.mvlog-hidden', '.mvlog-id', '.mvlog-hide-map'}: - continue - path = os.path.join(dirpath, name) - rel = os.path.relpath(path, root) - st = os.stat(path) - mtime_ns = getattr(st, 'st_mtime_ns', int(st.st_mtime * 1_000_000_000)) - entries.append([rel, st.st_size, mtime_ns]) -entries.sort() -print(hashlib.sha256(json.dumps(entries, ensure_ascii=False, separators=(',', ':')).encode('utf-8')).hexdigest()) -PY; - $cmd = 'python3 -c ' . escapeshellarg($script) . ' ' . escapeshellarg($dir); - $output = trim(shell_exec($cmd) ?? ''); - return $output !== '' ? $output : ''; -} -function input_dir_info($dir){ - $data = read_data($dir); - $files = list_files($dir); - return [ - 'name' => basename($dir), - 'article_id' => ensure_article_id($dir), - 'title' => $data['title'] ?: basename($dir), - 'sort_date' => input_dir_date_from_text($data['date'] ?? '') ?? input_dir_date_from_media($dir), - 'file_count' => count($files), - 'signature' => input_dir_signature($dir), - ]; -} -function load_input_dir_cache($dirs){ - $cacheFile = __DIR__ . '/cache/input-dirs.json'; - $cache = is_file($cacheFile) ? json_decode((string)file_get_contents($cacheFile), true) : []; - if (!is_array($cache) || ($cache['version'] ?? 0) !== 1) $cache = ['version'=>1,'items'=>[]]; - $oldItems = is_array($cache['items'] ?? null) ? $cache['items'] : []; - $items = []; $changed = false; - foreach ($dirs as $dir) { - $name = basename($dir); - $sig = input_dir_signature($dir); - $cached = $oldItems[$name] ?? null; - if (is_array($cached) && ($cached['signature'] ?? '') === $sig && !empty($cached['article_id'])) { - $items[$name] = $cached; - } else { - $items[$name] = input_dir_info($dir); - $changed = true; - } - } - if (array_diff(array_keys($oldItems), array_keys($items))) $changed = true; - if ($changed) file_put_contents($cacheFile, json_encode(['version'=>1,'generated_at'=>date('c'),'items'=>$items], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX); - return $items; -} -function sort_input_dirs_by_metadata_date(&$dirs, $dirInfo){ - usort($dirs, function($a, $b) use ($dirInfo) { - $ad = $dirInfo[basename($a)]['sort_date'] ?? '00000000'; - $bd = $dirInfo[basename($b)]['sort_date'] ?? '00000000'; - $cmp = strcmp($bd, $ad); - return $cmp !== 0 ? $cmp : strnatcasecmp(basename($b), basename($a)); - }); -} -function orphan_videos($config){ - $referenced = []; - foreach (input_dirs($config['uploads_dir']) as $dir) { - $stateFile = $dir . '/.movmaker-state.json'; - $state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : []; - if (is_array($state) && !empty($state['output'])) $referenced[basename((string)$state['output'])] = true; - } - $videos = glob($config['videos_dir'].'/*.{mp4,webm,mov,m4v}', GLOB_BRACE) ?: []; - return array_values(array_filter($videos, fn($v)=>empty($referenced[basename($v)]))); -} -function input_dir_status($dir){ - $stateFile = $dir . '/.movmaker-state.json'; - $lockDir = $dir . '/.movmaker-lock'; - $state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : []; - $status = is_array($state) ? (string)($state['status'] ?? '') : ''; - if (is_dir($lockDir)) return $status !== '' ? $status : 'processing'; - return in_array($status, ['processing', 'stale', 'error'], true) ? $status : ''; -} -function input_dir_running($dir){ - $stateFile = $dir . '/.movmaker-state.json'; - $lockDir = $dir . '/.movmaker-lock'; - $state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : []; - $status = is_array($state) ? (string)($state['status'] ?? '') : ''; - return is_dir($lockDir) || $status === 'processing'; -} -function input_dir_enabled($dir){ return is_file($dir . '/.movmaker-enabled'); } -function set_input_dir_enabled($dir, $enabled){ - $path = $dir . '/.movmaker-enabled'; - if ($enabled) { file_put_contents($path, "enabled\n"); chmod($path, 0664); } - elseif (is_file($path)) unlink($path); -} -function input_dir_preview($dir){ - if (is_file($dir . '/.movmaker-preview')) return true; - $stateFile = $dir . '/.movmaker-state.json'; - if (!is_file($stateFile)) return false; - $state = json_decode((string)file_get_contents($stateFile), true); - return is_array($state) && !empty($state['preview']); -} -function set_input_dir_preview($dir, $preview){ - $path = $dir . '/.movmaker-preview'; - if ($preview) { file_put_contents($path, "preview\n"); chmod($path, 0664); } - elseif (is_file($path)) unlink($path); - $stateFile = $dir . '/.movmaker-state.json'; - $state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : []; - if (!is_array($state)) $state = []; - $state['article_id'] = ensure_article_id($dir); - $state['preview'] = (bool)$preview; - $state['preview_updated_at'] = gmdate('c'); - file_put_contents($stateFile, json_encode($state, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n"); - chmod($stateFile, 0664); -} -function input_dir_visible($dir){ return !is_file($dir . '/.mvlog-hidden'); } -function set_input_dir_visible($dir, $visible){ - $path = $dir . '/.mvlog-hidden'; - if ($visible) { if (is_file($path)) unlink($path); } - else { file_put_contents($path, "hidden\n"); chmod($path, 0664); } -} -function input_dir_map_hidden($dir){ return is_file($dir . '/' . MAP_HIDDEN_FILE); } -function set_input_dir_map_hidden($dir, $hidden){ - $path = $dir . '/' . MAP_HIDDEN_FILE; - if ($hidden) { file_put_contents($path, "hide-map\n"); chmod($path, 0664); } - elseif (is_file($path)) unlink($path); -} -function is_image_media_file($name){ - $ext = strtolower(pathinfo((string)$name, PATHINFO_EXTENSION)); - return in_array($ext, ['jpg','jpeg','png','webp','gif'], true); -} -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 = 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 ensure_state_article_id($dir){ - $articleId = ensure_article_id($dir); - $stateFile = $dir . '/.movmaker-state.json'; - $state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : []; - if (!is_array($state)) $state = []; - if (($state['article_id'] ?? '') !== $articleId) { - $state['article_id'] = $articleId; - file_put_contents($stateFile, json_encode($state, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n"); - chmod($stateFile, 0664); - } - return $articleId; -} -function input_dir_state($dir){ - $stateFile = $dir . '/.movmaker-state.json'; - $state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : []; - if (!is_array($state)) $state = []; - $state['article_id'] = ($state['article_id'] ?? '') ?: read_article_id($dir); - return $state; -} -function active_worker_jobs($config){ - $jobs = []; - foreach (input_dirs($config['uploads_dir']) as $dir) { - $name = basename($dir); - $state = input_dir_state($dir); - $articleId = (string)($state['article_id'] ?? '') ?: ensure_article_id($dir); - $status = (string)($state['status'] ?? ''); - $locked = is_dir($dir . '/.movmaker-lock'); - if ($locked || $status === 'processing') { - $started = ''; - if (is_file($dir . '/.movmaker-lock/started_at')) $started = trim((string)file_get_contents($dir . '/.movmaker-lock/started_at')); - if ($started === '' && !empty($state)) $started = (string)($state['started_at'] ?? $state['updated_at'] ?? ''); - $jobs[] = ['name'=>$name,'id'=>$articleId,'status'=>$status ?: ($locked ? 'processing' : 'unknown'),'started_at'=>$started]; - } - } - return $jobs; -} -function format_job_age($startedAt){ - $ts = strtotime((string)$startedAt); - if (!$ts) return ''; - $seconds = max(0, time() - $ts); - $h = intdiv($seconds, 3600); - $m = intdiv($seconds % 3600, 60); - $s = $seconds % 60; - if ($h > 0) return $h . 'h ' . $m . 'm'; - if ($m > 0) return $m . 'm ' . $s . 's'; - return $s . 's'; -} -function input_dir_output($dir){ - $state = input_dir_state($dir); - return basename((string)($state['output'] ?? '')); -} -function regenerate_input_dir_maps($dir, $config){ - $python = trim((string)shell_exec('command -v python3 2>/dev/null')); - $script = __DIR__ . '/bin/gpsmap.py'; - if ($python === '' || !is_file($script)) return; - - $state = input_dir_state($dir); - $targets = []; - foreach (['output', 'preview_output'] as $key) { - $name = basename((string)($state[$key] ?? '')); - if ($name === '') continue; - $targets[] = $config['videos_dir'] . '/' . pathinfo($name, PATHINFO_FILENAME) . '_map.png'; - } - - // Fallback target if no output filename is known yet. - if (!$targets) { - $targets[] = $config['videos_dir'] . '/' . basename((string)$dir) . '_map.png'; - } - - $targets = array_values(array_unique($targets)); - - foreach ($targets as $outPng) { - $cmd = escapeshellarg($python) . ' ' . escapeshellarg($script) . ' ' . escapeshellarg($dir) . ' ' . escapeshellarg($outPng) . ' 2>&1'; - $output = (string)shell_exec($cmd); - if (is_file($outPng)) { - @chmod($outPng, 0664); - } else { - error_log('[mvlog] map regenerate failed for '.basename((string)$dir).' target='.basename((string)$outPng).' output='.trim($output)."\n", 3, '/var/log/mvlog_map.log'); - } - } -} -function cached_video_metadata($output, $fallback){ - $meta = ['title'=>$fallback['title'] ?? '', 'date'=>$fallback['date'] ?? '', 'location'=>$fallback['place'] ?? '', 'quote_da'=>$fallback['quote_da'] ?? '', 'description'=>$fallback['description'] ?? '']; - $cacheFile = __DIR__ . '/cache/videos.json'; - $cache = is_file($cacheFile) ? json_decode((string)file_get_contents($cacheFile), true) : []; - $cached = $cache['items'][$output]['metadata'] ?? null; - if (is_array($cached)) { - foreach (['title','date','location','quote_da','description'] as $k) if (!empty($cached[$k])) $meta[$k] = $cached[$k]; - } - return $meta; -} -function paginate($items, $page, $perPage = 8){ - $total = count($items); - $pages = max(1, (int)ceil($total / $perPage)); - $page = max(1, min((int)$page, $pages)); - return [$page, $pages, array_slice($items, ($page - 1) * $perPage, $perPage), $total]; -} -function page_links($tab, $page, $pages){ - if ($pages <= 1) return ''; - $extra = ''; - if ($tab === 'edit') { - $q = trim((string)($_GET['q'] ?? '')); - if ($q !== '') $extra .= '&q=' . rawurlencode($q); - if (!empty($_GET['f_preview'])) $extra .= '&f_preview=1'; - if (!empty($_GET['f_render'])) $extra .= '&f_render=1'; - if (!empty($_GET['f_shown'])) $extra .= '&f_shown=1'; - } - $html = '
'; - for ($i = 1; $i <= $pages; $i++) { - $class = $i === $page ? 'active' : ''; - $html .= ''.$i.''; - } - return $html . '
'; -} -function save_uploads($field, $dest, $allowed){ - if (empty($_FILES[$field])) return []; - $files = $_FILES[$field]; $saved = []; - $count = is_array($files['name']) ? count($files['name']) : 0; - for ($i=0; $i<$count; $i++) { - if ($files['error'][$i] === UPLOAD_ERR_NO_FILE) continue; - if ($files['error'][$i] !== UPLOAD_ERR_OK) throw new RuntimeException("Upload failed: ".$files['name'][$i]); - $ext = strtolower(pathinfo($files['name'][$i], PATHINFO_EXTENSION)); - if (!in_array($ext, $allowed, true)) throw new RuntimeException("File type not allowed: ".$files['name'][$i]); - $base = preg_replace('/[^A-Za-z0-9._-]+/', '_', basename($files['name'][$i])); - $target = $dest . '/' . sprintf('%03d_', count(glob($dest.'/*') ?: [])+count($saved)+1) . $base; - if (!move_uploaded_file($files['tmp_name'][$i], $target)) throw new RuntimeException("Cannot save upload"); - $saved[] = basename($target); - } - return $saved; -} -$msg = $_GET['msg'] ?? null; $err = null; -$allowedMedia = ['jpg','jpeg','png','webp','gif','mp4','mov','m4v','avi','mkv','webm']; -$allowedAudio = ['mp3','wav','m4a','aac','ogg','flac']; -$articleIndex = load_articles_index($config['uploads_dir']); -try { - if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $action = $_POST['action'] ?? 'create'; - if ($action === 'create') { - $title = trim($_POST['title'] ?? ''); - if ($title === '') throw new RuntimeException('Title is required.'); - $tmpSlug = '.upload-' . date('YmdHis') . '-' . bin2hex(random_bytes(4)); - $tmpDir = $config['uploads_dir'] . '/' . $tmpSlug; - if (!mkdir($tmpDir, 0775, true)) throw new RuntimeException('Cannot create temporary input directory.'); - chmod($tmpDir, 02775); - $media = save_uploads('media', $tmpDir, $allowedMedia); - if (!$media) { rrmdir($tmpDir); throw new RuntimeException('Upload at least one image or video.'); } - save_uploads('audio', $tmpDir, $allowedAudio); - $mediaDate = input_dir_date_from_text($_POST['date'] ?? '') ?? input_dir_date_from_media($tmpDir); - $slug = $mediaDate . '_' . slugify($title); - $dir = unique_input_dir($config['uploads_dir'], $slug); - $slug = basename($dir); - if (!rename($tmpDir, $dir)) { rrmdir($tmpDir); throw new RuntimeException('Cannot create input directory.'); } - chmod($dir, 02775); - $articleId = ensure_article_id($dir); - write_data($dir, $_POST); - set_input_dir_enabled($dir, false); - set_input_dir_preview($dir, false); - set_input_dir_visible($dir, false); - if (!empty($_POST['ajax'])) { - ajax_json(['ok'=>true, 'message'=>"Created movmaker input directory: in-dir/$slug", 'dir'=>$slug, 'id'=>$articleId, 'tab'=>'edit']); - } - header('Location: new.php?tab=edit'); - exit; - } elseif ($action === 'set_enabled') { - $dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex); - $articleId = ensure_state_article_id($dir); - $enabledNow = !empty($_POST['enabled']); - set_input_dir_enabled($dir, $enabledNow); - if ($enabledNow) { - set_input_dir_preview($dir, false); - } - $state = input_dir_state($dir); - $output = basename((string)($state['output'] ?? '')); - $canShow = $output !== '' && is_file($config['videos_dir'].'/'.$output) && !input_dir_preview($dir); - if (!$canShow) set_input_dir_visible($dir, false); - $message = ($enabledNow ? 'Enabled rendering for: in-dir/' : 'Disabled rendering for: in-dir/') . basename($dir); - if (!empty($_POST['ajax'])) { - ajax_json([ - 'ok' => true, - 'enabled' => $enabledNow, - 'preview' => input_dir_preview($dir), - 'visible' => input_dir_visible($dir) && $canShow, - 'can_show' => $canShow, - 'dir' => basename($dir), - 'id' => $articleId, - 'message' => $message, - ]); - } - header('Location: new.php?tab=edit&msg=' . rawurlencode($message)); - exit; - } elseif ($action === 'set_preview') { - $dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex); - $articleId = ensure_state_article_id($dir); - $previewNow = !empty($_POST['preview']); - set_input_dir_preview($dir, $previewNow); - if ($previewNow) { set_input_dir_enabled($dir, false); set_input_dir_visible($dir, false); } - $state = input_dir_state($dir); - $output = basename((string)($state['output'] ?? '')); - $canShow = $output !== '' && is_file($config['videos_dir'].'/'.$output) && !input_dir_preview($dir); - $message = ($previewNow ? 'Enabled preview for: in-dir/' : 'Disabled preview for: in-dir/') . basename($dir); - if (!empty($_POST['ajax'])) { - ajax_json([ - 'ok' => true, - 'preview' => $previewNow, - 'enabled' => input_dir_enabled($dir), - 'visible' => input_dir_visible($dir) && $canShow, - 'can_show' => $canShow, - 'dir' => basename($dir), - 'id' => $articleId, - 'message' => $message, - ]); - } - header('Location: new.php?tab=edit&msg=' . rawurlencode($message)); - exit; - } elseif ($action === 'set_visible') { - $dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex); - $articleId = ensure_state_article_id($dir); - $visibleNow = !empty($_POST['visible']); - $state = input_dir_state($dir); - $output = basename((string)($state['output'] ?? '')); - $canShow = $output !== '' && is_file($config['videos_dir'].'/'.$output) && !input_dir_preview($dir); - if ($visibleNow && !$canShow) throw new RuntimeException('Only finished full-quality videos can be shown.'); - set_input_dir_visible($dir, $visibleNow && $canShow); - // Attempt to send "Show enabled" notification (server-side) on enable. Non-fatal. - if ($visibleNow && $canShow) { - if (is_file(__DIR__ . '/lib/send_push.php')) { - include_once __DIR__ . '/lib/send_push.php'; - try_send_show_notification($articleId ?: basename($dir), $dir, __DIR__, '/var/log/mvlog_notify.log'); - } else { - error_log("[mvlog] send_push helper missing, cannot send notification for " . basename($dir) . "\n", 3, '/var/log/mvlog_notify.log'); - } - } - - $message = ($visibleNow ? 'Shown: in-dir/' : 'Hidden: in-dir/') . basename($dir); - $currentVisible = input_dir_visible($dir) && $canShow; - if (!empty($_POST['ajax'])) { - ajax_json([ - 'ok' => true, - 'visible' => $currentVisible, - 'can_show' => $canShow, - 'dir' => basename($dir), - 'id' => $articleId, - 'message' => $message, - ]); - } - header('Location: new.php?tab=edit&msg=' . rawurlencode($message)); - exit; - } elseif ($action === 'set_map_hidden') { - $dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex); - $articleId = ensure_state_article_id($dir); - $mapHiddenNow = !empty($_POST['map_hidden']); - set_input_dir_map_hidden($dir, $mapHiddenNow); - $message = ($mapHiddenNow ? 'Map hidden on index for: in-dir/' : 'Map visible on index for: in-dir/') . basename($dir); - if (!empty($_POST['ajax'])) { - ajax_json([ - 'ok' => true, - 'map_hidden' => $mapHiddenNow, - 'dir' => basename($dir), - 'id' => $articleId, - 'message' => $message, - ]); - } - header('Location: new.php?tab=edit&msg=' . rawurlencode($message)); - exit; - } elseif ($action === 'set_video_thumb') { - $logFile = __DIR__ . '/logs/image_magick.log'; - file_put_contents($logFile, "Entering set_video_thumb action...\n", FILE_APPEND); // LOG 1 - - $dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex); - $articleId = ensure_state_article_id($dir); - if (input_dir_running($dir)) throw new RuntimeException('This input directory is being rendered. Editing is disabled until the job completes.'); - $file = basename((string)($_POST['file'] ?? '')); - if ($file === '' || !is_image_media_file($file) || !is_file($dir . '/' . $file)) throw new RuntimeException('Invalid image file.'); - $state = input_dir_state($dir); - $meta = read_data($dir); // Need this for title and teaser - $outputs = []; - foreach (['output', 'preview_output'] as $key) { - $name = basename((string)($state[$key] ?? '')); - if ($name !== '' && is_file($config['videos_dir'] . '/' . $name)) $outputs[] = $name; - } - $outputs = array_values(array_unique($outputs)); - if (!$outputs) throw new RuntimeException('No rendered video found for this input directory yet.'); - - $src = $dir . '/' . $file; - $thumbName = $articleId . '_og.jpg'; - $target = $config['videos_dir'] . '/' . $thumbName; - if (is_file($target)) @unlink($target); - if (!@copy($src, $target)) { - file_put_contents($logFile, "Failed to copy thumbnail.\n", FILE_APPEND); // LOG 2 - throw new RuntimeException('Failed to write thumbnail.'); - } - @chmod($target, 0664); - - file_put_contents($logFile, "Thumbnail copied, now applying overlay...\n", FILE_APPEND); // LOG 3 - - // Apply text overlay after smart resizing/cropping to 1200x630 - apply_thumbnail_overlay($target, (string)($meta['title'] ?? ''), (string)($meta['teaser'] ?? '')); - $written = []; - foreach ($outputs as $out) { - $base = pathinfo($out, PATHINFO_FILENAME); - // Remove any old thumbnail files in thumbs_dir - foreach (['jpg','jpeg','png','webp','gif'] as $oldExt) { - $old = $config['thumbs_dir'] . '/' . $base . '.' . $oldExt; - if (is_file($old)) @unlink($old); - } - $thumbMeta = [ - 'article_id' => $articleId, - 'input_dir' => basename($dir), - 'source_file' => $file, - 'source_path' => 'in-dir/' . basename($dir) . '/' . $file, - 'thumb_file' => $thumbName, - 'thumb_path' => 'out-dir/' . $thumbName, - 'created_at' => gmdate('c'), - ]; - $thumbMetaPath = $config['videos_dir'] . '/' . $base . '_thumb.json'; - if (file_put_contents($thumbMetaPath, json_encode($thumbMeta, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n", LOCK_EX) === false) { - throw new RuntimeException('Failed to write thumbnail metadata.'); - } - @chmod($thumbMetaPath, 0664); - $written[] = basename($target); - } - - $message = 'Thumbnail set from ' . $file . ' for ' . count($written) . ' video(s).'; - if (!empty($_POST['ajax'])) { - ajax_json([ - 'ok' => true, - 'message' => $message, - 'dir' => basename($dir), - 'id' => $articleId, - 'written' => $written, - ]); - } - header('Location: new.php?tab=edit&id=' . rawurlencode((string)$articleId) . '&edit=' . rawurlencode(basename($dir)) . '&msg=' . rawurlencode($message)); - exit; - } elseif ($action === 'update') { - $dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex); - $articleId = ensure_state_article_id($dir); - if (input_dir_running($dir)) throw new RuntimeException('This input directory is being rendered. Editing is disabled until the job completes.'); - write_data($dir, $_POST); - save_uploads('media', $dir, $allowedMedia); - save_uploads('audio', $dir, $allowedAudio); - // Always regenerate map image(s) on Save changes. - regenerate_input_dir_maps($dir, $config); - if (!empty($_POST['ajax'])) ajax_json(['ok'=>true, 'message'=>'Updated input directory: in-dir/' . basename($dir), 'dir'=>basename($dir), 'id'=>$articleId]); - header('Location: new.php?tab=edit'); - exit; - } elseif ($action === 'delete_file') { - $dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex); - $articleId = ensure_state_article_id($dir); - if (input_dir_running($dir)) throw new RuntimeException('This input directory is being rendered. Editing is disabled until the job completes.'); - $file = basename((string)($_POST['file'] ?? '')); - if (!editable_file($file) || !is_file($dir.'/'.$file)) throw new RuntimeException('Invalid file.'); - unlink($dir.'/'.$file); - if (!empty($_POST['ajax'])) { - ajax_json(['ok'=>true, 'message'=>'Removed file: ' . $file, 'tab'=>'edit']); - } - header('Location: new.php?tab=edit'); - exit; - } elseif ($action === 'reorder_file') { - $dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex); - $articleId = ensure_state_article_id($dir); - if (input_dir_running($dir)) throw new RuntimeException('This input directory is being rendered. Editing is disabled until the job completes.'); - $file = basename((string)($_POST['file'] ?? '')); - $direction = strtolower(trim((string)($_POST['direction'] ?? ''))); - $result = move_visual_file_order($dir, $file, $direction); - $message = $result['changed'] ? ('Moved file ' . ($direction === 'up' ? 'up' : 'down') . ': ' . $file) : 'No move possible for file: ' . $file; - if (!empty($_POST['ajax'])) { - ajax_json([ - 'ok' => true, - 'message' => $message, - 'file' => $file, - 'direction' => $direction, - 'changed' => !empty($result['changed']), - 'index' => (int)($result['index'] ?? -1), - 'count' => (int)($result['count'] ?? 0), - 'dir' => basename($dir), - 'id' => $articleId, - ]); - } - header('Location: new.php?tab=edit&id=' . rawurlencode((string)$articleId) . '&edit=' . rawurlencode(basename($dir)) . '&msg=' . rawurlencode($message)); - exit; - } elseif ($action === 'delete_video') { - $file = basename((string)($_POST['video'] ?? '')); - $path = $config['videos_dir'] . '/' . $file; - if ($file === '' || !is_file($path)) throw new RuntimeException('Invalid video.'); - unlink($path); - $cache = __DIR__ . '/cache/videos.json'; - if (is_file($cache)) unlink($cache); - if (!empty($_POST['ajax'])) { - ajax_json(['ok'=>true, 'message'=>'Deleted video: ' . $file, 'tab'=>'videos']); - } - header('Location: new.php?tab=videos'); - exit; - } elseif ($action === 'delete_dir') { - $dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex); - $articleId = ensure_state_article_id($dir); - if (input_dir_running($dir)) throw new RuntimeException('This input directory is being rendered. Editing is disabled until the job completes.'); - $name = basename($dir); - $stateFile = $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 (!empty($_POST['delete_output']) && $output !== '') { - $outPath = $config['videos_dir'] . '/' . $output; - if (is_file($outPath)) unlink($outPath); - $cache = __DIR__ . '/cache/videos.json'; - if (is_file($cache)) unlink($cache); - } - rrmdir($dir); - if (!empty($_POST['ajax'])) { - ajax_json(['ok'=>true, 'message'=>'Deleted input directory: in-dir/' . $name, 'tab'=>'edit']); - } - header('Location: new.php?tab=edit'); - exit; - } - } -} catch (Throwable $e) { - if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['ajax'])) { - http_response_code(400); - ajax_json(['ok'=>false, 'error'=>$e->getMessage()]); - } - $err = $e->getMessage(); -} -$dirs = input_dirs($config['uploads_dir']); -$articleIndex = load_articles_index($config['uploads_dir'], $dirs); -$dirInfo = load_input_dir_cache($dirs); -sort_input_dirs_by_metadata_date($dirs, $dirInfo); -$orphanVideos = orphan_videos($config); -$tab = $_GET['tab'] ?? 'edit'; - -$rawKeyword = trim((string)($_GET['q'] ?? '')); -$keywordType = 'general'; -$keywordValue = $rawKeyword; -$filterPreview = !empty($_GET['f_preview']); -$filterRender = !empty($_GET['f_render']); -$filterShown = !empty($_GET['f_shown']); -$statusFilterActive = $filterPreview || $filterRender || $filterShown; -$searchEmpty = ($rawKeyword === '' && !$statusFilterActive); -if ($rawKeyword !== '' && preg_match('/^\s*(date|location|place)\s*=\s*(.+)\s*$/i', $rawKeyword, $m)) { - $keywordType = strtolower((string)$m[1]); - if ($keywordType === 'place') $keywordType = 'location'; - $keywordValue = trim((string)$m[2]); -} -if ($keywordValue !== '' || $statusFilterActive) { - $dirs = array_values(array_filter($dirs, function($dir) use ($keywordType, $keywordValue, $config, $statusFilterActive, $filterPreview, $filterRender, $filterShown) { - $data = read_data($dir); - $state = input_dir_state($dir); - $fullOutput = basename((string)($state['output'] ?? '')); - $previewOutput = basename((string)($state['preview_output'] ?? '')); - $hasFullVideo = $fullOutput !== '' && is_file($config['videos_dir'].'/'.$fullOutput); - $hasPreviewVideo = $previewOutput !== '' && is_file($config['videos_dir'].'/'.$previewOutput); - $preview = input_dir_preview($dir); - $enabled = input_dir_enabled($dir); - $canShow = $hasFullVideo && !$preview; - $visible = $canShow && input_dir_visible($dir); - - if ($statusFilterActive) { - $statusMatch = false; - if ($filterPreview && $preview) $statusMatch = true; - if ($filterRender && $enabled) $statusMatch = true; - if ($filterShown && $visible) $statusMatch = true; - if (!$statusMatch) return false; - } - - if ($keywordValue === '') return true; - - $title = (string)(($data['title'] ?? '') !== '' ? $data['title'] : basename($dir)); - $teaser = (string)($data['teaser'] ?? ''); - $description = (string)($data['description'] ?? ''); - $quoteDa = (string)($data['quote_da'] ?? ''); - $date = (string)($data['date'] ?? ''); - $location = (string)($data['place'] ?? ''); - - if ($keywordType === 'date') return ci_contains($date, $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)); -[$editPage, $editPages, $dirsPage, $dirsTotal] = paginate($dirs, $tab === 'edit' ? $page : 1); -[$videoPage, $videoPages, $orphanVideosPage, $orphanVideosTotal] = paginate($orphanVideos, $tab === 'videos' ? $page : 1); -$editName = $_GET['edit'] ?? ''; -$editId = $_GET['id'] ?? ''; -$editDir = null; $editStatus = ''; $editRunning = false; $editData = ['title'=>'','teaser'=>'','quote_da'=>'','date'=>'','place'=>'','description'=>'','captions'=>[],'video_audio'=>[]]; $editFiles = []; -if ($editName !== '' || $editId !== '') { try { $editDir = resolve_input_dir_from_request($config['uploads_dir'], $editId, $editName, $articleIndex); $editStatus = input_dir_status($editDir); $editRunning = input_dir_running($editDir); if (!$editRunning) { $editData = read_data($editDir); $editFiles = media_sort_files($editDir, list_files($editDir)); } } catch (Throwable $e) { $err = $e->getMessage(); } } -$runningJobs = active_worker_jobs($config); -?> -MVL Admin - -
-
- -

Videos without input dir

No orphan videos.


MB
-

No input directories yet.

(string)($data['title'] ?? ''),'teaser'=>(string)($data['teaser'] ?? ''),'date'=>(string)($data['date'] ?? ''),'location'=>(string)($data['place'] ?? ''),'quote_da'=>(string)($data['quote_da'] ?? ''),'description'=>(string)($data['description'] ?? '')]; $currentSignature=$info['signature'] ?? ''; $fullFingerprint=is_array($state)?(string)($state['fingerprint'] ?? ''):''; $previewFingerprint=is_array($state)?(string)($state['preview_fingerprint'] ?? ''):''; $editedSinceRender=$hasFullVideo && $currentSignature !== '' && $fullFingerprint !== '' && $currentSignature !== $fullFingerprint; $editedSincePreview=!$hasFullVideo && $previewVideo && $currentSignature !== '' && $previewFingerprint !== '' && $currentSignature !== $previewFingerprint; $staleLabel=$editedSinceRender ? 'Edited since render' : ($editedSincePreview ? 'Edited since preview' : ''); ?>
Map
No video yet

Thumb source:

-
- - - - - -
-
- - - - - -
-
- - - - - -
-
- - - - - -
- -
RenderingEdit

No videoPreview video

· files

-

Edit input dir

in-dir/

This input directory is . Editing is disabled until the job completes.
-

Edit input dir

in-dir/

Files and captions

No caption for audio files
-

Add new movie input

- - -
- - -