File size: 48,874 Bytes
76c3f6a 4317fe2 76c3f6a 426dafa 76c3f6a 282768c 84b1741 76c3f6a 84b1741 76c3f6a 84b1741 76c3f6a 84b1741 76c3f6a 426dafa 76c3f6a 5269f17 76c3f6a 3dde004 4317fe2 3dde004 76c3f6a 3dde004 76c3f6a 91e9031 76c3f6a 91e9031 76c3f6a 426dafa 76c3f6a e6acdcd 76c3f6a 4317fe2 76c3f6a 4317fe2 76c3f6a 91e9031 76c3f6a 426dafa 76c3f6a baccf9f 76c3f6a baccf9f 76c3f6a baccf9f 76c3f6a baccf9f 0efacc5 baccf9f 76c3f6a 91e9031 76c3f6a 91e9031 76c3f6a | 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 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 | import asyncio
from config import Config
from plugins.file_rename import process_thumbnail
from utils.ffmpeg import get_video_resolution, strip_and_mux_audio, get_media_info
from helper.database import codeflixbots as db
import os
import shutil
from utils.sub_watermark import extract_and_watermark_subs
import time
import re
import html
from urllib.parse import unquote
from pyrogram import enums
from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton
SEASON_EPISODE_PATTERNS = [
(re.compile(r'S(\d+)(?:E|EP)(\d+)'), ('season', 'episode')),
(re.compile(r'S(\d+)[\s-]*(?:E|EP)(\d+)'), ('season', 'episode')),
(re.compile(r'Season\s*(\d+)\s*Episode\s*(\d+)', re.IGNORECASE), ('season', 'episode')),
(re.compile(r'\[S(\d+)\]\[E(\d+)\]'), ('season', 'episode')),
(re.compile(r'S(\d+)[^\d]*(\d+)'), ('season', 'episode')),
(re.compile(r'(?:E|EP|Episode)\s*(\d+)', re.IGNORECASE), (None, 'episode')),
(re.compile(r'\b(\d+)\b'), (None, 'episode'))
]
def extract_season_episode(filename):
for pattern, (season_group, episode_group) in SEASON_EPISODE_PATTERNS:
match = pattern.search(filename)
if match:
season = match.group(1) if season_group else None
episode = match.group(2 if season_group else 1)
return season, episode
return None, None
def extract_quality_from_filename(filename):
match = re.search(r'(360p|480p|720p|1080p|1440p|2160p|4k)', filename, re.IGNORECASE)
if match:
return match.group(1).lower()
return "-"
def humanbytes(size):
if not size:
return "0 B"
power = 2**10
n = 0
Dic_powerN = {0: 'B', 1: 'KB', 2: 'MB', 3: 'GB', 4: 'TB'}
while size > power and n < 4:
size /= power
n += 1
return f"{round(size, 2)} {Dic_powerN[n]}"
process_queue = asyncio.Queue()
USER_CANCELLATIONS = set()
async def worker(client):
while True:
job = await process_queue.get()
user_id = job.get('user_id')
if user_id in USER_CANCELLATIONS:
print(f"Skipping job for cancelled user {user_id}")
try:
await client.send_message(job['chat_id'], f"๐ **Queued task skipped (Cancelled).**")
await client.delete_messages(job['chat_id'], job['status_msg_id'])
except Exception:
pass
process_queue.task_done()
continue
try:
await process_job(client, job)
except Exception as e:
print(f"Error processing job: {e}")
finally:
process_queue.task_done()
def get_progress_text(ep, state, anime, s_and_e, quality, filename):
return (
f"**[ Episode : {ep} ]**\n"
f"**state** - {state}\n"
f"**anime name** - {anime}\n"
f"**season and episode** - {s_and_e}\n"
f"**quality** - {quality}\n"
f"**Filename** - `{filename}`"
)
def sort_key(res_str):
if "480" in res_str: return 1
if "720" in res_str: return 2
if "1080" in res_str: return 3
if "4k" in res_str.lower(): return 4
return 10
def safe_format(template, **kwargs):
temp_tpl = template
# Force 02d formatting specifiers for season and episode placeholders
temp_tpl = re.sub(r'\{season(?::[^}]+)?\}', '{season:02d}', temp_tpl)
temp_tpl = re.sub(r'\{episode(?::[^}]+)?\}', '{episode:02d}', temp_tpl)
formatted_kwargs = dict(kwargs)
try:
if 'season' in formatted_kwargs:
formatted_kwargs['season'] = int(str(formatted_kwargs['season']).strip())
except Exception:
pass
try:
if 'episode' in formatted_kwargs:
formatted_kwargs['episode'] = int(str(formatted_kwargs['episode']).strip())
except Exception:
pass
try:
return temp_tpl.format(**formatted_kwargs)
except Exception:
# Fallback to simple replace
res = template
for k, v in kwargs.items():
val_str = str(v)
if k in ['season', 'episode']:
try:
if int(v) < 10:
val_str = f"{int(v):02d}"
except Exception:
pass
res = re.sub(r'\{' + k + r'(?::[^}]+)?\}', val_str, res)
return res
async def apply_metadata(input_path, output_path, user_id, client=None):
settings = await db.get_user_settings(user_id)
metadata_on = settings.get("metadata", "Off") != "Off"
metadata_txt = settings.get("metadata_txt", "") if metadata_on else ""
# FETCH PROPERLY:
audio_order = await db.get_audio_order(user_id)
default_audio = await db.get_audio_default(user_id)
keep_audio = await db.get_keep_audio(user_id)
sub_watermark = settings.get("sub_watermark")
# Fast copy if no processing is needed
if not (metadata_txt or audio_order or keep_audio or default_audio is not None or sub_watermark):
shutil.copy2(input_path, output_path)
return True
cmd_args = ["ffmpeg", "-y", "-i", input_path]
# 1. Handle Subtitle Watermark Extractor
watermarked_subs = []
if sub_watermark:
temp_dir = os.path.dirname(input_path)
watermarked_subs = await extract_and_watermark_subs(input_path, sub_watermark, temp_dir)
# DEBUG FALLBACK: Tell the user if it mysteriously failed returning None
if not watermarked_subs or not any(watermarked_subs):
if client:
try:
await client.send_message(user_id, f"โ ๏ธ Subtitle Debug: Extractor returned `{watermarked_subs}`. Subtitle mapping aborted.")
except Exception:
pass
for sub_file in watermarked_subs:
if sub_file:
cmd_args.extend(["-i", sub_file])
# 2. Assign Metadata Text Configs
if metadata_txt:
cmd_args.extend(["-metadata", f"title={metadata_txt}"])
cmd_args.extend(["-metadata", f"artist={metadata_txt}"])
cmd_args.extend(["-metadata", f"author={metadata_txt}"])
cmd_args.extend(["-metadata:s:v", f"title={metadata_txt}"])
cmd_args.extend(["-metadata:s:a", f"title={metadata_txt}"])
cmd_args.extend(["-metadata:s:s", f"title={metadata_txt}"])
# 3. Handle Map streams (Audio Reorder)
cmd_args.extend(["-map", "0:v"]) # Primary video
if keep_audio:
from utils.audio_tracks import resolve_keep_audio
kept_tracks = await resolve_keep_audio(input_path, keep_audio)
for track in kept_tracks:
cmd_args.extend(["-map", f"0:a:{track}?"])
elif audio_order:
try:
from utils.audio_tracks import probe_audio_streams
order = [part.strip() for part in str(audio_order).split(",") if part.strip()]
streams = await probe_audio_streams(input_path)
seen = set()
for at in order:
if at.isdigit() and int(at) < len(streams) and at not in seen:
cmd_args.extend(["-map", f"0:a:{at}?"])
seen.add(at)
for idx in range(len(streams)):
if str(idx) not in seen:
cmd_args.extend(["-map", f"0:a:{idx}?"])
except Exception:
cmd_args.extend(["-map", "0:a?"])
else:
cmd_args.extend(["-map", "0:a?"]) # Map all audios
# Inject Custom Watermarked Subs or Standard Subs
if watermarked_subs and any(watermarked_subs):
input_idx = 1
for sf in watermarked_subs:
if sf:
cmd_args.extend(["-map", f"{input_idx}:0?"])
input_idx += 1
else:
cmd_args.extend(["-map", "0:s?"]) # standard map
cmd_args.extend(["-map", "0:t?"]) # Fonts & Attachments
# 4. Enforce Selected Default Audio Display disposition
if default_audio is not None:
cmd_args.extend(["-disposition:a", "0"]) # Turn off all audios default
try:
cmd_args.extend([f"-disposition:a:{str(default_audio).strip()}", "default"])
except Exception:
pass
cmd_args.extend(["-c", "copy", output_path])
process = await asyncio.create_subprocess_exec(
*cmd_args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
while True:
try:
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=2.0)
break
except asyncio.TimeoutError:
if user_id in USER_CANCELLATIONS:
try:
process.kill()
except Exception:
pass
raise RuntimeError("TaskCancelledByUser")
# Subtitle Cleanup
for sf in watermarked_subs:
if sf and os.path.exists(sf):
try:
os.remove(sf)
except Exception:
pass
return process.returncode == 0
async def process_job(client, job):
user_id = job['user_id']
status_msg = job['status_msg_id']
chat_id = job['chat_id']
settings = await db.get_user_settings(user_id)
# 1. Fetch first video details context for filename extraction
original_name = "Unknown.mkv"
try:
first_vm = job['video_msgs'][0]
if "url" in first_vm:
url = first_vm["url"]
try:
from plugins.file_rename import resolve_remote_filename
resolved = await resolve_remote_filename(url, fallback="Unknown.mkv")
if resolved:
original_name = resolved
print(f"Resolved remote filename: {original_name}")
except Exception as e:
print(f"Could not resolve remote filename: {e}")
else:
first_v_msg = await client.get_messages(first_vm['chat_id'], first_vm['msg_id'])
original_name = getattr(first_v_msg.video or first_v_msg.document, "file_name", "Unknown.mkv")
except Exception as e:
print(f"Error fetching first video message profile: {e}")
# 2. Extract season and episode dynamically
season_ext, episode_ext = extract_season_episode(original_name)
if season_ext:
job['season'] = season_ext
if episode_ext:
job['episode'] = episode_ext
for key in ['season', 'episode']:
val = job.get(key, '01')
try:
if int(val) < 10:
job[key] = f"{int(val):02d}"
else:
job[key] = str(int(val))
except (ValueError, TypeError):
pass
# 3. Channel resolution based on auto routing settings
auto_routing = settings.get('auto_channel_match', 'on') == 'on'
target_chat = job.get('target_channel_id')
channel = None
if auto_routing:
# If not set in the job already, try auto matching
channel = await db.match_channel(user_id, original_name)
if channel and not target_chat:
target_chat = channel['channel_id']
if not target_chat:
target_chat = chat_id
if isinstance(target_chat, str) and (target_chat.startswith("-") or target_chat.isdigit()):
try:
target_chat = int(target_chat)
except ValueError:
pass
# 4. Pre-post "Episode : {episode_number}" to target chat
try:
await client.send_message(target_chat, f"**Episode : {job.get('episode', '01')}**")
except Exception as e:
print(f"Error pre-posting episode header to chat: {e}")
# Create the progress status message DIRECTLY inside the target channel!
status_msg_node = None
try:
status_msg_node = await client.send_message(target_chat, text="โณ Starting process...")
except Exception as e:
print(f"Error sending progress message to channel: {e}")
status_msg_node_id = status_msg_node.id if status_msg_node else None
ep = str(job.get('episode', '?')).zfill(2)
s = str(job.get('season', '?')).zfill(2)
# Dynamic anime name guessing
anime = job.get('anime_name')
if not anime:
if channel:
anime = channel.get('title', 'Unknown')
else:
try:
parts = original_name.split(' - ')
if len(parts) > 1:
s_first, ep_first = extract_season_episode(parts[0])
if s_first or ep_first:
anime = parts[1].split('[')[0].strip()
else:
anime = parts[0].split('[')[0].strip()
else:
anime = original_name.split('.')[0].strip()
except Exception:
anime = "Unknown"
s_e = f"S{s}E{ep}"
async def update_state(state_header, progress_bar="", quality="-", filename="-"):
disp_filename = filename
esc_header = html.escape(str(state_header))
esc_anime = html.escape(str(anime))
esc_quality = html.escape(str(quality))
esc_ep = html.escape(str(ep))
esc_filename = html.escape(str(disp_filename))
text = (
f"<blockquote>{esc_header}</blockquote>\n\n"
f"<blockquote>๐ฌ <b>๐๐ป๐ถ๐บ๐ฒ :</b> {esc_anime}\n"
f"๐ฆ <b>๐ค๐๐ฎ๐น๐ถ๐๐ :</b> {esc_quality}\n"
f"๐ข <b>๐๐ฝ๐ถ๐๐ผ๐ฑ๐ฒ :</b> {esc_ep}\n"
f"๐ <b>๐๐ถ๐น๐ฒ :</b> <code>{esc_filename}</code>"
)
if progress_bar:
esc_progress = html.escape(str(progress_bar))
text += f"\n\n<b>{esc_progress}</b>"
text += "</blockquote>"
if status_msg_node_id:
try:
await client.edit_message_text(target_chat, status_msg_node_id, text, parse_mode=enums.ParseMode.HTML)
return
except Exception:
pass
# Fallback to PM if channel edit fails
try:
await client.edit_message_text(chat_id, status_msg, text, parse_mode=enums.ParseMode.HTML)
except Exception:
pass
# Process start
loop = asyncio.get_running_loop()
dl_start = [0]
last_dl_update = [0]
def dl_progress_cb(current, total, file_label="", quality_label="-"):
if job.get('user_id') in USER_CANCELLATIONS:
raise RuntimeError("TaskCancelledByUser")
now = time.time()
if dl_start[0] == 0:
dl_start[0] = now
if now - last_dl_update[0] < 8 and current < total:
return
last_dl_update[0] = now
percent = (current / total) * 100 if total else 0
filled = int(percent / 10)
bar = "โ " * filled + "โก" * (10 - filled)
elapsed = now - dl_start[0]
speed = current / elapsed if elapsed > 0 else 0
eta = ""
if speed > 0:
eta_seconds = (total - current) / speed
eta = f" | ETA: {int(eta_seconds)}s"
speed_text = f"{humanbytes(speed)}/s" if speed else "โ/s"
progress_bar = f"[{bar}] {percent:.1f}% ({humanbytes(current)} of {humanbytes(total)} @ {speed_text}{eta})"
asyncio.run_coroutine_threadsafe(
update_state("๐ฅ ๐๐ผ๐๐ป๐น๐ผ๐ฎ๐ฑ๐ถ๐ป๐ด ๐๐ป๐ถ๐บ๐ฒ ๐๐ถ๐น๐ฒ...", progress_bar, quality_label, file_label),
loop
)
await update_state("โณ Starting resource downloads...")
# 2. Fetch video messages sequentially by message ID (preserving chronological forwarding order)
resolved_videos = []
in_order_msgs = sorted(job['video_msgs'], key=lambda x: x['msg_id'])
for vm in in_order_msgs:
try:
v_msg = await client.get_messages(vm['chat_id'], vm['msg_id'])
v_orig_name = getattr(v_msg.video or v_msg.document, "file_name", "")
q = extract_quality_from_filename(v_orig_name)
except Exception:
v_msg = None
q = "-"
resolved_videos.append((vm, q, v_msg))
# Preserve the exact sequence the user sent the videos in
# (Removed quality sorting as requested by user)
# Update job['video_msgs'] and cache messages list
job['video_msgs'] = [rv[0] for rv in resolved_videos]
cached_video_msgs = [rv[2] for rv in resolved_videos]
original_qualities = {}
predicted_filenames = {}
prefix = settings.get('prefix', '')
suffix = settings.get('suffix', '')
fmt = settings.get("rename_format", "{anime} - S{season:02d}E{episode:02d} [{language}] {quality} @suffix.mkv")
ext = ".mkv"
base_fmt = fmt
if fmt.lower().endswith(".mkv"):
base_fmt = fmt[:-4]
ext = ".mkv"
elif fmt.lower().endswith(".mp4"):
base_fmt = fmt[:-4]
ext = ".mp4"
for i, rv in enumerate(resolved_videos):
q = rv[1]
original_qualities[i] = q
try:
filename_base = safe_format(
base_fmt,
anime=anime,
season=job.get('season', '01'),
episode=job.get('episode', '01'),
language=job.get('language', 'Unknown'),
quality=q
)
if prefix: filename_base = f"{prefix} {filename_base}"
if suffix: filename_base = f"{filename_base} {suffix}"
predicted_filenames[i] = f"{filename_base}{ext}"
except Exception:
predicted_filenames[i] = "video.mkv"
job_id = f"job_{user_id}_{status_msg}"
work_dir = os.path.join("temp", job_id)
os.makedirs(work_dir, exist_ok=True)
custom_thumb = settings.get("thumbnail")
thumb_path = None
if custom_thumb:
try:
download_path = await client.download_media(message=custom_thumb, file_name=os.path.join(work_dir, "thumb.jpg"))
if download_path and os.path.exists(download_path):
if os.path.getsize(download_path) > 0:
thumb_size = await db.get_thumb_size(user_id)
thumb_path = await process_thumbnail(download_path, thumb_size)
else:
print("Thumbnail download resulted in a 0-byte file.")
os.remove(download_path)
except Exception as e:
print(f"Error downloading user thumbnail: {e}")
if custom_thumb and not thumb_path:
# It means the saved thumbnail failed to download correctly (e.g. FileReferenceExpired)
try:
await client.send_message(chat_id, "โ ๏ธ **Notice:** Your custom thumbnail is expired or could not be downloaded. It was ignored. Please set a new one in `/settings`.")
except Exception:
pass
try:
process_mode = settings.get("process_mode", "merge")
audio_path = None
# If ALL video sources are direct URLs, the MKV already has audio embedded.
# Skip the audio merge step automatically โ treat as rename_only.
all_urls = all("url" in vm for vm in job.get("video_msgs", []))
if all_urls and process_mode == "merge":
process_mode = "rename_only"
if process_mode != "rename_only":
try:
audio_msg = await client.get_messages(job['audio_msg']['chat_id'], job['audio_msg']['msg_id'])
if audio_msg.empty:
raise ValueError("Audio message deleted")
dl_start[0] = time.time()
last_dl_update[0] = 0
audio_path = await audio_msg.download(
file_name=os.path.join(work_dir, "audio.m4a"),
progress=lambda c, t: dl_progress_cb(c, t, "audio_stream", "-")
)
except Exception as e:
raise RuntimeError("Audio message not found or deleted by user. Please /clear_queue and try again.")
upload_type = settings.get('upload_type', 'document')
button_mode = settings.get('button_mode', 'off') == 'on'
prefix = settings.get('prefix', '')
suffix = settings.get('suffix', '')
filestore_username = settings.get("filestore_username", "").strip()
dump_channel_id = settings.get("dump_channel_id")
# Verify button parameters before continuing, fallback to regular if not configured
if button_mode and (not filestore_username or not dump_channel_id):
button_mode = False
try:
await client.send_message(chat_id, "โ ๏ธ **Warning:** Button Mode is enabled, but File Store Bot credentials are not configured in settings. Falling back to regular upload.")
except Exception:
pass
generated_links = []
uploaded_success_count = 0
for i, rv in enumerate(resolved_videos):
vm = rv[0]
q = rv[1]
v_msg = rv[2]
try:
if "url" in vm:
v_orig_name = f"video_{i}.mkv"
else:
if not v_msg:
v_msg = await client.get_messages(vm['chat_id'], vm['msg_id'])
v_orig_name = getattr(v_msg.video or v_msg.document or v_msg, "file_name", f"video_{i}.mkv")
# Update progress info for downloading this specific quality path
dl_start[0] = time.time()
last_dl_update[0] = 0
await update_state("๐ฅ ๐๐ผ๐๐ป๐น๐ผ๐ฎ๐ฑ๐ถ๐ป๐ด ๐ฉ๐ถ๐ฑ๐ฒ๐ผ...", "", q, predicted_filenames.get(i, v_orig_name))
if "url" in vm:
from utils.downloader import download_url_with_progress
async def url_prog(c, t):
dl_progress_cb(c, t, predicted_filenames.get(i, v_orig_name), q)
v_path = await download_url_with_progress(
vm["url"],
os.path.join(work_dir, f"video_{i}.mkv"),
progress_callback=url_prog
)
else:
v_path = await v_msg.download(
file_name=os.path.join(work_dir, f"video_{i}.mkv"),
progress=lambda c, t: dl_progress_cb(c, t, predicted_filenames.get(i, v_orig_name), q)
)
if os.path.exists(v_path) and os.path.getsize(v_path) == 0:
raise RuntimeError("Downloaded file size is 0 bytes. The URL or file might be restricted/blocked.")
# Probe video resolution if not found in filename
if q != "-":
res = q
else:
await update_state("๐ ๐ฃ๐ฟ๐ผ๐ฏ๐ถ๐ป๐ด ๐ฅ๐ฒ๐๐ผ๐น๐๐๐ถ๐ผ๐ป...", "", q, predicted_filenames.get(i, v_orig_name))
res = await get_video_resolution(v_path)
if res == "Unknown":
res = q
# Format final filename
fmt = settings.get("rename_format", "{anime} - S{season:02d}E{episode:02d} [{language}] {quality} @suffix.mkv")
ext = ".mkv"
base_fmt = fmt
if fmt.lower().endswith(".mkv"):
base_fmt = fmt[:-4]
ext = ".mkv"
elif fmt.lower().endswith(".mp4"):
base_fmt = fmt[:-4]
ext = ".mp4"
filename_base = safe_format(
base_fmt,
anime=anime,
season=job.get('season', '01'),
episode=job.get('episode', '01'),
language=job.get('language', 'Unknown'),
quality=res
)
if prefix: filename_base = f"{prefix} {filename_base}"
if suffix: filename_base = f"{filename_base} {suffix}"
filename = f"{filename_base}{ext}"
filename = filename.replace("/", "-").replace("\\", "-")
if process_mode == "rename_only":
await update_state("โ๏ธ ๐๐ป๐ฐ๐ผ๐ฑ๐ถ๐ป๐ด ๐ฉ๐ถ๐ฑ๐ฒ๐ผ...", "", res, filename)
out_path = os.path.join(work_dir, f"out_{res}.mkv")
shutil.copy2(v_path, out_path)
success = True
else:
await update_state("โ๏ธ ๐๐ป๐ฐ๐ผ๐ฑ๐ถ๐ป๐ด ๐ฉ๐ถ๐ฑ๐ฒ๐ผ...", "", res, filename)
out_path = os.path.join(work_dir, f"out_{res}.mkv")
success = await strip_and_mux_audio(v_path, audio_path, out_path, user_id)
if success:
final_path = os.path.join(work_dir, filename)
meta_ok = await apply_metadata(out_path, final_path, user_id, client)
if not meta_ok:
os.rename(out_path, final_path)
upload_label = "๐ค ๐จ๐ฝ๐น๐ผ๐ฎ๐ฑ๐ถ๐ป๐ด ๐ง๐ผ ๐๐๐บ๐ฝ..." if button_mode else "๐ค ๐จ๐ฝ๐น๐ผ๐ฎ๐ฑ๐ถ๐ป๐ด ๐ง๐ผ ๐๐ต๐ฎ๐ป๐ป๐ฒ๐น..."
await update_state(upload_label, "", res, filename)
# Calculate new metadata formats
languages_eng = "Unknown"
languages_reg = "Unknown"
duration_str = "00:00"
duration_sec = 0
file_size_str = "0 B"
if not os.path.exists(final_path) or os.path.getsize(final_path) == 0:
raise RuntimeError(f"Encoding failed! Created output file is empty/missing. Could be an IP block from server CDN.")
try:
file_size_str = humanbytes(os.path.getsize(final_path))
m_info = await get_media_info(final_path)
f_info = m_info.get("format", {})
if "duration" in f_info:
duration_sec = int(float(f_info["duration"]))
hours = duration_sec // 3600
minutes = (duration_sec % 3600) // 60
seconds = duration_sec % 60
if hours > 0:
duration_str = f"{hours}:{minutes:02d}:{seconds:02d}"
else:
duration_str = f"{minutes}:{seconds:02d}"
streams = m_info.get("streams", [])
found_langs = []
for s in streams:
if s.get("codec_type") == "audio":
lang = s.get("tags", {}).get("language", "und").lower()
if lang != "und":
found_langs.append(lang)
if found_langs:
seen = set()
unique_langs = [x for x in found_langs if not (x in seen or seen.add(x))]
lang_map_eng = {"tam": "Tamil", "hin": "Hindi", "tel": "Telugu", "eng": "English", "jpn": "Japanese", "mal": "Malayalam", "kan": "Kannada", "kor": "Korean"}
lang_map_reg = {"tam": "เฎคเฎฎเฎฟเฎดเฏ", "hin": "เคนเคฟเคจเฅเคฆเฅ", "tel": "เฐคเฑเฐฒเฑเฐเฑ", "eng": "English", "jpn": "ๆฅๆฌ่ช", "mal": "เดฎเดฒเดฏเดพเดณเฐ", "kan": "เฒเฒจเณเฒจเฒก", "kor": "ํ๊ตญ์ด"}
languages_eng = ", ".join([lang_map_eng.get(l, l.title()) for l in unique_langs])
languages_reg = ", ".join([lang_map_reg.get(l, lang_map_eng.get(l, l.title())) for l in unique_langs])
except Exception as e:
print(f"Error parsing advanced media info: {e}")
cap_fmt = settings.get("caption_format", "<b>{filename}</b>")
if not cap_fmt or cap_fmt == "{filename}":
cap_fmt = "<b>{filename}</b>"
caption = safe_format(
cap_fmt,
filename=filename,
anime=anime,
season=job.get('season', '01'),
episode=job.get('episode', '01'),
language=job.get('language', 'Unknown'),
quality=res,
size=file_size_str,
duration=duration_str,
languages=languages_eng,
languagesr=languages_reg
)
# Upload with progress bar
uploaded_msg = None
try:
upload_start = time.time()
last_update_time = [0]
def progress_cb(current, total):
if job.get('user_id') in USER_CANCELLATIONS:
raise RuntimeError("TaskCancelledByUser")
if total < 2 * 1024 * 1024:
return
now = time.time()
if now - last_update_time[0] < 8 and current < total:
return
last_update_time[0] = now
percent = (current / total) * 100 if total else 0
filled = int(percent / 10)
bar = "โ " * filled + "โก" * (10 - filled)
elapsed = now - upload_start
speed = current / elapsed if elapsed > 0 else 0
eta = ""
if speed > 0:
eta_seconds = (total - current) / speed
eta = f" | ETA: {int(eta_seconds)}s"
speed_text = f"{humanbytes(speed)}/s" if speed else "โ/s"
progress_bar_text = f"[{bar}] {percent:.1f}% ({humanbytes(current)} of {humanbytes(total)} @ {speed_text}{eta})"
asyncio.run_coroutine_threadsafe(
update_state(upload_label, progress_bar_text, res, filename),
loop
)
upload_chat_id = int(dump_channel_id) if button_mode else target_chat
actual_type = upload_type
if actual_type == "video":
uploaded_msg = await client.send_video(
chat_id=upload_chat_id,
video=final_path,
caption=caption,
parse_mode=enums.ParseMode.HTML,
thumb=thumb_path,
duration=duration_sec,
progress=progress_cb
)
else:
uploaded_msg = await client.send_document(
chat_id=upload_chat_id,
document=final_path,
caption=caption,
parse_mode=enums.ParseMode.HTML,
thumb=thumb_path,
progress=progress_cb
)
except Exception as e:
print(f"Error during progress upload: {e}. Retrying standard upload...")
upload_chat_id = int(dump_channel_id) if button_mode else target_chat
actual_type = upload_type
if actual_type == "video":
uploaded_msg = await client.send_video(upload_chat_id, video=final_path, caption=caption, parse_mode=enums.ParseMode.HTML, thumb=thumb_path, duration=duration_sec)
else:
uploaded_msg = await client.send_document(upload_chat_id, document=final_path, caption=caption, parse_mode=enums.ParseMode.HTML, thumb=thumb_path)
if button_mode and uploaded_msg:
import base64
msg_id_val = uploaded_msg.id
payload_str = f"get-{msg_id_val * abs(int(dump_channel_id))}"
payload_bytes = payload_str.encode("ascii")
base64_bytes = base64.urlsafe_b64encode(payload_bytes)
base64_string = base64_bytes.decode("ascii").strip("=")
bot_link = f"https://t.me/{filestore_username}?start={base64_string}"
generated_links.append((res, bot_link))
if uploaded_msg and Config.GLOBAL_DUMP_CHANNEL:
try:
upload_chat_id = int(dump_channel_id) if button_mode else target_chat
if Config.GLOBAL_DUMP_CHANNEL != upload_chat_id:
uinfo = await client.get_users(user_id)
global_cap = caption + f"\n\n๐ค **Processed By:** {uinfo.mention} (`{uinfo.id}`)"
await client.copy_message(
chat_id=Config.GLOBAL_DUMP_CHANNEL,
from_chat_id=upload_chat_id,
message_id=uploaded_msg.id,
caption=global_cap,
parse_mode=enums.ParseMode.HTML
)
except Exception as e:
print(f"Global dump failed: {e}")
# Clean up this resolution files immediately to conserve disk space
try:
if os.path.exists(v_path): os.remove(v_path)
if os.path.exists(out_path): os.remove(out_path)
if success and os.path.exists(final_path): os.remove(final_path)
except Exception:
pass
uploaded_success_count += 1
except Exception as e:
if "TaskCancelledByUser" in str(e):
print(f"Task for user {user_id} cancelled by request. Breaking loop.")
try:
await client.send_message(chat_id, "๐ **Task has been forcefully cancelled as requested.**")
except Exception:
pass
break # Break out to cancel the rest of the job completely
print(f"Error processing video resolution quality {q}: {e}")
try:
await client.send_message(chat_id, f"โ ๏ธ **Error processing/uploading {q} resolution:** `{e}`")
except Exception:
pass
# Delete temporary status message from channel to keep channel feed clean
if status_msg_node_id:
try:
await client.delete_messages(target_chat, status_msg_node_id)
except Exception as e:
print(f"Error deleting status message: {e}")
# Send buttons message/poster if button_mode is on
if button_mode and generated_links:
btn_post_fmt = settings.get("button_post_format")
if not btn_post_fmt:
btn_post_fmt = "<b>{anime} | Tamil Dubbed #Official</b>\n\n<b>Season : {season} | Episode : {episode}</b>\n\n<b>โผ๏ธNote - Click The Below Button to Get Episodes ๐</b>"
post_caption = safe_format(
btn_post_fmt,
anime=anime,
season=job.get('season', '01'),
episode=job.get('episode', '01')
)
inline_buttons = []
for qual, link in generated_links:
inline_buttons.append(InlineKeyboardButton(f"๐ฅ {qual}", url=link))
rows = [inline_buttons[x:x+2] for x in range(0, len(inline_buttons), 2)]
reply_markup = InlineKeyboardMarkup(rows)
button_post_thumb = custom_thumb
if settings.get("dest_poster_mode") == "on":
button_post_thumb = await db.match_poster(user_id, anime) or custom_thumb
if button_post_thumb:
try:
await client.send_photo(
chat_id=target_chat,
photo=button_post_thumb,
caption=post_caption,
parse_mode=enums.ParseMode.HTML,
reply_markup=reply_markup
)
except Exception as e:
print(f"Error posting photo to channel: {e}. Falling back to text.")
await client.send_message(
chat_id=target_chat,
text=post_caption,
parse_mode=enums.ParseMode.HTML,
reply_markup=reply_markup
)
else:
await client.send_message(
chat_id=target_chat,
text=post_caption,
parse_mode=enums.ParseMode.HTML,
reply_markup=reply_markup
)
# Announce the completed upload in the user's configured update channel.
if uploaded_success_count > 0 and settings.get("main_channel_mode") == "on" and settings.get("main_channel_id"):
try:
main_chat = settings["main_channel_id"]
if isinstance(main_chat, str) and main_chat.lstrip("-").isdigit():
main_chat = int(main_chat)
destination_link = generated_links[0][1] if generated_links else ""
channel_link = ""
if channel and channel.get("link"):
channel_link = channel["link"]
else:
dest_info = await client.get_chat(target_chat)
channel_link = (f"https://t.me/{dest_info.username}" if dest_info.username
else (dest_info.invite_link or ""))
if not destination_link:
destination_link = channel_link
episode_value = str(job.get("episode", "01"))
season_value = str(job.get("season", "01"))
try:
current_episode = int(re.search(r"\d+", episode_value).group())
except Exception:
current_episode = None
current_qualities = [q for q, _ in generated_links] if generated_links else ["-"]
previous = await db.get_last_main_post(user_id, anime, season_value) if current_episode is not None else None
episode_display = episode_value
qualities = current_qualities
range_first = range_last = current_episode
edit_existing = False
if previous and current_episode is not None and settings.get("main_batch_update", "on") == "on":
age = time.time() - previous.get("timestamp", 0)
if str(previous.get("channel_id")) == str(main_chat) and 0 <= age < 14400:
old_last = int(previous.get("last_episode", current_episode))
if old_last != current_episode:
range_first = min(int(previous.get("first_episode", old_last)), current_episode)
range_last = max(old_last, current_episode)
qualities = list(dict.fromkeys(previous.get("quals", []) + current_qualities))
else:
range_first = int(previous.get("first_episode", current_episode))
range_last = old_last
qualities = list(dict.fromkeys(previous.get("quals", []) + current_qualities))
episode_display = (f"{range_first:02d}" if range_first == range_last
else f"{range_first:02d} - {range_last:02d}")
edit_existing = True
quality_text = ", ".join(qualities)
caption_template = settings.get("main_channel_caption") or "<b>{anime}</b>\nSeason {season} โข Episode {episode}\n{quality}"
episode_token = "__MAIN_CHANNEL_EPISODE__"
caption_template = re.sub(r"\{episode(?::[^}]+)?\}", episode_token, caption_template)
main_caption = safe_format(
caption_template, anime=anime, season=season_value, episode=current_episode or 0,
quality=quality_text, language=job.get("language", "")
).replace(episode_token, episode_display)
main_buttons = []
for cfg in settings.get("main_channel_buttons", []):
url = str(cfg.get("url", "{dest_post_link}"))
url = url.replace("{dest_post_link}", destination_link).replace("{channel_link}", channel_link)
url = url.replace("{anime}", str(anime)).replace("{season}", str(job.get("season", "01")))
url = url.replace("{episode}", episode_display)
if url:
label = str(cfg.get("text", "Open"))
if settings.get("colored_buttons", "on") == "on":
style = cfg.get("style")
if not style:
legacy_color = str(cfg.get("color", "" )).lower()
style = {"๐ต": "primary", "blue": "primary", "primary": "primary",
"๐ข": "success", "green": "success", "success": "success",
"๐ด": "danger", "red": "danger", "danger": "danger"}.get(legacy_color)
style_emoji = {"primary": "๐ต", "success": "๐ข", "danger": "๐ด"}.get(style, "")
if style_emoji and not label.startswith(style_emoji):
label = f"{style_emoji} {label}"
main_buttons.append(InlineKeyboardButton(label, url=url))
layout = []
try:
widths = [max(1, int(n.strip())) for n in settings.get("main_buttons_layout", "1, 2").split(",")]
pos = 0
for width in widths:
if pos >= len(main_buttons):
break
layout.append(main_buttons[pos:pos + width])
pos += width
if pos < len(main_buttons):
layout.extend([main_buttons[i:i + 2] for i in range(pos, len(main_buttons), 2)])
except Exception:
layout = [main_buttons[i:i + 2] for i in range(0, len(main_buttons), 2)]
markup = InlineKeyboardMarkup(layout) if layout else None
poster = await db.match_poster(user_id, anime) or custom_thumb
edited = False
if edit_existing and previous:
try:
if previous.get("is_photo"):
await client.edit_message_caption(main_chat, previous["message_id"], main_caption,
parse_mode=enums.ParseMode.HTML, reply_markup=markup)
else:
await client.edit_message_text(main_chat, previous["message_id"], main_caption,
parse_mode=enums.ParseMode.HTML, reply_markup=markup)
edited = True
except Exception as edit_error:
# Avoid duplicate announcements if Telegram rejects an edit.
print(f"Could not edit existing main channel post: {edit_error}")
if not edit_existing:
if poster:
try:
sent = await client.send_photo(main_chat, poster, caption=main_caption,
parse_mode=enums.ParseMode.HTML, reply_markup=markup)
is_photo = True
except Exception:
sent = await client.send_message(main_chat, main_caption,
parse_mode=enums.ParseMode.HTML, reply_markup=markup)
is_photo = False
else:
sent = await client.send_message(main_chat, main_caption,
parse_mode=enums.ParseMode.HTML, reply_markup=markup)
is_photo = False
previous = {"message_id": sent.id, "is_photo": is_photo}
if current_episode is not None and (not edit_existing or edited):
await db.save_last_main_post(user_id, anime, season_value, {
"channel_id": main_chat,
"message_id": previous["message_id"],
"is_photo": previous.get("is_photo", False),
"first_episode": range_first,
"last_episode": range_last,
"timestamp": time.time(),
"quals": qualities,
"dest_post_link": destination_link
})
except Exception as e:
print(f"Error posting upload announcement to main channel: {e}")
# 5. Post completion sticker if configured
finish_sticker = settings.get("finish_sticker")
if finish_sticker:
try:
# Send to target chat
await client.send_sticker(target_chat, sticker=finish_sticker)
except Exception as e:
print(f"Error sending final sticker: {e}")
# Final notification in user's PM
try:
if job.get('user_id') in USER_CANCELLATIONS:
await client.edit_message_text(chat_id, status_msg, "โ **Task was cancelled by user.**")
elif uploaded_success_count > 0:
await client.edit_message_text(chat_id, status_msg, f"โ
**Processing Completed!**\nFiles successfully posted in target channel.")
else:
await client.edit_message_text(chat_id, status_msg, f"โ **Processing Failed!**\nNo files were successfully uploaded.")
except Exception:
pass
except Exception as re_err:
if str(re_err) == "TaskCancelledByUser":
try:
await client.edit_message_text(chat_id, status_msg, "โ **Task Cancelled.** Moving next...")
except Exception: pass
print(f"Job cancelled for user {job.get('user_id')}")
else:
try:
await client.edit_message_text(chat_id, status_msg, f"โ **Error during process:** `{re_err}`")
except Exception: pass
print(f"Job crashed with error: {re_err}")
finally:
shutil.rmtree(work_dir, ignore_errors=True)
log_msg_id = job.get("log_msg_id")
if log_msg_id and Config.LOG_CHANNEL:
try:
await client.delete_messages(Config.LOG_CHANNEL, log_msg_id)
except Exception as e:
print(f"Error deleting job trigger log message: {e}")
|