diff --git a/examples/enable_show_local.php b/examples/enable_show_local.php new file mode 100644 index 0000000..50e8ac9 --- /dev/null +++ b/examples/enable_show_local.php @@ -0,0 +1,184 @@ + show=1|0 +// Configure $MVLOG_ROOT and $PUSH_CONFIG below. + +// Configuration - update to match your server +$MVLOG_ROOT = '/var/www/html/mvlog'; +$PUSH_CONFIG_CANDIDATES = [ + "$MVLOG_ROOT/push.json", + getenv('HOME') !== false ? getenv('HOME') . '/.config/mvlog/push.json' : null, + '/etc/mvlog/push.json', +]; + +$LOGFILE = '/var/log/mvlog_notify.log'; + +$job = $_POST['job'] ?? ''; +$show = isset($_POST['show']) ? filter_var($_POST['show'], FILTER_VALIDATE_BOOLEAN) : true; + +// Basic validation for job name; adjust to your naming policy if needed +if (!preg_match('/^[0-9]{8}[-_][A-Za-z0-9._-]+$/', $job)) { + http_response_code(400); + echo 'invalid job'; + exit; +} + +$jobdir = "$MVLOG_ROOT/in-dir/$job"; +if (!is_dir($jobdir)) { + http_response_code(404); + echo 'job not found'; + exit; +} + +$enabled_file = "$jobdir/.movmaker-enabled"; +$hidden_file = "$jobdir/.mvlog-hidden"; +$subs_file = "$MVLOG_ROOT/cache/push_subscriptions.json"; +$cache_file = "$MVLOG_ROOT/cache/push_notifications.json"; + +// Find push config +$push_config = null; +foreach ($PUSH_CONFIG_CANDIDATES as $candidate) { + if (!$candidate) continue; + if (file_exists($candidate) && is_readable($candidate)) { + $push_config = $candidate; + break; + } +} + +if ($show) { + // Create marker file atomically + file_put_contents($enabled_file, date(DATE_ATOM) . PHP_EOL, LOCK_EX); + // Try to send push (non-fatal) + $sent = try_send_show_notification($job, $jobdir, $MVLOG_ROOT, $subs_file, $cache_file, $push_config, $LOGFILE); + if ($sent) { + echo 'enabled'; + } else { + echo 'enabled (no notification)'; + } +} else { + // Remove marker and do not touch notifications + @unlink($enabled_file); + echo 'disabled'; +} + +exit; + +function try_send_show_notification($job, $jobdir, $root, $subs_file, $cache_file, $push_config, $logfile) +{ + // Skip if hidden + if (file_exists($jobdir . '/.mvlog-hidden')) { + error_log("[mvlog] job $job hidden, skipping push\n", 3, $logfile); + return false; + } + + // Ensure we have subscriptions + if (!file_exists($subs_file)) { + error_log("[mvlog] subscriptions file missing, skipping push for $job\n", 3, $logfile); + return false; + } + $subs_raw = file_get_contents($subs_file); + if (!trim($subs_raw)) { + error_log("[mvlog] subscriptions empty, skipping push for $job\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); + return false; + } + + // Read cache and check if already shown + $cache = []; + if (file_exists($cache_file)) { + $cache = json_decode(@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); + return false; + } + + // Ensure push config present + if (!$push_config) { + error_log("[mvlog] push config not found, skipping push for $job\n", 3, $logfile); + return false; + } + + // Determine title (prefer output filename from state file) + $title = preg_replace('/_+/', ' ', $job); + $title = ucwords($title); + $state_file = $jobdir . '/.movmaker-state.json'; + if (file_exists($state_file)) { + $state = json_decode(@file_get_contents($state_file), true) ?: []; + if (!empty($state['output']) && is_string($state['output'])) { + $out = pathinfo($state['output'], PATHINFO_FILENAME); + if ($out) { + $title = preg_replace('/_+/', ' ', $out); + $title = ucwords($title); + } + } + } + + $payload = json_encode([ + 'title' => 'Show enabled', + 'body' => $title, + 'url' => "https://bubulescu.org/mvlog/?job=$job", + 'tag' => "mvlog-show-$job", + ], JSON_UNESCAPED_UNICODE); + + // Call node send_push.js with subscriptions on stdin + $send_script = escapeshellarg($root . '/send_push.js'); + $push_config_esc = escapeshellarg($push_config); + $payload_esc = escapeshellarg($payload); + $cmd = "node $send_script $push_config_esc $payload_esc"; + + $descriptorspec = [ + 0 => ['pipe', 'r'], // stdin + 1 => ['pipe', 'w'], // stdout + 2 => ['pipe', 'w'], // stderr + ]; + + $proc = proc_open($cmd, $descriptorspec, $pipes); + if (!is_resource($proc)) { + error_log("[mvlog] failed to start node send_push process for $job\n", 3, $logfile); + return false; + } + + // Write subscriptions to stdin + fwrite($pipes[0], $subs_raw); + fclose($pipes[0]); + + $stdout = stream_get_contents($pipes[1]); fclose($pipes[1]); + $stderr = stream_get_contents($pipes[2]); fclose($pipes[2]); + + $rc = proc_close($proc); + if ($rc !== 0) { + error_log("[mvlog] send_push failed for $job rc=$rc stdout=" . trim($stdout) . " stderr=" . trim($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)]; + + // 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); + 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); + return false; + } + + error_log("[mvlog] show-notification sent: $job\n", 3, $logfile); + return true; +} diff --git a/mvlog_worker.py b/mvlog_worker.py index ad94c86..a9dc23d 100755 --- a/mvlog_worker.py +++ b/mvlog_worker.py @@ -23,14 +23,11 @@ from shlex import quote ROOT = Path(__file__).resolve().parent MOVMAKER = ROOT / "movmaker.py" INTRO_LOGO = ROOT / "img" / "moto_travel.png" -PUSH_SENDER = ROOT / "send_push.js" -PUSH_CONFIG = Path.home() / ".config" / "mvlog" / "push.json" STATE_NAME = ".movmaker-state.json" LOCK_NAME = ".movmaker-lock" ENABLED_NAME = ".movmaker-enabled" PREVIEW_NAME = ".movmaker-preview" HIDDEN_NAME = ".mvlog-hidden" -NOTIFIED_CACHE = "cache/push_notifications.json" INTRO_LOGO_ALPHA = "0.5" MVLOG_WORKER_RECOMMENDED_INTERVAL_MINUTES = 5 @@ -171,74 +168,6 @@ def copy_job_from_remote(host: str, job_dir: str, local_input: Path) -> None: raise -def send_push_notification(host: str, remote_root: str, output: str) -> bool: - if not PUSH_SENDER.exists() or not PUSH_CONFIG.exists(): - return False - res = ssh(host, f"cat {remote_q(remote_root + '/cache/push_subscriptions.json')} 2>/dev/null", check=False) - if res.returncode != 0 or not res.stdout.strip(): - return False - title = output.rsplit('.', 1)[0].replace('_', ' ').title() - payload = json.dumps({ - "title": "New MVLog video", - "body": title, - "url": "https://bubulescu.org/mvlog/", - "tag": f"mvlog-{output}", - }, ensure_ascii=False) - proc = subprocess.run(["node", str(PUSH_SENDER), str(PUSH_CONFIG), payload], input=res.stdout, text=True, capture_output=True, check=False) - if proc.stdout.strip(): - print(proc.stdout.strip()) - if proc.stderr.strip(): - print(proc.stderr.strip(), file=sys.stderr) - return proc.returncode == 0 - - -def maybe_send_first_show_notification(host: str, remote_root: str, job_dir: str, job: str, state: dict | None = None) -> None: - """ - Send a web-push notification the first time the 'Show' checkbox is set for a job. - We record notifications in cache/push_notifications.json under the 'shown' map keyed by job - to ensure each job triggers only once when Show is enabled. - """ - # Don't notify for hidden jobs - if remote_file_exists(host, f"{job_dir}/{HIDDEN_NAME}"): - return - - cache_path = f"{remote_root}/{NOTIFIED_CACHE}" - cache = read_remote_json(host, cache_path) - shown = cache.get("shown") if isinstance(cache.get("shown"), dict) else {} - - # If we've already recorded that Show was sent for this job, skip - if job in shown: - return - - # Notify when the enabled marker exists (Show checked) - if not remote_file_exists(host, f"{job_dir}/{ENABLED_NAME}"): - return - - # Determine a title/url for the notification. Prefer existing output filename if present. - state = state if state is not None else read_state(host, job_dir) - output = state.get("output") if isinstance(state.get("output"), str) else "" - title = output.rsplit('.', 1)[0].replace('_', ' ').title() if output else job.replace('_', ' ').title() - payload = json.dumps({ - "title": "Show enabled", - "body": title, - "url": f"https://bubulescu.org/mvlog/?job={job}", - "tag": f"mvlog-show-{job}", - }, ensure_ascii=False) - - # Gather subscriptions and send - res = ssh(host, f"cat {remote_q(remote_root + '/cache/push_subscriptions.json')} 2>/dev/null", check=False) - if res.returncode != 0 or not res.stdout.strip(): - return - proc = subprocess.run(["node", str(PUSH_SENDER), str(PUSH_CONFIG), payload], input=res.stdout, text=True, capture_output=True, check=False) - if proc.stdout.strip(): - print(proc.stdout.strip()) - if proc.stderr.strip(): - print(proc.stderr.strip(), file=sys.stderr) - if proc.returncode == 0: - shown[job] = {"sent_at": now()} - cache["shown"] = shown - write_remote_json(host, cache_path, cache) - print(f"show-notification sent: {job}") def publish_output(host: str, remote_root: str, local_mp4: Path, old_output: str | None) -> str: @@ -292,7 +221,6 @@ def render_job(local_input: Path, local_out: Path, extra_args: list[str]) -> Pat def process_job(host: str, remote_root: str, job: str, work_dir: Path, extra_args: list[str], force: bool) -> None: job_dir = f"{remote_root}/in-dir/{job}" state = read_state(host, job_dir) - maybe_send_first_show_notification(host, remote_root, job_dir, job, state) full_requested = remote_file_exists(host, f"{job_dir}/{ENABLED_NAME}") preview_requested = remote_file_exists(host, f"{job_dir}/{PREVIEW_NAME}") if not full_requested and not preview_requested: @@ -386,7 +314,6 @@ def process_job(host: str, remote_root: str, job: str, work_dir: Path, extra_arg "output": output, "generated_at": now(), }) - maybe_send_first_show_notification(host, remote_root, job_dir, job, new_state) write_state(host, job_dir, new_state) print(f"published: {job} -> {output}") return