File size: 6,896 Bytes
58f00c9
 
 
 
6c3a31d
b737d7a
 
58f00c9
 
 
 
 
 
6c3a31d
58f00c9
 
 
6c3a31d
58f00c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6c3a31d
58f00c9
 
 
 
 
 
 
 
 
 
 
 
 
6c3a31d
58f00c9
21cde0c
58f00c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a670808
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
"""Validation + persistence for every user-editable setting.

Used by both the slash commands and the inline "send me the value" flow, so the
two can never disagree.  Functions return ``(ok, html_message)``.

``db`` is whatever keeps the setting: the ``Database`` for the user's own settings, a ``FormatSettings`` for a saved format
(the template and the caption live in formats, not in the user's settings).
"""
from __future__ import annotations

import re
from typing import Optional, Tuple

from .db import DEFAULT_CAPTION, Database
from .media import parse_track_tokens, validate_track_tokens
from .parser import (
    CAPTION_VARS,
    DEFAULT_TEMPLATE,
    TEMPLATE_VARS,
    finalize_name,
    render_template,
    sample_values,
    sanitize,
)
from .panels import fmt_tokens
from .utils import clip, esc

CLEAR_WORDS = {"off", "none", "clear", "reset", "-", "remove"}
META_KINDS = {
    "mt_title": "title", "mt_author": "author", "mt_artist": "artist",
    "mt_audio": "audio", "mt_subtitle": "subtitle", "mt_video": "video",
}
META_COMMANDS = {
    "settitle": "mt_title", "setauthor": "mt_author", "setartist": "mt_artist",
    "setaudio": "mt_audio", "setsubtitle": "mt_subtitle", "setvideo": "mt_video",
}
_TOKEN_RE = re.compile(r"\{(\w+)\}")


def preview_template(template: str) -> str:
    sample, values = sample_values()
    out = finalize_name(sanitize(render_template(template, values)), ".mkv")
    return f"Preview for <code>{esc(sample)}</code>:\n<code>{esc(out)}</code>"


def _is_clear(text: str) -> bool:
    return text.strip().lower() in CLEAR_WORDS


async def apply_setting(db: Database, uid: int, kind: str, text: str) -> Tuple[bool, str]:
    text = (text or "").strip()

    if kind == "template":
        if not text:
            return False, "❌ The template can't be empty."
        if _is_clear(text):
            text = DEFAULT_TEMPLATE
        if len(text) > 200:
            return False, "❌ That template is too long (200 characters max)."
        unknown = sorted(set(_TOKEN_RE.findall(text)) - set(TEMPLATE_VARS))
        await db.update_settings(uid, template=text)
        msg = "βœ… Template saved.\n" + preview_template(text)
        if unknown:
            msg += "\n⚠️ Unknown variables (kept as plain text): " + ", ".join("{%s}" % u for u in unknown)
        return True, msg

    if kind == "caption":
        if not text:
            return False, "❌ The caption can't be empty. Send <code>reset</code> to restore the default."
        if _is_clear(text):
            text = DEFAULT_CAPTION
        if len(text) > 800:
            return False, "❌ That caption is too long (800 characters max, formatting and link addresses included)."
        unknown = sorted(set(_TOKEN_RE.findall(text)) - set(CAPTION_VARS))
        await db.update_settings(uid, caption=text)
        msg = "βœ… Caption template saved."
        if unknown:
            msg += "\n⚠️ Unknown variables (kept as plain text): " + ", ".join("{%s}" % u for u in unknown)
        return True, msg

    if kind == "remove":
        if _is_clear(text):
            await db.update_settings(uid, remove_words=[])
            return True, "βœ… Remove-words list cleared."
        words = [w.strip() for w in re.split(r"[,\n]", text) if w.strip()]
        if not words:
            return False, "❌ Send at least one word, separated by commas."
        if len(words) > 50 or any(len(w) > 60 for w in words):
            return False, "❌ Up to 50 words of 60 characters each."
        await db.update_settings(uid, remove_words=words)
        return True, "βœ… I'll strip: " + esc(", ".join(words))

    if kind in META_KINDS:
        field = META_KINDS[kind]
        if not text:
            return False, "❌ Send a value, or <code>off</code> to clear it."
        if _is_clear(text):
            await db.update_settings(uid, meta={field: ""})
            return True, f"βœ… {field.title()} cleared."
        if len(text) > 200:
            return False, "❌ That's too long (200 characters max)."
        await db.update_settings(uid, meta={field: text})
        note = ""
        if not (await db.get_settings(uid))["metadata_on"]:
            note = "\nℹ️ Metadata is Off β€” switch it On in /metadata for this to apply."
        return True, f"βœ… {field.title()} set to <code>{esc(clip(text, 80))}</code>.{note}"

    if kind in ("keep_audio", "keep_subtitle"):
        label = "Keep Audio" if kind == "keep_audio" else "Keep Subtitle"
        if _is_clear(text):
            await db.update_settings(uid, **{kind: []})
            return True, f"βœ… {label} cleared β€” all tracks are kept."
        tokens = parse_track_tokens(text)
        if not tokens:
            return False, "❌ Send language names (<code>English Japanese</code>) or numbers (<code>2 1</code>)."
        problem = validate_track_tokens(tokens)
        if problem:
            return False, "❌ " + esc(problem)
        if len(tokens) > 12:
            return False, "❌ That's too many tracks (12 max)."
        await db.update_settings(uid, **{kind: tokens})
        msg = f"βœ… {label}: {esc(fmt_tokens(tokens))}"
        if not (await db.get_settings(uid))["metadata_on"]:
            await db.update_settings(uid, metadata_on=True)
            msg += "\nℹ️ Metadata was Off, so I turned it On. Use /metadata to switch it off."
        return True, msg

    raise ValueError(f"unknown setting kind {kind!r}")


def parse_channel(text: str) -> Tuple[bool, Optional[int], str]:
    """``-1001234567890`` -> id; ``off`` -> None."""
    text = text.strip()
    if _is_clear(text):
        return True, None, ""
    if re.fullmatch(r"-?\d{5,20}", text):
        return True, int(text), ""
    return False, None, "❌ Send the numeric chat id, like <code>-1001234567890</code> (not @username)."


_CHANNEL_LINK = re.compile(r"(?:https?://)?(?:www\.)?t\.me/c/(\d{5,15})(?:/\d+){0,2}/?", re.I)
CHANNEL_ID_HINT = "❌ Send the channel id, like <code>-1001234567890</code> (a channel id starts with -100), or <code>off</code> to clear it."


def parse_format_channel(text: str) -> Tuple[bool, Optional[int], str]:
    """The Channel option of a manual format: ``-1001234567890`` -> that id, ``off`` -> ``None``.

    A bare channel id (``1234567890``) and a link to a post of a private channel (``t.me/c/1234567890/5``) work too: both get
    the ``-100`` in front that Telegram puts on channel ids.
    """
    text = (text or "").strip()
    if _is_clear(text):
        return True, None, ""
    link = _CHANNEL_LINK.fullmatch(text)
    if link:
        return True, -1_000_000_000_000 - int(link.group(1)), ""
    if re.fullmatch(r"-?\d{5,20}", text):
        value = int(text)
        if value > 0:
            return True, -1_000_000_000_000 - value, ""
        if value <= -1_000_000_000_001:
            return True, value, ""
    return False, None, CHANNEL_ID_HINT