diff --git a/index.php b/index.php index 95f87d2..7b61a60 100644 --- a/index.php +++ b/index.php @@ -57,16 +57,60 @@ function map_hidden_outputs($config){ 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 ''; - $base = pathinfo($videoName, PATHINFO_FILENAME); - foreach (['jpg','jpeg','png','webp','gif'] as $ext) { - $file = $config['videos_dir'] . '/' . $base . '_thumb.' . $ext; - if (is_file($file)) return $config['public_videos'] . '/' . rawurlencode($base . '_thumb.' . $ext) . '?v=' . filemtime($file); + $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); @@ -181,8 +225,8 @@ usort($videos, function($a, $b) use ($videoCache) { $articleId = trim((string)($_GET['id'] ?? '')); if ($articleId !== '') { - $videos = array_values(array_filter($videos, function($v) use ($articleId) { - return pathinfo($v, PATHINFO_FILENAME) === $articleId; + $videos = array_values(array_filter($videos, function($v) use ($articleId, $config) { + return video_article_id($config, basename($v)) === $articleId; })); $rawKeyword = ''; $keywordType = 'general'; @@ -248,6 +292,21 @@ $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', @@ -307,15 +366,15 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)]; - - + + - + - + @@ -385,7 +444,8 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)]; $base]); + $articleIdForVideo = video_article_id($config, $name); + $permalinkUrl = $articleIdForVideo !== '' ? query_url(['id' => $articleIdForVideo]) : ''; $url = $config['public_videos'].'/'.rawurlencode($name); $m = $videoCache[$name]['metadata'] ?? video_metadata($v); $posterUrl = video_thumb_public_url($config, $name); @@ -394,7 +454,7 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)]; ?>
-

+

@@ -412,7 +472,7 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];

-

+

@@ -425,6 +485,7 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];

+
" href="">
+ diff --git a/lib/send_push.php b/lib/send_push.php index 37dba79..0e019bd 100644 --- a/lib/send_push.php +++ b/lib/send_push.php @@ -97,7 +97,11 @@ function try_send_show_notification($articleRefOrJob, $jobdir, $mvlog_root = nul $job = basename((string)$jobdir); $articleRef = strtolower(trim((string)$articleRefOrJob)); $articleId = mvlog_article_id_is_valid($articleRef) ? $articleRef : mvlog_read_article_id((string)$jobdir); - $identity = $articleId !== '' ? $articleId : $job; + if ($articleId === '') { + error_log("[mvlog] article id missing, skipping push for $job\n", 3, $logfile); + return false; + } + $identity = $articleId; // Don't notify for hidden jobs if (is_file($jobdir . '/.mvlog-hidden')) { @@ -169,14 +173,12 @@ function try_send_show_notification($articleRefOrJob, $jobdir, $mvlog_root = nul if ($articleTitle === '') $articleTitle = mvlog_nice_title((string)$job); - $filterUrl = 'https://bubulescu.org/?' . http_build_query([ - 'q' => $articleTitle, - ]); + $articleUrl = './?id=' . rawurlencode($articleId); $payload = json_encode([ 'title' => $articleTitle, 'body' => 'New aticle @ MVL!', - 'url' => $filterUrl, + 'url' => $articleUrl, 'tag' => "mvlog-show-{$identity}", ], JSON_UNESCAPED_UNICODE); diff --git a/new.php b/new.php index a6b6e18..9187e61 100644 --- a/new.php +++ b/new.php @@ -48,17 +48,160 @@ function ascii_safe($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'; - $cmd = sprintf( - 'convert %s -font %s -pointsize 30 -fill "#F3F4F6" -gravity northwest -annotate +20+20 %s -font %s -pointsize 20 -fill "#EADFC9" -gravity northwest -annotate +20+60 %s %s', - escapeshellarg($imagePath), - escapeshellarg($fontPath), - escapeshellarg($title), - escapeshellarg($fontPath), - escapeshellarg($teaser), - escapeshellarg($imagePath) + $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, bool $cutLongWords = false) 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", $cutLongWords); + $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 = 84; $size >= 54; $size -= 4) { + $width = max(16, min(46, (int)round(23 + (84 - $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, 34, 6, true); + $teaserLines = max(1, substr_count($teaserText, "\n") + 1); + + // Move the whole block a bit left and higher while keeping left-aligned text. + $left = max(14, (int)round($imgW * 0.046)); + $titleY = max(20, (int)round($imgH * 0.062)); + $titleLineStep = max(68, (int)round($imgH * 0.043)); + $teaserY = max((int)round($imgH * 0.235), $titleY + ($titleLines * $titleLineStep) + max(54, (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) ); - shell_exec($cmd); + 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 70,82 %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) ?: []; } @@ -585,16 +728,39 @@ 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 ''; - $base = pathinfo($videoName, PATHINFO_FILENAME); - foreach (['jpg','jpeg','png','webp','gif'] as $ext) { - $file = $config['videos_dir'] . '/' . $base . '_thumb.' . $ext; - if (is_file($file)) return $config['public_videos'] . '/' . rawurlencode($base . '_thumb.' . $ext) . '?v=' . filemtime($file); + $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'; @@ -647,9 +813,13 @@ function input_dir_output($dir){ return basename((string)($state['output'] ?? '')); } function regenerate_input_dir_maps($dir, $config){ + $logFile = '/var/log/mvlog_map.log'; $python = trim((string)shell_exec('command -v python3 2>/dev/null')); $script = __DIR__ . '/bin/gpsmap.py'; - if ($python === '' || !is_file($script)) return; + if ($python === '' || !is_file($script)) { + file_put_contents($logFile, '[mvlog] map regenerate skipped for ' . basename((string)$dir) . ' (python=' . ($python !== '' ? 'yes' : 'no') . ', script=' . (is_file($script) ? 'yes' : 'no') . ")\n", FILE_APPEND); + return; + } $state = input_dir_state($dir); $targets = []; @@ -665,14 +835,19 @@ function regenerate_input_dir_maps($dir, $config){ } $targets = array_values(array_unique($targets)); + file_put_contents($logFile, '[mvlog] map regenerate requested for ' . basename((string)$dir) . ' targets=' . implode(', ', array_map('basename', $targets)) . "\n", FILE_APPEND); foreach ($targets as $outPng) { + file_put_contents($logFile, '[mvlog] map regenerate start for ' . basename((string)$dir) . ' target=' . basename((string)$outPng) . "\n", FILE_APPEND); $cmd = escapeshellarg($python) . ' ' . escapeshellarg($script) . ' ' . escapeshellarg($dir) . ' ' . escapeshellarg($outPng) . ' 2>&1'; $output = (string)shell_exec($cmd); if (is_file($outPng)) { @chmod($outPng, 0664); + clearstatcache(true, $outPng); + $size = @filesize($outPng); + file_put_contents($logFile, '[mvlog] map regenerate done for ' . basename((string)$dir) . ' target=' . basename((string)$outPng) . ' bytes=' . ($size !== false ? $size : 'unknown') . "\n", FILE_APPEND); } 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'); + file_put_contents($logFile, '[mvlog] map regenerate failed for ' . basename((string)$dir) . ' target=' . basename((string)$outPng) . ' output=' . trim($output) . "\n", FILE_APPEND); } } } @@ -860,6 +1035,9 @@ try { 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.'); @@ -876,6 +1054,19 @@ try { 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); @@ -884,15 +1075,20 @@ try { $old = $config['thumbs_dir'] . '/' . $base . '.' . $oldExt; if (is_file($old)) @unlink($old); } - - // Copy new thumbnail to videos_dir with _thumb.jpg suffix - $target = $config['videos_dir'] . '/' . $base . '_thumb.jpg'; - if (!@copy($src, $target)) throw new RuntimeException('Failed to write thumbnail.'); - @chmod($target, 0664); - - // Apply text overlay - apply_thumbnail_overlay($target, $meta['title'], $meta['teaser']); - + $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); } @@ -912,11 +1108,15 @@ try { $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); + file_put_contents('/var/log/mvlog_map.log', '[mvlog] save changes pressed for ' . basename($dir) . ' id=' . $articleId . "\n", FILE_APPEND); + try { + write_data($dir, $_POST); + save_uploads('media', $dir, $allowedMedia); + save_uploads('audio', $dir, $allowedAudio); + } finally { + // Always regenerate map image(s) whenever Save changes is pressed. + 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; @@ -1067,7 +1267,7 @@ $runningJobs = active_worker_jobs($config);

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
+

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:

@@ -1111,7 +1311,7 @@ $runningJobs = active_worker_jobs($config);
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
+

Edit input dir

in-dir/

Files and captions

No caption for audio files

Add new movie input