Files
omni-parse/omni/serve.py

111 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Веб-панель 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()