271 lines
8.9 KiB
Python
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
|
|
|