feat(tower): transfers journal begin mark end
This commit is contained in:
+82
-1
@@ -2,7 +2,7 @@
|
|||||||
"""Shared content logic for the Lisael Box on Tower: feeds config, RSS, transcode,
|
"""Shared content logic for the Lisael Box on Tower: feeds config, RSS, transcode,
|
||||||
covers, staging/manifest ops, box state, push. Imported by lisael_refresh.py (cron)
|
covers, staging/manifest ops, box state, push. Imported by lisael_refresh.py (cron)
|
||||||
and lisael_server.py (web)."""
|
and lisael_server.py (web)."""
|
||||||
import hashlib, io, json, os, ssl, subprocess, sys, tempfile, threading, urllib.request
|
import hashlib, io, json, os, ssl, subprocess, sys, tempfile, threading, time, urllib.request
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
@@ -433,3 +433,84 @@ def try_push(ip, delta):
|
|||||||
_busy.add(ip)
|
_busy.add(ip)
|
||||||
threading.Thread(target=push_batch, args=(ip, delta), daemon=True).start()
|
threading.Thread(target=push_batch, args=(ip, delta), daemon=True).start()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# ---- transfers journal ----
|
||||||
|
TRANSFERS_JSON = os.environ.get("LISAEL_TRANSFERS", os.path.join(BASE, "transfers.json"))
|
||||||
|
TRANSFERS_MAX = 200
|
||||||
|
|
||||||
|
_active = None # active batch dict or None
|
||||||
|
_active_lock = threading.Lock()
|
||||||
|
|
||||||
|
def _now_iso():
|
||||||
|
return time.strftime("%Y-%m-%dT%H:%M:%S")
|
||||||
|
|
||||||
|
def transfers_begin(box_ip, files):
|
||||||
|
"""files: list of (name, subdir[, size]); size resolved via getsize if missing."""
|
||||||
|
global _active
|
||||||
|
recs = []
|
||||||
|
for it in files:
|
||||||
|
name, subdir = it[0], it[1]
|
||||||
|
size = it[2] if len(it) > 2 else None
|
||||||
|
if size is None:
|
||||||
|
src = STAGING_ROUTINES if subdir == "routines" else STAGING
|
||||||
|
try:
|
||||||
|
size = os.path.getsize(os.path.join(src, name))
|
||||||
|
except OSError:
|
||||||
|
size = 0
|
||||||
|
recs.append({"name": name, "dir": subdir, "size": size, "status": "pending",
|
||||||
|
"started": None, "ended": None, "duration_s": None,
|
||||||
|
"rate_bps": None, "error": None, "box_ip": box_ip,
|
||||||
|
"_t0": None})
|
||||||
|
with _active_lock:
|
||||||
|
_active = {"box_ip": box_ip, "started": _now_iso(),
|
||||||
|
"total": len(recs), "current": 0, "files": recs}
|
||||||
|
|
||||||
|
def transfers_mark(name, status, error=None):
|
||||||
|
with _active_lock:
|
||||||
|
if not _active:
|
||||||
|
return
|
||||||
|
for f in _active["files"]:
|
||||||
|
if f["name"] != name:
|
||||||
|
continue
|
||||||
|
if status == "sending":
|
||||||
|
f["status"] = "sending"; f["started"] = _now_iso(); f["_t0"] = time.monotonic()
|
||||||
|
_active["current"] += 1
|
||||||
|
elif status in ("done", "failed"):
|
||||||
|
f["status"] = status; f["ended"] = _now_iso(); f["error"] = error
|
||||||
|
if f.get("_t0") is not None:
|
||||||
|
dur = max(time.monotonic() - f["_t0"], 0.001)
|
||||||
|
f["duration_s"] = round(dur, 3)
|
||||||
|
f["rate_bps"] = int((f["size"] or 0) / dur)
|
||||||
|
return
|
||||||
|
|
||||||
|
def transfers_active():
|
||||||
|
with _active_lock:
|
||||||
|
if not _active:
|
||||||
|
return None
|
||||||
|
# strip internal _t0 for the API view
|
||||||
|
files = [{k: v for k, v in f.items() if k != "_t0"} for f in _active["files"]]
|
||||||
|
return {**_active, "files": files}
|
||||||
|
|
||||||
|
def _transfers_history_load():
|
||||||
|
if os.path.exists(TRANSFERS_JSON):
|
||||||
|
try:
|
||||||
|
return json.load(open(TRANSFERS_JSON, encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return []
|
||||||
|
|
||||||
|
def transfers_history(limit=TRANSFERS_MAX):
|
||||||
|
return _transfers_history_load()[-limit:]
|
||||||
|
|
||||||
|
def transfers_end():
|
||||||
|
global _active
|
||||||
|
with _active_lock:
|
||||||
|
if not _active:
|
||||||
|
return
|
||||||
|
done = [{k: v for k, v in f.items() if k != "_t0"}
|
||||||
|
for f in _active["files"] if f["status"] in ("done", "failed")]
|
||||||
|
_active = None
|
||||||
|
hist = _transfers_history_load()
|
||||||
|
hist.extend(done)
|
||||||
|
hist = hist[-TRANSFERS_MAX:]
|
||||||
|
_atomic_write_json(TRANSFERS_JSON, hist)
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import os, tempfile, time, json
|
||||||
|
os.environ["LISAEL_BASE"] = tempfile.mkdtemp(prefix="transfers_test_")
|
||||||
|
os.environ["LISAEL_STAGING"] = os.path.join(os.environ["LISAEL_BASE"], "podcasts")
|
||||||
|
os.environ["LISAEL_STAGING_ROUTINES"] = os.path.join(os.environ["LISAEL_BASE"], "routines")
|
||||||
|
os.environ["LISAEL_TRANSFERS"] = os.path.join(os.environ["LISAEL_BASE"], "transfers.json")
|
||||||
|
import lisael_content as C
|
||||||
|
|
||||||
|
def test_begin_mark_end_cycle():
|
||||||
|
C.transfers_begin("192.168.0.250", [("a.mp3", "routines", 1000),
|
||||||
|
("routines.json", "routines", 50)])
|
||||||
|
a = C.transfers_active()
|
||||||
|
assert a["box_ip"] == "192.168.0.250" and a["total"] == 2 and a["current"] == 0
|
||||||
|
assert all(f["status"] == "pending" for f in a["files"])
|
||||||
|
C.transfers_mark("a.mp3", "sending")
|
||||||
|
assert C.transfers_active()["current"] == 1
|
||||||
|
time.sleep(0.05)
|
||||||
|
C.transfers_mark("a.mp3", "done")
|
||||||
|
f = next(x for x in C.transfers_active()["files"] if x["name"] == "a.mp3")
|
||||||
|
assert f["status"] == "done" and f["duration_s"] > 0 and f["rate_bps"] > 0
|
||||||
|
C.transfers_mark("routines.json", "sending")
|
||||||
|
C.transfers_mark("routines.json", "failed", "boom")
|
||||||
|
C.transfers_end()
|
||||||
|
assert C.transfers_active() is None
|
||||||
|
hist = C.transfers_history()
|
||||||
|
names = {h["name"]: h for h in hist}
|
||||||
|
assert names["a.mp3"]["status"] == "done"
|
||||||
|
assert names["routines.json"]["status"] == "failed" and names["routines.json"]["error"] == "boom"
|
||||||
|
|
||||||
|
def test_history_truncates_200():
|
||||||
|
for i in range(250):
|
||||||
|
C.transfers_begin("1.2.3.4", [(f"f{i}.mp3", "routines", 10)])
|
||||||
|
C.transfers_mark(f"f{i}.mp3", "sending"); C.transfers_mark(f"f{i}.mp3", "done")
|
||||||
|
C.transfers_end()
|
||||||
|
assert len(C.transfers_history()) == 200
|
||||||
|
# the most recent survive
|
||||||
|
assert any(h["name"] == "f249.mp3" for h in C.transfers_history())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
for name, fn in sorted(globals().items()):
|
||||||
|
if name.startswith("test_"):
|
||||||
|
fn(); print("ok", name)
|
||||||
|
print("ALL OK")
|
||||||
Reference in New Issue
Block a user