111 lines
4.4 KiB
Python
111 lines
4.4 KiB
Python
"""Веб-панель omni: статика + JSON API поверх той же SQLite.
|
||
|
||
Запуск: python -m omni serve --port 8077
|
||
"""
|
||
import json
|
||
import pathlib
|
||
import threading
|
||
import urllib.parse
|
||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||
|
||
from . import feeds, sources, store
|
||
|
||
WEB = pathlib.Path(__file__).resolve().parent.parent / "web"
|
||
JOBS = {}
|
||
_LOCK = threading.Lock()
|
||
|
||
|
||
def _run_feed(job_id, category, limit, tags):
|
||
picked = sources.pick(category, None, tags)
|
||
try:
|
||
items, report = feeds.fetch_many(picked, limit=limit, workers=8)
|
||
new, dup = store.add(items)
|
||
with _LOCK:
|
||
JOBS[job_id] = {"state": "done", "sources": len(picked), "fetched": len(items),
|
||
"new": new, "dup": dup, "empty": sum(1 for _, c in report if not c),
|
||
"report": sorted(report, key=lambda x: -x[1])[:40]}
|
||
except Exception as e:
|
||
with _LOCK:
|
||
JOBS[job_id] = {"state": "error", "error": f"{type(e).__name__}: {e}"[:200]}
|
||
|
||
|
||
class Handler(BaseHTTPRequestHandler):
|
||
server_version = "omni/1.0"
|
||
|
||
def log_message(self, *a):
|
||
pass
|
||
|
||
def _send(self, code, body, ctype="application/json; charset=utf-8"):
|
||
data = body if isinstance(body, bytes) else str(body).encode("utf-8")
|
||
self.send_response(code)
|
||
self.send_header("Content-Type", ctype)
|
||
self.send_header("Content-Length", str(len(data)))
|
||
self.send_header("Cache-Control", "no-store")
|
||
self.end_headers()
|
||
self.wfile.write(data)
|
||
|
||
def _json(self, obj, code=200):
|
||
self._send(code, json.dumps(obj, ensure_ascii=False))
|
||
|
||
def do_GET(self):
|
||
u = urllib.parse.urlsplit(self.path)
|
||
q = urllib.parse.parse_qs(u.query)
|
||
path = u.path
|
||
|
||
if path in ("/", "/index.html"):
|
||
f = WEB / "index.html"
|
||
if not f.exists():
|
||
return self._send(404, "no ui", "text/plain")
|
||
return self._send(200, f.read_bytes(), "text/html; charset=utf-8")
|
||
|
||
if path == "/api/stats":
|
||
s = store.stats()
|
||
return self._json({"items": s["items"], "priced": s["priced"], "runs": s["runs"],
|
||
"runs_ok": s["runs_ok"],
|
||
"by_provider": [{"name": n, "count": c} for n, c in s["by_provider"]],
|
||
"by_kind": [{"name": n, "count": c} for n, c in s["by_kind"]],
|
||
"sources": len(sources.SOURCES), "categories": sources.stats()})
|
||
|
||
if path == "/api/sources":
|
||
return self._json({"total": len(sources.SOURCES), "categories": sources.stats(),
|
||
"items": [{"name": s.name, "category": s.category, "kind": s.kind,
|
||
"adapter": s.adapter, "url": s.url, "tags": list(s.tags)}
|
||
for s in sources.SOURCES]})
|
||
|
||
if path == "/api/items":
|
||
cat = (q.get("category") or [None])[0]
|
||
query = (q.get("q") or [""])[0].strip()
|
||
limit = min(int((q.get("limit") or ["60"])[0]), 300)
|
||
offset = int((q.get("offset") or ["0"])[0])
|
||
rows = store.browse(category=cat, q=query, limit=limit, offset=offset)
|
||
return self._json({"count": len(rows), "items": rows})
|
||
|
||
if path.startswith("/api/job/"):
|
||
jid = path.rsplit("/", 1)[-1]
|
||
return self._json(JOBS.get(jid) or {"state": "unknown"})
|
||
|
||
return self._send(404, "not found", "text/plain")
|
||
|
||
def do_POST(self):
|
||
u = urllib.parse.urlsplit(self.path)
|
||
n = int(self.headers.get("Content-Length") or 0)
|
||
body = {}
|
||
if n:
|
||
try:
|
||
body = json.loads(self.rfile.read(n) or b"{}")
|
||
except Exception:
|
||
body = {}
|
||
if u.path == "/api/feed":
|
||
jid = f"job{len(JOBS) + 1}"
|
||
with _LOCK:
|
||
JOBS[jid] = {"state": "running"}
|
||
threading.Thread(target=_run_feed, args=(jid, body.get("category"), int(body.get("limit") or 12),
|
||
body.get("tags")), daemon=True).start()
|
||
return self._json({"job": jid})
|
||
return self._json({"error": "not found"}, 404)
|
||
|
||
|
||
def serve(port=8077):
|
||
srv = ThreadingHTTPServer(("127.0.0.1", port), Handler)
|
||
print(f"omni ui: http://127.0.0.1:{port} (источников {len(sources.SOURCES)})")
|
||
srv.serve_forever() |