абузы: free-панели new-api на вкладке (роут /api/panels + грид), CSS вынесен из JS, панельный классификатор даёт freeapi-вес, тесты
Some checks failed
ci / test (3.10) (push) Has been cancelled
ci / test (3.11) (push) Has been cancelled
ci / test (3.12) (push) Has been cancelled

This commit is contained in:
Vlad Gothbreach
2026-10-27 02:06:32 +03:00
parent b09cb5f873
commit 16512bf4a1
21 changed files with 1386 additions and 130 deletions

View File

@@ -115,6 +115,33 @@ def _names_in_category(cat):
return [s.name for s in sources.all_sources() if s.category == cat]
def _run_state(status):
"""Класс статуса прогона: ok / empty / none / err.
"ok", "ok 0/10", "ok-304" — успех (startswith); "пусто"/"empty" — источник ответил пусто,
это не ошибка; отсутствующий статус (None/"") — отдельный класс none; остальное — err.
"""
v = " ".join(str(status or "").split()).lower()
if not v:
return "none"
if v.startswith("ok"):
return "ok"
if v in ("пусто", "empty"):
return "empty"
return "err"
def _since_days(days):
"""?days=N -> граница since (YYYY-MM-DD) для store.browse2; 0 — без фильтра.
Единая логика для /api/items и экспортов: «сутки/неделя/месяц» значат одно и то же.
"""
if not days:
return None
import datetime as _dt
return (_dt.date.today() - _dt.timedelta(days=days)).isoformat()
def _run_feed(job_id, category, limit, tags):
picked = sources.pick(category, None, tags)
_job(job_id, state="running", total=len(picked), done=0)
@@ -211,6 +238,18 @@ class Handler(BaseHTTPRequestHandler):
return self._send(404, "no ui", "text/plain")
return self._send(200, f.read_bytes(), "text/html; charset=utf-8")
if path == "/api/panels":
import json as _json
fp = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "panels.json")
try:
with open(fp, "r", encoding="utf-8") as f:
raw = _json.load(f)
except Exception:
raw = []
rows = raw if isinstance(raw, list) else (raw.get("panels") or raw.get("items") or [])
rows = sorted(rows, key=lambda x: -(x.get("free_count") or x.get("free") or 0))
return self._json({"ok": True, "total": len(rows), "panels": rows[:200]})
if path == "/api/stats":
s = store.stats()
per_source = store.count_by_source()
@@ -264,6 +303,7 @@ class Handler(BaseHTTPRequestHandler):
import time as _t
con = store._con() if hasattr(store, "_con") else None
rows = []
by_status = {"ok": 0, "empty": 0, "err": 0, "none": 0}
try:
import sqlite3 as _s3
from .config import DB as _DB
@@ -274,12 +314,22 @@ class Handler(BaseHTTPRequestHandler):
d = dict(r)
ts = d.get("ts") or 0
d["when"] = _t.strftime("%Y-%m-%d %H:%M", _t.localtime(ts)) if ts else ""
d["state"] = _run_state(d.get("status"))
d["ok"] = d["state"] == "ok"
rows.append(d)
total = db.execute("SELECT count(*) FROM runs").fetchone()[0]
# статусы считаем по всей таблице: total-ok из среза limit давал ложные «ошибки»
sql = ("SELECT COALESCE(SUM(CASE WHEN lower(trim(status)) LIKE 'ok%' THEN 1 ELSE 0 END),0),"
" COALESCE(SUM(CASE WHEN lower(trim(status)) IN ('пусто','empty') THEN 1 ELSE 0 END),0),"
" COALESCE(SUM(CASE WHEN status IS NULL OR trim(status)='' THEN 1 ELSE 0 END),0)"
" FROM runs")
n_ok, n_empty, n_none = db.execute(sql).fetchone()
by_status = {"ok": int(n_ok or 0), "empty": int(n_empty or 0), "none": int(n_none or 0)}
by_status["err"] = max(0, int(total) - by_status["ok"] - by_status["empty"] - by_status["none"])
db.close()
except Exception as e:
return self._json({"error": "runs: %s" % e})
return self._json({"items": rows, "total": total, "shown": len(rows)})
return self._json({"items": rows, "total": total, "shown": len(rows), "by_status": by_status})
if path == "/api/abuse":
from . import abuse as _ab
@@ -355,13 +405,13 @@ class Handler(BaseHTTPRequestHandler):
typ = (q.get("type") or ["all"])[0]
src = (q.get("src") or [""])[0].strip() or None
days = _intv(q, "days", 0, 0, 3650)
since = None
if days:
import datetime as _dt
since = (_dt.date.today() - _dt.timedelta(days=days)).isoformat()
since = _since_days(days)
rows, total = store.browse2(q=query, names=_names_in_category(cat), limit=limit, offset=offset,
sort=sort, priced=priced, typ=typ, since=since, src=src)
from . import abuse as _ab
for _r in rows:
# find нужен itemRow/findChips: browse2 сам признаки абуза не считает
_r["find"] = _ab.extract(" ".join((_r.get("title") or "", _r.get("text") or "")))
_r["title"] = _clean_txt(_r.get("title"), 180)
_r["text"] = _clean_txt(_r.get("text"), 260)
_r["published"] = _fmt_date(_r.get("published"))
@@ -374,8 +424,11 @@ class Handler(BaseHTTPRequestHandler):
priced = (q.get("priced") or ["0"])[0] in ("1", "true", "on")
sort = (q.get("sort") or ["score"])[0]
typ = (q.get("type") or ["all"])[0]
rows, total = store.browse2(q=query, names=_names_in_category(cat), limit=5000, offset=0,
sort=sort, priced=priced, typ=typ)
since = _since_days(_intv(q, "days", 0, 0, 3650))
limit = _intv(q, "limit", 1000, 1, 5000)
src = (q.get("src") or [""])[0].strip() or None
rows, total = store.browse2(q=query, names=_names_in_category(cat), limit=limit, offset=0,
sort=sort, priced=priced, typ=typ, since=since, src=src)
import io as _io
buf = _io.StringIO()
w = _csv.writer(buf, delimiter=";")
@@ -445,12 +498,17 @@ class Handler(BaseHTTPRequestHandler):
if path == "/api/export.json":
cat = (q.get("category") or [None])[0]
priced = (q.get("priced") or ["0"])[0] in ("1", "true", "on")
since = _since_days(_intv(q, "days", 0, 0, 3650))
src = (q.get("src") or [""])[0].strip() or None
rows, total = store.browse2(q=(q.get("q") or [""])[0].strip(),
names=_names_in_category(cat),
limit=_intv(q, "limit", 500, 1, 5000),
limit=_intv(q, "limit", 1000, 1, 5000),
sort=(q.get("sort") or ["score"])[0],
typ=(q.get("type") or ["all"])[0])
data = json.dumps({"total": total, "items": rows}, ensure_ascii=False, indent=1).encode("utf-8")
typ=(q.get("type") or ["all"])[0],
priced=priced, since=since, src=src)
data = json.dumps({"total": total, "count": len(rows), "items": rows},
ensure_ascii=False, indent=1).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Disposition", 'attachment; filename="omni.json"')