Spaces:
No application file
No application file
Separated download functionality and added post init.
Browse files
bot.py
CHANGED
|
@@ -7,6 +7,7 @@ import os
|
|
| 7 |
import asyncio
|
| 8 |
import yt_dlp
|
| 9 |
from telegram import Update, InputFile
|
|
|
|
| 10 |
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
|
| 11 |
from telegram.constants import ParseMode
|
| 12 |
from dotenv import load_dotenv
|
|
@@ -51,9 +52,9 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
|
|
| 51 |
user = update.effective_user
|
| 52 |
welcome_message = (
|
| 53 |
f"👋 Hello {user.mention_html()}!\n\n"
|
| 54 |
-
"I'm your friendly video downloading bot
|
| 55 |
-
"
|
| 56 |
-
"
|
| 57 |
"Keep in mind Telegram has a file size limit of about 50MB for bots, "
|
| 58 |
"so I'll try to get a version under that size.\n\n"
|
| 59 |
"Type /help for more information."
|
|
@@ -64,7 +65,8 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
|
| 64 |
"""Sends a help message when the /help command is issued."""
|
| 65 |
help_text = (
|
| 66 |
"ℹ️ **How to use me:**\n"
|
| 67 |
-
"1.
|
|
|
|
| 68 |
"2. I will process the link, download the video, and send it back to you.\n\n"
|
| 69 |
"**Supported Sites:**\n"
|
| 70 |
"I use `yt-dlp`, which supports hundreds of websites. Common ones include YouTube, "
|
|
@@ -77,11 +79,7 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
|
| 77 |
)
|
| 78 |
await update.message.reply_text(help_text, parse_mode=ParseMode.MARKDOWN)
|
| 79 |
|
| 80 |
-
|
| 81 |
-
# --- Main Video Processing Logic ---
|
| 82 |
-
async def handle_video_url(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
| 83 |
-
"""Handles messages containing video URLs."""
|
| 84 |
-
url = update.message.text
|
| 85 |
chat_id = update.effective_chat.id
|
| 86 |
|
| 87 |
if not url.startswith(('http://', 'https://')):
|
|
@@ -102,7 +100,7 @@ async def handle_video_url(update: Update, context: ContextTypes.DEFAULT_TYPE) -
|
|
| 102 |
ydl_opts = {
|
| 103 |
'format': f'bestvideo[ext=mp4][filesize<{MAX_FILE_SIZE_MB}M]+bestaudio[ext=m4a]/best[ext=mp4][filesize<{MAX_FILE_SIZE_MB}M]/best[filesize<{MAX_FILE_SIZE_MB}M]',
|
| 104 |
'outtmpl': os.path.join(DOWNLOAD_PATH, '%(title).200B.%(ext)s'), # Limit title length to avoid overly long filenames
|
| 105 |
-
|
| 106 |
# 'quiet': True, # Suppress yt-dlp console output
|
| 107 |
'merge_output_format': 'mp4', # Ensure output is mp4 if merging is needed
|
| 108 |
# # 'max_filesize': MAX_FILE_SIZE_MB * 1024 * 1024, # Alternative way to specify max filesize
|
|
@@ -111,7 +109,7 @@ async def handle_video_url(update: Update, context: ContextTypes.DEFAULT_TYPE) -
|
|
| 111 |
# 'preferedformat': 'mp4',
|
| 112 |
# }],
|
| 113 |
# 'logger': logger, # Send yt-dlp logs to our logger
|
| 114 |
-
|
| 115 |
}
|
| 116 |
|
| 117 |
# Use asyncio.to_thread to run blocking yt-dlp code in a separate thread
|
|
@@ -179,7 +177,29 @@ async def handle_video_url(update: Update, context: ContextTypes.DEFAULT_TYPE) -
|
|
| 179 |
logger.error(f"yt-dlp generic error for URL {url}: {e}")
|
| 180 |
await processing_message.edit_text(f"❌ An error occurred during video processing: {str(e)}")
|
| 181 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
if downloaded_file_path and os.path.exists(downloaded_file_path):
|
| 184 |
file_size = os.path.getsize(downloaded_file_path)
|
| 185 |
|
|
@@ -228,19 +248,20 @@ async def handle_video_url(update: Update, context: ContextTypes.DEFAULT_TYPE) -
|
|
| 228 |
|
| 229 |
|
| 230 |
except Exception as e:
|
| 231 |
-
logger.error(f"An unexpected error occurred for URL {url}: {e}", exc_info=True)
|
| 232 |
await processing_message.edit_text("❌ An unexpected error occurred. Please try again later.")
|
| 233 |
finally:
|
| 234 |
-
#
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
# Clear any stored filename from chat_data
|
| 242 |
context.chat_data.pop(f'download_filename_{chat_id}', None)
|
| 243 |
context.chat_data.pop(f'download_error_{chat_id}', None)
|
|
|
|
| 244 |
|
| 245 |
|
| 246 |
# --- yt-dlp Progress Hook ---
|
|
@@ -314,6 +335,15 @@ async def download_progress_hook(d, update: Update, context: ContextTypes.DEFAUL
|
|
| 314 |
except Exception:
|
| 315 |
pass # Ignore if editing fails
|
| 316 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
|
| 318 |
# --- Main Bot Execution ---
|
| 319 |
def main() -> None:
|
|
@@ -325,14 +355,16 @@ def main() -> None:
|
|
| 325 |
ensure_download_path_exists()
|
| 326 |
|
| 327 |
# Create the Application and pass it your bot's token.
|
| 328 |
-
application = Application.builder().token(TELEGRAM_BOT_TOKEN).build()
|
| 329 |
|
| 330 |
# Add command handlers
|
| 331 |
application.add_handler(CommandHandler("start", start_command))
|
| 332 |
application.add_handler(CommandHandler("help", help_command))
|
|
|
|
|
|
|
| 333 |
|
| 334 |
# Add message handler for video URLs (non-command text messages)
|
| 335 |
-
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_video_url))
|
| 336 |
|
| 337 |
# Run the bot until the user presses Ctrl-C
|
| 338 |
logger.info("Bot starting...")
|
|
|
|
| 7 |
import asyncio
|
| 8 |
import yt_dlp
|
| 9 |
from telegram import Update, InputFile
|
| 10 |
+
from telegram import BotCommand
|
| 11 |
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
|
| 12 |
from telegram.constants import ParseMode
|
| 13 |
from dotenv import load_dotenv
|
|
|
|
| 52 |
user = update.effective_user
|
| 53 |
welcome_message = (
|
| 54 |
f"👋 Hello {user.mention_html()}!\n\n"
|
| 55 |
+
"I'm your friendly video downloading bot.\n"
|
| 56 |
+
"Use the `/download <video_url>` command to fetch a video.\n\n"
|
| 57 |
+
"Example: `/download https://www.youtube.com/watch?v=your_video_id`\n\n" # Corrected example URL
|
| 58 |
"Keep in mind Telegram has a file size limit of about 50MB for bots, "
|
| 59 |
"so I'll try to get a version under that size.\n\n"
|
| 60 |
"Type /help for more information."
|
|
|
|
| 65 |
"""Sends a help message when the /help command is issued."""
|
| 66 |
help_text = (
|
| 67 |
"ℹ️ **How to use me:**\n"
|
| 68 |
+
"1. Use the `/download` command followed by a direct URL to the video you want to download.\n"
|
| 69 |
+
" Example: `/download https://www.example.com/video.mp4`\n"
|
| 70 |
"2. I will process the link, download the video, and send it back to you.\n\n"
|
| 71 |
"**Supported Sites:**\n"
|
| 72 |
"I use `yt-dlp`, which supports hundreds of websites. Common ones include YouTube, "
|
|
|
|
| 79 |
)
|
| 80 |
await update.message.reply_text(help_text, parse_mode=ParseMode.MARKDOWN)
|
| 81 |
|
| 82 |
+
async def downloader(update: Update, context: ContextTypes.DEFAULT_TYPE, url: str) -> tuple:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
chat_id = update.effective_chat.id
|
| 84 |
|
| 85 |
if not url.startswith(('http://', 'https://')):
|
|
|
|
| 100 |
ydl_opts = {
|
| 101 |
'format': f'bestvideo[ext=mp4][filesize<{MAX_FILE_SIZE_MB}M]+bestaudio[ext=m4a]/best[ext=mp4][filesize<{MAX_FILE_SIZE_MB}M]/best[filesize<{MAX_FILE_SIZE_MB}M]',
|
| 102 |
'outtmpl': os.path.join(DOWNLOAD_PATH, '%(title).200B.%(ext)s'), # Limit title length to avoid overly long filenames
|
| 103 |
+
'noplaylist': True, # Download only single video if playlist URL is given
|
| 104 |
# 'quiet': True, # Suppress yt-dlp console output
|
| 105 |
'merge_output_format': 'mp4', # Ensure output is mp4 if merging is needed
|
| 106 |
# # 'max_filesize': MAX_FILE_SIZE_MB * 1024 * 1024, # Alternative way to specify max filesize
|
|
|
|
| 109 |
# 'preferedformat': 'mp4',
|
| 110 |
# }],
|
| 111 |
# 'logger': logger, # Send yt-dlp logs to our logger
|
| 112 |
+
'progress_hooks': [lambda d: download_progress_hook(d, update, context, processing_message.message_id)],
|
| 113 |
}
|
| 114 |
|
| 115 |
# Use asyncio.to_thread to run blocking yt-dlp code in a separate thread
|
|
|
|
| 177 |
logger.error(f"yt-dlp generic error for URL {url}: {e}")
|
| 178 |
await processing_message.edit_text(f"❌ An error occurred during video processing: {str(e)}")
|
| 179 |
return
|
| 180 |
+
except:
|
| 181 |
+
logger.error(f"An overarching unexpected error occurred while downloading URL {url}: {e}", exc_info=True)
|
| 182 |
+
await processing_message.edit_text("❌ An unexpected error occurred. Please try again later.")
|
| 183 |
+
return downloaded_file_path, processing_message
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
# --- Main Video Processing Logic ---
|
| 187 |
+
async def download_command_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
| 188 |
+
"""Handles messages containing video URLs."""
|
| 189 |
+
chat_id = update.effective_chat.id
|
| 190 |
|
| 191 |
+
if not context.args:
|
| 192 |
+
await update.message.reply_text(
|
| 193 |
+
"⚠️ Please provide a video URL after the /download command.\n"
|
| 194 |
+
"Example: `/download https://www.youtube.com/watch?v=dQw4w9WgXcQ`", # Corrected example URL
|
| 195 |
+
parse_mode=ParseMode.MARKDOWN
|
| 196 |
+
)
|
| 197 |
+
return
|
| 198 |
+
|
| 199 |
+
url = context.args[0] # The first argument after /download
|
| 200 |
+
|
| 201 |
+
try:
|
| 202 |
+
downloaded_file_path, processing_message = await downloader(update, context, url)
|
| 203 |
if downloaded_file_path and os.path.exists(downloaded_file_path):
|
| 204 |
file_size = os.path.getsize(downloaded_file_path)
|
| 205 |
|
|
|
|
| 248 |
|
| 249 |
|
| 250 |
except Exception as e:
|
| 251 |
+
logger.error(f"An overarching unexpected error occurred for URL {url}: {e}", exc_info=True)
|
| 252 |
await processing_message.edit_text("❌ An unexpected error occurred. Please try again later.")
|
| 253 |
finally:
|
| 254 |
+
# Clean up the downloaded file
|
| 255 |
+
if downloaded_file_path and os.path.exists(downloaded_file_path):
|
| 256 |
+
try:
|
| 257 |
+
os.remove(downloaded_file_path)
|
| 258 |
+
logger.info(f"Cleaned up downloaded file: {downloaded_file_path}")
|
| 259 |
+
except OSError as e:
|
| 260 |
+
logger.error(f"Error deleting file {downloaded_file_path}: {e}")
|
| 261 |
# Clear any stored filename from chat_data
|
| 262 |
context.chat_data.pop(f'download_filename_{chat_id}', None)
|
| 263 |
context.chat_data.pop(f'download_error_{chat_id}', None)
|
| 264 |
+
context.chat_data.pop(f'last_progress_msg_{chat_id}', None)
|
| 265 |
|
| 266 |
|
| 267 |
# --- yt-dlp Progress Hook ---
|
|
|
|
| 335 |
except Exception:
|
| 336 |
pass # Ignore if editing fails
|
| 337 |
|
| 338 |
+
async def post_init(application: Application) -> None:
|
| 339 |
+
"""Sets the bot's commands after initialization."""
|
| 340 |
+
commands = [
|
| 341 |
+
BotCommand("start", "Starts the bot and shows a welcome message."),
|
| 342 |
+
BotCommand("help", "Shows the help message with instructions."),
|
| 343 |
+
BotCommand("download", "Downloads a video from a given URL (e.g., /download <URL>).")
|
| 344 |
+
]
|
| 345 |
+
await application.bot.set_my_commands(commands)
|
| 346 |
+
logger.info("Bot commands have been set programmatically.")
|
| 347 |
|
| 348 |
# --- Main Bot Execution ---
|
| 349 |
def main() -> None:
|
|
|
|
| 355 |
ensure_download_path_exists()
|
| 356 |
|
| 357 |
# Create the Application and pass it your bot's token.
|
| 358 |
+
application = Application.builder().token(TELEGRAM_BOT_TOKEN).post_init(post_init).build()
|
| 359 |
|
| 360 |
# Add command handlers
|
| 361 |
application.add_handler(CommandHandler("start", start_command))
|
| 362 |
application.add_handler(CommandHandler("help", help_command))
|
| 363 |
+
application.add_handler(CommandHandler("download", download_command_handler)) # New download handler
|
| 364 |
+
|
| 365 |
|
| 366 |
# Add message handler for video URLs (non-command text messages)
|
| 367 |
+
# application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_video_url))
|
| 368 |
|
| 369 |
# Run the bot until the user presses Ctrl-C
|
| 370 |
logger.info("Bot starting...")
|