Compare commits
11 Commits
d4ea28add2
...
ab6bbac02b
| Author | SHA1 | Date | |
|---|---|---|---|
| ab6bbac02b | |||
| 14cb0bd7d4 | |||
| c2b609e769 | |||
| 94ffce1cbd | |||
| e0d01bd617 | |||
| d758541156 | |||
| b979621dd4 | |||
| f91cc9844e | |||
| bddbd69371 | |||
| 6e39a2dad2 | |||
| 1ba3848bed |
@@ -327,7 +327,7 @@ def main():
|
||||
|
||||
ntfy = NtfyPoller(
|
||||
config.NTFY_TOPIC,
|
||||
poll_interval=config.NTFY_POLL_INTERVAL,
|
||||
reconnect_delay=config.NTFY_RECONNECT_DELAY,
|
||||
display_secs=config.MESSAGE_DISPLAY_SECS,
|
||||
)
|
||||
ntfy_ok = ntfy.start()
|
||||
|
||||
@@ -61,8 +61,8 @@ MODE = "poetry" if "--poetry" in sys.argv or "-p" in sys.argv else "news"
|
||||
FIREHOSE = "--firehose" in sys.argv
|
||||
|
||||
# ─── NTFY MESSAGE QUEUE ──────────────────────────────────
|
||||
NTFY_TOPIC = "https://ntfy.sh/klubhaus_terminal_mainline/json?since=20s&poll=1"
|
||||
NTFY_POLL_INTERVAL = 15 # seconds between polls
|
||||
NTFY_TOPIC = "https://ntfy.sh/klubhaus_terminal_mainline/json"
|
||||
NTFY_RECONNECT_DELAY = 5 # seconds before reconnecting after a dropped stream
|
||||
MESSAGE_DISPLAY_SECS = 30 # how long a message holds the screen
|
||||
|
||||
# ─── FONT RENDERING ──────────────────────────────────────
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""
|
||||
ntfy.sh message poller — standalone, zero internal dependencies.
|
||||
ntfy.sh SSE stream listener — standalone, zero internal dependencies.
|
||||
Reusable by any visualizer:
|
||||
|
||||
from engine.ntfy import NtfyPoller
|
||||
poller = NtfyPoller("https://ntfy.sh/my_topic/json?since=20s&poll=1")
|
||||
poller = NtfyPoller("https://ntfy.sh/my_topic/json")
|
||||
poller.start()
|
||||
# in render loop:
|
||||
msg = poller.get_active_message()
|
||||
@@ -16,21 +16,22 @@ import json
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
||||
|
||||
|
||||
class NtfyPoller:
|
||||
"""Background poller for ntfy.sh topics."""
|
||||
"""SSE stream listener for ntfy.sh topics. Messages arrive in ~1s (network RTT)."""
|
||||
|
||||
def __init__(self, topic_url, poll_interval=15, display_secs=30):
|
||||
def __init__(self, topic_url, reconnect_delay=5, display_secs=30):
|
||||
self.topic_url = topic_url
|
||||
self.poll_interval = poll_interval
|
||||
self.reconnect_delay = reconnect_delay
|
||||
self.display_secs = display_secs
|
||||
self._message = None # (title, body, monotonic_timestamp) or None
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def start(self):
|
||||
"""Start background polling thread. Returns True."""
|
||||
t = threading.Thread(target=self._poll_loop, daemon=True)
|
||||
"""Start background stream thread. Returns True."""
|
||||
t = threading.Thread(target=self._stream_loop, daemon=True)
|
||||
t.start()
|
||||
return True
|
||||
|
||||
@@ -50,22 +51,36 @@ class NtfyPoller:
|
||||
with self._lock:
|
||||
self._message = None
|
||||
|
||||
def _poll_loop(self):
|
||||
def _build_url(self, last_id=None):
|
||||
"""Build the stream URL, substituting since= to avoid message replays on reconnect."""
|
||||
parsed = urlparse(self.topic_url)
|
||||
params = parse_qs(parsed.query, keep_blank_values=True)
|
||||
params["since"] = [last_id if last_id else "20s"]
|
||||
new_query = urlencode({k: v[0] for k, v in params.items()})
|
||||
return urlunparse(parsed._replace(query=new_query))
|
||||
|
||||
def _stream_loop(self):
|
||||
last_id = None
|
||||
while True:
|
||||
try:
|
||||
url = self._build_url(last_id)
|
||||
req = urllib.request.Request(
|
||||
self.topic_url, headers={"User-Agent": "mainline/0.1"}
|
||||
url, headers={"User-Agent": "mainline/0.1"}
|
||||
)
|
||||
resp = urllib.request.urlopen(req, timeout=10)
|
||||
for line in (
|
||||
resp.read().decode("utf-8", errors="replace").strip().split("\n")
|
||||
):
|
||||
if not line.strip():
|
||||
continue
|
||||
# timeout=90 keeps the socket alive through ntfy.sh keepalive heartbeats
|
||||
resp = urllib.request.urlopen(req, timeout=90)
|
||||
while True:
|
||||
line = resp.readline()
|
||||
if not line:
|
||||
break # server closed connection — reconnect
|
||||
try:
|
||||
data = json.loads(line)
|
||||
data = json.loads(line.decode("utf-8", errors="replace"))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
# Advance cursor on every event (message + keepalive) to
|
||||
# avoid replaying already-seen events after a reconnect.
|
||||
if "id" in data:
|
||||
last_id = data["id"]
|
||||
if data.get("event") == "message":
|
||||
with self._lock:
|
||||
self._message = (
|
||||
@@ -75,4 +90,4 @@ class NtfyPoller:
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(self.poll_interval)
|
||||
time.sleep(self.reconnect_delay)
|
||||
|
||||
BIN
fonts/AgorTechnoDemo-Regular.otf
Normal file
BIN
fonts/AgorTechnoDemo-Regular.otf
Normal file
Binary file not shown.
BIN
fonts/CubaTechnologyDemo-Regular.otf
Normal file
BIN
fonts/CubaTechnologyDemo-Regular.otf
Normal file
Binary file not shown.
BIN
fonts/ModernSpaceDemo-Regular.otf
Normal file
BIN
fonts/ModernSpaceDemo-Regular.otf
Normal file
Binary file not shown.
BIN
fonts/RaceHugoDemo-Regular.otf
Normal file
BIN
fonts/RaceHugoDemo-Regular.otf
Normal file
Binary file not shown.
BIN
fonts/Resond-Regular.otf
Normal file
BIN
fonts/Resond-Regular.otf
Normal file
Binary file not shown.
BIN
fonts/Synthetix.otf
Normal file
BIN
fonts/Synthetix.otf
Normal file
Binary file not shown.
2
hk.pkl
2
hk.pkl
@@ -11,6 +11,7 @@ hooks {
|
||||
}
|
||||
["ruff"] = (Builtins.ruff) {
|
||||
prefix = "uv run"
|
||||
check = "ruff check engine/ tests/"
|
||||
fix = "ruff check --fix --unsafe-fixes engine/ tests/"
|
||||
}
|
||||
}
|
||||
@@ -19,6 +20,7 @@ hooks {
|
||||
steps {
|
||||
["ruff"] = (Builtins.ruff) {
|
||||
prefix = "uv run"
|
||||
check = "ruff check engine/ tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,15 +15,15 @@ class TestNtfyPollerInit:
|
||||
"""Default values are set correctly."""
|
||||
poller = NtfyPoller("http://example.com/topic")
|
||||
assert poller.topic_url == "http://example.com/topic"
|
||||
assert poller.poll_interval == 15
|
||||
assert poller.reconnect_delay == 5
|
||||
assert poller.display_secs == 30
|
||||
|
||||
def test_init_custom_values(self):
|
||||
"""Custom values are set correctly."""
|
||||
poller = NtfyPoller(
|
||||
"http://example.com/topic", poll_interval=5, display_secs=60
|
||||
"http://example.com/topic", reconnect_delay=10, display_secs=60
|
||||
)
|
||||
assert poller.poll_interval == 5
|
||||
assert poller.reconnect_delay == 10
|
||||
assert poller.display_secs == 60
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user