omni: менеджер источников в панели (338 источников, вкл/выкл, добавление, health-check, фоновые джобы)
This commit is contained in:
131
omni/source_state.py
Normal file
131
omni/source_state.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""Состояние источников: включён/выключен, пользовательские источники, результаты проверок.
|
||||
|
||||
Две таблицы в той же SQLite, что и записи:
|
||||
source_flags (name, enabled, status, items, note, checked)
|
||||
source_custom (name, category, kind, url, adapter, tags, limit, detail, added)
|
||||
"""
|
||||
import json
|
||||
import sqlite3
|
||||
import time
|
||||
|
||||
from . import config
|
||||
|
||||
SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS source_flags (
|
||||
name TEXT PRIMARY KEY,
|
||||
enabled INTEGER DEFAULT 1,
|
||||
status INTEGER,
|
||||
items INTEGER,
|
||||
note TEXT,
|
||||
checked INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS source_custom (
|
||||
name TEXT PRIMARY KEY,
|
||||
category TEXT, kind TEXT, url TEXT, adapter TEXT,
|
||||
tags TEXT, lim INTEGER DEFAULT 20, detail INTEGER DEFAULT 0, added INTEGER
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
def conn():
|
||||
c = sqlite3.connect(str(config.DB), timeout=30)
|
||||
c.execute("PRAGMA journal_mode=WAL")
|
||||
c.executescript(SCHEMA)
|
||||
return c
|
||||
|
||||
|
||||
def flags():
|
||||
"""{name: {enabled, status, items, note, checked}}"""
|
||||
with conn() as c:
|
||||
rows = c.execute("SELECT name, enabled, status, items, note, checked FROM source_flags").fetchall()
|
||||
return {r[0]: {"enabled": bool(r[1]), "status": r[2], "items": r[3], "note": r[4], "checked": r[5]}
|
||||
for r in rows}
|
||||
|
||||
|
||||
def set_enabled(name, enabled):
|
||||
now = int(time.time())
|
||||
with conn() as c:
|
||||
c.execute("INSERT INTO source_flags(name, enabled, checked) VALUES(?,?,?) "
|
||||
"ON CONFLICT(name) DO UPDATE SET enabled=excluded.enabled",
|
||||
(name, 1 if enabled else 0, now))
|
||||
return {"name": name, "enabled": bool(enabled)}
|
||||
|
||||
|
||||
def record_check(name, status, items, note=""):
|
||||
now = int(time.time())
|
||||
with conn() as c:
|
||||
c.execute("INSERT INTO source_flags(name, enabled, status, items, note, checked) VALUES(?,1,?,?,?,?) "
|
||||
"ON CONFLICT(name) DO UPDATE SET status=excluded.status, items=excluded.items, "
|
||||
"note=excluded.note, checked=excluded.checked",
|
||||
(name, status, items, str(note)[:160], now))
|
||||
return {"name": name, "status": status, "items": items, "note": str(note)[:160], "checked": now}
|
||||
|
||||
|
||||
def custom():
|
||||
with conn() as c:
|
||||
rows = c.execute("SELECT name, category, kind, url, adapter, tags, lim, detail, added "
|
||||
"FROM source_custom ORDER BY added DESC").fetchall()
|
||||
out = []
|
||||
for r in rows:
|
||||
out.append({"name": r[0], "category": r[1], "kind": r[2], "url": r[3], "adapter": r[4],
|
||||
"tags": tuple(json.loads(r[5] or "[]")), "limit": r[6] or 20,
|
||||
"detail": bool(r[7]), "extra": {}, "added": r[8]})
|
||||
return out
|
||||
|
||||
|
||||
def add_custom(name, category, url, kind=None, adapter=None, tags=(), limit=20, detail=False):
|
||||
name = (name or "").strip()
|
||||
url = (url or "").strip()
|
||||
if not name or not url:
|
||||
raise ValueError("нужны name и url")
|
||||
if not url.startswith("http"):
|
||||
raise ValueError("url должен начинаться с http")
|
||||
kind, adapter = kind or guess_kind(url), adapter or guess_adapter(url, kind)
|
||||
with conn() as c:
|
||||
c.execute("INSERT OR REPLACE INTO source_custom(name, category, kind, url, adapter, tags, lim, detail, added) "
|
||||
"VALUES(?,?,?,?,?,?,?,?,?)",
|
||||
(name, category or "custom", kind, url, adapter,
|
||||
json.dumps(list(tags), ensure_ascii=False), int(limit), 1 if detail else 0, int(time.time())))
|
||||
return {"name": name, "category": category or "custom", "kind": kind, "url": url,
|
||||
"adapter": adapter, "tags": list(tags), "limit": limit, "detail": bool(detail)}
|
||||
|
||||
|
||||
def remove_custom(name):
|
||||
with conn() as c:
|
||||
c.execute("DELETE FROM source_custom WHERE name=?", (name,))
|
||||
c.execute("DELETE FROM source_flags WHERE name=?", (name,))
|
||||
return {"removed": name}
|
||||
|
||||
|
||||
def guess_kind(url):
|
||||
u = url.lower().rstrip("/")
|
||||
path = u.split("://", 1)[-1]
|
||||
path = path[path.find("/"):] if "/" in path else ""
|
||||
last = path.rsplit("/", 1)[-1]
|
||||
if "github.com" in u and u.count("/") <= 4:
|
||||
return "github"
|
||||
if last in ("rss", "feed", "atom", "rss.xml", "feed.xml", "atom.xml", "index.xml", "rss2"):
|
||||
return "rss"
|
||||
if u.endswith(".json") or "/latest.json" in u or "api." in u or "/api/" in u:
|
||||
return "json"
|
||||
if any(x in u for x in (".rss", "rss.xml", "/feed", ".atom", "/rss/", "atom.xml")):
|
||||
return "rss"
|
||||
return "html"
|
||||
|
||||
|
||||
def guess_adapter(url, kind=None):
|
||||
u = url.lower()
|
||||
kind = kind or guess_kind(url)
|
||||
if "github.com" in u:
|
||||
return "github_org"
|
||||
if "discourse" in u or u.endswith("/latest.json"):
|
||||
return "discourse"
|
||||
if kind == "json":
|
||||
return "generic_json"
|
||||
if u.endswith(".json") or "api." in u:
|
||||
return "generic_json"
|
||||
if "t.me/s/" in u:
|
||||
return "tg_web"
|
||||
if kind == "rss":
|
||||
return "rss"
|
||||
return "html_listing"
|
||||
Reference in New Issue
Block a user