Spaces:
Running
Running
File size: 31,238 Bytes
58f00c9 6c3a31d 58f00c9 21cde0c 6c3a31d 58f00c9 1753544 58f00c9 1753544 6c3a31d 1753544 58f00c9 f4c871b 58f00c9 f4c871b 58f00c9 6c3a31d 58f00c9 6c3a31d 58f00c9 6c3a31d 58f00c9 6c3a31d 58f00c9 6c3a31d b737d7a 58f00c9 6c3a31d 58f00c9 6c3a31d b737d7a 58f00c9 b737d7a 58f00c9 6c3a31d 58f00c9 6c3a31d 58f00c9 6c3a31d b737d7a 1753544 58f00c9 5f400f8 58f00c9 5f400f8 6c3a31d 5f400f8 6c3a31d 5f400f8 6c3a31d 5f400f8 6c3a31d 529b973 6c3a31d 5f400f8 6c3a31d 5f400f8 6c3a31d 5f400f8 6c3a31d 5f400f8 6c3a31d 5f400f8 529b973 6c3a31d 529b973 f4c871b 5f400f8 21cde0c f4c871b 5f400f8 f4c871b a670808 6c3a31d a670808 6c3a31d a670808 6c3a31d a670808 6c3a31d 5f400f8 529b973 5f400f8 529b973 5f400f8 a670808 6c3a31d f4c871b 5f400f8 6c3a31d 5f400f8 6c3a31d f4c871b 6c3a31d f4c871b 6c3a31d f4c871b 6c3a31d f4c871b 6c3a31d f4c871b 6c3a31d f4c871b 6c3a31d f4c871b 5f400f8 529b973 5f400f8 6c3a31d 5f400f8 6c3a31d 5f400f8 6c3a31d 5f400f8 58f00c9 5f400f8 b737d7a 529b973 21cde0c 529b973 5f400f8 a670808 5f400f8 6c3a31d 5f400f8 58f00c9 | 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 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 | """Inline-menu panels: pure functions returning ``(html_text, button_rows)``.
Button rows are lists of ``(label, callback_data)``; ``to_buttons`` converts them
for Telethon. Panel texts stay well below Telegram's 1024-character caption
limit because the home banner makes every panel a photo caption.
"""
from __future__ import annotations
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
from . import richtext
from .db import DEFAULT_CAPTION, DEFAULT_FORMAT
from .formats import KIND_ICONS, KIND_LABELS, has_metadata, other_kind
from .media import LANGUAGES, canonical_lang
from .parser import CAPTION_VARS, TEMPLATE_VARS
from .utils import clip, code_each, esc
Row = List[Tuple[str, str]]
Panel = Tuple[str, List[Row]]
DOT = "◈"
# The variable lists shown in the panels and prompts: every variable is its own <code> block, so tapping one copies only it.
TEMPLATE_VARIABLES = code_each("{%s}" % name for name in TEMPLATE_VARS)
CAPTION_VARIABLES = code_each("{%s}" % name for name in CAPTION_VARS if name != "ext") # {ext} was never listed for captions
# What {1episode} ... {3episode} do, in one line (they are the episode with zeros in front, 2 to 4 digits wide)
EPISODE_ZEROS = (
"Zeros in front of the episode: <code>{1episode}</code> makes it 2 digits (4 → 04), <code>{2episode}</code> 3 digits (4 → 004), "
"<code>{3episode}</code> 4 digits (4 → 0004). A number that already has that many digits stays as it is."
)
INLINE = "inline:" # a button whose data starts with this opens Telegram's chat picker, with "@bot <the rest>" typed in the chat
def share_query(fid: int) -> str:
"""The inline query a Share format button types into the chosen chat (``App.on_inline`` answers it with a card)."""
return f"share_{fid}"
def to_buttons(rows: Sequence[Row]):
from telethon.tl.custom import Button
def button(label: str, data: str):
if data.startswith(INLINE):
return Button.switch_inline(label, query=data[len(INLINE):], same_peer=False)
return Button.inline(label, data.encode())
return [[button(label, data) for label, data in row] for row in rows]
def without_inline(rows: Sequence[Row]) -> List[Row]:
"""The rows with every Share format button turned into the plain one that explains how to switch inline mode on."""
prefix = INLINE + "share_"
return [[(label, "fm:sh:" + data[len(prefix):] if data.startswith(prefix) else data) for label, data in row] for row in rows]
def nav(back: str = "", *, home: bool = True) -> List[Row]:
row: Row = []
if back:
row.append(("◀ Back", f"m:{back}"))
if home and back != "home":
row.append(("🏠 Home", "m:home"))
row.append(("✖ Close", "x:close"))
return [row]
def _check(label: str, active: bool) -> str:
return f"{label} ✅" if active else label
def _code(value: str, limit: int = 100) -> str:
return f"<code>{esc(clip(value, limit))}</code>" if value else "—"
def fmt_tokens(tokens: Iterable[str]) -> str:
"""``["english", "2"]`` -> ``English, 2``"""
out = []
for t in tokens:
out.append(t if t.isdigit() else (canonical_lang(t) or t).title())
return ", ".join(out) if out else "—"
# ---------------------------------------------------------------------------
# panels
# ---------------------------------------------------------------------------
def _auto_name(auto: Optional[Dict[str, Any]]) -> str:
"""The auto format in use, for a panel; ``auto`` is ``None`` for someone who has none."""
return f"<b>{esc(clip(auto['name'], 30))}</b>" if auto else "none saved (the default is used)"
def _template_of(auto: Optional[Dict[str, Any]]) -> str:
return auto["template"] if auto else DEFAULT_FORMAT["template"]
def home(s: Dict[str, Any], *, owner: bool, auto: Optional[Dict[str, Any]] = None) -> Panel:
"""The main menu. ``auto`` is the auto format in use (``None``: the user has none)."""
channel = f"channel <code>{s['target_channel']}</code>" if s.get("target_channel") else "this chat"
metadata_on = s["metadata_on"] or bool(auto and has_metadata(auto)) # a format with metadata of its own writes it, switch or not
text = (
"<b>🏷 Auto Rename Bot</b>\n\n"
"Send me a file (document, video or audio) and I'll rename it with your auto format and send it back.\n\n"
f"<b>Mode:</b> <code>{s['mode'].upper()}</code> · <b>Type:</b> <code>{s['file_type'].upper()}</code>"
f" · <b>Metadata:</b> <code>{'ON' if metadata_on else 'OFF'}</code>\n"
f"<b>Auto format:</b> {_auto_name(auto)}\n"
f"<b>Template:</b> {_code(_template_of(auto), 120)}\n"
f"<b>Send to:</b> {channel}"
)
rows: List[Row] = [
[("Auto Format", "m:auto"), ("Rename", "m:rename")],
[("Metadata", "m:meta"), ("File Type", "m:ftype")],
[("Formats", "m:formats"), ("Help", "m:help")] + ([("Admin", "m:admin")] if owner else []),
[("✖ Close", "x:close")],
]
return text, rows
def rename(s: Dict[str, Any], auto: Optional[Dict[str, Any]] = None) -> Panel:
words = ", ".join(s["remove_words"]) or "—"
text = (
"<b>RENAME MODE</b>\n\n"
"Filename mode reads details from the file name.\n"
"Caption mode reads details from the message caption.\n\n"
f"Current mode: <code>{s['mode'].upper()}</code>\n"
"Command: /mode\n\n"
f"<b>Template</b> of the auto format {_auto_name(auto)}: {_code(_template_of(auto), 120)}\n"
"Change it in /formats.\n\n"
f"<b>Remove words:</b> {esc(clip(words, 100))}"
)
rows: List[Row] = [
[(_check("Filename", s["mode"] == "filename"), "set:mode:filename"),
(_check("Caption", s["mode"] == "caption"), "set:mode:caption")],
[("Remove words", "in:remove")],
] + nav("home")
return text, rows
def metadata(s: Dict[str, Any]) -> Panel:
m = s["meta"]
on = s["metadata_on"]
lines = [f"<b>🏷 Your default metadata is currently: {'On' if on else 'Off'}</b>", ""]
for label, key in (("Title", "title"), ("Author", "author"), ("Artist", "artist"),
("Audio", "audio"), ("Subtitle", "subtitle"), ("Video", "video")):
lines.append(f"{DOT} <b>{label}</b> ▸ {_code(m.get(key, ''), 60)}")
lines.append(f"{DOT} <b>Keep Audio</b> ▸ {esc(fmt_tokens(s['keep_audio']))}")
lines.append(f"{DOT} <b>Keep Subtitle</b> ▸ {esc(fmt_tokens(s['keep_subtitle']))}")
lines += ["", "Auto renames use it when their format has no metadata of its own. To put it in a format, tap "
"<b>Apply default metadata</b> on the format."]
rows: List[Row] = [
[(_check("On", on), "set:meta:on"), (_check("Off", not on), "set:meta:off")],
[("Title", "in:mt_title"), ("Author", "in:mt_author"), ("Artist", "in:mt_artist")],
[("Audio", "in:mt_audio"), ("Subtitle", "in:mt_subtitle"), ("Video", "in:mt_video")],
[("Tool", "m:tool")],
[("How to Set Metadata", "help:meta")],
] + nav("home")
return "\n".join(lines), rows
def tool(s: Dict[str, Any]) -> Panel:
text = (
"<b>🛠 TOOL — keep tracks</b>\n\n"
"Only the tracks you list are kept, in the order you list them. Everything else is dropped.\n\n"
f"{DOT} <b>Keep Audio</b> ▸ {esc(fmt_tokens(s['keep_audio']))}\n"
f"{DOT} <b>Keep Subtitle</b> ▸ {esc(fmt_tokens(s['keep_subtitle']))}\n\n"
"Use language names (<code>English Japanese</code>) or track numbers (<code>2 1</code>).\n"
"Commands: /keepaudio · /keepsubtitle · /tracks (reply to a file)\n"
"Needs metadata to be On."
)
rows: List[Row] = [
[("Keep Audio", "in:keep_audio"), ("Keep Subtitle", "in:keep_subtitle")],
[("Clear Audio", "clr:keep_audio"), ("Clear Subtitle", "clr:keep_subtitle")],
] + nav("meta")
return text, rows
def metadata_help() -> Panel:
text = (
"<b>How to set metadata</b>\n\n"
"Tap a field, then send the value — or use a command:\n"
"/settitle Tokyo Revengers\n"
"/setauthor @MyChannel\n"
"/setartist @MyChannel\n"
"/setaudio {lang} Audio\n"
"/setsubtitle {lang}\n"
"/setvideo @MyChannel\n\n"
"Send <code>off</code> instead of a value to clear a field. "
"<code>{lang}</code> becomes each track's language name.\n"
"Then switch metadata On. Works for mkv, mp4, m4v, mov and webm (title/artist tags for mp3, m4a, flac, ogg, opus)."
)
return text, nav("meta")
def filetype(s: Dict[str, Any]) -> Panel:
cur = s["file_type"]
text = (
"<b>FILE TYPE</b>\n\n"
"How renamed files are sent:\n"
"• <b>Same as original</b> — video stays video, audio stays audio, a document stays a document\n"
"• <b>Document / Video / Audio</b> — always send it that way\n\n"
"The file extension always stays the same.\n\n"
f"Current: <code>{cur.upper()}</code>\nCommand: /filetype"
)
rows: List[Row] = [
[(_check("Same as original", cur == "same"), "set:ftype:same"),
(_check("Document", cur == "document"), "set:ftype:document")],
[(_check("Video", cur == "video"), "set:ftype:video"), (_check("Audio", cur == "audio"), "set:ftype:audio")],
] + nav("home")
return text, rows
def help_panel() -> Panel:
text = (
"<b>How to use</b>\n\n"
"1. Send a file — I rename it with your <b>auto format</b> (pick it in the /start menu) and send it back.\n"
"2. /rename — one file with a <b>manual format</b>: pick one, then send the file.\n"
"3. /formats — 12 auto and 12 manual formats. A format holds the template, caption, thumbnail and metadata: edit, copy between kinds, share.\n"
"4. /mode — read details from the file name or the caption.\n"
"5. /metadata, /keepaudio, /keepsubtitle — default metadata and tracks (formats can copy it).\n"
"6. /filetype — send as document, video or audio.\n"
"7. /sequence then /sort — put files in episode order.\n"
"8. /check, /mediainfo, /ethumb, /speed — inspect files.\n"
"9. /size and /resize (reply to a picture) — its dimensions, or a copy at an exact size.\n"
"10. /queue and /cancel — manage jobs.\n\n"
f"Template variables: {TEMPLATE_VARIABLES}\n"
"A [bracket] group is dropped when a variable inside it is empty."
)
return text, nav("home")
def admin(owner_id: int, admins: Sequence[int], running: int, waiting: int, log_channel: Any) -> Panel:
shown = ", ".join(f"<code>{a}</code>" for a in list(admins)[:10]) or "none"
more = f" (+{len(admins) - 10} more)" if len(admins) > 10 else ""
text = (
"<b>ADMIN</b>\n\n"
f"<b>Owner:</b> <code>{owner_id}</code>\n"
f"<b>Allowed users:</b> {shown}{more}\n"
f"<b>Queue:</b> {running} running · {waiting} waiting\n"
f"<b>Log channel:</b> {('<code>%s</code>' % log_channel) if log_channel else 'not set (LOG_CHANNEL)'}\n\n"
"Commands: /addadmin <id> · /deladmin <id> · /admins · /smedia"
)
rows: List[Row] = [
[("📊 Stats", "adm:stats"), ("🟢 Status", "adm:status")],
[("🛑 Cancel all jobs", "adm:clear")],
] + nav("home")
return text, rows
# ---------------------------------------------------------------------------
# saved formats
# ---------------------------------------------------------------------------
FORMAT_FIELD_LABELS = (
("title", "Title"), ("author", "Author"), ("artist", "Artist"),
("audio", "Audio"), ("subtitle", "Subtitle"), ("video", "Video"),
)
def _format_buttons(formats: Sequence[Dict[str, Any]], prefix: str, active_id: int = 0) -> List[Row]:
"""One button per format, two to a row; the one in use (``active_id``) is ticked."""
rows: List[Row] = []
row: Row = []
for f in formats:
row.append((_check(clip(f["name"], 24), f["id"] == active_id), f"{prefix}:{f['id']}"))
if len(row) == 2:
rows.append(row)
row = []
if row:
rows.append(row)
return rows
def formats_home(auto_count: int, manual_count: int, limit: int) -> Panel:
"""/formats: the two kinds of format, to choose from."""
text = (
"<b>🗂 FORMATS</b>\n\n"
"A format saves its own template, caption, thumbnail, metadata titles and Keep Audio / Keep Subtitle lists "
f"under a name, like <i>One Piece</i>. You can keep {limit} of each kind:\n\n"
"🤖 <b>Auto rename</b> — renames every file you send me. Choose the one in use in the /start menu.\n"
"✋ <b>Manual rename</b> — you pick one with /rename, for a single file.\n\n"
"A format can be copied from one kind to the other."
)
rows: List[Row] = [[(f"🤖 Auto rename ({auto_count}/{limit})", "fm:l:auto"), (f"✋ Manual rename ({manual_count}/{limit})", "fm:l:manual")]]
return text, rows + nav("home")
def formats_menu(formats: Sequence[Dict[str, Any]], limit: int, kind: str = "manual", active_id: int = 0) -> Panel:
"""The formats of one kind (``active_id``: the auto format in use)."""
if kind == "auto":
intro = "These rename every file you send me. The one marked ✅ is in use: tap a format to view or edit it."
else:
intro = "You pick one of these with /rename, for a single file. Tap a format to view or edit it."
text = f"<b>{KIND_ICONS[kind]} {KIND_LABELS[kind].upper()} FORMATS</b> ({len(formats)}/{limit})\n\n{intro}"
if not formats:
text += f"\n\nYou have no {kind} formats yet."
rows = _format_buttons(formats, "fm:v", active_id)
if len(formats) < limit:
rows.append([("➕ New format", f"fm:new:{kind}")])
else:
text += f"\n\nYou have all {limit} {kind} formats. Delete one to add another."
return text, rows + nav("formats")
def auto_menu(formats: Sequence[Dict[str, Any]], active_id: int, limit: int) -> Panel:
"""The /start menu's Auto Format screen: choose the auto format that renames your files."""
text = (
"<b>🤖 AUTO RENAME FORMAT</b>\n\n"
"Every file you send me is renamed with the format marked ✅. Tap another one to use it instead.\n"
"For a single file with a manual format, use /rename."
)
if not formats:
text += (
"\n\nYou have no auto formats yet, so the default template is used. Create one to set your own template, "
"caption, thumbnail and metadata."
)
rows = _format_buttons(formats, "au", active_id)
if active_id:
rows.append([("✏️ Edit the one in use", f"fm:v:{active_id}")])
last: Row = [("⚙️ Manage", "fm:l:auto")]
if len(formats) < limit:
last.append(("➕ New format", "fm:new:auto"))
return text, rows + [last] + nav("home")
def _custom_caption(fmt: Dict[str, Any]) -> bool:
"""Has the format a caption of its own? Otherwise it sends the new file name in bold."""
return fmt["caption"] != DEFAULT_CAPTION
THUMB_NONE = "— (a frame of the video is used)"
def _format_lines(fmt: Dict[str, Any], *, thumb_yes: str, thumb_no: str) -> List[str]:
"""What a format holds, one line per field."""
m = fmt["meta"]
lines = [
f"{DOT} <b>Template</b> ▸ {_code(fmt['template'], 60)}",
f"{DOT} <b>Caption</b> ▸ {richtext.preview(fmt['caption'], 60)}" + ("" if _custom_caption(fmt) else " · default"),
f"{DOT} <b>Thumbnail</b> ▸ " + (thumb_yes if fmt.get("has_thumb") else thumb_no),
]
for key, label in FORMAT_FIELD_LABELS:
lines.append(f"{DOT} <b>{label}</b> ▸ {_code(m.get(key, ''), 50)}")
lines.append(f"{DOT} <b>Keep Audio</b> ▸ {esc(clip(fmt_tokens(fmt['keep_audio']), 50))}")
lines.append(f"{DOT} <b>Keep Subtitle</b> ▸ {esc(clip(fmt_tokens(fmt['keep_subtitle']), 50))}")
return lines
def format_view(fmt: Dict[str, Any], *, share_inline: bool = False, active: bool = False, upload_on: bool = False) -> Panel:
"""A format's screen. ``active``: it is the auto format in use. ``share_inline``: the Share format button opens the chat
picker straight away; that only works while inline mode is on for the bot (Telegram refuses the message otherwise), so
without it the button explains how. ``upload_on``: uploading to the Database Channel is set up, so a manual format shows
its Channel option."""
kind = fmt.get("kind", "manual")
has_channel_option = upload_on and kind == "manual"
lines = [f"<b>🗂 {esc(clip(fmt['name'], 30))}</b> · {KIND_ICONS[kind]} {KIND_LABELS[kind]}", ""]
lines += _format_lines(fmt, thumb_yes="✅ saved", thumb_no=THUMB_NONE)
if has_channel_option:
lines.append(f"{DOT} <b>Channel</b> ▸ " + (_code(str(fmt["channel"])) if fmt.get("channel") else "— (optional)"))
if kind == "manual":
lines += ["", "Use it with /rename. Mode, file type and the rest come from your normal settings."]
elif active:
lines += ["", "✅ In use: every file you send is renamed with it."]
else:
lines += ["", "Not in use. Tap Use for auto rename to rename your files with it."]
if kind == "auto" and not has_metadata(fmt):
lines.append("No metadata of its own: your default metadata is used while it is On.")
fid = fmt["id"]
thumb_row: Row = [("Thumbnail", f"fm:th:{fid}")]
if fmt.get("has_thumb"):
thumb_row.append(("🗑 Delete Thumb", f"fm:thd:{fid}"))
rows: List[Row] = [
[("Name", f"fm:in:{fid}:name"), ("Template", f"fm:in:{fid}:template"), ("Caption", f"fm:in:{fid}:caption")],
thumb_row,
]
if _custom_caption(fmt):
rows.append([("↩ Reset Caption", f"fm:cr:{fid}")])
rows += [
[("Title", f"fm:in:{fid}:title"), ("Author", f"fm:in:{fid}:author"), ("Artist", f"fm:in:{fid}:artist")],
[("Audio", f"fm:in:{fid}:audio"), ("Subtitle", f"fm:in:{fid}:subtitle"), ("Video", f"fm:in:{fid}:video")],
[("Keep Audio", f"fm:in:{fid}:keep_audio"), ("Keep Subtitle", f"fm:in:{fid}:keep_subtitle")],
]
if has_channel_option:
channel_row: Row = [("Channel", f"fm:in:{fid}:channel")]
if fmt.get("channel"):
channel_row.append(("🗑 Clear Channel", f"fm:chc:{fid}"))
rows.append(channel_row)
rows += [
[("🏷 Apply default metadata", f"fm:dm:{fid}")],
[(f"📋 Copy to {other_kind(kind)} formats", f"fm:cp:{fid}")],
]
if kind == "auto" and not active:
rows.append([("✅ Use for auto rename", f"fm:use:{fid}")])
rows += [
[("📤 Share format", INLINE + share_query(fid) if share_inline else f"fm:sh:{fid}")],
[("🗑 Delete format", f"fm:del:{fid}")],
[("◀ Back", f"fm:l:{kind}"), ("🏠 Home", "m:home"), ("✖ Close", "x:close")],
]
return "\n".join(lines), rows
def format_copied(source: Dict[str, Any], copy: Dict[str, Any]) -> Panel:
"""What copying a format to the other kind shows."""
kind = copy["kind"]
text = (
f"✅ <b>{esc(clip(source['name'], 30))}</b> is copied to your {kind} formats as <b>{esc(clip(copy['name'], 30))}</b>.\n"
"The copy is a format of its own: change it without touching the original."
)
rows: List[Row] = [
[("📂 Open the copy", f"fm:v:{copy['id']}")],
[("◀ Back to the original", f"fm:v:{source['id']}")],
[("✖ Close", "x:close")],
]
return text, rows
def format_metadata_applied(fmt: Dict[str, Any]) -> str:
"""The note shown above a format after "Apply default metadata"."""
return f"✅ Your default metadata is now in <b>{esc(clip(fmt['name'], 30))}</b>."
INLINE_OFF = "Sharing needs inline mode. In @BotFather: /mybots → this bot → Bot Settings → Inline Mode → Turn on. Then tap Share format again."
SHARE_GONE = "⚠️ <b>That shared format isn't available anymore.</b>\nThe person who shared it deleted it. Ask them to share it again."
def _a(kind: str) -> str:
"""``An auto`` / ``A manual``"""
return f"{'An' if kind == 'auto' else 'A'} {kind}"
def share_card(fmt: Dict[str, Any]) -> Tuple[str, str, str]:
"""``(title, description, html text)`` of the card that is sent to the chat the owner picked."""
name = clip(fmt["name"], 30)
kind = fmt.get("kind", "manual")
return (
f"🗂 {name}",
"Tap to send this format to the chat",
f"📦 <b>{esc(name)}</b>\n{_a(kind)} rename format for this bot. Open it to see what's in it and add it to your own formats.",
)
def shared_format(fmt: Dict[str, Any], token: str, *, mine: bool = False) -> Panel:
"""What opening a shared format shows: what is in it, and a button to add it to your own formats."""
kind = fmt.get("kind", "manual")
lines = [
f"<b>📦 {esc(clip(fmt['name'], 30))}</b> · {KIND_ICONS[kind]} {KIND_LABELS[kind]}",
"This is your own format." if mine else f"{_a(kind)} rename format shared with you.",
"",
]
lines += _format_lines(fmt, thumb_yes="✅ included", thumb_no=THUMB_NONE)
use = "/rename" if kind == "manual" else "auto rename"
if mine:
lines += ["", "Whoever you send it to can look at it here and add a copy to their own formats."]
rows: List[Row] = [[("🗂 Open format", f"fm:v:{fmt['id']}")]]
else:
lines += ["", f"Add it to your {kind} formats to use it for {use}. You can change anything in your copy afterwards."]
rows = [[("➕ Add to my formats", f"fs:add:{token}")]]
return "\n".join(lines), rows + [[("✖ Close", "x:close")]]
def shared_added(fmt: Dict[str, Any]) -> Panel:
kind = fmt.get("kind", "manual")
use = "Use it with /rename" if kind == "manual" else "Choose it in the /start menu to rename your files with it"
text = f"✅ <b>{esc(clip(fmt['name'], 30))}</b> is now in your {kind} formats.\n{use}, or open it to change anything."
return text, [[("🗂 Open format", f"fm:v:{fmt['id']}")], [("✖ Close", "x:close")]]
def format_delete_confirm(fmt: Dict[str, Any]) -> Panel:
text = f"<b>Delete the format “{esc(clip(fmt['name'], 30))}”?</b>\n\nIts template, caption, thumbnail and metadata are removed. This can't be undone."
return text, [[("🗑 Yes, delete", f"fm:ok:{fmt['id']}"), ("◀ Keep it", f"fm:v:{fmt['id']}")]]
def rename_picker(formats: Sequence[Dict[str, Any]]) -> Panel:
"""/rename: the manual formats (an auto format is chosen in the /start menu)."""
if not formats:
text = (
"<b>🏷 MANUAL RENAME</b>\n\nYou have no manual formats yet.\n"
"A format keeps a template, caption, thumbnail and metadata under a name, like <i>One Piece</i>. Create one, then come back here.\n"
"Files you just send me are renamed with your auto format."
)
return text, [[("➕ Create a format", "fm:new:manual")], [("✖ Close", "x:close")]]
text = "<b>🏷 MANUAL RENAME</b>\n\nChoose the manual format to rename with. I'll ask for your file next."
rows = _format_buttons(formats, "rn:p") + [[("⚙️ Manage formats", "fm:l:manual"), ("✖ Close", "x:close")]]
return text, rows
def rename_ready(fmt: Dict[str, Any], minutes: int) -> Panel:
text = (
f"✅ <b>{esc(clip(fmt['name'], 30))}</b> selected.\n\n"
"Now send me the file to rename. I'll apply this format to it and send it back.\n"
"It works for one file at a time: for another file, send /rename again.\n"
f"I'll wait {minutes} minutes. /cancel to stop."
)
return text, [[("🔄 Change format", "rn:menu"), ("⏹ Stop", "rn:stop")]]
def _detail_lines(info: Any) -> str:
"""Title, Season, Episode and Quality always (``None`` when not found); Year and Codec only when found."""
fields = [("Title", info.title), ("Season", info.season), ("Episode", info.episode), ("Quality", info.quality)]
fields += [(label, value) for label, value in (("Year", info.year), ("Codec", info.codec)) if value]
return "\n".join(f"➦ {label} ⇛ <code>{esc(clip(value, 60)) if value else 'None'}</code>" for label, value in fields)
def check_text(mode: str, name_info: Any, caption_info: Any, new_name: str) -> str:
"""What /check shows: the details read from the file name and from the caption, and the name they give."""
return (
"<b>Yᴏᴜ Cᴀɴ Cʜᴀɴɢᴇ Mᴏᴅᴇ Bʏ</b> /mode\n\n"
f"<b>Rᴇɴᴀᴍɪɴɢ Mᴏᴅᴇ:</b> <code>{esc(mode.upper())}</code>\n\n"
f"<b>Fɪʟᴇɴᴀᴍᴇ:</b>\n{_detail_lines(name_info)}\n\n"
f"<b>Cᴀᴘᴛɪᴏɴ Mᴏᴅᴇ:</b>\n{_detail_lines(caption_info)}\n\n"
f"<b>Nᴇᴡ Nᴀᴍᴇ:</b>\n<code>{esc(clip(new_name, 150))}</code>"
)
def ethumb_prompt(minutes: int) -> Panel:
text = (
"<b>🖼 EXTRACT THUMBNAIL</b>\n\n"
"Send me a file or video and I'll send back its thumbnail.\n"
"It works for one file at a time: for another file, send /ethumb again.\n"
f"I'll wait {minutes} minutes. /cancel to stop."
)
return text, [[("⏹ Stop", "et:stop")]]
def smedia_menu(current: str) -> Panel:
"""/smedia: ``current`` is what /start shows above the menu now: ``image``, ``gif`` or ``""`` (nothing)."""
shown = {"image": "an image", "gif": "a GIF"}.get(current, "nothing, just the menu")
text = (
"<b>🖼 START MEDIA</b>\n\n"
f"/start shows: <b>{shown}</b>\n\n"
"Choose what goes above the menu. I keep a copy of it, so it survives restarts."
)
rows: List[Row] = [[("🖼 Image", "sm:image"), ("🎞 GIF", "sm:gif")]]
if current:
rows.append([("🗑 Remove", "sm:rm")])
return text, rows + [[("✖ Close", "x:close")]]
def smedia_prompt(kind: str, minutes: int) -> Panel:
what = "the image (a photo, or an image sent as a file)" if kind == "image" else "the GIF (a Telegram GIF or animation)"
text = (
f"<b>{'🖼' if kind == 'image' else '🎞'} Send {what}.</b>\n\n"
"I'll set it as the /start media and show you the result.\n"
f"I'll wait {minutes} minutes. /cancel to stop."
)
return text, [[("◀ Back", "sm:menu"), ("⏹ Stop", "sm:stop")]]
def sequence_intro(limit: int) -> Panel:
text = (
"<b>📑 SEQUENCE</b>\n\n"
f"Send me the files to put in order (up to {limit}), in any order. I only collect them for now.\n"
"When they're all here, send /sort: I'll send them back in episode order, then list the missing and repeated episodes.\n"
"The episode is read from the file name, or from the caption when your /mode is Caption.\n"
"/cancel to throw them away."
)
return text, [[("✖ Cancel", "sq:cx")]]
def sequence_status(count: int, limit: int) -> Panel:
text = f"📥 <b>{count} file{'s' if count != 1 else ''} collected</b>"
if count >= limit:
text += f"\n\nThat's the most I can sort at once ({limit}). Send /sort now."
else:
text += "\n\nSend more files, or /sort when you're done."
return text, [[("🔢 Sort now", "sq:sort"), ("🗑 Discard", "sq:cx")]]
# ---------------------------------------------------------------------------
# input prompts
# ---------------------------------------------------------------------------
PROMPTS: Dict[str, str] = {
"remove": (
"<b>Send the words to strip from names.</b>\n\n"
"Separate them with commas, e.g. <code>@SomeChannel, [Group], www.site.com</code>\n"
"Send <code>off</code> to clear the list."
),
"mt_title": "<b>Send the Title</b> to write into the file's metadata (or <code>off</code> to clear).",
"mt_author": "<b>Send the Author</b> to write into the file's metadata (or <code>off</code> to clear).",
"mt_artist": "<b>Send the Artist</b> to write into the file's metadata (or <code>off</code> to clear).",
"mt_audio": "<b>Send the title for audio tracks</b> (or <code>off</code> to clear).\n<code>{lang}</code> becomes each track's language, e.g. <code>{lang} Audio</code>.",
"mt_subtitle": "<b>Send the title for subtitle tracks</b> (or <code>off</code> to clear).\n<code>{lang}</code> becomes each track's language.",
"mt_video": "<b>Send the title for the video track</b> (or <code>off</code> to clear).",
"keep_audio": (
"<b>Which audio tracks should I keep?</b>\n\n"
"Send language names (<code>English Japanese</code>) or track numbers (<code>2 1</code>). "
"The order you give is the output order; all other tracks are dropped.\n"
"Send <code>off</code> to keep everything."
),
"keep_subtitle": (
"<b>Which subtitle tracks should I keep?</b>\n\n"
"Send language names (<code>English</code>) or track numbers (<code>2</code>). "
"The order you give is the output order; all other tracks are dropped.\n"
"Send <code>off</code> to keep everything."
),
}
def input_prompt(kind: str, back: str) -> Panel:
return PROMPTS[kind], [[("✖ Cancel", f"in:cancel:{back}")]]
FORMAT_PROMPTS: Dict[str, str] = {
"new": "<b>Send a name for the new format</b>, like <code>One Piece</code> or <code>Dragon Ball</code> (30 characters max).",
"name": "<b>Send the new name</b> for this format (30 characters max).",
"thumb": "<b>Send a photo</b> to use as this format's thumbnail.",
"template": (
"<b>Send the new filename template.</b>\n\n"
f"Variables: {TEMPLATE_VARIABLES}\n"
f"{EPISODE_ZEROS}\n"
"A [bracket] or (paren) group disappears when a variable inside it is empty.\n"
"If only an episode number is found, the season defaults to 01.\n\n"
"Example: <code>[S{season}E{episode}] {title} [{quality}]</code>"
),
"caption": (
"<b>Send the caption for this format.</b>\n\n"
f"Variables: {CAPTION_VARIABLES}\n"
"Format it the way you do in any message (<b>bold</b>, <i>italic</i>, links…) and I'll keep it exactly like that. "
"No HTML needed.\n"
"Send <code>reset</code> to go back to the default: the new file name in bold."
),
"title": PROMPTS["mt_title"], "author": PROMPTS["mt_author"], "artist": PROMPTS["mt_artist"],
"audio": PROMPTS["mt_audio"], "subtitle": PROMPTS["mt_subtitle"], "video": PROMPTS["mt_video"],
"keep_audio": PROMPTS["keep_audio"], "keep_subtitle": PROMPTS["keep_subtitle"],
"channel": (
"<b>Send the id of the channel</b> this format's episodes are published in, like <code>-1001234567890</code>.\n\n"
"After /rename I can upload the episode to the Database Channel; the channel post bot then posts it in this channel.\n"
"It is optional: send <code>off</code> to clear it (the episode is then only uploaded, nothing is posted)."
),
}
def format_prompt(field: str, fid: int, kind: str = "") -> Panel:
"""The "send me the value" screen for one field of a format (``fid`` 0 = a format of ``kind`` that is being created)."""
cancel = f"fm:cx:{fid}" if fid else (f"fm:l:{kind}" if kind else "m:formats")
return FORMAT_PROMPTS[field], [[("✖ Cancel", cancel)]]
def known_languages() -> str:
return ", ".join(sorted(LANGUAGES))
|