sanch1tx commited on
Commit
040e255
Β·
verified Β·
1 Parent(s): 08c754e

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +84 -82
main.py CHANGED
@@ -15,12 +15,17 @@ from telethon.errors import FloodWaitError
15
  from telethon.tl.types import DocumentAttributeVideo
16
 
17
  # πŸ”Ή Configuration
 
18
  API_ID = int(os.environ.get("API_ID", 20687211))
19
  API_HASH = os.environ.get("API_HASH", "4523f58b045175baaeaf1ba29733f31c")
20
  BOT_TOKEN = os.environ.get("BOT_TOKEN", "8318388017:AAGCgx_XJqBcyOpdt-5DlMNTS7wU9Qk6J4c")
 
21
  BOT_OWNER_ID = int(os.environ.get("BOT_OWNER_ID", 7014665654))
22
 
23
- DOWNLOAD_DIR = "downloads" # Safe download location
 
 
 
24
 
25
  # πŸ”Ή Configure Logging
26
  logging.basicConfig(
@@ -33,13 +38,14 @@ logging.getLogger('telethon').setLevel(logging.WARNING)
33
 
34
  # πŸ”Ή Initialize
35
  try:
36
- bot = TelegramClient("MegaNZBot", API_ID, API_HASH)
 
37
  mega = Megatools()
38
  except Exception as e:
39
  logger.critical(f"Failed to initialize bot or Megatools: {e}")
40
  exit(1)
41
 
42
- # --- Core Helper Functions ---
43
 
44
  def progress_bar(percentage, bar_length=20):
45
  """Generates a text-based progress bar."""
@@ -84,53 +90,52 @@ def clean_directory(directory):
84
 
85
  async def get_video_duration(file_path):
86
  """Tries to get video duration using ffprobe."""
87
- command = [
88
- "ffprobe", "-v", "quiet", "-print_format", "json",
89
- "-show_format", file_path
90
- ]
91
  try:
 
 
 
 
92
  process = await asyncio.create_subprocess_exec(
93
  *command,
94
  stdout=asyncio.subprocess.PIPE,
95
  stderr=asyncio.subprocess.PIPE
96
  )
97
  stdout, stderr = await process.communicate()
 
98
  if process.returncode == 0:
99
  data = json.loads(stdout)
100
  return int(float(data['format']['duration']))
101
  else:
102
- logger.warning(f"ffprobe failed: {stderr.decode()}")
103
  return 0
104
  except FileNotFoundError:
105
- logger.warning("ffprobe not found. Cannot get video duration.")
106
  return 0
107
  except Exception as e:
108
- logger.error(f"Error getting video duration: {e}")
109
  return 0
110
 
111
- async def upload_file(chat, file_path, filename, status_msg, status_prefix=""):
 
 
 
112
  """
113
- Handles the entire upload process for a single file:
114
- - Generates caption
115
- - Shows upload progress
116
- - Gets video duration
117
- - Sends the file
118
- - Deletes the local file
119
  """
120
  try:
121
  size_bytes = os.path.getsize(file_path)
122
  size_mb = round(size_bytes / (1024 * 1024), 2)
123
  ext = os.path.splitext(filename)[1].lower() or ".file"
124
-
125
  caption = (
126
  f"πŸ“‚ **File:** `{filename}`\n"
127
  f"πŸ“¦ **Size:** {size_mb} MB\n"
128
  f"πŸ“„ **Extension:** `{ext}`"
129
  )
130
-
131
- status_text_prefix = f"πŸ“€ **Uploading {status_prefix}:**\n`{filename}`"
132
- await edit_message(status_msg, f"{status_text_prefix}\n{progress_bar(0)} 0%")
133
 
 
 
134
  last_update_time = 0
135
  async def progress_callback(current, total):
136
  nonlocal last_update_time
@@ -139,144 +144,142 @@ async def upload_file(chat, file_path, filename, status_msg, status_prefix=""):
139
  last_update_time = current_time
140
  percent = int(current * 100 / total)
141
  bar = progress_bar(percent)
142
- await edit_message(status_msg, f"{status_text_prefix}\n{bar} {percent}%")
143
 
 
144
  is_video = ext in [".mp4", ".mov", ".mkv", ".avi"]
145
- is_image = ext in [".jpg", ".jpeg", ".png", ".webp"]
146
 
147
  attributes = []
148
  if is_video:
149
  duration = await get_video_duration(file_path)
150
  attributes.append(DocumentAttributeVideo(duration=duration, w=0, h=0, supports_streaming=True))
151
-
152
  await bot.send_file(
153
  chat,
154
  file_path,
155
  caption=caption,
156
  attributes=attributes or None,
157
- spoiler=True, # Send all files with spoiler
158
- force_document=not (is_video or is_image),
159
  progress_callback=progress_callback
160
  )
161
- logger.info(f"Uploaded {filename}.")
162
 
 
 
 
163
  except Exception as e:
164
  logger.error(f"Failed to upload file {file_path}: {e}", exc_info=True)
165
- await status_msg.reply(f"⚠️ Failed to upload file: `{filename}`. Error: {e}")
 
 
166
  finally:
167
- if os.path.exists(file_path):
168
- os.remove(file_path) # Delete file after upload
169
-
170
- # --- End of Helper Functions ---
171
-
172
 
173
  @bot.on(events.NewMessage(pattern="(?i)https://mega\\.nz/"))
174
  async def handle_mega(event):
175
  message_text = event.message.text
176
  chat = event.chat_id
177
  user_id = event.sender_id
178
-
179
  status_msg = await event.reply("πŸ” **Processing your link...**")
180
-
181
  match = re.search(r"(https://mega\.nz/[^\s)\]]+)", message_text)
 
182
  if not match:
183
- logger.warning(f"Could not parse a mega.nz link from: {message_text}")
184
  await edit_message(status_msg, "❌ **Error:** Could not find a valid mega.nz link.")
185
  await delete_message(status_msg, delay=10)
186
  return
187
-
188
  url = match.group(1).strip()
189
 
190
- # Use a unique, safe download directory for this job
191
  job_id = str(uuid.uuid4())
192
  download_dir = os.path.join(DOWNLOAD_DIR, f"user_{user_id}", job_id)
193
  os.makedirs(download_dir, exist_ok=True)
194
 
195
  try:
196
- if "/folder/" in url:
197
- # --- FOLDER LOGIC (Download all, then upload one-by-one) ---
 
 
198
  logger.info(f"User {user_id} sent folder link: {url}")
199
  await edit_message(status_msg, "πŸ“ **Folder link detected.**\n⬇️ Downloading all files... (This may take a while)")
200
-
201
  try:
202
- # This is the blocking call that works in your environment
 
203
  mega.download(url, path=download_dir)
204
  logger.info(f"Folder downloaded to: {download_dir}")
205
  except Exception as e:
206
  logger.error(f"Folder download failed for {url}: {e}", exc_info=True)
207
- await edit_message(status_msg, f"❌ **Error:** Folder download failed:\n`{e}`")
208
- await delete_message(status_msg, delay=10)
209
- clean_directory(download_dir)
210
- return
211
 
212
- await edit_message(status_msg, "βœ… **Download complete!**\nPreparing to upload files...")
213
 
214
- uploaded_count = 0
215
  files_to_upload = []
216
  for root, dirs, files in os.walk(download_dir):
217
  for file in files:
218
  files_to_upload.append((os.path.join(root, file), file))
219
-
220
  if not files_to_upload:
221
  await edit_message(status_msg, "ℹ️ The folder appears to be empty.")
222
  await delete_message(status_msg, delay=10)
223
- clean_directory(download_dir)
224
  return
225
 
226
  await edit_message(status_msg, f"Found {len(files_to_upload)} files. Starting upload...")
227
-
228
- for i, (file_path, filename) in enumerate(files_to_upload):
229
- # Use the helper function to upload
230
- await upload_file(
231
- chat,
232
- file_path,
233
- filename,
234
- status_msg,
235
- status_prefix=f"file {i+1}/{len(files_to_upload)}"
236
- )
237
- uploaded_count += 1
238
- # The helper function now handles file deletion
239
-
240
  await edit_message(status_msg, f"βœ… **Upload complete!**\nSuccessfully sent {uploaded_count} file(s).")
241
- await delete_message(status_msg, delay=10)
242
 
243
  else:
244
  # --- SINGLE FILE LOGIC ---
245
  logger.info(f"User {user_id} sent file link: {url}")
246
  await edit_message(status_msg, "⏳ **Fetching file info...**")
 
 
 
 
 
 
 
247
 
248
- filename = mega.filename(url)
249
  if not filename:
250
- logger.warning(f"Could not fetch filename for: {url}")
251
- await edit_message(status_msg, "❌ **Error:** Could not fetch filename. The link might be invalid.")
252
- await delete_message(status_msg, delay=10)
253
  return
254
 
255
- temp_path = os.path.join(download_dir, filename)
256
  await edit_message(status_msg, f"⬇️ **Downloading:**\n`{filename}`")
257
 
258
  try:
259
- # Use download for single files as it's reliable
260
- mega.download(url, path=temp_path)
261
- logger.info(f"Downloaded to: {temp_path}")
262
  except Exception as e:
263
- logger.error(f"Download failed for {url}: {e}", exc_info=True)
264
- await edit_message(status_msg, f"❌ **Error:** Download failed:\n`{e}`")
265
- await delete_message(status_msg, delay=10)
266
- return # temp_path will be cleaned in finally
267
 
268
- # Use the upload helper function
269
- await upload_file(chat, temp_path, filename, status_msg)
270
- await delete_message(status_msg, delay=0) # Delete status msg immediately
 
271
 
272
  except Exception as e:
273
- logger.error(f"Error processing file {url}: {e}", exc_info=True)
274
  await edit_message(status_msg, f"⚠️ **An unexpected error occurred:**\n`{e}`")
275
  await delete_message(status_msg, delay=10)
 
276
  finally:
277
  # Clean up the entire job directory
278
  clean_directory(download_dir)
279
 
 
280
 
281
  @bot.on(events.NewMessage(pattern="/start"))
282
  async def start(event):
@@ -304,11 +307,10 @@ async def help_command(event):
304
  "**How to use:**\n"
305
  "1. Send me any public `mega.nz` file link.\n"
306
  "2. Send me any public `mega.nz` folder link.\n"
307
- "3. I will download the file(s) and upload them to this chat.\n\n"
308
  "**Features:**\n"
309
  "βœ… Upload progress.\n"
310
  "βœ… Supports both files and folders.\n"
311
- "βœ… Folder files are sent 1-by-1.\n"
312
  "βœ… Gets video duration (if `ffprobe` is installed).\n"
313
  "βœ… Cleans up files after upload to save space."
314
  )
 
15
  from telethon.tl.types import DocumentAttributeVideo
16
 
17
  # πŸ”Ή Configuration
18
+ # Best practice: Use environment variables, with fallbacks for easy testing
19
  API_ID = int(os.environ.get("API_ID", 20687211))
20
  API_HASH = os.environ.get("API_HASH", "4523f58b045175baaeaf1ba29733f31c")
21
  BOT_TOKEN = os.environ.get("BOT_TOKEN", "8318388017:AAGCgx_XJqBcyOpdt-5DlMNTS7wU9Qk6J4c")
22
+ # Set this env variable to your own Telegram User ID
23
  BOT_OWNER_ID = int(os.environ.get("BOT_OWNER_ID", 7014665654))
24
 
25
+ # --- File Management ---
26
+ # We use a /data directory which is created in the Dockerfile for persistent storage
27
+ SESSION_FILE = "/data/MegaNZBot"
28
+ DOWNLOAD_DIR = "downloads" # This will be created inside /app
29
 
30
  # πŸ”Ή Configure Logging
31
  logging.basicConfig(
 
38
 
39
  # πŸ”Ή Initialize
40
  try:
41
+ # Use the /data path for the session file to fix permission errors in Docker
42
+ bot = TelegramClient(SESSION_FILE, API_ID, API_HASH)
43
  mega = Megatools()
44
  except Exception as e:
45
  logger.critical(f"Failed to initialize bot or Megatools: {e}")
46
  exit(1)
47
 
48
+ # --- Helper Functions ---
49
 
50
  def progress_bar(percentage, bar_length=20):
51
  """Generates a text-based progress bar."""
 
90
 
91
  async def get_video_duration(file_path):
92
  """Tries to get video duration using ffprobe."""
 
 
 
 
93
  try:
94
+ command = [
95
+ "ffprobe", "-v", "quiet", "-print_format", "json",
96
+ "-show_format", file_path
97
+ ]
98
  process = await asyncio.create_subprocess_exec(
99
  *command,
100
  stdout=asyncio.subprocess.PIPE,
101
  stderr=asyncio.subprocess.PIPE
102
  )
103
  stdout, stderr = await process.communicate()
104
+
105
  if process.returncode == 0:
106
  data = json.loads(stdout)
107
  return int(float(data['format']['duration']))
108
  else:
109
+ logger.warning(f"ffprobe failed (duration): {stderr.decode()}")
110
  return 0
111
  except FileNotFoundError:
112
+ logger.warning("ffprobe not found. Cannot get video duration. (Did you install ffmpeg?)")
113
  return 0
114
  except Exception as e:
115
+ logger.error(f"Error in get_video_duration: {e}")
116
  return 0
117
 
118
+ # --- End of Helper Functions ---
119
+
120
+
121
+ async def upload_file(chat, status_msg, file_path, filename, file_index, total_files):
122
  """
123
+ Handles the upload process for a single file, including progress,
124
+ captions, and cleanup.
 
 
 
 
125
  """
126
  try:
127
  size_bytes = os.path.getsize(file_path)
128
  size_mb = round(size_bytes / (1024 * 1024), 2)
129
  ext = os.path.splitext(filename)[1].lower() or ".file"
130
+
131
  caption = (
132
  f"πŸ“‚ **File:** `{filename}`\n"
133
  f"πŸ“¦ **Size:** {size_mb} MB\n"
134
  f"πŸ“„ **Extension:** `{ext}`"
135
  )
 
 
 
136
 
137
+ await edit_message(status_msg, f"πŸ“€ **Uploading file {file_index}/{total_files}:**\n`{filename}`\n{progress_bar(0)} 0%")
138
+
139
  last_update_time = 0
140
  async def progress_callback(current, total):
141
  nonlocal last_update_time
 
144
  last_update_time = current_time
145
  percent = int(current * 100 / total)
146
  bar = progress_bar(percent)
147
+ await edit_message(status_msg, f"πŸ“€ **Uploading file {file_index}/{total_files}:**\n`{filename}`\n{bar} {percent}%")
148
 
149
+ # Handle file types
150
  is_video = ext in [".mp4", ".mov", ".mkv", ".avi"]
 
151
 
152
  attributes = []
153
  if is_video:
154
  duration = await get_video_duration(file_path)
155
  attributes.append(DocumentAttributeVideo(duration=duration, w=0, h=0, supports_streaming=True))
156
+
157
  await bot.send_file(
158
  chat,
159
  file_path,
160
  caption=caption,
161
  attributes=attributes or None,
162
+ spoiler=True,
163
+ force_document=not is_video, # Send as document if not a video
164
  progress_callback=progress_callback
165
  )
 
166
 
167
+ logger.info(f"Uploaded {filename}.")
168
+ return True
169
+
170
  except Exception as e:
171
  logger.error(f"Failed to upload file {file_path}: {e}", exc_info=True)
172
+ await bot.send_message(chat, f"⚠️ Failed to upload file: `{filename}`. Error: {e}")
173
+ return False
174
+
175
  finally:
176
+ # Clean up individual file after upload
177
+ if os.path.exists(file_path):
178
+ os.remove(file_path)
 
 
179
 
180
  @bot.on(events.NewMessage(pattern="(?i)https://mega\\.nz/"))
181
  async def handle_mega(event):
182
  message_text = event.message.text
183
  chat = event.chat_id
184
  user_id = event.sender_id
185
+
186
  status_msg = await event.reply("πŸ” **Processing your link...**")
187
+
188
  match = re.search(r"(https://mega\.nz/[^\s)\]]+)", message_text)
189
+
190
  if not match:
 
191
  await edit_message(status_msg, "❌ **Error:** Could not find a valid mega.nz link.")
192
  await delete_message(status_msg, delay=10)
193
  return
194
+
195
  url = match.group(1).strip()
196
 
197
+ # Sandboxed directory for this specific job
198
  job_id = str(uuid.uuid4())
199
  download_dir = os.path.join(DOWNLOAD_DIR, f"user_{user_id}", job_id)
200
  os.makedirs(download_dir, exist_ok=True)
201
 
202
  try:
203
+ is_folder = "/folder/" in url
204
+
205
+ if is_folder:
206
+ # --- FOLDER LOGIC ---
207
  logger.info(f"User {user_id} sent folder link: {url}")
208
  await edit_message(status_msg, "πŸ“ **Folder link detected.**\n⬇️ Downloading all files... (This may take a while)")
209
+
210
  try:
211
+ # This is a blocking call. It will download the entire folder.
212
+ # This is the only method that works in your environment.
213
  mega.download(url, path=download_dir)
214
  logger.info(f"Folder downloaded to: {download_dir}")
215
  except Exception as e:
216
  logger.error(f"Folder download failed for {url}: {e}", exc_info=True)
217
+ raise Exception(f"Folder download failed: {e}")
 
 
 
218
 
219
+ await edit_message(status_msg, "βœ… **Download complete!**\nPreparing to upload...")
220
 
221
+ # --- Upload 1-by-1 ---
222
  files_to_upload = []
223
  for root, dirs, files in os.walk(download_dir):
224
  for file in files:
225
  files_to_upload.append((os.path.join(root, file), file))
226
+
227
  if not files_to_upload:
228
  await edit_message(status_msg, "ℹ️ The folder appears to be empty.")
229
  await delete_message(status_msg, delay=10)
 
230
  return
231
 
232
  await edit_message(status_msg, f"Found {len(files_to_upload)} files. Starting upload...")
233
+
234
+ uploaded_count = 0
235
+ for i, (file_path, file_name) in enumerate(files_to_upload):
236
+ if await upload_file(chat, status_msg, file_path, file_name, i+1, len(files_to_upload)):
237
+ uploaded_count += 1
238
+
 
 
 
 
 
 
 
239
  await edit_message(status_msg, f"βœ… **Upload complete!**\nSuccessfully sent {uploaded_count} file(s).")
 
240
 
241
  else:
242
  # --- SINGLE FILE LOGIC ---
243
  logger.info(f"User {user_id} sent file link: {url}")
244
  await edit_message(status_msg, "⏳ **Fetching file info...**")
245
+
246
+ try:
247
+ filename = mega.filename(url)
248
+ except MegaError as e:
249
+ logger.error(f"MegaError (filename): {e}", exc_info=True)
250
+ await edit_message(status_msg, f"❌ **Error:** Link seems invalid or expired.\n`{e}`")
251
+ return
252
 
 
253
  if not filename:
254
+ await edit_message(status_msg, "❌ **Error:** Could not fetch filename.")
 
 
255
  return
256
 
257
+ file_path = os.path.join(download_dir, filename)
258
  await edit_message(status_msg, f"⬇️ **Downloading:**\n`{filename}`")
259
 
260
  try:
261
+ # Blocking call to download the single file
262
+ mega.download(url, path=file_path)
263
+ logger.info(f"File downloaded to: {file_path}")
264
  except Exception as e:
265
+ logger.error(f"File download failed for {url}: {e}", exc_info=True)
266
+ raise Exception(f"File download failed: {e}")
 
 
267
 
268
+ await upload_file(chat, status_msg, file_path, filename, 1, 1)
269
+ await edit_message(status_msg, "βœ… **Upload complete!**")
270
+
271
+ await delete_message(status_msg, delay=10)
272
 
273
  except Exception as e:
274
+ logger.error(f"An unexpected error occurred processing {url}: {e}", exc_info=True)
275
  await edit_message(status_msg, f"⚠️ **An unexpected error occurred:**\n`{e}`")
276
  await delete_message(status_msg, delay=10)
277
+
278
  finally:
279
  # Clean up the entire job directory
280
  clean_directory(download_dir)
281
 
282
+ # --- Standard Bot Commands ---
283
 
284
  @bot.on(events.NewMessage(pattern="/start"))
285
  async def start(event):
 
307
  "**How to use:**\n"
308
  "1. Send me any public `mega.nz` file link.\n"
309
  "2. Send me any public `mega.nz` folder link.\n"
310
+ "3. I will download the file(s) and upload them directly to this chat.\n\n"
311
  "**Features:**\n"
312
  "βœ… Upload progress.\n"
313
  "βœ… Supports both files and folders.\n"
 
314
  "βœ… Gets video duration (if `ffprobe` is installed).\n"
315
  "βœ… Cleans up files after upload to save space."
316
  )