1aab35bc3d
Episodes are 18-24 MB and the box's link tops out near 100 KB/s, so full-size pushes time out. Transcode to mono 48 kbps MP3 (a few MB, plenty for spoken-word kids podcasts); the _a48 tag gives a fresh name so the box re-pulls the light version. Ack /register immediately and push the delta on a background thread (one batch per box) so the box's short register timeout never trips.
79 lines
3.2 KiB
Python
79 lines
3.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Always-on: POST /register {id, have} -> push the staging delta to the box's
|
|
file-server (:8080). The box IP is the request source address."""
|
|
import json, os, threading, urllib.request
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
|
|
STAGING = os.environ.get("LISAEL_STAGING", "/home/clems/lisael-content/podcasts")
|
|
PORT = int(os.environ.get("LISAEL_PORT", "9555"))
|
|
BOX_PORT = int(os.environ.get("LISAEL_BOX_PORT", "8080"))
|
|
|
|
def staging_files():
|
|
if not os.path.isdir(STAGING):
|
|
return []
|
|
return sorted(f for f in os.listdir(STAGING) if not f.startswith("."))
|
|
|
|
def compute_delta(staging, have):
|
|
"""Files the box is missing; manifest.json only when something else changed."""
|
|
haveset = set(have)
|
|
files = [f for f in staging if f != "manifest.json" and f not in haveset]
|
|
if files and "manifest.json" in staging:
|
|
files.append("manifest.json")
|
|
return files
|
|
|
|
def push_file(ip, name):
|
|
data = open(os.path.join(STAGING, name), "rb").read()
|
|
url = f"http://{ip}:{BOX_PORT}/put?name=podcasts/{name}"
|
|
# Generous timeout: a multi-MB episode written to the box's slow microSD can
|
|
# take well over a minute, especially during boot when the box is also doing
|
|
# its first weather TLS fetch.
|
|
with urllib.request.urlopen(urllib.request.Request(url, data=data, method="POST"),
|
|
timeout=300) as r:
|
|
return r.status
|
|
|
|
# Pushes run off the request thread (one batch per box at a time) so the box's
|
|
# short register timeout never waits on a multi-file, multi-minute transfer.
|
|
_busy = set()
|
|
_busy_lock = threading.Lock()
|
|
|
|
def push_batch(ip, delta):
|
|
ok = 0
|
|
for f in delta:
|
|
try:
|
|
push_file(ip, f); ok += 1
|
|
except Exception as e:
|
|
print("push fail", f, e, flush=True)
|
|
print(f"push done {ip} {ok}/{len(delta)}", flush=True)
|
|
with _busy_lock:
|
|
_busy.discard(ip)
|
|
|
|
class H(BaseHTTPRequestHandler):
|
|
def do_POST(self):
|
|
if self.path.split("?")[0] != "/register":
|
|
self.send_error(404); return
|
|
ip = self.client_address[0]
|
|
try:
|
|
n = int(self.headers.get("Content-Length", 0))
|
|
body = json.loads(self.rfile.read(n) or b"{}")
|
|
except Exception:
|
|
self.send_error(400); return
|
|
delta = compute_delta(staging_files(), body.get("have", []) or [])
|
|
print(f"register {ip} id={body.get('id')} have={len(body.get('have',[]) or [])} push={len(delta)}",
|
|
flush=True)
|
|
with _busy_lock:
|
|
already = ip in _busy
|
|
if delta and not already:
|
|
_busy.add(ip)
|
|
if delta and not already:
|
|
threading.Thread(target=push_batch, args=(ip, delta), daemon=True).start()
|
|
# Respond immediately; the (possibly slow) push runs in the background.
|
|
self.send_response(200); self.send_header("Content-Type", "application/json"); self.end_headers()
|
|
self.wfile.write(json.dumps({"queued": len(delta), "busy": already}).encode())
|
|
|
|
def log_message(self, *a):
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
print(f"lisael-content on :{PORT} staging={STAGING}", flush=True)
|
|
ThreadingHTTPServer(("0.0.0.0", PORT), H).serve_forever()
|