File size: 4,930 Bytes
ff3c435
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7e31451
ff3c435
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152ccef
ff3c435
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
"""Normalize public links and render structured parser results."""
import html
import re
from collections import OrderedDict
from urllib.parse import parse_qs, urlsplit


def normalize(raw):
    try:
        url = urlsplit(raw.strip())
        if url.scheme not in ("https", "http") or url.username or url.password or url.port:
            raise ValueError("Нужна обычная ссылка http/https")
        host = url.hostname.lower().removeprefix("www.").removeprefix("m.")
        path = url.path.rstrip("/")
        if host in ("t.me", "telegram.me"):
            if path.startswith("/c/"):
                raise ValueError("Закрытая ссылка Telegram: нужен публичный адрес поста")
            match = re.fullmatch(r"/(?:s/)?([A-Za-z][A-Za-z0-9_]{2,})/([1-9]\d*)", path)
            if match:
                slug, post = match.groups()
                slug = slug.lower()
                return {"url": f"https://t.me/{slug}/{post}", "platform": "telegram", "group": slug, "a": slug, "b": post}
        elif host in ("vk.com", "vk.ru"):
            wall = path
            if "w" in parse_qs(url.query):
                wall = "/" + parse_qs(url.query)["w"][0]
            match = re.fullmatch(r"/wall(-?[1-9]\d*)_([1-9]\d*)", wall)
            if match and "reply" not in parse_qs(url.query):
                owner, post = match.groups()
                return {"url": f"https://vk.com/wall{owner}_{post}", "platform": "vk", "group": owner, "a": int(owner), "b": post}
        elif host == "max.ru":
            match = re.fullmatch(r"/([A-Za-z0-9_]+)/([A-Za-z0-9_-]+)", path)
            if match:
                slug, post = match.groups()
                slug = slug.lower()
                return {"url": f"https://max.ru/{slug}/{post}", "platform": "max", "group": slug.lower(), "a": slug, "b": post}
        raise ValueError("Площадка или формат ссылки пока не поддерживаются")
    except (AttributeError, TypeError):
        raise ValueError("Некорректная ссылка") from None


def human_views(value):
    divisor, suffix = (1_000_000, "M") if value >= 1_000_000 else (1000, "K")
    return f"{value / divisor:.1f}".rstrip("0").rstrip(".").replace(".", ",") + suffix


def clean_title(value):
    # Strip legacy HTML emoji representations as well as unicode emoji clusters.
    value = re.sub(r"<[^>]+>", "", html.unescape(str(value)))
    value = re.sub(r"[\U0001F000-\U0001FAFF\u2600-\u27BF\uFE0E\uFE0F\u200D]", "", value)
    value = re.sub(r"[#*0-9]\u20e3", "", value)
    return re.sub(r"\s+", " ", value).strip()


def render(records):
    chunks, plain, errors = [], [], []
    for platform, label in (("telegram", "Telegram"), ("vk", "ВКонтакте"), ("max", "Макс")):
        selected = [r for r in records if r["platform"] == platform]
        if not selected:
            continue
        good = [r for r in selected if r.get("views") is not None and not r.get("error")]
        groups = OrderedDict()
        for record in good:
            groups.setdefault(record["group"], []).append(record)
        total = sum(r["views"] for r in good)
        chunks.extend([f"<h2>{label}</h2>", f"<p>Суммарно посты собрали <b>{human_views(total)}</b> просмотров.</p>"])
        plain.extend([label, f"Суммарно посты собрали {human_views(total)} просмотров."])
        if len(good) != len(selected):
            note = f"Неполный итог: получены просмотры {len(good)} из {len(selected)} постов."
            chunks.append(f"<p><i>{note}</i></p>")
            plain.append(note)
        chunks.append("<ol>")
        ordered = sorted(groups.values(), key=lambda items: sum(r["views"] for r in items), reverse=True)
        for index, items in enumerate(ordered, 1):
            title = clean_title(items[0]["title"]) or items[0]["group"]
            links = [f'<a href="{html.escape(r.get("display_url") or r["url"], quote=True)}">{html.escape(title if i == 0 else "ещё")}</a>' for i, r in enumerate(items)]
            views = " + ".join(human_views(r["views"]) for r in items)
            chunks.append(f'<li>{" + ".join(links)} — {views}</li>')
            plain.append(f'{index}. {title}{" + ещё" * (len(items) - 1)} — {views}')
        chunks.append("</ol>")
        plain.append("")
    for record in records:
        if record.get("error"):
            errors.append(f'{record["url"]}: {record["error"]}')
    if errors:
        chunks.append("<h2>Не удалось обработать</h2><ul>")
        chunks.extend(f"<li>{html.escape(error)}</li>" for error in errors)
        chunks.append("</ul>")
        plain.extend(["Не удалось обработать", *errors])
    return {"html": "\n".join(chunks), "text": "\n".join(plain), "errors": errors, "records": records}