Spaces:
Running
Running
github-actions[bot] commited on
Commit ·
1753544
1
Parent(s): ce89343
Automated sync from GitHub
Browse files- bot/app.py +2 -2
- bot/panels.py +10 -5
- bot/utils.py +7 -0
- tests/test_app.py +8 -1
- tests/test_panels.py +23 -1
- tests/tgfakes.py +20 -1
bot/app.py
CHANGED
|
@@ -489,7 +489,7 @@ class App:
|
|
| 489 |
await self._reply(
|
| 490 |
event,
|
| 491 |
f"<b>Current template:</b>\n<code>{esc(s['template'])}</code>\n\n{preview_template(s['template'])}\n\n"
|
| 492 |
-
"Variables:
|
| 493 |
"A [bracket] group is dropped when a variable inside it is empty.\n\n"
|
| 494 |
"Change it: <code>/template [S{season}E{episode}] {title} [{quality}]</code>",
|
| 495 |
)
|
|
@@ -505,7 +505,7 @@ class App:
|
|
| 505 |
await self._reply(
|
| 506 |
event,
|
| 507 |
f"<b>Current caption template:</b>\n<code>{esc(s['caption'])}</code>\n\n"
|
| 508 |
-
"Variables:
|
| 509 |
"Change it: <code>/caption <b>{filename}</b></code> · restore: <code>/caption reset</code>",
|
| 510 |
)
|
| 511 |
|
|
|
|
| 489 |
await self._reply(
|
| 490 |
event,
|
| 491 |
f"<b>Current template:</b>\n<code>{esc(s['template'])}</code>\n\n{preview_template(s['template'])}\n\n"
|
| 492 |
+
f"Variables: {panels.TEMPLATE_VARIABLES}\n"
|
| 493 |
"A [bracket] group is dropped when a variable inside it is empty.\n\n"
|
| 494 |
"Change it: <code>/template [S{season}E{episode}] {title} [{quality}]</code>",
|
| 495 |
)
|
|
|
|
| 505 |
await self._reply(
|
| 506 |
event,
|
| 507 |
f"<b>Current caption template:</b>\n<code>{esc(s['caption'])}</code>\n\n"
|
| 508 |
+
f"Variables: {panels.CAPTION_VARIABLES}\n"
|
| 509 |
"Change it: <code>/caption <b>{filename}</b></code> · restore: <code>/caption reset</code>",
|
| 510 |
)
|
| 511 |
|
bot/panels.py
CHANGED
|
@@ -9,13 +9,18 @@ from __future__ import annotations
|
|
| 9 |
from typing import Any, Dict, Iterable, List, Sequence, Tuple
|
| 10 |
|
| 11 |
from .media import LANGUAGES, canonical_lang
|
| 12 |
-
from .
|
|
|
|
| 13 |
|
| 14 |
Row = List[Tuple[str, str]]
|
| 15 |
Panel = Tuple[str, List[Row]]
|
| 16 |
|
| 17 |
DOT = "◈"
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
def to_buttons(rows: Sequence[Row]):
|
| 21 |
from telethon.tl.custom import Button
|
|
@@ -96,7 +101,7 @@ def caption(s: Dict[str, Any], has_thumb: bool) -> Panel:
|
|
| 96 |
text = (
|
| 97 |
"<b>CAPTION & THUMBNAIL</b>\n\n"
|
| 98 |
f"<b>Caption template:</b> {_code(s['caption'], 160)}\n"
|
| 99 |
-
"Variables:
|
| 100 |
"Telegram HTML works, e.g. <code><b>{filename}</b></code>\n\n"
|
| 101 |
f"<b>Thumbnail:</b> {'✅ saved' if has_thumb else '❌ none (a frame of the video is used)'}\n"
|
| 102 |
"Send me a photo to set your thumbnail."
|
|
@@ -193,7 +198,7 @@ def help_panel() -> Panel:
|
|
| 193 |
"8. /sequence then /sort — put files in episode order.\n"
|
| 194 |
"9. /check, /mediainfo, /ethumb, /resize, /speed — inspect files and images.\n"
|
| 195 |
"10. /queue and /cancel — manage running jobs.\n\n"
|
| 196 |
-
"Template variables:
|
| 197 |
"A [bracket] group is dropped when a variable inside it is empty."
|
| 198 |
)
|
| 199 |
return text, nav("home")
|
|
@@ -427,14 +432,14 @@ def sequence_status(count: int, limit: int) -> Panel:
|
|
| 427 |
PROMPTS: Dict[str, str] = {
|
| 428 |
"template": (
|
| 429 |
"<b>Send the new filename template.</b>\n\n"
|
| 430 |
-
"Variables:
|
| 431 |
"A [bracket] or (paren) group disappears when a variable inside it is empty.\n"
|
| 432 |
"If only an episode number is found, the season defaults to 01.\n\n"
|
| 433 |
"Example: <code>[S{season}E{episode}] {title} [{quality}]</code>"
|
| 434 |
),
|
| 435 |
"caption": (
|
| 436 |
"<b>Send the new caption template.</b>\n\n"
|
| 437 |
-
"Variables:
|
| 438 |
"Telegram HTML works (<code><b></code>, <code><i></code>, <code><code></code>)."
|
| 439 |
),
|
| 440 |
"remove": (
|
|
|
|
| 9 |
from typing import Any, Dict, Iterable, List, Sequence, Tuple
|
| 10 |
|
| 11 |
from .media import LANGUAGES, canonical_lang
|
| 12 |
+
from .parser import CAPTION_VARS, TEMPLATE_VARS
|
| 13 |
+
from .utils import clip, code_each, esc
|
| 14 |
|
| 15 |
Row = List[Tuple[str, str]]
|
| 16 |
Panel = Tuple[str, List[Row]]
|
| 17 |
|
| 18 |
DOT = "◈"
|
| 19 |
|
| 20 |
+
# The variable lists shown in the panels and prompts: every variable is its own <code> block, so tapping one copies only it.
|
| 21 |
+
TEMPLATE_VARIABLES = code_each("{%s}" % name for name in TEMPLATE_VARS)
|
| 22 |
+
CAPTION_VARIABLES = code_each("{%s}" % name for name in CAPTION_VARS if name != "ext") # {ext} was never listed for captions
|
| 23 |
+
|
| 24 |
|
| 25 |
def to_buttons(rows: Sequence[Row]):
|
| 26 |
from telethon.tl.custom import Button
|
|
|
|
| 101 |
text = (
|
| 102 |
"<b>CAPTION & THUMBNAIL</b>\n\n"
|
| 103 |
f"<b>Caption template:</b> {_code(s['caption'], 160)}\n"
|
| 104 |
+
f"Variables: {CAPTION_VARIABLES}\n"
|
| 105 |
"Telegram HTML works, e.g. <code><b>{filename}</b></code>\n\n"
|
| 106 |
f"<b>Thumbnail:</b> {'✅ saved' if has_thumb else '❌ none (a frame of the video is used)'}\n"
|
| 107 |
"Send me a photo to set your thumbnail."
|
|
|
|
| 198 |
"8. /sequence then /sort — put files in episode order.\n"
|
| 199 |
"9. /check, /mediainfo, /ethumb, /resize, /speed — inspect files and images.\n"
|
| 200 |
"10. /queue and /cancel — manage running jobs.\n\n"
|
| 201 |
+
f"Template variables: {TEMPLATE_VARIABLES}\n"
|
| 202 |
"A [bracket] group is dropped when a variable inside it is empty."
|
| 203 |
)
|
| 204 |
return text, nav("home")
|
|
|
|
| 432 |
PROMPTS: Dict[str, str] = {
|
| 433 |
"template": (
|
| 434 |
"<b>Send the new filename template.</b>\n\n"
|
| 435 |
+
f"Variables: {TEMPLATE_VARIABLES}\n"
|
| 436 |
"A [bracket] or (paren) group disappears when a variable inside it is empty.\n"
|
| 437 |
"If only an episode number is found, the season defaults to 01.\n\n"
|
| 438 |
"Example: <code>[S{season}E{episode}] {title} [{quality}]</code>"
|
| 439 |
),
|
| 440 |
"caption": (
|
| 441 |
"<b>Send the new caption template.</b>\n\n"
|
| 442 |
+
f"Variables: {CAPTION_VARIABLES}\n"
|
| 443 |
"Telegram HTML works (<code><b></code>, <code><i></code>, <code><code></code>)."
|
| 444 |
),
|
| 445 |
"remove": (
|
bot/utils.py
CHANGED
|
@@ -2,12 +2,19 @@
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
import html
|
|
|
|
| 5 |
|
| 6 |
|
| 7 |
def esc(value: object) -> str:
|
| 8 |
return html.escape(str(value), quote=False)
|
| 9 |
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
def human_size(num: float) -> str:
|
| 12 |
num = float(num)
|
| 13 |
for unit in ("B", "KB", "MB", "GB", "TB"):
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
import html
|
| 5 |
+
from typing import Iterable
|
| 6 |
|
| 7 |
|
| 8 |
def esc(value: object) -> str:
|
| 9 |
return html.escape(str(value), quote=False)
|
| 10 |
|
| 11 |
|
| 12 |
+
def code_each(items: Iterable[object]) -> str:
|
| 13 |
+
"""``<code>a</code> <code>b</code>``: one block per item. Telegram copies a whole ``<code>`` block when it is tapped,
|
| 14 |
+
so a list inside a single block can't be copied item by item."""
|
| 15 |
+
return " ".join(f"<code>{esc(item)}</code>" for item in items)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
def human_size(num: float) -> str:
|
| 19 |
num = float(num)
|
| 20 |
for unit in ("B", "KB", "MB", "GB", "TB"):
|
tests/test_app.py
CHANGED
|
@@ -17,7 +17,7 @@ from bot.queue import JobQueue
|
|
| 17 |
from .conftest import needs_ffmpeg
|
| 18 |
from .appkit import ADMIN_CFG, ADMIN_DB, EPISODE, EPISODE_NEW, LOG, OWNER, STRANGER, click, data_of, env_for, jpeg, prefs, say, send_doc
|
| 19 |
from .tgfakes import (
|
| 20 |
-
FakeCallback, FakeClient, FakeEvent, FakeMessage, dispatch, document_message, flat_buttons, make_file,
|
| 21 |
)
|
| 22 |
|
| 23 |
# ---------------------------------------------------------------------------
|
|
@@ -180,6 +180,13 @@ async def test_template_caption_and_removewords_commands(ctx):
|
|
| 180 |
assert (await prefs(ctx))["remove_words"] == []
|
| 181 |
|
| 182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
async def test_settings_are_per_user(ctx):
|
| 184 |
await say(ctx, OWNER, "/mode caption")
|
| 185 |
await say(ctx, ADMIN_CFG, "/filetype audio")
|
|
|
|
| 17 |
from .conftest import needs_ffmpeg
|
| 18 |
from .appkit import ADMIN_CFG, ADMIN_DB, EPISODE, EPISODE_NEW, LOG, OWNER, STRANGER, click, data_of, env_for, jpeg, prefs, say, send_doc
|
| 19 |
from .tgfakes import (
|
| 20 |
+
FakeCallback, FakeClient, FakeEvent, FakeMessage, dispatch, document_message, flat_buttons, make_file, variables_line,
|
| 21 |
)
|
| 22 |
|
| 23 |
# ---------------------------------------------------------------------------
|
|
|
|
| 180 |
assert (await prefs(ctx))["remove_words"] == []
|
| 181 |
|
| 182 |
|
| 183 |
+
async def test_template_and_caption_commands_list_each_variable_as_its_own_copyable_block(ctx):
|
| 184 |
+
line, blocks = variables_line((await say(ctx, OWNER, "/template")).texts[0])
|
| 185 |
+
assert blocks == line.split(": ", 1)[1].split() and "{quality}" in blocks and len(blocks) == 8
|
| 186 |
+
line, blocks = variables_line((await say(ctx, OWNER, "/caption")).texts[0])
|
| 187 |
+
assert blocks == line.split(": ", 1)[1].split() and "{size}" in blocks and len(blocks) == 10
|
| 188 |
+
|
| 189 |
+
|
| 190 |
async def test_settings_are_per_user(ctx):
|
| 191 |
await say(ctx, OWNER, "/mode caption")
|
| 192 |
await say(ctx, ADMIN_CFG, "/filetype audio")
|
tests/test_panels.py
CHANGED
|
@@ -8,10 +8,13 @@ from bot import panels
|
|
| 8 |
from bot.app import BACK, PANEL_NAMES
|
| 9 |
from bot.db import DEFAULT_SETTINGS
|
| 10 |
from bot.media import LANGUAGES
|
|
|
|
| 11 |
|
| 12 |
-
from .tgfakes import button_rows
|
| 13 |
|
| 14 |
CAPTION_LIMIT = 1000 # Telegram allows 1024 for a caption; keep a margin
|
|
|
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
def visible(text: str) -> str:
|
|
@@ -76,6 +79,25 @@ def test_every_panel_html_is_balanced():
|
|
| 76 |
assert text.count(f"<{tag}>") == text.count(f"</{tag}>"), (name, tag)
|
| 77 |
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
def test_every_input_button_has_a_prompt_and_a_back_target():
|
| 80 |
assert set(panels.PROMPTS) == set(BACK)
|
| 81 |
assert set(BACK.values()) <= PANEL_NAMES
|
|
|
|
| 8 |
from bot.app import BACK, PANEL_NAMES
|
| 9 |
from bot.db import DEFAULT_SETTINGS
|
| 10 |
from bot.media import LANGUAGES
|
| 11 |
+
from bot.utils import code_each
|
| 12 |
|
| 13 |
+
from .tgfakes import button_rows, variables_line
|
| 14 |
|
| 15 |
CAPTION_LIMIT = 1000 # Telegram allows 1024 for a caption; keep a margin
|
| 16 |
+
TEMPLATE_LIST = "{title} {season} {episode} {quality} {year} {codec} {filename} {ext}"
|
| 17 |
+
CAPTION_LIST = "{filename} {original} {title} {season} {episode} {quality} {year} {codec} {size} {duration}"
|
| 18 |
|
| 19 |
|
| 20 |
def visible(text: str) -> str:
|
|
|
|
| 79 |
assert text.count(f"<{tag}>") == text.count(f"</{tag}>"), (name, tag)
|
| 80 |
|
| 81 |
|
| 82 |
+
@pytest.mark.parametrize("name, text, shown", [
|
| 83 |
+
("template prompt", panels.PROMPTS["template"], TEMPLATE_LIST),
|
| 84 |
+
("caption prompt", panels.PROMPTS["caption"], CAPTION_LIST),
|
| 85 |
+
("help panel", panels.help_panel()[0], TEMPLATE_LIST),
|
| 86 |
+
("caption panel", panels.caption(settings(), True)[0], CAPTION_LIST),
|
| 87 |
+
])
|
| 88 |
+
def test_every_variable_is_its_own_tap_to_copy_block(name, text, shown):
|
| 89 |
+
# Tapping a <code> block in Telegram copies all of it, so "{quality}" must not share a block with the other variables.
|
| 90 |
+
line, blocks = variables_line(text)
|
| 91 |
+
assert line.split(": ", 1)[1] == shown, name # the list reads the same as it always did
|
| 92 |
+
assert blocks == shown.split(), name # ...with one block per variable
|
| 93 |
+
assert "<code>{quality}</code>" in text, name
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def test_code_each_makes_one_escaped_block_per_item():
|
| 97 |
+
assert code_each(["{a}", "<b>", 3]) == "<code>{a}</code> <code><b></code> <code>3</code>"
|
| 98 |
+
assert code_each([]) == ""
|
| 99 |
+
|
| 100 |
+
|
| 101 |
def test_every_input_button_has_a_prompt_and_a_back_target():
|
| 102 |
assert set(panels.PROMPTS) == set(BACK)
|
| 103 |
assert set(BACK.values()) <= PANEL_NAMES
|
tests/tgfakes.py
CHANGED
|
@@ -7,10 +7,14 @@ the real event builders, patterns and ``func`` filters are exercised too.
|
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
import inspect
|
|
|
|
| 10 |
from types import SimpleNamespace
|
| 11 |
-
from typing import Any, List, Optional
|
| 12 |
|
| 13 |
from telethon import events
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
|
| 16 |
def make_file(name: Optional[str] = "Movie.mkv", size: int = 5 * 1024 * 1024, mime: str = "video/x-matroska",
|
|
@@ -218,3 +222,18 @@ def button_rows(buttons: Any) -> List[List[tuple]]:
|
|
| 218 |
|
| 219 |
def flat_buttons(buttons: Any) -> List[tuple]:
|
| 220 |
return [b for row in button_rows(buttons) for b in row]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
import inspect
|
| 10 |
+
import re
|
| 11 |
from types import SimpleNamespace
|
| 12 |
+
from typing import Any, List, Optional, Tuple
|
| 13 |
|
| 14 |
from telethon import events
|
| 15 |
+
from telethon.extensions import html as telegram_html
|
| 16 |
+
from telethon.helpers import add_surrogate, del_surrogate
|
| 17 |
+
from telethon.tl.types import MessageEntityCode
|
| 18 |
|
| 19 |
|
| 20 |
def make_file(name: Optional[str] = "Movie.mkv", size: int = 5 * 1024 * 1024, mime: str = "video/x-matroska",
|
|
|
|
| 222 |
|
| 223 |
def flat_buttons(buttons: Any) -> List[tuple]:
|
| 224 |
return [b for row in button_rows(buttons) for b in row]
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def variables_line(html_text: str) -> Tuple[str, List[str]]:
|
| 228 |
+
"""The "Variables: …" line of a message as Telegram receives it (parsed by Telethon's own HTML parser): its text, and
|
| 229 |
+
the ``<code>`` blocks on it. Tapping a block copies exactly its text, so these are the things a user can copy."""
|
| 230 |
+
plain, entities = telegram_html.parse(html_text)
|
| 231 |
+
units, start = add_surrogate(plain), 0 # entity offsets count UTF-16 units
|
| 232 |
+
for line in units.split("\n"):
|
| 233 |
+
end = start + len(line)
|
| 234 |
+
if re.match(r"(?:Template v|V)ariables: ", line):
|
| 235 |
+
blocks = [del_surrogate(units[e.offset:e.offset + e.length]) for e in entities
|
| 236 |
+
if isinstance(e, MessageEntityCode) and start <= e.offset < end]
|
| 237 |
+
return del_surrogate(line), blocks
|
| 238 |
+
start = end + 1
|
| 239 |
+
raise AssertionError("no Variables line in: " + plain)
|