#!/usr/bin/python3 """Run movie_recs and push the result to ntfy as a markdown notification. Intended for the movie-recs.timer systemd unit. Failures are pushed too -- a recommendation job that silently stops producing is worse than a noisy one. """ import base64 import json import os import subprocess import sys import urllib.request NTFY = "https://ntfy.rcjohnstone.com/movies" # ntfy is auth-default-access: deny-all, so publishing needs the `bot` # credential. Mirrored from the rbw entry `ntfy-bot`; rbw is the source of # truth. Kept in a 0600 file because this runs unattended from a timer. NTFY_ENV = os.path.expanduser("~/.config/ntfy/publish.env") RECS = "/home/connor/.local/bin/movie_recs" COUNT = "10" def _auth_header(): user, pw = os.environ.get("NTFY_USER"), os.environ.get("NTFY_PASS") if not (user and pw): try: with open(NTFY_ENV) as fh: vals = dict( line.strip().split("=", 1) for line in fh if "=" in line and not line.startswith("#")) user, pw = vals.get("NTFY_USER"), vals.get("NTFY_PASS") except OSError: return None if not (user and pw): return None token = base64.b64encode(f"{user}:{pw}".encode()).decode() return f"Basic {token}" def push(title, body, tags, priority="default", markdown=True): headers = { "Title": title, "Tags": tags, "Priority": priority, "Content-Type": "text/plain; charset=utf-8", } auth = _auth_header() if auth: headers["Authorization"] = auth if markdown: headers["Markdown"] = "yes" req = urllib.request.Request( NTFY, data=body.encode("utf-8"), headers=headers, method="POST") with urllib.request.urlopen(req, timeout=30) as r: return r.status def main(): try: proc = subprocess.run( [RECS, "-n", COUNT, "--json"], capture_output=True, text=True, timeout=2400) except subprocess.TimeoutExpired: push("Movie recs failed", "movie_recs timed out after 40 minutes.", "warning", "high", markdown=False) return 1 if proc.returncode != 0: tail = (proc.stderr or "no stderr").strip().splitlines()[-6:] push("Movie recs failed", "exit %d\n\n%s" % (proc.returncode, "\n".join(tail)), "warning", "high", markdown=False) return 1 try: data = json.loads(proc.stdout) except json.JSONDecodeError: push("Movie recs failed", "could not parse output as JSON", "warning", "high", markdown=False) return 1 recs = data.get("recommendations", []) if not recs: push("Movie recs: nothing today", "The run completed but every candidate was filtered out.", "warning", "default", markdown=False) return 0 lines = [] for r in recs: t = r["tmdb"] lines.append("**%s** (%s) ยท %s/10, %sk votes" % (t["title"], t["year"], t["score"], t["votes"] // 1000)) lines.append("%s" % r.get("reason", "").strip()) lines.append("*from: %s*" % ", ".join(r.get("anchors", []))) lines.append("") dropped = (len(data.get("rejected_not_in_tmdb", [])) + len(data.get("rejected_already_owned", [])) + len(data.get("rejected_below_min_votes", [])) + len(data.get("rejected_invented_cluster", []))) lines.append("_%d shown, %d candidates filtered_" % (len(recs), dropped)) push("%d film recommendations" % len(recs), "\n".join(lines), "clapper") return 0 if __name__ == "__main__": sys.exit(main())