From 03889fe6e25f0e69df2aecaa25b7101c40976338 Mon Sep 17 00:00:00 2001 From: hbrain Date: Tue, 26 May 2026 13:48:33 +0000 Subject: [PATCH] Send web push notifications from worker --- .gitignore | 3 +- mvlog_worker.py | 23 ++++++ package-lock.json | 190 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 19 +++++ send_push.js | 33 ++++++++ 5 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 package-lock.json create mode 100644 package.json create mode 100755 send_push.js diff --git a/.gitignore b/.gitignore index 19c1b38..6d6e001 100644 --- a/.gitignore +++ b/.gitignore @@ -14,10 +14,11 @@ out/ *.avi *.webm -# Local input media and bundled local tools +# Local input media, bundled local tools, JS deps input*/ media/ ffmpeg/ +node_modules/ # Assistant/project-local context AGENTS.md diff --git a/mvlog_worker.py b/mvlog_worker.py index 8289403..38f0a02 100755 --- a/mvlog_worker.py +++ b/mvlog_worker.py @@ -21,6 +21,8 @@ from shlex import quote ROOT = Path(__file__).resolve().parent MOVMAKER = ROOT / "movmaker.py" +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" @@ -125,6 +127,26 @@ 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) -> None: + if not PUSH_SENDER.exists() or not PUSH_CONFIG.exists(): + return + 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 + 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) + + def publish_output(host: str, remote_root: str, local_mp4: Path, old_output: str | None) -> str: out_name = local_mp4.name remote_tmp = f"{remote_root}/out-dir/.{out_name}.tmp" @@ -197,6 +219,7 @@ def process_job(host: str, remote_root: str, job: str, work_dir: Path, extra_arg return output = publish_output(host, remote_root, local_mp4, old_output) + send_push_notification(host, remote_root, output) write_state(host, job_dir, { "status": "done", "fingerprint": fingerprint_after, diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5c3f13f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,190 @@ +{ + "name": "movmaker", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "movmaker", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "web-push": "^3.6.7" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "license": "MIT" + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/http_ece": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz", + "integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/web-push": { + "version": "3.6.7", + "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz", + "integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==", + "license": "MPL-2.0", + "dependencies": { + "asn1.js": "^5.3.0", + "http_ece": "1.2.0", + "https-proxy-agent": "^7.0.0", + "jws": "^4.0.0", + "minimist": "^1.2.5" + }, + "bin": { + "web-push": "src/cli.js" + }, + "engines": { + "node": ">= 16" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7f74038 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "movmaker", + "version": "1.0.0", + "description": "Simple Python + ffmpeg movie maker.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://piagent:a8caa8227e2d80f84f4746c90aba3fdafea6d28e@git.novosel.dk/marijo/movmaker.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "web-push": "^3.6.7" + } +} diff --git a/send_push.js b/send_push.js new file mode 100755 index 0000000..b22014c --- /dev/null +++ b/send_push.js @@ -0,0 +1,33 @@ +#!/usr/bin/env node +const fs = require('fs'); +const webpush = require('web-push'); + +function readStdin() { + return new Promise(resolve => { + let data = ''; + process.stdin.setEncoding('utf8'); + process.stdin.on('data', chunk => data += chunk); + process.stdin.on('end', () => resolve(data)); + }); +} + +(async () => { + const configPath = process.argv[2] || `${process.env.HOME}/.config/mvlog/push.json`; + const payload = process.argv[3] || '{}'; + const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8')); + const input = JSON.parse(await readStdin() || '[]'); + const subs = Array.isArray(input) ? input : []; + if (!cfg.public_key || !cfg.private_key || !subs.length) return; + webpush.setVapidDetails(cfg.subject || 'mailto:admin@bubulescu.org', cfg.public_key, cfg.private_key); + let sent = 0; + for (const sub of subs) { + try { + await webpush.sendNotification(sub, payload); + sent++; + } catch (err) { + const code = err && err.statusCode ? ` status=${err.statusCode}` : ''; + console.error(`push failed${code}: ${err.message || err}`); + } + } + console.log(`push sent: ${sent}/${subs.length}`); +})().catch(err => { console.error(err.stack || err); process.exit(1); });