Use article IDs for permalinks and OG images

This commit is contained in:
hbrain 2026-06-02 09:50:00 +00:00
parent fbc4f1045a
commit 37e43b6bca
3 changed files with 168 additions and 59 deletions

View file

@ -122,11 +122,18 @@ function try_send_show_notification($job, $jobdir, $root, $subs_file, $cache_fil
} }
} }
$article_id_file = $jobdir . '/.mvlog-id';
$article_id = file_exists($article_id_file) ? strtolower(trim((string)@file_get_contents($article_id_file))) : '';
if (!preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $article_id)) {
error_log("[mvlog] article id missing, skipping push for $job\n", 3, $logfile);
return false;
}
$payload = json_encode([ $payload = json_encode([
'title' => 'Show enabled', 'title' => 'Show enabled',
'body' => $title, 'body' => $title,
'url' => "https://bubulescu.org/mvlog/?job=$job", 'url' => './?id=' . rawurlencode($article_id),
'tag' => "mvlog-show-$job", 'tag' => "mvlog-show-$article_id",
], JSON_UNESCAPED_UNICODE); ], JSON_UNESCAPED_UNICODE);
// Call node send_push.js with subscriptions on stdin // Call node send_push.js with subscriptions on stdin

View file

@ -57,16 +57,60 @@ function map_hidden_outputs($config){
return $hidden; 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){ function video_thumb_public_url($config, $videoName){
$videoName = basename((string)$videoName); $videoName = basename((string)$videoName);
if ($videoName === '') return ''; if ($videoName === '') return '';
$base = pathinfo($videoName, PATHINFO_FILENAME); $info = function_exists('video_thumb_source_info') ? video_thumb_source_info($config, $videoName) : [];
foreach (['jpg','jpeg','png','webp','gif'] as $ext) { $sourcePath = (string)($info['source_path'] ?? '');
$file = $config['videos_dir'] . '/' . $base . '_thumb.' . $ext; if ($sourcePath !== '') {
if (is_file($file)) return $config['public_videos'] . '/' . rawurlencode($base . '_thumb.' . $ext) . '?v=' . filemtime($file); $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 ''; 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){ function video_metadata($path){
$base = pathinfo($path, PATHINFO_FILENAME); $base = pathinfo($path, PATHINFO_FILENAME);
@ -181,8 +225,8 @@ usort($videos, function($a, $b) use ($videoCache) {
$articleId = trim((string)($_GET['id'] ?? '')); $articleId = trim((string)($_GET['id'] ?? ''));
if ($articleId !== '') { if ($articleId !== '') {
$videos = array_values(array_filter($videos, function($v) use ($articleId) { $videos = array_values(array_filter($videos, function($v) use ($articleId, $config) {
return pathinfo($v, PATHINFO_FILENAME) === $articleId; return video_article_id($config, basename($v)) === $articleId;
})); }));
$rawKeyword = ''; $rawKeyword = '';
$keywordType = 'general'; $keywordType = 'general';
@ -248,6 +292,21 @@ $slice = array_slice($videos, ($page - 1) * $per, $per);
$activeFilterCount = ($keywordValue !== '') ? 1 : 0; $activeFilterCount = ($keywordValue !== '') ? 1 : 0;
$baseParams = ($rawKeyword !== '') ? ['q' => $rawKeyword] : []; $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 = [ $headerTitles = [
'Multiple Voices Log', 'Multiple Voices Log',
'MultiVerse Log', 'MultiVerse Log',
@ -307,15 +366,15 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
<meta property="og:site_name" content="Bubulescu.Org"> <meta property="og:site_name" content="Bubulescu.Org">
<!-- Image --> <!-- Image -->
<meta property="og:image" content="https://bubulescu.org/assets/img/bubulescuorg.jpg"> <meta property="og:image" content="<?=h($ogImageUrl)?>">
<meta property="og:image:secure_url" content="https://bubulescu.org/assets/img/bubulescuorg.jpg"> <meta property="og:image:secure_url" content="<?=h($ogImageUrl)?>">
<meta property="og:image:type" content="image/jpeg"> <meta property="og:image:type" content="image/jpeg">
<meta property="og:image:width" content="1200"> <meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630"> <meta property="og:image:height" content="630">
<meta property="og:image:alt" content="MVLog: journal of an unreliable narrator."> <meta property="og:image:alt" content="<?=h($ogImageAlt)?>">
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://bubulescu.org/assets/img/bubulescuorg.jpg"> <meta name="twitter:image" content="<?=h($ogImageUrl)?>">
<link rel="alternate" type="application/rss+xml" title="MVLog RSS" href="feed.php"> <link rel="alternate" type="application/rss+xml" title="MVLog RSS" href="feed.php">
<link rel="icon" type="image/png" href="assets/img/moto_travel.png"> <link rel="icon" type="image/png" href="assets/img/moto_travel.png">
@ -385,7 +444,8 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
<?php foreach($slice as $v): <?php foreach($slice as $v):
$name = basename($v); $name = basename($v);
$base = pathinfo($name, PATHINFO_FILENAME); $base = pathinfo($name, PATHINFO_FILENAME);
$permalinkUrl = query_url(['id' => $base]); $articleIdForVideo = video_article_id($config, $name);
$permalinkUrl = $articleIdForVideo !== '' ? query_url(['id' => $articleIdForVideo]) : '';
$url = $config['public_videos'].'/'.rawurlencode($name); $url = $config['public_videos'].'/'.rawurlencode($name);
$m = $videoCache[$name]['metadata'] ?? video_metadata($v); $m = $videoCache[$name]['metadata'] ?? video_metadata($v);
$posterUrl = video_thumb_public_url($config, $name); $posterUrl = video_thumb_public_url($config, $name);
@ -394,7 +454,7 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
?> ?>
<article class="video-row"> <article class="video-row">
<div class="video-row-head video-row-head-mobile"> <div class="video-row-head video-row-head-mobile">
<h2 class="video-row-title"><a href="<?=h($permalinkUrl)?>"><?=h($m['title'])?></a></h2> <h2 class="video-row-title"><?php if($permalinkUrl !== ''): ?><a href="<?=h($permalinkUrl)?>"><?=h($m['title'])?></a><?php else: ?><?=h($m['title'])?><?php endif; ?></h2>
<p class="meta"> <p class="meta">
<span><a href="<?=h($dateFilterUrl)?>"><?=h($m['date'])?></a></span> <span><a href="<?=h($dateFilterUrl)?>"><?=h($m['date'])?></a></span>
<?php if(!empty($m['location'])): ?><span><a href="<?=h($locFilterUrl)?>"><?=h($m['location'])?></a></span><?php endif; ?> <?php if(!empty($m['location'])): ?><span><a href="<?=h($locFilterUrl)?>"><?=h($m['location'])?></a></span><?php endif; ?>
@ -412,7 +472,7 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
<?php endif; ?> <?php endif; ?>
</div> </div>
<div class="video-info"> <div class="video-info">
<h2 class="video-row-title video-row-title-desktop"><a href="<?=h($permalinkUrl)?>"><?=h($m['title'])?></a></h2> <h2 class="video-row-title video-row-title-desktop"><?php if($permalinkUrl !== ''): ?><a href="<?=h($permalinkUrl)?>"><?=h($m['title'])?></a><?php else: ?><?=h($m['title'])?><?php endif; ?></h2>
<p class="meta meta-desktop"> <p class="meta meta-desktop">
<span><a href="<?=h($dateFilterUrl)?>"><?=h($m['date'])?></a></span> <span><a href="<?=h($dateFilterUrl)?>"><?=h($m['date'])?></a></span>
<?php if(!empty($m['location'])): ?><span><a href="<?=h($locFilterUrl)?>"><?=h($m['location'])?></a></span><?php endif; ?> <?php if(!empty($m['location'])): ?><span><a href="<?=h($locFilterUrl)?>"><?=h($m['location'])?></a></span><?php endif; ?>

130
new.php
View file

@ -62,9 +62,8 @@ function apply_thumbnail_overlay($imagePath, $title, $teaser) {
throw new RuntimeException('Thumbnail font file is missing.'); throw new RuntimeException('Thumbnail font file is missing.');
} }
$dims = @getimagesize($imagePath); $imgW = 1200;
$imgW = max(1, (int)($dims[0] ?? 1920)); $imgH = 630;
$imgH = max(1, (int)($dims[1] ?? 1080));
$normalize = static function ($text): string { $normalize = static function ($text): string {
$text = trim((string)$text); $text = trim((string)$text);
@ -92,11 +91,11 @@ function apply_thumbnail_overlay($imagePath, $title, $teaser) {
}; };
$fitTitleText = static function ($text) use ($wrapText): array { $fitTitleText = static function ($text) use ($wrapText): array {
$bestText = ''; $bestText = '';
$bestSize = 285; $bestSize = 95;
$bestWidth = 18; $bestWidth = 28;
$bestLines = 1; $bestLines = 1;
for ($size = 320; $size >= 240; $size -= 5) { for ($size = 94; $size >= 60; $size -= 4) {
$width = max(14, min(28, (int)round(22 + (285 - $size) / 8))); $width = max(16, min(46, (int)round(24 + (94 - $size) / 2.7)));
$candidate = $wrapText($text, $width); $candidate = $wrapText($text, $width);
$lines = substr_count($candidate, "\n") + 1; $lines = substr_count($candidate, "\n") + 1;
$bestText = $candidate; $bestText = $candidate;
@ -109,14 +108,14 @@ function apply_thumbnail_overlay($imagePath, $title, $teaser) {
}; };
[$titleText, $titleFontSize, $titleWrapWidth, $titleLines] = $fitTitleText($title); [$titleText, $titleFontSize, $titleWrapWidth, $titleLines] = $fitTitleText($title);
$teaserText = $clampWrappedText($teaser, 34, 5); $teaserText = $clampWrappedText($teaser, 38, 6);
$teaserLines = max(1, substr_count($teaserText, "\n") + 1); $teaserLines = max(1, substr_count($teaserText, "\n") + 1);
// Move the whole block a bit right while keeping left-aligned text. // Move the whole block a bit right while keeping left-aligned text.
$left = max(18, (int)round($imgW * 0.055)); $left = max(18, (int)round($imgW * 0.055));
$titleY = max(24, (int)round($imgH * 0.06)); $titleY = max(24, (int)round($imgH * 0.06));
$titleLineStep = max(230, (int)round($imgH * 0.085)); $titleLineStep = max(68, (int)round($imgH * 0.043));
$teaserY = max((int)round($imgH * 0.27), $titleY + ($titleLines * $titleLineStep) + max(140, (int)round($imgH * 0.06))); $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_'); $overlayBase = tempnam(sys_get_temp_dir(), 'thumb_overlay_');
$outputPath = dirname($imagePath) . '/' . basename($imagePath) . '.tmp.' . uniqid('', true) . '.jpg'; $outputPath = dirname($imagePath) . '/' . basename($imagePath) . '.tmp.' . uniqid('', true) . '.jpg';
@ -127,7 +126,7 @@ function apply_thumbnail_overlay($imagePath, $title, $teaser) {
@unlink($overlayBase); @unlink($overlayBase);
$cmd1 = sprintf( $cmd1 = sprintf(
'/usr/bin/convert -size %dx%d xc:none -font %s -pointsize %d -fill "#F3F4F6" -stroke black -strokewidth 7 -interline-spacing 2 -gravity northwest -annotate +%d+%d %s -font %s -pointsize 220 -fill "#EADFC9" -stroke black -strokewidth 6 -interline-spacing -90 -gravity northwest -annotate +%d+%d %s %s 2>&1', '/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, $imgW,
$imgH, $imgH,
escapeshellarg($titleFontPath), escapeshellarg($titleFontPath),
@ -150,25 +149,54 @@ function apply_thumbnail_overlay($imagePath, $title, $teaser) {
throw new RuntimeException('Failed to render thumbnail overlay: ' . trim((string)$out1)); throw new RuntimeException('Failed to render thumbnail overlay: ' . trim((string)$out1));
} }
$cmd2 = sprintf( $maxBytes = 600 * 1024;
'/usr/bin/convert %s -modulate 56,78 %s -composite %s 2>&1', $qualities = [88, 84, 80, 76, 72, 68, 64, 60, 56, 52, 48];
escapeshellarg($imagePath), $finalPath = '';
escapeshellarg($overlayPath), $finalSize = 0;
escapeshellarg($outputPath) $finalOut = '';
);
file_put_contents($logFile, "Composite CMD: " . $cmd2 . "\n", FILE_APPEND);
$out2 = shell_exec($cmd2);
file_put_contents($logFile, "Composite Output: " . $out2 . "\n", FILE_APPEND);
if (!is_file($outputPath) || filesize($outputPath) < 100) { foreach ($qualities as $quality) {
@unlink($overlayPath); $attemptPath = dirname($imagePath) . '/' . basename($imagePath) . '.tmp.' . uniqid('', true) . '.jpg';
@unlink($outputPath); $cmd2 = sprintf(
throw new RuntimeException('Failed to compose thumbnail overlay: ' . trim((string)$out2)); '/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 (!@rename($outputPath, $imagePath)) { if ($finalPath === '') {
@unlink($overlayPath); @unlink($overlayPath);
@unlink($outputPath); @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.'); throw new RuntimeException('Failed to replace thumbnail image.');
} }
@ -700,13 +728,27 @@ function is_image_media_file($name){
$ext = strtolower(pathinfo((string)$name, PATHINFO_EXTENSION)); $ext = strtolower(pathinfo((string)$name, PATHINFO_EXTENSION));
return in_array($ext, ['jpg','jpeg','png','webp','gif'], true); 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){ function video_thumb_public_url($config, $videoName){
$videoName = basename((string)$videoName); $videoName = basename((string)$videoName);
if ($videoName === '') return ''; if ($videoName === '') return '';
$base = pathinfo($videoName, PATHINFO_FILENAME); $info = video_thumb_source_info($config, $videoName);
foreach (['jpg','jpeg','png','webp','gif'] as $ext) { $sourcePath = (string)($info['source_path'] ?? '');
$file = $config['videos_dir'] . '/' . $base . '_thumb.' . $ext; if ($sourcePath !== '') {
if (is_file($file)) return $config['public_videos'] . '/' . rawurlencode($base . '_thumb.' . $ext) . '?v=' . filemtime($file); $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 ''; return '';
} }
@ -1003,6 +1045,19 @@ try {
if (!$outputs) throw new RuntimeException('No rendered video found for this input directory yet.'); if (!$outputs) throw new RuntimeException('No rendered video found for this input directory yet.');
$src = $dir . '/' . $file; $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 = []; $written = [];
foreach ($outputs as $out) { foreach ($outputs as $out) {
$base = pathinfo($out, PATHINFO_FILENAME); $base = pathinfo($out, PATHINFO_FILENAME);
@ -1011,25 +1066,13 @@ try {
$old = $config['thumbs_dir'] . '/' . $base . '.' . $oldExt; $old = $config['thumbs_dir'] . '/' . $base . '.' . $oldExt;
if (is_file($old)) @unlink($old); 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)) {
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
apply_thumbnail_overlay($target, (string)($meta['title'] ?? ''), (string)($meta['teaser'] ?? ''));
$thumbMeta = [ $thumbMeta = [
'article_id' => $articleId, 'article_id' => $articleId,
'input_dir' => basename($dir), 'input_dir' => basename($dir),
'source_file' => $file, 'source_file' => $file,
'source_path' => 'in-dir/' . basename($dir) . '/' . $file, 'source_path' => 'in-dir/' . basename($dir) . '/' . $file,
'thumb_file' => $thumbName,
'thumb_path' => 'out-dir/' . $thumbName,
'created_at' => gmdate('c'), 'created_at' => gmdate('c'),
]; ];
$thumbMetaPath = $config['videos_dir'] . '/' . $base . '_thumb.json'; $thumbMetaPath = $config['videos_dir'] . '/' . $base . '_thumb.json';
@ -1037,7 +1080,6 @@ try {
throw new RuntimeException('Failed to write thumbnail metadata.'); throw new RuntimeException('Failed to write thumbnail metadata.');
} }
@chmod($thumbMetaPath, 0664); @chmod($thumbMetaPath, 0664);
$written[] = basename($target); $written[] = basename($target);
} }