Spaces:
Paused
Paused
Delete helper_func/mux.py
Browse files- helper_func/mux.py +0 -539
helper_func/mux.py
DELETED
|
@@ -1,539 +0,0 @@
|
|
| 1 |
-
from config import Config
|
| 2 |
-
import time
|
| 3 |
-
import re
|
| 4 |
-
import asyncio
|
| 5 |
-
import os
|
| 6 |
-
import logging
|
| 7 |
-
from pyrogram import Client
|
| 8 |
-
from datetime import datetime, timedelta
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
progress_pattern = re.compile(
|
| 12 |
-
r'(frame|fps|size|time|bitrate|speed)\s*\=\s*(\S+)'
|
| 13 |
-
)
|
| 14 |
-
|
| 15 |
-
def parse_progress(line):
|
| 16 |
-
items = {
|
| 17 |
-
key: value for key, value in progress_pattern.findall(line)
|
| 18 |
-
}
|
| 19 |
-
if not items:
|
| 20 |
-
return None
|
| 21 |
-
return items
|
| 22 |
-
|
| 23 |
-
async def readlines(stream):
|
| 24 |
-
pattern = re.compile(br'[\r\n]+')
|
| 25 |
-
|
| 26 |
-
data = bytearray()
|
| 27 |
-
while not stream.at_eof():
|
| 28 |
-
lines = pattern.split(data)
|
| 29 |
-
data[:] = lines.pop(-1)
|
| 30 |
-
|
| 31 |
-
for line in lines:
|
| 32 |
-
yield line
|
| 33 |
-
|
| 34 |
-
data.extend(await stream.read(1024))
|
| 35 |
-
|
| 36 |
-
# Add a dictionary to store encoding progress data for each user
|
| 37 |
-
encoding_progress = {}
|
| 38 |
-
|
| 39 |
-
# Add at the top with other global variables
|
| 40 |
-
active_processes = {} # To track active downloads and encodings per user
|
| 41 |
-
|
| 42 |
-
async def read_stderr(start, msg, process):
|
| 43 |
-
# Get chat_id from the message to track progress per user
|
| 44 |
-
chat_id = msg.chat.id
|
| 45 |
-
|
| 46 |
-
# Initialize progress tracking for this user
|
| 47 |
-
if chat_id not in encoding_progress:
|
| 48 |
-
encoding_progress[chat_id] = {
|
| 49 |
-
'last_update_time': time.time(),
|
| 50 |
-
'total_duration': None,
|
| 51 |
-
'speed_samples': [],
|
| 52 |
-
'max_samples': 10
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
user_progress = encoding_progress[chat_id]
|
| 56 |
-
duration_pattern = re.compile(r'Duration: (\d+):(\d+):(\d+\.\d+)')
|
| 57 |
-
|
| 58 |
-
async for line in readlines(process.stderr):
|
| 59 |
-
line = line.decode('utf-8')
|
| 60 |
-
|
| 61 |
-
# Try to extract duration if we don't have it yet
|
| 62 |
-
if user_progress['total_duration'] is None:
|
| 63 |
-
duration_match = duration_pattern.search(line)
|
| 64 |
-
if duration_match:
|
| 65 |
-
h, m, s = duration_match.groups()
|
| 66 |
-
user_progress['total_duration'] = float(h) * 3600 + float(m) * 60 + float(s)
|
| 67 |
-
|
| 68 |
-
progress = parse_progress(line)
|
| 69 |
-
if progress:
|
| 70 |
-
now = time.time()
|
| 71 |
-
elapsed = now - start
|
| 72 |
-
|
| 73 |
-
# Build basic progress message
|
| 74 |
-
text = '🎬 <b>Encoding in Progress</b>\n\n'
|
| 75 |
-
text += '📊 Size: {}\n'.format(format_size(progress['size']))
|
| 76 |
-
text += '⏱️ Time: {}\n'.format(progress['time'])
|
| 77 |
-
text += '⚡ Speed: {}\n'.format(progress['speed'])
|
| 78 |
-
|
| 79 |
-
# Add estimated time remaining if we can calculate it
|
| 80 |
-
if 'time' in progress and user_progress['total_duration'] is not None:
|
| 81 |
-
# Parse the current timestamp
|
| 82 |
-
time_parts = progress['time'].split(':')
|
| 83 |
-
if len(time_parts) == 3:
|
| 84 |
-
h, m, s = time_parts
|
| 85 |
-
current_time = float(h) * 3600 + float(m) * 60 + float(s)
|
| 86 |
-
|
| 87 |
-
# Calculate progress percentage
|
| 88 |
-
percent_done = min(99.9, (current_time / user_progress['total_duration']) * 100)
|
| 89 |
-
text += '📈 Progress: {:.1f}%\n'.format(percent_done)
|
| 90 |
-
|
| 91 |
-
# Extract speed for better estimation
|
| 92 |
-
if 'speed' in progress:
|
| 93 |
-
speed_match = re.search(r'(\d+\.?\d*)x', progress['speed'])
|
| 94 |
-
if speed_match:
|
| 95 |
-
speed = float(speed_match.group(1))
|
| 96 |
-
|
| 97 |
-
# Add to our speed samples
|
| 98 |
-
user_progress['speed_samples'].append((now, speed, current_time))
|
| 99 |
-
|
| 100 |
-
# Keep only recent samples
|
| 101 |
-
if len(user_progress['speed_samples']) > user_progress['max_samples']:
|
| 102 |
-
user_progress['speed_samples'].pop(0)
|
| 103 |
-
|
| 104 |
-
# Calculate weighted average speed (more recent samples have higher weight)
|
| 105 |
-
if len(user_progress['speed_samples']) >= 3:
|
| 106 |
-
total_weight = 0
|
| 107 |
-
weighted_speed = 0
|
| 108 |
-
|
| 109 |
-
for i, (sample_time, sample_speed, _) in enumerate(user_progress['speed_samples']):
|
| 110 |
-
weight = (i + 1) ** 2
|
| 111 |
-
weighted_speed += sample_speed * weight
|
| 112 |
-
total_weight += weight
|
| 113 |
-
|
| 114 |
-
avg_speed = weighted_speed / total_weight if total_weight > 0 else speed
|
| 115 |
-
|
| 116 |
-
# Calculate remaining time
|
| 117 |
-
remaining_time = (user_progress['total_duration'] - current_time) / avg_speed
|
| 118 |
-
text += '⏳ ETA: {}\n'.format(format_time_duration(remaining_time))
|
| 119 |
-
else:
|
| 120 |
-
# Fallback method if speed isn't available
|
| 121 |
-
if percent_done > 0:
|
| 122 |
-
estimated_total = elapsed / (percent_done / 100)
|
| 123 |
-
remaining = estimated_total - elapsed
|
| 124 |
-
|
| 125 |
-
# Apply a safety factor
|
| 126 |
-
remaining *= 2.5 # Multiply the estimate to be safe
|
| 127 |
-
|
| 128 |
-
if remaining > 0:
|
| 129 |
-
text += 'Estimated time remaining: {} (rough estimate)\n'.format(format_time_duration(remaining))
|
| 130 |
-
|
| 131 |
-
# Update message at regular intervals
|
| 132 |
-
if round(now - user_progress['last_update_time']) >= 5:
|
| 133 |
-
try:
|
| 134 |
-
await msg.edit(text)
|
| 135 |
-
user_progress['last_update_time'] = now
|
| 136 |
-
except:
|
| 137 |
-
pass
|
| 138 |
-
|
| 139 |
-
# Clean up when encoding is done
|
| 140 |
-
if chat_id in encoding_progress:
|
| 141 |
-
del encoding_progress[chat_id]
|
| 142 |
-
|
| 143 |
-
def format_time_duration(seconds):
|
| 144 |
-
"""Format seconds into human readable time (e.g., 2h 30m 5s)"""
|
| 145 |
-
hours = seconds // 3600
|
| 146 |
-
minutes = (seconds % 3600) // 60
|
| 147 |
-
seconds = seconds % 60
|
| 148 |
-
|
| 149 |
-
parts = []
|
| 150 |
-
if hours > 0:
|
| 151 |
-
parts.append(f"{int(hours)}h")
|
| 152 |
-
if minutes > 0:
|
| 153 |
-
parts.append(f"{int(minutes)}m")
|
| 154 |
-
if seconds > 0 or not parts: # include seconds if it's the only non-zero value
|
| 155 |
-
parts.append(f"{int(seconds)}s")
|
| 156 |
-
|
| 157 |
-
return " ".join(parts)
|
| 158 |
-
|
| 159 |
-
def format_size(size_str):
|
| 160 |
-
"""Convert size string to human readable format"""
|
| 161 |
-
try:
|
| 162 |
-
# Remove 'kB' or any other suffix and convert to bytes
|
| 163 |
-
size = float(size_str.replace('kB', '')) * 1024 # Convert kB to bytes
|
| 164 |
-
|
| 165 |
-
for unit in ['B', 'KB', 'MB', 'GB']:
|
| 166 |
-
if size < 1024.0:
|
| 167 |
-
return "{:.2f} {}".format(size, unit)
|
| 168 |
-
size /= 1024.0
|
| 169 |
-
return "{:.2f} TB".format(size)
|
| 170 |
-
except:
|
| 171 |
-
return size_str # Return original string if conversion fails
|
| 172 |
-
|
| 173 |
-
async def softmux_vid(vid_filename, sub_filename, msg):
|
| 174 |
-
chat_id = msg.chat.id
|
| 175 |
-
|
| 176 |
-
# Set active process
|
| 177 |
-
active_processes[chat_id] = 'encoding'
|
| 178 |
-
start = time.time()
|
| 179 |
-
out_location = None # Define outside try block for cleanup
|
| 180 |
-
|
| 181 |
-
try:
|
| 182 |
-
vid = Config.DOWNLOAD_DIR+'/'+vid_filename
|
| 183 |
-
sub = Config.DOWNLOAD_DIR+'/'+sub_filename
|
| 184 |
-
|
| 185 |
-
out_file = '.'.join(vid_filename.split('.')[:-1])
|
| 186 |
-
output = out_file+'1.mkv'
|
| 187 |
-
out_location = Config.DOWNLOAD_DIR+'/'+output
|
| 188 |
-
sub_ext = sub_filename.split('.')[-1].lower()
|
| 189 |
-
|
| 190 |
-
# Set the appropriate subtitle codec based on file extension
|
| 191 |
-
if sub_ext == 'vtt':
|
| 192 |
-
subtitle_codec = 'webvtt'
|
| 193 |
-
else:
|
| 194 |
-
subtitle_codec = sub_ext
|
| 195 |
-
|
| 196 |
-
command = [
|
| 197 |
-
'ffmpeg', '-hide_banner',
|
| 198 |
-
'-fflags', '+genpts',
|
| 199 |
-
'-i',vid,
|
| 200 |
-
'-i',sub,
|
| 201 |
-
'-map','1:0','-map','0',
|
| 202 |
-
'-disposition:s:0','default',
|
| 203 |
-
'-c:v','copy',
|
| 204 |
-
'-c:a','copy',
|
| 205 |
-
'-c:s',subtitle_codec,
|
| 206 |
-
'-y',out_location
|
| 207 |
-
]
|
| 208 |
-
|
| 209 |
-
process = await asyncio.create_subprocess_exec(
|
| 210 |
-
*command,
|
| 211 |
-
stdout=asyncio.subprocess.PIPE,
|
| 212 |
-
stderr=asyncio.subprocess.PIPE,
|
| 213 |
-
)
|
| 214 |
-
|
| 215 |
-
await asyncio.wait([
|
| 216 |
-
asyncio.create_task(read_stderr(start, msg, process)),
|
| 217 |
-
asyncio.create_task(process.wait())
|
| 218 |
-
])
|
| 219 |
-
|
| 220 |
-
if process.returncode == 0:
|
| 221 |
-
elapsed_time = round(time.time() - start)
|
| 222 |
-
await msg.edit(
|
| 223 |
-
f'✅ <b>Encoding Completed Successfully!</b>\n\n'
|
| 224 |
-
f'⏱️ Time Taken: {format_time_duration(elapsed_time)}'
|
| 225 |
-
)
|
| 226 |
-
await asyncio.sleep(1)
|
| 227 |
-
return output
|
| 228 |
-
else:
|
| 229 |
-
await msg.edit(
|
| 230 |
-
'❌ <b>Encoding Failed</b>\n\n'
|
| 231 |
-
'An error occurred during the encoding process.\n'
|
| 232 |
-
'Please try again or use different settings.'
|
| 233 |
-
)
|
| 234 |
-
# Clean up failed encoding file
|
| 235 |
-
if out_location and os.path.exists(out_location):
|
| 236 |
-
try:
|
| 237 |
-
os.remove(out_location)
|
| 238 |
-
logging.info(f"Removed failed encoding file: {output}")
|
| 239 |
-
except Exception as e:
|
| 240 |
-
logging.error(f"Error removing failed encoding file: {e}")
|
| 241 |
-
return False
|
| 242 |
-
|
| 243 |
-
except Exception as e:
|
| 244 |
-
logging.error(f"Error during encoding: {e}")
|
| 245 |
-
# Clean up failed encoding file
|
| 246 |
-
if out_location and os.path.exists(out_location):
|
| 247 |
-
try:
|
| 248 |
-
os.remove(out_location)
|
| 249 |
-
logging.info(f"Removed failed encoding file: {output}")
|
| 250 |
-
except Exception as e:
|
| 251 |
-
logging.error(f"Error removing failed encoding file: {e}")
|
| 252 |
-
await msg.edit(
|
| 253 |
-
'❌ <b>Encoding Error</b>\n\n'
|
| 254 |
-
'An unexpected error occurred.\n'
|
| 255 |
-
'Please try again or contact support.'
|
| 256 |
-
)
|
| 257 |
-
return False
|
| 258 |
-
finally:
|
| 259 |
-
# Remove active process status
|
| 260 |
-
if chat_id in active_processes:
|
| 261 |
-
del active_processes[chat_id]
|
| 262 |
-
|
| 263 |
-
# Define resolution options
|
| 264 |
-
RESOLUTIONS = {
|
| 265 |
-
'8k': '7680x4320',
|
| 266 |
-
'4k': '3840x2160',
|
| 267 |
-
'1440p': '2560x1440',
|
| 268 |
-
'1080p': '1920x1080',
|
| 269 |
-
'720p': '1280x720',
|
| 270 |
-
'480p': '854x480',
|
| 271 |
-
'360p': '640x360',
|
| 272 |
-
'240p': '426x240',
|
| 273 |
-
'144p': '256x144',
|
| 274 |
-
'original': 'original'
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
# Define codec options
|
| 278 |
-
CODECS = {
|
| 279 |
-
'h264': {
|
| 280 |
-
'ffmpeg_codec': 'libx264',
|
| 281 |
-
'description': 'H.264 (AVC) - Widely compatible'
|
| 282 |
-
},
|
| 283 |
-
'h265': {
|
| 284 |
-
'ffmpeg_codec': 'libx265',
|
| 285 |
-
'description': 'H.265 (HEVC) - Better compression, smaller file size'
|
| 286 |
-
}
|
| 287 |
-
}
|
| 288 |
-
|
| 289 |
-
async def hardmux_vid(vid_filename, sub_filename, msg, crf='23', preset='medium', resolution='original', codec='h264', fps='original', custom_filename=None, audio_bitrate='128k', audio_track='all'):
|
| 290 |
-
chat_id = msg.chat.id
|
| 291 |
-
|
| 292 |
-
# Set active process
|
| 293 |
-
active_processes[chat_id] = 'encoding'
|
| 294 |
-
start = time.time()
|
| 295 |
-
out_location = None # Define outside try block for cleanup
|
| 296 |
-
|
| 297 |
-
try:
|
| 298 |
-
vid = Config.DOWNLOAD_DIR + '/' + vid_filename
|
| 299 |
-
sub = None if not sub_filename else Config.DOWNLOAD_DIR + '/' + sub_filename
|
| 300 |
-
|
| 301 |
-
# Get the base output file name
|
| 302 |
-
out_file = '.'.join(vid_filename.split('.')[:-1])
|
| 303 |
-
|
| 304 |
-
# If custom filename is provided, extract its extension
|
| 305 |
-
if custom_filename:
|
| 306 |
-
# Extract extension from custom filename
|
| 307 |
-
custom_ext = custom_filename.split('.')[-1].lower()
|
| 308 |
-
# Use the custom extension but keep the same pattern
|
| 309 |
-
output = out_file + '_VidSub.' + custom_ext
|
| 310 |
-
else:
|
| 311 |
-
# Use default mp4 extension
|
| 312 |
-
output = out_file + '_VidSub.mp4'
|
| 313 |
-
|
| 314 |
-
out_location = Config.DOWNLOAD_DIR + '/' + output
|
| 315 |
-
|
| 316 |
-
# Base command without filters
|
| 317 |
-
filter_complex = ''
|
| 318 |
-
|
| 319 |
-
# Add resolution scaling if not original
|
| 320 |
-
if resolution != 'original':
|
| 321 |
-
filter_complex = f"scale={RESOLUTIONS[resolution]}"
|
| 322 |
-
|
| 323 |
-
# Add frame rate filter if not original
|
| 324 |
-
if fps != 'original':
|
| 325 |
-
fps_filter = f"fps={fps}"
|
| 326 |
-
filter_complex = f"{fps_filter},{filter_complex}" if filter_complex else fps_filter
|
| 327 |
-
|
| 328 |
-
# Add subtitle filter if subtitle file exists
|
| 329 |
-
if sub:
|
| 330 |
-
# Get subtitle extension
|
| 331 |
-
sub_ext = sub_filename.split('.')[-1].lower()
|
| 332 |
-
|
| 333 |
-
# Set up font configuration
|
| 334 |
-
font_dir = "./fonts" # Directory containing fonts
|
| 335 |
-
default_font = "HelveticaRounded LT Std BdCn"
|
| 336 |
-
|
| 337 |
-
# Different handling based on subtitle format
|
| 338 |
-
if sub_ext == 'ass' or sub_ext == 'ssa':
|
| 339 |
-
subtitle_filter = f"ass='{sub}':fontsdir='{font_dir}'"
|
| 340 |
-
else:
|
| 341 |
-
subtitle_filter = f"subtitles='{sub}':fontsdir='{font_dir}':force_style='FontName={default_font},FontSize=18'"
|
| 342 |
-
|
| 343 |
-
# Add subtitle filter to the chain
|
| 344 |
-
filter_complex = f"{filter_complex},{subtitle_filter}" if filter_complex else subtitle_filter
|
| 345 |
-
|
| 346 |
-
# Get the codec command
|
| 347 |
-
video_codec = CODECS[codec]['ffmpeg_codec']
|
| 348 |
-
|
| 349 |
-
# Base command
|
| 350 |
-
command = [
|
| 351 |
-
'ffmpeg', '-hide_banner',
|
| 352 |
-
'-fflags', '+genpts',
|
| 353 |
-
'-i', vid,
|
| 354 |
-
]
|
| 355 |
-
|
| 356 |
-
# Add filter complex if we have any filters
|
| 357 |
-
if filter_complex:
|
| 358 |
-
command.extend(['-vf', filter_complex])
|
| 359 |
-
|
| 360 |
-
# Add encoding parameters
|
| 361 |
-
command.extend([
|
| 362 |
-
'-c:v', video_codec,
|
| 363 |
-
'-crf', crf,
|
| 364 |
-
'-preset', preset,
|
| 365 |
-
'-c:a', 'aac',
|
| 366 |
-
'-b:a', audio_bitrate,
|
| 367 |
-
])
|
| 368 |
-
|
| 369 |
-
# Handle audio track mapping
|
| 370 |
-
if audio_track == 'all':
|
| 371 |
-
# Map all audio tracks
|
| 372 |
-
command.extend([
|
| 373 |
-
'-map', '0:v:0', # First video stream
|
| 374 |
-
'-map', '0:a', # All audio streams
|
| 375 |
-
])
|
| 376 |
-
else:
|
| 377 |
-
# Map specific audio track (convert from 1-based to 0-based indexing)
|
| 378 |
-
audio_stream_index = int(audio_track) - 1
|
| 379 |
-
command.extend([
|
| 380 |
-
'-map', '0:v:0', # First video stream
|
| 381 |
-
'-map', f'0:a:{audio_stream_index}', # Specific audio stream
|
| 382 |
-
])
|
| 383 |
-
|
| 384 |
-
# Add remaining parameters
|
| 385 |
-
command.extend([
|
| 386 |
-
'-map_metadata', '-1', # Remove all metadata
|
| 387 |
-
'-sn', # Remove all subtitle streams
|
| 388 |
-
])
|
| 389 |
-
|
| 390 |
-
# Add codec-specific parameters
|
| 391 |
-
if codec == 'h265':
|
| 392 |
-
command.extend([
|
| 393 |
-
'-x265-params', 'log-level=error',
|
| 394 |
-
'-tag:v', 'hvc1'
|
| 395 |
-
])
|
| 396 |
-
|
| 397 |
-
# Add output file
|
| 398 |
-
command.extend(['-y', out_location])
|
| 399 |
-
|
| 400 |
-
process = await asyncio.create_subprocess_exec(
|
| 401 |
-
*command,
|
| 402 |
-
stdout=asyncio.subprocess.PIPE,
|
| 403 |
-
stderr=asyncio.subprocess.PIPE,
|
| 404 |
-
)
|
| 405 |
-
|
| 406 |
-
await asyncio.wait([
|
| 407 |
-
asyncio.create_task(read_stderr(start, msg, process)),
|
| 408 |
-
asyncio.create_task(process.wait())
|
| 409 |
-
])
|
| 410 |
-
|
| 411 |
-
if process.returncode == 0:
|
| 412 |
-
elapsed_time = round(time.time() - start)
|
| 413 |
-
await msg.edit(
|
| 414 |
-
f'✅ <b>Encoding Completed Successfully!</b>\n\n'
|
| 415 |
-
f'⏱️ Time Taken: {format_time_duration(elapsed_time)}'
|
| 416 |
-
)
|
| 417 |
-
await asyncio.sleep(1)
|
| 418 |
-
return output
|
| 419 |
-
else:
|
| 420 |
-
await msg.edit(
|
| 421 |
-
'❌ <b>Encoding Failed</b>\n\n'
|
| 422 |
-
'An error occurred during the encoding process.\n'
|
| 423 |
-
'Please try again or use different settings.'
|
| 424 |
-
)
|
| 425 |
-
# Clean up failed encoding file
|
| 426 |
-
if out_location and os.path.exists(out_location):
|
| 427 |
-
try:
|
| 428 |
-
os.remove(out_location)
|
| 429 |
-
logging.info(f"Removed failed encoding file: {output}")
|
| 430 |
-
except Exception as e:
|
| 431 |
-
logging.error(f"Error removing failed encoding file: {e}")
|
| 432 |
-
return False
|
| 433 |
-
|
| 434 |
-
except Exception as e:
|
| 435 |
-
logging.error(f"Error during encoding: {e}")
|
| 436 |
-
# Clean up failed encoding file
|
| 437 |
-
if out_location and os.path.exists(out_location):
|
| 438 |
-
try:
|
| 439 |
-
os.remove(out_location)
|
| 440 |
-
logging.info(f"Removed failed encoding file: {output}")
|
| 441 |
-
except Exception as e:
|
| 442 |
-
logging.error(f"Error removing failed encoding file: {e}")
|
| 443 |
-
await msg.edit(
|
| 444 |
-
'❌ <b>Encoding Error</b>\n\n'
|
| 445 |
-
'An unexpected error occurred.\n'
|
| 446 |
-
'Please try again or contact support.'
|
| 447 |
-
)
|
| 448 |
-
return False
|
| 449 |
-
finally:
|
| 450 |
-
# Remove active process status
|
| 451 |
-
if chat_id in active_processes:
|
| 452 |
-
del active_processes[chat_id]
|
| 453 |
-
|
| 454 |
-
async def check_active_process(client, chat_id, message_id):
|
| 455 |
-
"""Check if user has an active process and send appropriate message"""
|
| 456 |
-
if chat_id in active_processes:
|
| 457 |
-
process_type = active_processes[chat_id]
|
| 458 |
-
process_messages = {
|
| 459 |
-
'download': '⚠️ Please wait until the current download completes!',
|
| 460 |
-
'encoding': '⚠️ Please wait until the current encoding completes!',
|
| 461 |
-
'upload': '⚠️ Please wait until the current upload completes!'
|
| 462 |
-
}
|
| 463 |
-
await client.edit_message_text(
|
| 464 |
-
chat_id=chat_id,
|
| 465 |
-
message_id=message_id,
|
| 466 |
-
text=process_messages.get(process_type, '⚠️ Another process is running!')
|
| 467 |
-
)
|
| 468 |
-
return True
|
| 469 |
-
return False
|
| 470 |
-
|
| 471 |
-
# Create a function to log and set process status
|
| 472 |
-
def set_process_status(chat_id, status):
|
| 473 |
-
"""Set and log process status"""
|
| 474 |
-
active_processes[chat_id] = status
|
| 475 |
-
logging.info(f"Setting process status for {chat_id} to {status}")
|
| 476 |
-
|
| 477 |
-
# Create a function to log and clear process status
|
| 478 |
-
def clear_process_status(chat_id):
|
| 479 |
-
"""Clear and log process status"""
|
| 480 |
-
if chat_id in active_processes:
|
| 481 |
-
status = active_processes[chat_id]
|
| 482 |
-
del active_processes[chat_id]
|
| 483 |
-
logging.info(f"Cleared process status {status} for {chat_id}")
|
| 484 |
-
|
| 485 |
-
# Add this function to clean up old files
|
| 486 |
-
async def cleanup_old_files():
|
| 487 |
-
"""
|
| 488 |
-
Recursively delete files older than 12 hours from download directory and its subdirectories.
|
| 489 |
-
Also removes empty subdirectories after file cleanup.
|
| 490 |
-
"""
|
| 491 |
-
while True:
|
| 492 |
-
try:
|
| 493 |
-
current_time = datetime.now()
|
| 494 |
-
timeout = timedelta(hours=12)
|
| 495 |
-
main_dir = Config.DOWNLOAD_DIR
|
| 496 |
-
|
| 497 |
-
logging.info(f"Starting scheduled cleanup of files older than 12 hours")
|
| 498 |
-
|
| 499 |
-
# First pass: Delete old files recursively
|
| 500 |
-
for root, dirs, files in os.walk(main_dir, topdown=False):
|
| 501 |
-
# Process each file in the current directory
|
| 502 |
-
for filename in files:
|
| 503 |
-
try:
|
| 504 |
-
filepath = os.path.join(root, filename)
|
| 505 |
-
|
| 506 |
-
# Get file's last modification time
|
| 507 |
-
file_time = datetime.fromtimestamp(os.path.getmtime(filepath))
|
| 508 |
-
|
| 509 |
-
# If file is older than 12 hours
|
| 510 |
-
if current_time - file_time > timeout:
|
| 511 |
-
try:
|
| 512 |
-
os.remove(filepath)
|
| 513 |
-
logging.info(f"Deleted old file: {filepath} (Age: {current_time - file_time})")
|
| 514 |
-
except Exception as e:
|
| 515 |
-
logging.error(f"Error deleting old file {filepath}: {e}")
|
| 516 |
-
except Exception as e:
|
| 517 |
-
logging.error(f"Error processing file {filename}: {e}")
|
| 518 |
-
|
| 519 |
-
# Second pass: Remove empty subdirectories
|
| 520 |
-
for root, dirs, files in os.walk(main_dir, topdown=False):
|
| 521 |
-
# Skip the main download directory
|
| 522 |
-
if root == main_dir:
|
| 523 |
-
continue
|
| 524 |
-
|
| 525 |
-
try:
|
| 526 |
-
# Check if directory is empty
|
| 527 |
-
if not os.listdir(root):
|
| 528 |
-
os.rmdir(root)
|
| 529 |
-
logging.info(f"Removed empty directory: {root}")
|
| 530 |
-
except Exception as e:
|
| 531 |
-
logging.error(f"Error removing empty directory {root}: {e}")
|
| 532 |
-
|
| 533 |
-
logging.info(f"Completed scheduled cleanup")
|
| 534 |
-
|
| 535 |
-
except Exception as e:
|
| 536 |
-
logging.error(f"Error in cleanup task: {e}")
|
| 537 |
-
|
| 538 |
-
# Run every hour
|
| 539 |
-
await asyncio.sleep(3600)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|