Add source
This commit is contained in:
270
bot/modules/media_loader/sender.py
Normal file
270
bot/modules/media_loader/sender.py
Normal file
@@ -0,0 +1,270 @@
|
||||
"""
|
||||
Sending files to users
|
||||
"""
|
||||
from pathlib import Path
|
||||
from pyrogram import Client
|
||||
from pyrogram.types import Message
|
||||
from typing import Optional
|
||||
import aiohttp
|
||||
import aiofiles
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def download_thumbnail(url: str, output_path: str) -> Optional[str]:
|
||||
"""
|
||||
Download thumbnail from URL
|
||||
|
||||
Args:
|
||||
url: Thumbnail URL
|
||||
output_path: Path to save
|
||||
|
||||
Returns:
|
||||
Path to downloaded file or None
|
||||
"""
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as response:
|
||||
if response.status == 200:
|
||||
async with aiofiles.open(output_path, 'wb') as f:
|
||||
async for chunk in response.content.iter_chunked(8192):
|
||||
await f.write(chunk)
|
||||
return output_path
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to download thumbnail: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def send_file_to_user(
|
||||
client: Client,
|
||||
chat_id: int,
|
||||
file_path: str,
|
||||
caption: Optional[str] = None,
|
||||
thumbnail: Optional[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Send file to user
|
||||
|
||||
Args:
|
||||
client: Pyrogram client
|
||||
chat_id: Chat ID
|
||||
file_path: Path to file
|
||||
caption: File caption
|
||||
thumbnail: Path to thumbnail or URL
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
thumbnail_path = None
|
||||
try:
|
||||
file = Path(file_path)
|
||||
|
||||
if not file.exists():
|
||||
logger.error(f"File not found: {file_path}")
|
||||
return False
|
||||
|
||||
# Maximum file size for Telegram (2GB)
|
||||
max_size = 2 * 1024 * 1024 * 1024
|
||||
file_size = file.stat().st_size
|
||||
|
||||
# If file is larger than 2GB, split into parts
|
||||
if file_size > max_size:
|
||||
logger.info(f"File too large ({file_size / (1024*1024*1024):.2f} GB), splitting into parts...")
|
||||
return await send_large_file_in_parts(
|
||||
client, chat_id, file_path, caption, thumbnail
|
||||
)
|
||||
|
||||
# Process thumbnail (can be URL or file path)
|
||||
if thumbnail:
|
||||
if thumbnail.startswith(('http://', 'https://')):
|
||||
# This is a URL - download thumbnail
|
||||
thumbnail_path = f"downloads/thumb_{file.stem}.jpg"
|
||||
downloaded = await download_thumbnail(thumbnail, thumbnail_path)
|
||||
if downloaded:
|
||||
thumbnail_path = downloaded
|
||||
else:
|
||||
thumbnail_path = None # Don't use thumbnail if download failed
|
||||
else:
|
||||
# This is a file path
|
||||
thumb_file = Path(thumbnail)
|
||||
if thumb_file.exists():
|
||||
thumbnail_path = thumbnail
|
||||
else:
|
||||
thumbnail_path = None
|
||||
|
||||
# Determine file type
|
||||
if file.suffix.lower() in ['.mp4', '.avi', '.mov', '.mkv', '.webm']:
|
||||
# Video - if no thumbnail, try to generate one
|
||||
if not thumbnail_path:
|
||||
from bot.utils.file_processor import generate_thumbnail
|
||||
thumbnail_path_temp = f"downloads/thumb_{file.stem}.jpg"
|
||||
if await generate_thumbnail(str(file), thumbnail_path_temp):
|
||||
thumbnail_path = thumbnail_path_temp
|
||||
|
||||
await client.send_video(
|
||||
chat_id=chat_id,
|
||||
video=str(file),
|
||||
caption=caption,
|
||||
thumb=thumbnail_path
|
||||
)
|
||||
elif file.suffix.lower() in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
|
||||
# Image
|
||||
await client.send_photo(
|
||||
chat_id=chat_id,
|
||||
photo=str(file),
|
||||
caption=caption
|
||||
)
|
||||
elif file.suffix.lower() in ['.mp3', '.wav', '.ogg', '.m4a', '.flac']:
|
||||
# Audio
|
||||
await client.send_audio(
|
||||
chat_id=chat_id,
|
||||
audio=str(file),
|
||||
caption=caption,
|
||||
thumb=thumbnail_path
|
||||
)
|
||||
else:
|
||||
# Document
|
||||
await client.send_document(
|
||||
chat_id=chat_id,
|
||||
document=str(file),
|
||||
caption=caption,
|
||||
thumb=thumbnail_path
|
||||
)
|
||||
|
||||
logger.info(f"File sent to user {chat_id}: {file_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending file: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
finally:
|
||||
# Delete temporary thumbnail if it was downloaded
|
||||
if thumbnail_path and thumbnail_path.startswith("downloads/thumb_"):
|
||||
try:
|
||||
thumb_file = Path(thumbnail_path)
|
||||
if thumb_file.exists():
|
||||
thumb_file.unlink()
|
||||
logger.debug(f"Temporary thumbnail deleted: {thumbnail_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete temporary thumbnail: {e}")
|
||||
|
||||
|
||||
async def send_large_file_in_parts(
|
||||
client: Client,
|
||||
chat_id: int,
|
||||
file_path: str,
|
||||
caption: Optional[str] = None,
|
||||
thumbnail: Optional[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Send large file in parts
|
||||
|
||||
Args:
|
||||
client: Pyrogram client
|
||||
chat_id: Chat ID
|
||||
file_path: Path to file
|
||||
caption: File caption
|
||||
thumbnail: Path to thumbnail or URL
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
from bot.utils.file_splitter import split_file, delete_file_parts, get_part_info
|
||||
|
||||
parts = []
|
||||
try:
|
||||
# Split file into parts
|
||||
parts = await split_file(file_path)
|
||||
part_info = get_part_info(parts)
|
||||
total_parts = part_info["total_parts"]
|
||||
|
||||
logger.info(f"Sending file in parts: {total_parts} parts")
|
||||
|
||||
# Send each part
|
||||
for part_num, part_path in enumerate(parts, 1):
|
||||
part_caption = None
|
||||
if caption:
|
||||
part_caption = f"{caption}\n\n📦 Part {part_num} of {total_parts}"
|
||||
else:
|
||||
part_caption = f"📦 Part {part_num} of {total_parts}"
|
||||
|
||||
# Send thumbnail only with first part
|
||||
part_thumbnail = thumbnail if part_num == 1 else None
|
||||
|
||||
try:
|
||||
await client.send_document(
|
||||
chat_id=chat_id,
|
||||
document=str(part_path),
|
||||
caption=part_caption,
|
||||
thumb=part_thumbnail
|
||||
)
|
||||
logger.info(f"Sent part {part_num}/{total_parts}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending part {part_num}: {e}", exc_info=True)
|
||||
# Continue sending other parts
|
||||
continue
|
||||
|
||||
logger.info(f"File sent in parts to user {chat_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending large file in parts: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
finally:
|
||||
# Delete file parts after sending
|
||||
if parts:
|
||||
await delete_file_parts(parts)
|
||||
logger.debug("File parts deleted")
|
||||
|
||||
|
||||
async def delete_file(file_path: str, max_retries: int = 3) -> bool:
|
||||
"""
|
||||
Delete file with retries
|
||||
|
||||
Args:
|
||||
file_path: Path to file
|
||||
max_retries: Maximum number of retries
|
||||
|
||||
Returns:
|
||||
True if successful
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
file = Path(file_path)
|
||||
if not file.exists():
|
||||
return True # File already doesn't exist
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
file.unlink()
|
||||
logger.info(f"File deleted: {file_path}")
|
||||
return True
|
||||
except PermissionError as e:
|
||||
# File may be locked by another process
|
||||
if attempt < max_retries - 1:
|
||||
wait_time = (attempt + 1) * 0.5 # Exponential backoff
|
||||
logger.warning(f"File locked, retrying in {wait_time}s: {file_path}")
|
||||
await asyncio.sleep(wait_time)
|
||||
else:
|
||||
logger.error(f"Failed to delete file after {max_retries} attempts (locked): {file_path}")
|
||||
# Add to cleanup queue for background cleanup task
|
||||
from bot.utils.file_cleanup import add_file_to_cleanup_queue
|
||||
add_file_to_cleanup_queue(str(file_path))
|
||||
return False
|
||||
except Exception as e:
|
||||
if attempt < max_retries - 1:
|
||||
wait_time = (attempt + 1) * 0.5
|
||||
logger.warning(f"Error deleting file, retrying in {wait_time}s: {e}")
|
||||
await asyncio.sleep(wait_time)
|
||||
else:
|
||||
logger.error(f"Error deleting file after {max_retries} attempts: {e}", exc_info=True)
|
||||
# Add to cleanup queue for background cleanup task
|
||||
from bot.utils.file_cleanup import add_file_to_cleanup_queue
|
||||
add_file_to_cleanup_queue(str(file_path))
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user