Switch admin/API flows to article IDs with legacy job fallback

This commit is contained in:
hbrain 2026-05-31 18:04:21 +02:00
parent 3cc2828bc0
commit efcf87d00c
5 changed files with 205 additions and 67 deletions

View file

@ -7,17 +7,57 @@ mvlog_require_login();
$MVLOG_ROOT = dirname(__DIR__);
header('Content-Type: application/json; charset=utf-8');
$job = $_POST['job'] ?? '';
$id = strtolower(trim((string)($_POST['id'] ?? '')));
$job = trim((string)($_POST['job'] ?? ''));
$max_frames = isset($_POST['max_frames']) ? intval($_POST['max_frames']) : 16;
if (!preg_match('/^[0-9]{8}[-_][A-Za-z0-9._-]+$/', $job)) {
http_response_code(400);
echo json_encode(['status'=>'error','message'=>'invalid job']);
exit;
function mvlog_article_id_is_valid(string $id): bool {
return (bool)preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $id);
}
$jobdir = $MVLOG_ROOT . '/in-dir/' . $job;
if (!is_dir($jobdir)) {
function mvlog_legacy_job_is_valid(string $job): bool {
return (bool)preg_match('/^[0-9]{8}[-_][A-Za-z0-9._-]+$/', $job);
}
function mvlog_read_article_id(string $jobdir): string {
$path = $jobdir . '/.mvlog-id';
if (!is_file($path)) return '';
$value = strtolower(trim((string)@file_get_contents($path)));
return mvlog_article_id_is_valid($value) ? $value : '';
}
function mvlog_find_jobdir_by_id(string $mvlogRoot, string $articleId): ?string {
if (!mvlog_article_id_is_valid($articleId)) return null;
$cacheFile = $mvlogRoot . '/cache/articles-index.json';
if (is_file($cacheFile)) {
$cache = json_decode((string)@file_get_contents($cacheFile), true);
if (is_array($cache) && is_array($cache['by_id'] ?? null)) {
$name = (string)($cache['by_id'][$articleId] ?? '');
if ($name !== '') {
$candidate = $mvlogRoot . '/in-dir/' . basename($name);
if (is_dir($candidate)) return $candidate;
}
}
}
foreach (glob($mvlogRoot . '/in-dir/*', GLOB_ONLYDIR) ?: [] as $dir) {
if (mvlog_read_article_id($dir) === $articleId) return $dir;
}
return null;
}
$jobdir = null;
if ($id !== '' && mvlog_article_id_is_valid($id)) {
$jobdir = mvlog_find_jobdir_by_id($MVLOG_ROOT, $id);
}
if ($jobdir === null && $job !== '' && mvlog_legacy_job_is_valid($job)) {
$candidate = $MVLOG_ROOT . '/in-dir/' . basename($job);
if (is_dir($candidate)) $jobdir = $candidate;
}
if ($jobdir === null || !is_dir($jobdir)) {
http_response_code(404);
echo json_encode(['status'=>'error','message'=>'job not found']);
exit;

View file

@ -3,7 +3,7 @@
* send_push.php
* Helper function to send "Show enabled" web-push notifications from the webserver.
*
* Function: try_send_show_notification(string $job, string $jobdir, string $mvlog_root = null, string $logfile = null): bool
* Function: try_send_show_notification(string $articleRefOrJob, string $jobdir, string $mvlog_root = null, string $logfile = null): bool
*
* Returns true on successful send and cache update, false otherwise.
*/
@ -73,7 +73,20 @@ function mvlog_nice_title(string $s): string
return trim(ucwords(str_replace(['_', '-'], ' ', $s)));
}
function try_send_show_notification($job, $jobdir, $mvlog_root = null, $logfile = null)
function mvlog_article_id_is_valid(string $id): bool
{
return (bool)preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $id);
}
function mvlog_read_article_id(string $jobdir): string
{
$path = rtrim($jobdir, '/') . '/.mvlog-id';
if (!is_file($path)) return '';
$value = strtolower(trim((string)@file_get_contents($path)));
return mvlog_article_id_is_valid($value) ? $value : '';
}
function try_send_show_notification($articleRefOrJob, $jobdir, $mvlog_root = null, $logfile = null)
{
if ($mvlog_root === null) $mvlog_root = dirname(__DIR__);
if ($logfile === null) $logfile = '/var/log/mvlog_notify.log';
@ -81,6 +94,11 @@ function try_send_show_notification($job, $jobdir, $mvlog_root = null, $logfile
$subs_file = $mvlog_root . '/cache/push_subscriptions.json';
$cache_file = $mvlog_root . '/cache/push_notifications.json';
$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;
// Don't notify for hidden jobs
if (is_file($jobdir . '/.mvlog-hidden')) {
error_log("[mvlog] job $job hidden, skipping push\n", 3, $logfile);
@ -89,17 +107,17 @@ function try_send_show_notification($job, $jobdir, $mvlog_root = null, $logfile
// Gather subscriptions
if (!is_file($subs_file)) {
error_log("[mvlog] subscriptions file missing, skipping push for $job\n", 3, $logfile);
error_log("[mvlog] subscriptions file missing, skipping push for $identity\n", 3, $logfile);
return false;
}
$subs_raw = @file_get_contents($subs_file);
if (!is_string($subs_raw) || trim($subs_raw) === '') {
error_log("[mvlog] subscriptions empty, skipping push for $job\n", 3, $logfile);
error_log("[mvlog] subscriptions empty, skipping push for $identity\n", 3, $logfile);
return false;
}
$subs = json_decode($subs_raw, true);
if (!is_array($subs) || count($subs) === 0) {
error_log("[mvlog] subscriptions invalid/empty, skipping push for $job\n", 3, $logfile);
error_log("[mvlog] subscriptions invalid/empty, skipping push for $identity\n", 3, $logfile);
return false;
}
@ -109,21 +127,22 @@ function try_send_show_notification($job, $jobdir, $mvlog_root = null, $logfile
$cache = json_decode((string)@file_get_contents($cache_file), true) ?: [];
}
$shown = isset($cache['shown']) && is_array($cache['shown']) ? $cache['shown'] : [];
if (isset($shown[$job])) {
error_log("[mvlog] push already sent for $job, skipping\n", 3, $logfile);
// Transition support: honor both new (ID) and legacy (job name) keys.
if (isset($shown[$identity]) || ($identity !== $job && isset($shown[$job]))) {
error_log("[mvlog] push already sent for $identity (job=$job), skipping\n", 3, $logfile);
return false;
}
// Resolve push config consistently (same source model as push_config.php)
$cfg = mvlog_load_push_config($mvlog_root);
if (!$cfg) {
error_log("[mvlog] push config not found/invalid, skipping push for $job\n", 3, $logfile);
error_log("[mvlog] push config not found/invalid, skipping push for $identity\n", 3, $logfile);
return false;
}
$tmp_push_config = mvlog_write_temp_push_json($cfg);
if (!$tmp_push_config) {
error_log("[mvlog] failed to create temp push config for $job\n", 3, $logfile);
error_log("[mvlog] failed to create temp push config for $identity\n", 3, $logfile);
return false;
}
@ -135,7 +154,7 @@ function try_send_show_notification($job, $jobdir, $mvlog_root = null, $logfile
$state_file = $jobdir . '/.movmaker-state.json';
if (is_file($state_file)) {
$state = json_decode(@file_get_contents($state_file), true) ?: [];
$state = json_decode((string)@file_get_contents($state_file), true) ?: [];
if (!empty($state['output']) && is_string($state['output'])) {
$out = pathinfo($state['output'], PATHINFO_FILENAME);
if ($out) {
@ -158,7 +177,7 @@ function try_send_show_notification($job, $jobdir, $mvlog_root = null, $logfile
'title' => $articleTitle,
'body' => 'New MVLog article is now visible',
'url' => $filterUrl,
'tag' => "mvlog-show-{$job}",
'tag' => "mvlog-show-{$identity}",
], JSON_UNESCAPED_UNICODE);
// Call node send_push.js with subscriptions on stdin
@ -176,7 +195,7 @@ function try_send_show_notification($job, $jobdir, $mvlog_root = null, $logfile
$proc = proc_open($cmd, $descriptorspec, $pipes);
if (!is_resource($proc)) {
@unlink($tmp_push_config);
error_log("[mvlog] failed to start node send_push process for $job\n", 3, $logfile);
error_log("[mvlog] failed to start node send_push process for $identity\n", 3, $logfile);
return false;
}
@ -191,29 +210,33 @@ function try_send_show_notification($job, $jobdir, $mvlog_root = null, $logfile
@unlink($tmp_push_config);
if ($rc !== 0) {
error_log("[mvlog] send_push failed for $job rc=$rc stdout=" . trim((string)$stdout) . " stderr=" . trim((string)$stderr) . "\n", 3, $logfile);
error_log("[mvlog] send_push failed for $identity rc=$rc stdout=" . trim((string)$stdout) . " stderr=" . trim((string)$stderr) . "\n", 3, $logfile);
return false;
}
// On success, record in cache['shown']
if (!is_array($cache)) $cache = [];
if (!isset($cache['shown']) || !is_array($cache['shown'])) $cache['shown'] = [];
$cache['shown'][$job] = ['sent_at' => date(DATE_ATOM)];
$cache['shown'][$identity] = [
'sent_at' => date(DATE_ATOM),
'job' => $job,
'article_id' => $articleId,
];
// Atomic write: write to tmp then rename
$tmp = $cache_file . '.tmp.' . bin2hex(random_bytes(6));
$json = json_encode($cache, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
if (@file_put_contents($tmp, $json, LOCK_EX) === false) {
error_log("[mvlog] failed to write cache temp file for $job\n", 3, $logfile);
error_log("[mvlog] failed to write cache temp file for $identity\n", 3, $logfile);
return false;
}
@chmod($tmp, 0664);
if (!@rename($tmp, $cache_file)) {
@unlink($tmp);
error_log("[mvlog] failed to atomically write cache file for $job\n", 3, $logfile);
error_log("[mvlog] failed to atomically write cache file for $identity\n", 3, $logfile);
return false;
}
error_log("[mvlog] show-notification sent: $job (cfg=" . ($cfg['_source'] ?? 'unknown') . ")\n", 3, $logfile);
error_log("[mvlog] show-notification sent: identity=$identity job=$job (cfg=" . ($cfg['_source'] ?? 'unknown') . ")\n", 3, $logfile);
return true;
}