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"
{esc_header}
\n\n" f"
๐ŸŽฌ ๐—”๐—ป๐—ถ๐—บ๐—ฒ : {esc_anime}\n" f"๐Ÿ“ฆ ๐—ค๐˜‚๐—ฎ๐—น๐—ถ๐˜๐˜† : {esc_quality}\n" f"๐Ÿ”ข ๐—˜๐—ฝ๐—ถ๐˜€๐—ผ๐—ฑ๐—ฒ : {esc_ep}\n" f"๐Ÿ“ ๐—™๐—ถ๐—น๐—ฒ : {esc_filename}" ) if progress_bar: esc_progress = html.escape(str(progress_bar)) text += f"\n\n{esc_progress}" text += "
" 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", "{filename}") if not cap_fmt or cap_fmt == "{filename}": cap_fmt = "{filename}" 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 = "{anime} | Tamil Dubbed #Official\n\nSeason : {season} | Episode : {episode}\n\nโ€ผ๏ธNote - Click The Below Button to Get Episodes ๐Ÿ‘‡" 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 "{anime}\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}")