from pyrogram import Client, filters
from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
from helper.database import codeflixbots as db
from utils.job_queue import process_queue
import os
import asyncio
from pyromod.exceptions import ListenerTimeout
from config import Config
command_filter = filters.create(lambda _, __, msg: bool(msg.text and msg.text.startswith("/")))
@Client.on_message(filters.command("set_name") & filters.private)
async def set_name_cmd(client: Client, message: Message):
if not await db.is_user_authorized(message.from_user.id):
return await message.reply_text("❌ You are not authorized to use this bot. Please contact the Owner/Admin to authorize you.")
is_uploader_mode = await db.get_auto_upload(message.from_user.id)
if not is_uploader_mode:
return await message.reply_text("Auto Upload mode is OFF. Turn it ON in /settings to use this.")
args = message.text.split(" ", 1)
if len(args) < 2:
await message.reply_text("Usage: `/set_name `")
return
anime_name = args[1].strip()
user_id = message.from_user.id
chat_id = message.chat.id
await client.stop_listening(chat_id=chat_id, user_id=user_id)
# Retrieve user settings
settings = await db.get_user_settings(user_id)
auto_match_enabled = settings.get("auto_channel_match", "on") == "on"
state = await db.get_user_state(user_id)
job = state.get("current_job", {})
job["anime_name"] = anime_name
matched_channel = None
if auto_match_enabled:
matched_channel = await db.match_channel(user_id, anime_name)
if matched_channel:
job["target_channel_id"] = matched_channel["channel_id"]
job["target_channel_title"] = matched_channel.get("title", f"Channel {matched_channel['channel_id']}")
state["current_job"] = job
await db.update_user_state(user_id, state)
await message.reply_text(
f"✅ **Anime Name Set:** `{anime_name}`\n"
f"📺 **Auto-matched Target Channel:** {job['target_channel_title']} (`{job['target_channel_id']}`)\n\n"
"Use `/start_process` to start processing."
)
return
# Defer destination choice until /start_process so the task can still be
# routed manually or sent back to the user's DM without blocking /set_name.
state["current_job"] = job
await db.update_user_state(user_id, state)
await message.reply_text(
f"✅ Anime name set: `{anime_name}`.\n"
"Use `/start_process`; if no routing channel matches, you can choose a channel or DM upload."
)
return
@Client.on_message(filters.command("set_details") & filters.private)
async def set_details_cmd(client: Client, message: Message):
if not await db.is_user_authorized(message.from_user.id):
return await message.reply_text("❌ You are not authorized to use this bot. Please contact the Owner/Admin to authorize you.")
is_uploader_mode = await db.get_auto_upload(message.from_user.id)
if not is_uploader_mode:
return await message.reply_text("Auto Upload mode is OFF. Turn it ON in /settings to use this.")
await client.stop_listening(chat_id=message.chat.id, user_id=message.from_user.id)
args = message.text.split(" ", 1)
if len(args) < 2:
guide = (
"📝 **Set Details Command Guide**\n\n"
"This command helps you forcibly define the episode and season in case they are missing from the filename.\n\n"
"**Format:**\n"
"`/set_details , , `\n\n"
"**Example:**\n"
"`/set_details 3, 1` (Set as Episode 3, Season 1)\n"
"*Note: Comma `,` is required to separate the values!*"
)
await message.reply_text(guide)
return
parts = [p.strip() for p in args[1].split(",")]
if len(parts) < 2:
await message.reply_text("❌ Please provide at least episode, and season separated by commas. Example: `/set_details 3, 1`")
return
state = await db.get_user_state(message.from_user.id)
job = state.get("current_job", {})
job["episode"] = parts[0]
job["season"] = parts[1]
if len(parts) > 2:
job["target_channel_id"] = parts[2]
state["current_job"] = job
await db.update_user_state(message.from_user.id, state)
await message.reply_text(f"Details saved: E{job['episode']} S{job['season']}")
@Client.on_message((filters.video | filters.document | filters.audio | filters.text) & filters.private & ~filters.command([c for c in ["start", "settings", "set_name", "clear_queue", "setaudioorder", "resetaudioorder", "set_auto_audio_order", "reset_auto_audio_order", "set_leech_audio_order", "reset_leech_audio_order", "set_default_audio", "resetdefaultaudio", "set_sub_watermark", "set_details", "cancel_task", "start_process", "extract"]]))
async def handle_files(client: Client, message: Message):
if not await db.is_user_authorized(message.from_user.id):
return await message.reply_text("❌ You are not authorized to use this bot. Please contact the Owner/Admin to authorize you.")
is_uploader_mode = await db.get_auto_upload(message.from_user.id)
if not is_uploader_mode:
message.continue_propagation()
return
is_audio = getattr(message, "audio", None) is not None
is_video = getattr(message, "video", None) is not None
is_url = False
if message.text:
text = message.text.strip()
if text.startswith("http://") or text.startswith("https://"):
is_url = True
else:
message.continue_propagation()
return
# If it is a document, determine if it is audio or video or other
if message.document:
mime = message.document.mime_type or ""
name = message.document.file_name or ""
ext = os.path.splitext(name)[1].lower()
audio_exts = {".mp3", ".wav", ".m4a", ".flac", ".ogg", ".opus", ".aac"}
video_exts = {".mp4", ".mkv", ".avi", ".mov", ".webm", ".m4v", ".ts"}
if mime.startswith("audio/") or ext in audio_exts:
is_audio = True
elif mime.startswith("video/") or ext in video_exts:
is_video = True
else:
# Ignore other files (like JSON, text, etc.) from process queue
message.continue_propagation()
if not is_audio and not is_video and not is_url:
message.continue_propagation()
return
settings = await db.get_user_settings(message.from_user.id)
process_mode = settings.get("process_mode", "merge")
if is_video and process_mode == "extract":
from plugins.extract import initiate_extraction
await initiate_extraction(client, message)
return
if is_audio:
audio_info = {"chat_id": message.chat.id, "msg_id": message.id}
await db.set_current_job_audio(message.from_user.id, audio_info)
resp = "Audio added to current job."
else:
video_info = {"chat_id": message.chat.id, "msg_id": message.id}
if is_url:
video_info["url"] = message.text.strip()
await db.push_current_job_video(message.from_user.id, video_info)
state = await db.get_user_state(message.from_user.id)
job = state.get("current_job", {})
videos = job.get("video_msgs", [])
resp = f"{'Link' if is_url else 'Video'} added to current job. Total videos: {len(videos)} \n Use /set_name to set anime name for the file"
await message.reply_text(resp)
@Client.on_message(filters.command("clear_queue") & filters.private)
async def clear_queue_cmd(client: Client, message: Message):
is_uploader_mode = await db.get_auto_upload(message.from_user.id)
if not is_uploader_mode:
return await message.reply_text("Auto Upload mode is OFF. Turn it ON in /settings to use this.")
return
state = await db.get_user_state(message.from_user.id)
state["current_job"] = {}
await db.update_user_state(message.from_user.id, state)
await message.reply_text("✅ **Your queue has been cleared!** You can now send new files.")
@Client.on_message(filters.command("cancel_task") & filters.private)
async def cancel_task_cmd(client: Client, message: Message):
is_uploader_mode = await db.get_auto_upload(message.from_user.id)
if not is_uploader_mode:
return await message.reply_text("Auto Upload mode is OFF. Turn it ON in /settings to use this.")
return
from utils.job_queue import USER_CANCELLATIONS
USER_CANCELLATIONS.add(message.from_user.id)
state = await db.get_user_state(message.from_user.id)
state["current_job"] = {}
await db.update_user_state(message.from_user.id, state)
await message.reply_text("🛑 **Cancellation Requested!**\nAll currently running and queued tasks have been forcibly stopped and cleared.")
@Client.on_message(filters.command("start_process") & filters.private)
async def start_process_cmd(client: Client, message: Message):
if not await db.is_user_authorized(message.from_user.id):
return await message.reply_text("❌ You are not authorized to use this bot. Please contact the Owner/Admin to authorize you.")
is_uploader_mode = await db.get_auto_upload(message.from_user.id)
if not is_uploader_mode:
return await message.reply_text("Auto Upload mode is OFF. Turn it ON in /settings to use this.")
from utils.job_queue import USER_CANCELLATIONS
USER_CANCELLATIONS.discard(message.from_user.id)
await client.stop_listening(chat_id=message.chat.id, user_id=message.from_user.id)
settings = await db.get_user_settings(message.from_user.id)
process_mode = settings.get("process_mode", "merge")
state = await db.get_user_state(message.from_user.id)
job = state.get("current_job", {})
if "anime_name" not in job:
job["anime_name"] = None
if "video_msgs" not in job or not job["video_msgs"]:
await message.reply_text("No videos added.")
return
if process_mode == "merge":
all_urls = all("url" in vm for vm in job.get("video_msgs", []))
if not all_urls and "audio_msg" not in job:
await message.reply_text("No audio added.")
return
# Use a saved per-task destination or an existing routing match when one
# exists. Otherwise let the user pick a channel or their private chat.
target = job.get("target_channel_id") or job.get("target_chat_id")
if not target and settings.get("auto_channel_match", "on") == "on":
match_text = str(job.get("anime_name") or "")
if not match_text:
try:
vm = job["video_msgs"][0]
if "url" in vm:
from plugins.file_rename import resolve_remote_filename
match_text = await resolve_remote_filename(vm["url"], fallback="")
if match_text:
vm["file_name"] = match_text
else:
media_msg = await client.get_messages(vm["chat_id"], vm["msg_id"])
media = media_msg.video or media_msg.document
match_text = getattr(media, "file_name", "") or ""
except Exception:
pass
matched = await db.match_channel(message.from_user.id, match_text) if match_text else None
if matched:
job["target_channel_id"] = matched["channel_id"]
job["target_channel_title"] = matched.get("title", "Routing channel")
target = matched["channel_id"]
if not target:
await db.update_user_state(message.from_user.id, state)
return await message.reply_text(
"📤 Routing channel match kidaikkala. Upload destination choose pannunga:",
reply_markup=InlineKeyboardMarkup([
[InlineKeyboardButton("📺 Set Channel", callback_data="process_destination_channel")],
[InlineKeyboardButton("💬 Upload to Bot DM", callback_data="process_destination_dm")],
])
)
# Log triggering start process
log_msg_id = None
if Config.LOG_CHANNEL:
try:
log_msg = (
f"🚀 **Bot Job Triggered**\n\n"
f"• **Anime:** `{job.get('anime_name')}`\n"
f"• **Videos Count:** `{len(job.get('video_msgs', []))}`"
)
sent_log_msg = await client.send_message(Config.LOG_CHANNEL, log_msg)
log_msg_id = sent_log_msg.id
except Exception as e:
print(f"Error logging job start: {e}")
status_msg = await message.reply_text("Queuing job...")
full_job = {
"user_id": message.from_user.id,
"chat_id": message.chat.id,
"status_msg_id": status_msg.id,
"log_msg_id": log_msg_id,
**job
}
await process_queue.put(full_job)
# clear job
state["current_job"] = {}
await db.update_user_state(message.from_user.id, state)
await status_msg.edit_text("Job queued successfully! Position in queue: " + str(process_queue.qsize()))
async def _queue_destination_choice(client, query, destination=None):
user_id = query.from_user.id
if not await db.is_user_authorized(user_id) or not await db.get_auto_upload(user_id):
return await query.answer("Auto Upload is off or access is not authorized.", show_alert=True)
state = await db.get_user_state(user_id)
job = state.get("current_job", {})
if not job.get("video_msgs"):
return await query.answer("No pending video task found.", show_alert=True)
if destination:
job["target_channel_id"] = destination.id
job["target_channel_title"] = getattr(destination, "title", "Routing channel")
else:
job["target_channel_id"] = query.message.chat.id
job["target_channel_title"] = "Your Bot DM"
status = await query.message.reply_text("Queuing job...")
full_job = {"user_id": user_id, "chat_id": query.message.chat.id,
"status_msg_id": status.id, **job}
await process_queue.put(full_job)
state["current_job"] = {}
await db.update_user_state(user_id, state)
await query.message.edit_text("✅ Job queued! Position: " + str(process_queue.qsize()))
await query.answer()
@Client.on_callback_query(filters.regex(r"^process_destination_dm$"))
async def process_destination_dm_cb(client: Client, query: CallbackQuery):
await _queue_destination_choice(client, query)
@Client.on_callback_query(filters.regex(r"^process_destination_channel$"))
async def process_destination_channel_cb(client: Client, query: CallbackQuery):
user_id, chat_id = query.from_user.id, query.message.chat.id
if not await db.is_user_authorized(user_id) or not await db.get_auto_upload(user_id):
return await query.answer("Auto Upload is off or access is not authorized.", show_alert=True)
await query.message.edit_text(
"Send channel ID, public @username/link, or forward a post from the channel.",
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Cancel", callback_data="process_destination_cancel")]])
)
await query.answer()
try:
response = await client.listen(chat_id=chat_id, user_id=user_id, timeout=300)
from utils.channel_resolver import resolve_routing_channel
forwarded = getattr(response, "forward_from_chat", None)
if not forwarded and getattr(response, "forward_origin", None):
forwarded = getattr(response.forward_origin, "chat", None)
channel = await resolve_routing_channel(client, response.text if response.text else None, forwarded)
member = await client.get_chat_member(channel.id, "me")
member_status = getattr(member, "status", "")
member_status = str(getattr(member_status, "value", member_status)).lower().rsplit(".", 1)[-1]
if member_status not in {"administrator", "owner", "creator"}:
raise ValueError("Bot must be an admin in that channel to upload there.")
await response.delete()
await _queue_destination_choice(client, query, channel)
except Exception as exc:
await query.message.reply_text(f"❌ Channel setup failed: {exc}\nRun `/start_process` and try again.")
@Client.on_callback_query(filters.regex(r"^process_destination_cancel$"))
async def process_destination_cancel_cb(client: Client, query: CallbackQuery):
await query.answer("Task is still saved. You can run /start_process again.")