# address-field-enhancement ## Дизайн ### 1. Backend: парсер адреса и получение названия **Новая функция в `sources/sources.py`** (или отдельный модуль — решаю по месту; парсинг адреса — чистая функция без зависимостей): ```python _TG_HANDLE_RE = re.compile(r"^(?:https?://)?(?:www\.)?(?:t\.me|telegram\.me)/(?:s/)?([a-zA-Z][a-zA-Z0-9_]{3,31})$") _RSS_URL_RE = re.compile(r"^https?://", re.I) def parse_address(address: str, crawler: str) -> dict: """Разбирает одно поле Address в словарь полей источника. telegram: address = @handle | t.me/handle | https://t.me/s/handle | handle → {slug: handle, channel: handle, url: https://t.me/handle} rss: address = https://...feed.xml | http://... → {feed_url: address, url: address} (slug — из домена/ручного ввода) Возвращает dict с заполненными полями, пустые — None. Гарантирует валидность по _validate_source (slug для telegram; feed_url для rss). """ addr = (address or "").strip() if crawler == "telegram": m = _TG_HANDLE_RE.match(addr) or _TG_HANDLE_RE.match("https://t.me/" + addr.lstrip("@/")) if not m: return {} handle = m.group(1) return {"slug": handle, "channel": handle, "url": f"https://t.me/{handle}"} else: # rss if not _RSS_URL_RE.match(addr): return {} # slug берём из имени файла/домена, чтобы был стабильным и уникальным from urllib.parse import urlparse host = urlparse(addr).netloc.replace("www.", "").split(".")[0] return {"feed_url": addr, "url": addr, "slug": host} ``` **Получение названия (route preview)** — в `web/app.py`: ```python @app.get("/sources/preview") async def sources_preview(request: Request, address: str = "", crawler: str = "telegram"): """GET /sources/preview?address=t.me/linuxklub&crawler=telegram → JSON. telegram: get_entity из Telethon → name=title канала. rss: feedparser на адрес → name=feed.title. Ошибки (канал не найден, сеть) → {"error": "..."} (200, чтобы JS читал). """ _require_auth(request) if not (address or "").strip(): return {"error": "адрес пуст"} parsed = parse_address(address, crawler) if not parsed: return {"error": "не могу разобрать адрес для crawler=" + crawler} name = None if crawler == "telegram": from crawler.telegram_crawler import make_client from config import TG_SESSION_DIR import asyncio async def _get_title(): client = make_client(TG_SESSION_DIR) await client.start() try: ent = await client.get_entity(parsed["channel"]) return ent.title finally: await client.disconnect() try: name = asyncio.run(_get_title()) except Exception as e: return {"error": f"TG: {e}", **parsed} else: import feedparser try: d = feedparser.parse(parsed["feed_url"]) if d.bozo and not d.feed.get("title"): return {"error": f"фид не читается: {d.bozo_exception}", **parsed} name = d.feed.get("title") or None except Exception as e: return {"error": f"RSS: {e}", **parsed} return {"slug": parsed["slug"], "name": name, **parsed} ``` `_form_source` — замена url/channel/feed_url одиночным address: ```python def _form_source(form) -> dict: def s(k, default=None): v = form.get(k) return (v or "").strip() if v else default crawler = s("crawler", "telegram") src = { "slug": s("slug"), "name": s("name"), "crawler": crawler, "direction": s("direction"), "lang": s("lang", "ru"), "priority": s("priority", "P1"), "enabled": bool(form.get("enabled")), "own": bool(form.get("own")), } parsed = parse_address(s("address"), crawler) if crawler == "telegram": src["channel"] = parsed.get("channel") or s("channel") src["url"] = parsed.get("url") or s("url") src["feed_url"] = s("feed_url") else: src["feed_url"] = parsed.get("feed_url") or s("feed_url") src["url"] = parsed.get("url") or s("url") src["channel"] = s("channel") if not src["slug"] and parsed.get("slug"): src["slug"] = parsed["slug"] return src ``` `_validate_source`: для rss достаточно проверки feed_url (уже есть), для telegram — channel (уже есть). Доп. проверка: если crawler=rss и address не URL → _form_source уже вернул пустые, валидатор отдаст 'rss-источнику нужен feed_url'. Ничего менять не нужно. **DIRECTION_OPTIONS** — в app.py при рендере /sources: ```python @app.get("/sources") async def sources(request: Request, did: str = ""): ... conn = _db() rows = conn.execute( "SELECT DISTINCT direction FROM sources WHERE direction IS NOT NULL AND direction != '' ORDER BY direction" ).fetchall() directions = sorted(set(DIRECTIONS) | {r[0] for r in rows}) return templates.TemplateResponse("sources.html", { "sources": srcs, "did": did, "directions": directions, "request": request, }) ``` ### 2. Frontend: форма Поля `url`, `channel`, `feed_url` из формы удаляются, вместо них: ```html