From efcf87d00ccf54856f6b5c43c24a2dd75695cd92 Mon Sep 17 00:00:00 2001 From: hbrain Date: Sun, 31 May 2026 18:04:21 +0200 Subject: [PATCH] Switch admin/API flows to article IDs with legacy job fallback --- api/enable_show.php | 90 ++++++++++++++++++++++++++++++-------- job_status.php | 5 +++ lib/generate_data_sync.php | 54 ++++++++++++++++++++--- lib/send_push.php | 57 +++++++++++++++++------- new.php | 66 +++++++++++++++++----------- 5 files changed, 205 insertions(+), 67 deletions(-) diff --git a/api/enable_show.php b/api/enable_show.php index 94dc6d3..6ff800d 100644 --- a/api/enable_show.php +++ b/api/enable_show.php @@ -4,28 +4,86 @@ require __DIR__ . '/../auth.php'; mvlog_require_login(); // Web-server side handler to toggle "Show" for an MVLog job and send a web-push immediately if appropriate. -// Usage (POST): job= show=1|0 +// Preferred usage (POST): id= show=1|0 +// Legacy fallback (POST): job= show=1|0 $MVLOG_ROOT = dirname(__DIR__); -$job = $_POST['job'] ?? ''; +$id = trim((string)($_POST['id'] ?? '')); +$job = trim((string)($_POST['job'] ?? '')); $show = isset($_POST['show']) ? filter_var($_POST['show'], FILTER_VALIDATE_BOOLEAN) : true; -// Basic validation -if (!preg_match('/^[0-9]{8}[-_][A-Za-z0-9._-]+$/', $job)) { - http_response_code(400); - echo '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 ''; + $id = strtolower(trim((string)@file_get_contents($path))); + return mvlog_article_id_is_valid($id) ? $id : ''; +} + +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; +} + +function mvlog_resolve_jobdir(string $mvlogRoot, string $id, string $job, ?string &$resolvedJob, ?string &$resolvedId): ?string { + $resolvedJob = null; + $resolvedId = null; + + if ($id !== '' && mvlog_article_id_is_valid($id)) { + $dir = mvlog_find_jobdir_by_id($mvlogRoot, strtolower($id)); + if ($dir !== null) { + $resolvedJob = basename($dir); + $resolvedId = mvlog_read_article_id($dir); + return $dir; + } + } + + if ($job !== '' && mvlog_legacy_job_is_valid($job)) { + $dir = $mvlogRoot . '/in-dir/' . basename($job); + if (is_dir($dir)) { + $resolvedJob = basename($dir); + $resolvedId = mvlog_read_article_id($dir); + return $dir; + } + } + + return null; +} + +$resolvedJob = null; +$resolvedId = null; +$jobdir = mvlog_resolve_jobdir($MVLOG_ROOT, strtolower($id), $job, $resolvedJob, $resolvedId); +if ($jobdir === null) { http_response_code(404); echo 'job not found'; exit; } $enabled_file = "$jobdir/.movmaker-enabled"; -$cache_file = "$MVLOG_ROOT/cache/push_notifications.json"; if ($show) { file_put_contents($enabled_file, date(DATE_ATOM) . PHP_EOL, LOCK_EX); @@ -35,20 +93,16 @@ if ($show) { if (is_file(__DIR__ . '/../lib/send_push.php')) { include_once __DIR__ . '/../lib/send_push.php'; try { - $sent = try_send_show_notification($job, $jobdir, $MVLOG_ROOT, '/var/log/mvlog_notify.log'); + $sent = try_send_show_notification($resolvedId ?: $resolvedJob, $jobdir, $MVLOG_ROOT, '/var/log/mvlog_notify.log'); } catch (Throwable $e) { - error_log("[mvlog] notify exception for $job: " . $e->getMessage() . "\n", 3, '/var/log/mvlog_notify.log'); + error_log("[mvlog] notify exception for " . ($resolvedId ?: $resolvedJob) . ": " . $e->getMessage() . "\n", 3, '/var/log/mvlog_notify.log'); $sent = false; } } else { - error_log("[mvlog] send_push helper missing, cannot send notification for $job\n", 3, '/var/log/mvlog_notify.log'); + error_log("[mvlog] send_push helper missing, cannot send notification for " . ($resolvedId ?: $resolvedJob) . "\n", 3, '/var/log/mvlog_notify.log'); } - if ($sent) { - echo 'enabled'; - } else { - echo 'enabled (no notification)'; - } + echo $sent ? 'enabled' : 'enabled (no notification)'; } else { @unlink($enabled_file); echo 'disabled'; diff --git a/job_status.php b/job_status.php index 7e1352e..4e55db7 100644 --- a/job_status.php +++ b/job_status.php @@ -10,8 +10,12 @@ foreach ($dirs as $dir) { $name = basename($dir); $stateFile = $dir . '/.movmaker-state.json'; $lockDir = $dir . '/.movmaker-lock'; + $idFile = $dir . '/.mvlog-id'; $state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : []; $status = is_array($state) ? (string)($state['status'] ?? '') : ''; + $articleId = is_array($state) ? strtolower((string)($state['article_id'] ?? '')) : ''; + if ($articleId === '' && is_file($idFile)) $articleId = strtolower(trim((string)file_get_contents($idFile))); + if (!preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $articleId)) $articleId = ''; $locked = is_dir($lockDir); if ($locked || $status === 'processing') { $started = ''; @@ -19,6 +23,7 @@ foreach ($dirs as $dir) { if ($started === '' && is_array($state)) $started = (string)($state['started_at'] ?? $state['updated_at'] ?? ''); $jobs[] = [ 'name' => $name, + 'id' => $articleId, 'status' => $status ?: ($locked ? 'processing' : 'unknown'), 'started_at' => $started, ]; diff --git a/lib/generate_data_sync.php b/lib/generate_data_sync.php index c6033aa..c079753 100644 --- a/lib/generate_data_sync.php +++ b/lib/generate_data_sync.php @@ -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; diff --git a/lib/send_push.php b/lib/send_push.php index c06107d..6d9fbc9 100644 --- a/lib/send_push.php +++ b/lib/send_push.php @@ -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; } diff --git a/new.php b/new.php index f49d9f4..bfbd32c 100644 --- a/new.php +++ b/new.php @@ -84,7 +84,7 @@ function load_articles_index($base, $dirs = null){ foreach ($dirs as $dir) { $name = basename((string)$dir); $id = read_article_id($dir); - if (($id === '' || !empty($byId[$id])) && !is_dir($dir . '/.movmaker-lock')) { + if ($id === '' || !empty($byId[$id])) { $id = ensure_article_id($dir, $byId); } if ($id !== '' && empty($byId[$id])) $byId[$id] = $name; @@ -561,13 +561,14 @@ function active_worker_jobs($config){ 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,'status'=>$status ?: ($locked ? 'processing' : 'unknown'),'started_at'=>$started]; + $jobs[] = ['name'=>$name,'id'=>$articleId,'status'=>$status ?: ($locked ? 'processing' : 'unknown'),'started_at'=>$started]; } } return $jobs; @@ -763,7 +764,7 @@ try { if ($visibleNow && $canShow) { if (is_file(__DIR__ . '/lib/send_push.php')) { include_once __DIR__ . '/lib/send_push.php'; - try_send_show_notification(basename($dir), $dir, __DIR__, '/var/log/mvlog_notify.log'); + 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'); } @@ -942,7 +943,7 @@ $runningJobs = active_worker_jobs($config);

Videos without input dir

No orphan videos.


MB
-

No input directories yet.

Map
No video yet
+

No input directories yet.

Map
No video yet
@@ -1319,8 +1320,10 @@ async function handleSwitchToggle(form, checkbox){ } function applySwitchPayload(payload){ - if(!payload || !payload.dir) return; - const row = document.querySelector('.admin-video-row[data-job="'+payload.dir+'"]'); + if(!payload) return; + let row = null; + if(payload.id) row = document.querySelector('.admin-video-row[data-id="'+payload.id+'"]'); + if(!row && payload.dir) row = document.querySelector('.admin-video-row[data-job="'+payload.dir+'"]'); if(!row) return; function setSwitch(type, value){ const target = row.querySelector('form[data-switch="'+type+'"] input[type="checkbox"]'); @@ -1366,12 +1369,13 @@ function applySwitchPayload(payload){ if (m > 0) return m + 'm ' + s + 's'; return s + 's'; } - function syncEditButtons(runningJobs){ + function syncEditButtons(runningIds, runningJobs){ document.querySelectorAll('.admin-video-row[data-job]').forEach(function(row){ const job=row.dataset.job || ''; + const articleId=row.dataset.id || ''; const edit=row.querySelector('[data-edit-action="1"]'); if(!edit) return; - const running=runningJobs.has(job); + const running=(articleId && runningIds.has(articleId)) || runningJobs.has(job); if(edit.tagName === 'A'){ if(running){ if(!edit.dataset.editHref) edit.dataset.editHref = edit.getAttribute('href') || ''; @@ -1397,9 +1401,10 @@ function applySwitchPayload(payload){ const r=await fetch('job_status.php',{cache:'no-store'}); if(!r.ok) throw new Error('status failed'); const data=await r.json(); - const jobs=(data.jobs||[]).filter(function(job){ return job && job.name; }); - const runningJobs=new Set(jobs.map(function(job){ return String(job.name); })); - syncEditButtons(runningJobs); + const jobs=(data.jobs||[]).filter(function(job){ return job && (job.name || job.id); }); + const runningIds=new Set(jobs.map(function(job){ return String(job.id || ''); }).filter(function(v){ return v !== ''; })); + const runningJobs=new Set(jobs.map(function(job){ return String(job.name || ''); }).filter(function(v){ return v !== ''; })); + syncEditButtons(runningIds, runningJobs); if(!jobs.length){ box.hidden=true; box.innerHTML=''; @@ -1418,7 +1423,6 @@ function applySwitchPayload(payload){
Input dirs are saved under in-dir/.
-