131 lines
4.9 KiB
Python
131 lines
4.9 KiB
Python
"""Состояние источников: включён/выключен, пользовательские источники, результаты проверок.
|
||
|
||
Две таблицы в той же 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" |