Files
tg_loader/bot/modules/media_loader/sender.py
2025-12-04 00:12:56 +03:00

271 lines
8.9 KiB
Python

"""
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