github-actions[bot] commited on
Commit
1753544
·
1 Parent(s): ce89343

Automated sync from GitHub

Browse files
Files changed (6) hide show
  1. bot/app.py +2 -2
  2. bot/panels.py +10 -5
  3. bot/utils.py +7 -0
  4. tests/test_app.py +8 -1
  5. tests/test_panels.py +23 -1
  6. 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: <code>{title} {season} {episode} {quality} {year} {codec} {filename} {ext}</code>\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,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: <code>{filename} {original} {title} {season} {episode} {quality} {year} {codec} {size} {duration}</code>\n"
509
  "Change it: <code>/caption &lt;b&gt;{filename}&lt;/b&gt;</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 &lt;b&gt;{filename}&lt;/b&gt;</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 .utils import clip, esc
 
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 &amp; THUMBNAIL</b>\n\n"
98
  f"<b>Caption template:</b> {_code(s['caption'], 160)}\n"
99
- "Variables: <code>{filename} {original} {title} {season} {episode} {quality} {year} {codec} {size} {duration}</code>\n"
100
  "Telegram HTML works, e.g. <code>&lt;b&gt;{filename}&lt;/b&gt;</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: <code>{title} {season} {episode} {quality} {year} {codec} {filename} {ext}</code>\n"
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: <code>{title} {season} {episode} {quality} {year} {codec} {filename} {ext}</code>\n"
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: <code>{filename} {original} {title} {season} {episode} {quality} {year} {codec} {size} {duration}</code>\n"
438
  "Telegram HTML works (<code>&lt;b&gt;</code>, <code>&lt;i&gt;</code>, <code>&lt;code&gt;</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 &amp; 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>&lt;b&gt;{filename}&lt;/b&gt;</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>&lt;b&gt;</code>, <code>&lt;i&gt;</code>, <code>&lt;code&gt;</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>&lt;b&gt;</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)