Send web push notifications from worker

This commit is contained in:
hbrain 2026-05-26 13:48:33 +00:00
parent 3ed06d95bc
commit 03889fe6e2
5 changed files with 267 additions and 1 deletions

3
.gitignore vendored
View file

@ -14,10 +14,11 @@ out/
*.avi *.avi
*.webm *.webm
# Local input media and bundled local tools # Local input media, bundled local tools, JS deps
input*/ input*/
media/ media/
ffmpeg/ ffmpeg/
node_modules/
# Assistant/project-local context # Assistant/project-local context
AGENTS.md AGENTS.md

View file

@ -21,6 +21,8 @@ from shlex import quote
ROOT = Path(__file__).resolve().parent ROOT = Path(__file__).resolve().parent
MOVMAKER = ROOT / "movmaker.py" MOVMAKER = ROOT / "movmaker.py"
PUSH_SENDER = ROOT / "send_push.js"
PUSH_CONFIG = Path.home() / ".config" / "mvlog" / "push.json"
STATE_NAME = ".movmaker-state.json" STATE_NAME = ".movmaker-state.json"
LOCK_NAME = ".movmaker-lock" LOCK_NAME = ".movmaker-lock"
ENABLED_NAME = ".movmaker-enabled" ENABLED_NAME = ".movmaker-enabled"
@ -125,6 +127,26 @@ def copy_job_from_remote(host: str, job_dir: str, local_input: Path) -> None:
raise 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: def publish_output(host: str, remote_root: str, local_mp4: Path, old_output: str | None) -> str:
out_name = local_mp4.name out_name = local_mp4.name
remote_tmp = f"{remote_root}/out-dir/.{out_name}.tmp" 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 return
output = publish_output(host, remote_root, local_mp4, old_output) output = publish_output(host, remote_root, local_mp4, old_output)
send_push_notification(host, remote_root, output)
write_state(host, job_dir, { write_state(host, job_dir, {
"status": "done", "status": "done",
"fingerprint": fingerprint_after, "fingerprint": fingerprint_after,

190
package-lock.json generated Normal file
View file

@ -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"
}
}
}
}

19
package.json Normal file
View file

@ -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"
}
}

33
send_push.js Executable file
View file

@ -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); });