Enutrof commited on
Commit
0a5efea
·
1 Parent(s): f8f6e4d

Updated bot.py minimally and duplicated to get app.py

Browse files
Files changed (2) hide show
  1. app.py +426 -0
  2. bot.py +74 -23
app.py ADDED
@@ -0,0 +1,426 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # pylint: disable=unused-argument, wrong-import-position
3
+ # This program is dedicated to the public domain under the MIT license.
4
+
5
+ import logging
6
+ import os
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
14
+
15
+ load_dotenv() # take environment variables
16
+
17
+ # Enable logging
18
+ logging.basicConfig(
19
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
20
+ )
21
+ # set higher logging level for httpx to avoid all GET and POST requests being logged
22
+ logging.getLogger("httpx").setLevel(logging.WARNING)
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # --- Configuration ---
27
+ TELEGRAM_BOT_TOKEN = os.getenv("YOUR_TELEGRAM_BOT_TOKEN") # Replace with your bot token
28
+ DOWNLOAD_PATH = "video_downloads/" # Folder to store downloaded videos temporarily
29
+ MAX_FILE_SIZE_MB = 49 # Telegram's typical bot upload limit is 50MB, stay slightly under
30
+
31
+ # --- Helper Functions ---
32
+ def ensure_download_path_exists():
33
+ """Creates the download directory if it doesn't exist."""
34
+ if not os.path.exists(DOWNLOAD_PATH):
35
+ try:
36
+ os.makedirs(DOWNLOAD_PATH)
37
+ logger.info(f"Created download directory: {DOWNLOAD_PATH}")
38
+ except OSError as e:
39
+ logger.error(f"Error creating download directory {DOWNLOAD_PATH}: {e}")
40
+ # Depending on the desired behavior, you might want to exit or raise the exception
41
+ # For this example, we'll log and continue, but downloads will likely fail.
42
+
43
+
44
+ async def send_typing_action(update: Update, context: ContextTypes.DEFAULT_TYPE):
45
+ """Sends a typing action to indicate the bot is working."""
46
+ await context.bot.send_chat_action(chat_id=update.effective_chat.id, action='upload_video')
47
+
48
+ def is_time_like(text: str) -> bool:
49
+ """Basic check if a string looks like a time argument (contains ':' or is all digits)."""
50
+ if not text:
51
+ return False
52
+ return ':' in text or text.isdigit()
53
+
54
+ # --- Bot Command Handlers ---
55
+ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
56
+ """Sends a welcome message when the /start command is issued."""
57
+ user = update.effective_user
58
+ welcome_message = (
59
+ f"👋 Hello {user.mention_html()}!\n\n"
60
+ "I'm your video downloading bot.\n"
61
+ "Use `/download <URL> [START_TIME] [END_TIME]` to fetch a video or a segment.\n"
62
+ "Times are optional (e.g., `MM:SS` or `HH:MM:SS`).\n\n"
63
+ "Example (full video): `/download <your_video_url>`\n"
64
+ "Example (segment): `/download <your_video_url> 00:10 00:50`\n\n"
65
+ "Type /help for more detailed information."
66
+ )
67
+ await update.message.reply_html(welcome_message)
68
+
69
+ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
70
+ """Sends a help message when the /help command is issued."""
71
+ help_text = (
72
+ "ℹ️ **How to use me:**\n"
73
+ "Use the `/download` command followed by a video URL.\n"
74
+ "You can optionally specify a start and/or end time for the segment.\n\n"
75
+ "**Formats:**\n"
76
+ "1. `/download <VIDEO_URL>`\n"
77
+ " Downloads the full video.\n\n"
78
+ "2. `/download <VIDEO_URL> <START_TIME>`\n"
79
+ " Downloads from `START_TIME` to the end of the video.\n"
80
+ " Example: `/download <url> 01:20` (starts at 1 min 20 secs)\n\n"
81
+ "3. `/download <VIDEO_URL> <START_TIME> <END_TIME>`\n"
82
+ " Downloads the segment between `START_TIME` and `END_TIME`.\n"
83
+ " Example: `/download <url> 00:30 02:15`\n\n"
84
+ "Time format can be `MM:SS` or `HH:MM:SS` (e.g., `1:23` or `00:01:23`).\n"
85
+ "Use `0` or `00:00` for the beginning if specifying an end time only (e.g. `/download <url> 0 00:55`).\n\n"
86
+ "**Supported Sites:**\n"
87
+ "Most sites supported by `yt-dlp` (YouTube, Vimeo, Twitter, etc.).\n\n"
88
+ "**File Size Limit:**\n"
89
+ "Telegram bots can only send files up to ~50MB. I'll try to get a version under this. "
90
+ "Segments are more likely to fit!"
91
+ )
92
+ await update.message.reply_text(help_text, parse_mode=ParseMode.MARKDOWN)
93
+
94
+ async def downloader(update: Update, context: ContextTypes.DEFAULT_TYPE, url: str,
95
+ start_time_str: str, end_time_str: str) -> tuple:
96
+ chat_id = update.effective_chat.id
97
+
98
+ if not url.startswith(('http://', 'https://')):
99
+ await update.message.reply_text("⚠️ That doesn't look like a valid URL. Please send a direct link to a video.")
100
+ return
101
+
102
+ processing_message = await update.message.reply_text("🔍 Got your link! Processing and trying to download the video...")
103
+ await send_typing_action(update, context)
104
+
105
+ downloaded_file_path = None # To store the path of the downloaded file
106
+
107
+ # Get the current event loop to schedule coroutines from the hook
108
+ main_event_loop = asyncio.get_event_loop()
109
+
110
+ try:
111
+ # yt-dlp options
112
+ # We aim for a good quality mp4 file under 50MB.
113
+ # 'bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]/bv*+ba/b' is a common format string.
114
+ # We add filesize limits.
115
+ # Note: yt-dlp's filesize_approx can sometimes be inaccurate.
116
+ ydl_opts = {
117
+ '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]',
118
+ 'outtmpl': os.path.join(DOWNLOAD_PATH, '%(title).200B.%(ext)s'), # Limit title length to avoid overly long filenames
119
+ 'noplaylist': True, # Download only single video if playlist URL is given
120
+ # 'quiet': True, # Suppress yt-dlp console output
121
+ 'merge_output_format': 'mp4', # Ensure output is mp4 if merging is needed
122
+ 'max_filesize': MAX_FILE_SIZE_MB * 1024 * 1024, # Alternative way to specify max filesize
123
+ # 'postprocessors': [{
124
+ # 'key': 'FFmpegVideoConvertor',
125
+ # 'preferedformat': 'mp4',
126
+ # }],
127
+ # 'logger': logger, # Send yt-dlp logs to our logger
128
+ 'progress_hooks': [
129
+ lambda d: asyncio.run_coroutine_threadsafe(
130
+ download_progress_hook(d, update, context, processing_message.message_id),
131
+ main_event_loop
132
+ )
133
+ ],
134
+ # 'verbose': True
135
+ }
136
+
137
+ # Use asyncio.to_thread to run blocking yt-dlp code in a separate thread
138
+ # This prevents the bot from freezing during download.
139
+ loop = asyncio.get_event_loop()
140
+
141
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
142
+ # We need to run extract_info and download in a thread-safe way
143
+ # First, get info to check file size if possible (though format selection handles much of this)
144
+ try:
145
+ # Using download=False first to inspect, then download=True
146
+ # This is a bit more complex; for simplicity, we'll try direct download
147
+ # with format selectors doing the heavy lifting for size.
148
+ logger.info(f"Attempting to download: {url}")
149
+ # The actual download happens here
150
+ # result = await loop.run_in_executor(None, ydl.download, [url])
151
+
152
+ # More robust way to get the filename:
153
+ info_dict = await loop.run_in_executor(None, lambda: ydl.extract_info(url, download=False))
154
+
155
+ # Try to find a suitable format based on filesize if extract_info provides it
156
+ # This is an advanced step; ydl_opts['format'] usually handles it.
157
+ # For now, we rely on the format selector.
158
+
159
+ # Perform the download
160
+ await loop.run_in_executor(None, lambda: ydl.download([url]))
161
+
162
+ # Determine the filename
163
+ # ydl.prepare_filename(info_dict) is usually reliable IF info_dict is from a non-download extract_info call
164
+ # If download=True was used, we need to find the file.
165
+ # A common way is to list files in DOWNLOAD_PATH if we expect only one.
166
+ # For this example, we rely on the outtmpl and the hook to get the filename.
167
+ # The `status: finished` hook will give us the final filename.
168
+ # We'll retrieve it from context.chat_data if the hook sets it.
169
+
170
+ downloaded_file_path = context.chat_data.pop(f'download_filename_{chat_id}', None)
171
+
172
+ if not downloaded_file_path or not os.path.exists(downloaded_file_path):
173
+ # Fallback: try to find the latest mp4 file in the directory (less robust)
174
+ list_of_files = [os.path.join(DOWNLOAD_PATH, f) for f in os.listdir(DOWNLOAD_PATH) if f.endswith(".mp4")]
175
+ if list_of_files:
176
+ downloaded_file_path = max(list_of_files, key=os.path.getctime)
177
+ logger.info(f"Found downloaded file by fallback: {downloaded_file_path}")
178
+ else:
179
+ logger.error("Could not determine downloaded file path after download.")
180
+ await processing_message.edit_text("❌ Download seemed to complete, but I couldn't find the file. Please try again.")
181
+ return
182
+
183
+ file_size = os.path.getsize(downloaded_file_path)
184
+ logger.info(f"File downloaded: {downloaded_file_path}, Size: {file_size / (1024*1024):.2f} MB")
185
+
186
+
187
+ except yt_dlp.utils.DownloadError as e:
188
+ logger.error(f"yt-dlp DownloadError for URL {url}: {e}")
189
+ error_message = f"❌ Failed to download video.\nError: `{str(e)}`"
190
+ if "Unsupported URL" in str(e):
191
+ error_message = "❌ Sorry, this website or video URL is not supported."
192
+ elif "Video unavailable" in str(e):
193
+ error_message = "❌ This video is unavailable or private."
194
+ elif "Unable to extract" in str(e):
195
+ error_message = "❌ Could not extract video information. The link might be broken or unsupported."
196
+ await processing_message.edit_text(error_message, parse_mode=ParseMode.MARKDOWN)
197
+ return
198
+ except Exception as e: # Catch other yt-dlp related errors
199
+ logger.error(f"yt-dlp generic error for URL {url}: {e}")
200
+ await processing_message.edit_text(f"❌ An error occurred during video processing: {str(e)}")
201
+ return
202
+ # if start_time_str and end_time_str: # Both start and end time
203
+ # logger.info(f"Segment (start-end): recode=mp4, download_sections, force_keyframes, pp_args for FFmpegVideoConvertor.")
204
+
205
+ # elif start_time_str: # Only start time given
206
+ # logger.info(f"Segment (start-onwards): recode=mp4, download_sections, force_keyframes, pp_args with -ss for FFmpegVideoConvertor.")
207
+
208
+ # else: # Full video download
209
+ # logger.info(f"Full video download requested for {url}")
210
+
211
+ except Exception as e:
212
+ logger.error(f"An overarching unexpected error occurred while downloading URL {url}: {e}", exc_info=True)
213
+ await processing_message.edit_text("❌ An unexpected error occurred. Please try again later.")
214
+ return downloaded_file_path, processing_message
215
+
216
+
217
+ # --- Main Video Processing Logic ---
218
+ async def download_command_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
219
+ """Handles messages containing video URLs."""
220
+ chat_id = update.effective_chat.id
221
+
222
+ if not context.args:
223
+ await update.message.reply_text(
224
+ "⚠️ Please provide a video URL after the /download command.\n"
225
+ "Example: `/download https://www.youtube.com/watch?v=dQw4w9WgXcQ`", # Corrected example URL
226
+ parse_mode=ParseMode.MARKDOWN
227
+ )
228
+ return
229
+
230
+ url = context.args[0] # The first argument after /download
231
+
232
+ # Parse optional start and end times
233
+ start_time_str = None
234
+ end_time_str = None
235
+
236
+ if len(context.args) >= 2:
237
+ potential_start_time = context.args[1]
238
+ if is_time_like(potential_start_time): # Basic check
239
+ start_time_str = potential_start_time
240
+ else:
241
+ await update.message.reply_text(f"⚠️ '{potential_start_time}' doesn't look like a valid start time (e.g., MM:SS). Downloading full video or stopping.")
242
+ # Decide if you want to proceed with full download or stop. For now, we'll proceed assuming no valid time.
243
+
244
+ if len(context.args) >= 3 and start_time_str: # Only look for end_time if start_time was plausible
245
+ potential_end_time = context.args[2]
246
+ if is_time_like(potential_end_time):
247
+ end_time_str = potential_end_time
248
+ else:
249
+ await update.message.reply_text(f"⚠️ '{potential_end_time}' doesn't look like a valid end time. Will download from {start_time_str} to end if applicable.")
250
+
251
+ downloaded_file_path, processing_message = await downloader(update, context, url, start_time_str, end_time_str)
252
+
253
+ try:
254
+ if downloaded_file_path and os.path.exists(downloaded_file_path):
255
+ file_size = os.path.getsize(downloaded_file_path)
256
+
257
+ if file_size > MAX_FILE_SIZE_MB * 1024 * 1024:
258
+ logger.warning(f"Video {downloaded_file_path} is too large: {file_size / (1024*1024):.2f} MB")
259
+ await processing_message.edit_text(
260
+ f"⚠️ The downloaded video is too large ({file_size / (1024*1024):.2f} MB) "
261
+ f"for me to send via Telegram (max ~{MAX_FILE_SIZE_MB}MB). I tried to get a smaller version."
262
+ )
263
+ return # No cleanup here, user might want to access it if bot is self-hosted
264
+
265
+ logger.info(f"PRE-SEND CHECK: Attempting to send video from path: '{downloaded_file_path}'")
266
+ logger.info(f"PRE-SEND CHECK: Does file exist at this exact path? {os.path.exists(downloaded_file_path)}")
267
+ logger.info(f"PRE-SEND CHECK: File size is {file_size / (1024*1024):.2f} MB")
268
+
269
+ await processing_message.edit_text("✅ Download complete! Now uploading to Telegram...")
270
+ await send_typing_action(update, context)
271
+
272
+ try:
273
+ with open(downloaded_file_path, 'rb') as video_file:
274
+ # For InputFile, you can pass a file path directly or a file-like object.
275
+ # Using a file path directly is often simpler.
276
+ # await context.bot.send_video(chat_id=chat_id, video=video_file, supports_streaming=True, caption=os.path.basename(downloaded_file_path))
277
+ logger.info("Attempting to send video using the file object directly...") # New log line
278
+ sent_message = await context.bot.send_video(
279
+ chat_id=chat_id,
280
+ video=video_file, # USE THIS INSTEAD
281
+ filename=os.path.basename(downloaded_file_path), # Good to add filename when sending file object
282
+ caption=f"🎬 Here's your video!\nOriginal URL: {url}",
283
+ supports_streaming=True,
284
+ read_timeout=180, # Increased timeout slightly for testing
285
+ write_timeout=180, # Increased timeout slightly for testing
286
+ connect_timeout=60
287
+ )
288
+ await processing_message.delete() # Delete "Processing..." message
289
+ logger.info(f"Video sent to chat_id {chat_id}: {downloaded_file_path}")
290
+
291
+ except Exception as e: # Catch errors during Telegram upload
292
+ logger.error(f"Error sending video to Telegram: {e}")
293
+ await processing_message.edit_text(f"❌ Failed to upload video to Telegram: {str(e)}")
294
+ else:
295
+ if not context.chat_data.get(f'download_error_{chat_id}'): # If no specific download error was already sent by hook
296
+ await processing_message.edit_text("❌ Download failed or no file was produced. Please check the URL or try again.")
297
+ # Clear any error flag
298
+ context.chat_data.pop(f'download_error_{chat_id}', None)
299
+
300
+
301
+ except Exception as e:
302
+ logger.error(f"An overarching unexpected error occurred for URL {url}: {e}", exc_info=True)
303
+ await processing_message.edit_text("❌ An unexpected error occurred. Please try again later.")
304
+ finally:
305
+ # Clean up the downloaded file
306
+ if downloaded_file_path and os.path.exists(downloaded_file_path):
307
+ try:
308
+ os.remove(downloaded_file_path)
309
+ logger.info(f"Cleaned up downloaded file: {downloaded_file_path}")
310
+ except OSError as e:
311
+ logger.error(f"Error deleting file {downloaded_file_path}: {e}")
312
+ # Clear any stored filename from chat_data
313
+ context.chat_data.pop(f'download_filename_{chat_id}', None)
314
+ context.chat_data.pop(f'download_error_{chat_id}', None)
315
+ context.chat_data.pop(f'last_progress_msg_{chat_id}', None)
316
+
317
+
318
+ # --- yt-dlp Progress Hook ---
319
+ # Keep track of messages to edit for progress (to avoid spamming)
320
+ progress_message_ids = {} # chat_id: message_id
321
+
322
+ async def download_progress_hook(d, update: Update, context: ContextTypes.DEFAULT_TYPE, initial_message_id: int):
323
+ """yt-dlp progress hook to update Telegram message."""
324
+ chat_id = update.effective_chat.id
325
+
326
+ if d['status'] == 'downloading':
327
+ percent_str = d.get('_percent_str', 'N/A')
328
+ # Remove ANSI codes if present (yt-dlp might use them)
329
+ percent_str = percent_str.replace('\x1b[0;94m', '').replace('\x1b[0m', '')
330
+
331
+ total_bytes_str = d.get('_total_bytes_str', 'N/A')
332
+ speed_str = d.get('_speed_str', 'N/A')
333
+ eta_str = d.get('_eta_str', 'N/A')
334
+
335
+ # To avoid hitting Telegram rate limits, only update message periodically
336
+ # This simple version updates on each hook call, which might be too frequent.
337
+ # A better approach would involve a timer or updating every N percent.
338
+ try:
339
+ # Use the initial "Processing..." message to show progress
340
+ current_progress_message = f"Downloading...\nProgress: {percent_str}\nSize: {total_bytes_str}\nSpeed: {speed_str}\nETA: {eta_str}"
341
+
342
+ # Only edit if the message content has changed significantly
343
+ last_message = context.chat_data.get(f'last_progress_msg_{chat_id}', "")
344
+ if last_message != current_progress_message: # Basic check to avoid identical edits
345
+ await context.bot.edit_message_text(
346
+ text=current_progress_message,
347
+ chat_id=chat_id,
348
+ message_id=initial_message_id
349
+ )
350
+ context.chat_data[f'last_progress_msg_{chat_id}'] = current_progress_message
351
+ except Exception as e:
352
+ # logger.warning(f"Could not edit progress message: {e}") # Can be noisy
353
+ pass # Ignore if editing fails (e.g., message not found or too old)
354
+
355
+ elif d['status'] == 'finished':
356
+ logger.info(f"yt-dlp finished processing for chat {chat_id}. Filename: {d.get('filename') or d.get('info_dict', {}).get('_filename')}")
357
+ # Store the final filename in chat_data to be picked up by the main handler
358
+ # yt-dlp provides filename in different places depending on version/context
359
+ final_filename = d.get('filename') # For when download=True
360
+ if not final_filename and d.get('info_dict'): # For when download=False then True, or from info_dict
361
+ final_filename = d['info_dict'].get('_filename')
362
+
363
+ if final_filename:
364
+ context.chat_data[f'download_filename_{chat_id}'] = final_filename
365
+
366
+ try:
367
+ await context.bot.edit_message_text(
368
+ text="✅ Download finished by yt-dlp. Preparing to send...",
369
+ chat_id=chat_id,
370
+ message_id=initial_message_id
371
+ )
372
+ except Exception:
373
+ pass # Ignore if editing fails
374
+ # Clean up last progress message cache
375
+ context.chat_data.pop(f'last_progress_msg_{chat_id}', None)
376
+
377
+ elif d['status'] == 'error':
378
+ logger.error(f"yt-dlp reported an error for chat {chat_id}.")
379
+ context.chat_data[f'download_error_{chat_id}'] = True # Flag an error
380
+ try:
381
+ await context.bot.edit_message_text(
382
+ text="❌ yt-dlp encountered an error during download.",
383
+ chat_id=chat_id,
384
+ message_id=initial_message_id
385
+ )
386
+ except Exception:
387
+ pass # Ignore if editing fails
388
+
389
+ async def post_init(application: Application) -> None:
390
+ """Sets the bot's commands after initialization."""
391
+ commands = [
392
+ BotCommand("start", "Starts the bot and shows a welcome message."),
393
+ BotCommand("help", "Shows the help message with instructions."),
394
+ BotCommand("download", "Downloads a video or segment. Usage: /download <URL> [start] [end]")
395
+ ]
396
+ await application.bot.set_my_commands(commands)
397
+ logger.info("Bot commands have been set programmatically.")
398
+
399
+ # --- Main Bot Execution ---
400
+ def main() -> None:
401
+ """Starts the bot."""
402
+ if TELEGRAM_BOT_TOKEN == "YOUR_TELEGRAM_BOT_TOKEN":
403
+ logger.error("CRITICAL: Bot token is not set! Please replace 'YOUR_TELEGRAM_BOT_TOKEN' with your actual bot token.")
404
+ return
405
+
406
+ ensure_download_path_exists()
407
+
408
+ # Create the Application and pass it your bot's token.
409
+ application = Application.builder().token(TELEGRAM_BOT_TOKEN).post_init(post_init).build()
410
+
411
+ # Add command handlers
412
+ application.add_handler(CommandHandler("start", start_command))
413
+ application.add_handler(CommandHandler("help", help_command))
414
+ application.add_handler(CommandHandler("download", download_command_handler)) # New download handler
415
+
416
+
417
+ # Add message handler for video URLs (non-command text messages)
418
+ # application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_video_url))
419
+
420
+ # Run the bot until the user presses Ctrl-C
421
+ logger.info("Bot starting...")
422
+ application.run_polling(allowed_updates=Update.ALL_TYPES)
423
+
424
+
425
+ if __name__ == "__main__":
426
+ main()
bot.py CHANGED
@@ -45,19 +45,24 @@ async def send_typing_action(update: Update, context: ContextTypes.DEFAULT_TYPE)
45
  """Sends a typing action to indicate the bot is working."""
46
  await context.bot.send_chat_action(chat_id=update.effective_chat.id, action='upload_video')
47
 
 
 
 
 
 
48
 
49
  # --- Bot Command Handlers ---
50
  async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
51
  """Sends a welcome message when the /start command is issued."""
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."
61
  )
62
  await update.message.reply_html(welcome_message)
63
 
@@ -65,21 +70,29 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
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, "
73
- "Vimeo, Twitter, Facebook, Instagram, and many more.\n\n"
74
  "**File Size Limit:**\n"
75
- "Please remember that Telegram bots can only send files up to 50MB. "
76
- "I'll try to select a video quality that fits this limit. If a video is too large, "
77
- "I might not be able to send it.\n\n"
78
- "If you encounter any issues, try a different URL or check if the video is publicly accessible."
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://')):
@@ -91,6 +104,9 @@ async def downloader(update: Update, context: ContextTypes.DEFAULT_TYPE, url: st
91
 
92
  downloaded_file_path = None # To store the path of the downloaded file
93
 
 
 
 
94
  try:
95
  # yt-dlp options
96
  # We aim for a good quality mp4 file under 50MB.
@@ -103,13 +119,19 @@ async def downloader(update: Update, context: ContextTypes.DEFAULT_TYPE, url: st
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
107
  # 'postprocessors': [{
108
  # 'key': 'FFmpegVideoConvertor',
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,7 +199,16 @@ async def downloader(update: Update, context: ContextTypes.DEFAULT_TYPE, url: st
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
@@ -198,8 +229,28 @@ async def download_command_handler(update: Update, context: ContextTypes.DEFAULT
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
 
@@ -207,7 +258,7 @@ async def download_command_handler(update: Update, context: ContextTypes.DEFAULT
207
  logger.warning(f"Video {downloaded_file_path} is too large: {file_size / (1024*1024):.2f} MB")
208
  await processing_message.edit_text(
209
  f"⚠️ The downloaded video is too large ({file_size / (1024*1024):.2f} MB) "
210
- "for me to send via Telegram (max ~50MB). I tried to get a smaller version."
211
  )
212
  return # No cleanup here, user might want to access it if bot is self-hosted
213
 
@@ -340,7 +391,7 @@ async def post_init(application: Application) -> None:
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.")
 
45
  """Sends a typing action to indicate the bot is working."""
46
  await context.bot.send_chat_action(chat_id=update.effective_chat.id, action='upload_video')
47
 
48
+ def is_time_like(text: str) -> bool:
49
+ """Basic check if a string looks like a time argument (contains ':' or is all digits)."""
50
+ if not text:
51
+ return False
52
+ return ':' in text or text.isdigit()
53
 
54
  # --- Bot Command Handlers ---
55
  async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
56
  """Sends a welcome message when the /start command is issued."""
57
  user = update.effective_user
58
  welcome_message = (
59
+ f"👋 Hello {user.mention_html()}!\n\n"
60
+ "I'm your video downloading bot.\n"
61
+ "Use `/download <URL> [START_TIME] [END_TIME]` to fetch a video or a segment.\n"
62
+ "Times are optional (e.g., `MM:SS` or `HH:MM:SS`).\n\n"
63
+ "Example (full video): `/download <your_video_url>`\n"
64
+ "Example (segment): `/download <your_video_url> 00:10 00:50`\n\n"
65
+ "Type /help for more detailed information."
66
  )
67
  await update.message.reply_html(welcome_message)
68
 
 
70
  """Sends a help message when the /help command is issued."""
71
  help_text = (
72
  "ℹ️ **How to use me:**\n"
73
+ "Use the `/download` command followed by a video URL.\n"
74
+ "You can optionally specify a start and/or end time for the segment.\n\n"
75
+ "**Formats:**\n"
76
+ "1. `/download <VIDEO_URL>`\n"
77
+ " Downloads the full video.\n\n"
78
+ "2. `/download <VIDEO_URL> <START_TIME>`\n"
79
+ " Downloads from `START_TIME` to the end of the video.\n"
80
+ " Example: `/download <url> 01:20` (starts at 1 min 20 secs)\n\n"
81
+ "3. `/download <VIDEO_URL> <START_TIME> <END_TIME>`\n"
82
+ " Downloads the segment between `START_TIME` and `END_TIME`.\n"
83
+ " Example: `/download <url> 00:30 02:15`\n\n"
84
+ "Time format can be `MM:SS` or `HH:MM:SS` (e.g., `1:23` or `00:01:23`).\n"
85
+ "Use `0` or `00:00` for the beginning if specifying an end time only (e.g. `/download <url> 0 00:55`).\n\n"
86
  "**Supported Sites:**\n"
87
+ "Most sites supported by `yt-dlp` (YouTube, Vimeo, Twitter, etc.).\n\n"
 
88
  "**File Size Limit:**\n"
89
+ "Telegram bots can only send files up to ~50MB. I'll try to get a version under this. "
90
+ "Segments are more likely to fit!"
 
 
91
  )
92
  await update.message.reply_text(help_text, parse_mode=ParseMode.MARKDOWN)
93
 
94
+ async def downloader(update: Update, context: ContextTypes.DEFAULT_TYPE, url: str,
95
+ start_time_str: str, end_time_str: str) -> tuple:
96
  chat_id = update.effective_chat.id
97
 
98
  if not url.startswith(('http://', 'https://')):
 
104
 
105
  downloaded_file_path = None # To store the path of the downloaded file
106
 
107
+ # Get the current event loop to schedule coroutines from the hook
108
+ main_event_loop = asyncio.get_event_loop()
109
+
110
  try:
111
  # yt-dlp options
112
  # We aim for a good quality mp4 file under 50MB.
 
119
  'noplaylist': True, # Download only single video if playlist URL is given
120
  # 'quiet': True, # Suppress yt-dlp console output
121
  'merge_output_format': 'mp4', # Ensure output is mp4 if merging is needed
122
+ 'max_filesize': MAX_FILE_SIZE_MB * 1024 * 1024, # Alternative way to specify max filesize
123
  # 'postprocessors': [{
124
  # 'key': 'FFmpegVideoConvertor',
125
  # 'preferedformat': 'mp4',
126
  # }],
127
  # 'logger': logger, # Send yt-dlp logs to our logger
128
+ 'progress_hooks': [
129
+ lambda d: asyncio.run_coroutine_threadsafe(
130
+ download_progress_hook(d, update, context, processing_message.message_id),
131
+ main_event_loop
132
+ )
133
+ ],
134
+ # 'verbose': True
135
  }
136
 
137
  # Use asyncio.to_thread to run blocking yt-dlp code in a separate thread
 
199
  logger.error(f"yt-dlp generic error for URL {url}: {e}")
200
  await processing_message.edit_text(f"❌ An error occurred during video processing: {str(e)}")
201
  return
202
+ # if start_time_str and end_time_str: # Both start and end time
203
+ # logger.info(f"Segment (start-end): recode=mp4, download_sections, force_keyframes, pp_args for FFmpegVideoConvertor.")
204
+
205
+ # elif start_time_str: # Only start time given
206
+ # logger.info(f"Segment (start-onwards): recode=mp4, download_sections, force_keyframes, pp_args with -ss for FFmpegVideoConvertor.")
207
+
208
+ # else: # Full video download
209
+ # logger.info(f"Full video download requested for {url}")
210
+
211
+ except Exception as e:
212
  logger.error(f"An overarching unexpected error occurred while downloading URL {url}: {e}", exc_info=True)
213
  await processing_message.edit_text("❌ An unexpected error occurred. Please try again later.")
214
  return downloaded_file_path, processing_message
 
229
 
230
  url = context.args[0] # The first argument after /download
231
 
232
+ # Parse optional start and end times
233
+ start_time_str = None
234
+ end_time_str = None
235
+
236
+ if len(context.args) >= 2:
237
+ potential_start_time = context.args[1]
238
+ if is_time_like(potential_start_time): # Basic check
239
+ start_time_str = potential_start_time
240
+ else:
241
+ await update.message.reply_text(f"⚠️ '{potential_start_time}' doesn't look like a valid start time (e.g., MM:SS). Downloading full video or stopping.")
242
+ # Decide if you want to proceed with full download or stop. For now, we'll proceed assuming no valid time.
243
+
244
+ if len(context.args) >= 3 and start_time_str: # Only look for end_time if start_time was plausible
245
+ potential_end_time = context.args[2]
246
+ if is_time_like(potential_end_time):
247
+ end_time_str = potential_end_time
248
+ else:
249
+ await update.message.reply_text(f"⚠️ '{potential_end_time}' doesn't look like a valid end time. Will download from {start_time_str} to end if applicable.")
250
+
251
+ downloaded_file_path, processing_message = await downloader(update, context, url, start_time_str, end_time_str)
252
+
253
  try:
 
254
  if downloaded_file_path and os.path.exists(downloaded_file_path):
255
  file_size = os.path.getsize(downloaded_file_path)
256
 
 
258
  logger.warning(f"Video {downloaded_file_path} is too large: {file_size / (1024*1024):.2f} MB")
259
  await processing_message.edit_text(
260
  f"⚠️ The downloaded video is too large ({file_size / (1024*1024):.2f} MB) "
261
+ f"for me to send via Telegram (max ~{MAX_FILE_SIZE_MB}MB). I tried to get a smaller version."
262
  )
263
  return # No cleanup here, user might want to access it if bot is self-hosted
264
 
 
391
  commands = [
392
  BotCommand("start", "Starts the bot and shows a welcome message."),
393
  BotCommand("help", "Shows the help message with instructions."),
394
+ BotCommand("download", "Downloads a video or segment. Usage: /download <URL> [start] [end]")
395
  ]
396
  await application.bot.set_my_commands(commands)
397
  logger.info("Bot commands have been set programmatically.")