diff --git a/api_server.py b/api_server.py index 4508fb8..414d001 100644 --- a/api_server.py +++ b/api_server.py @@ -1,11 +1,15 @@ import logging import json +import re from contextlib import asynccontextmanager -from fastapi import FastAPI, HTTPException, Response -from fastapi.responses import HTMLResponse +from fastapi import FastAPI, HTTPException, Response, BackgroundTasks +from fastapi.responses import HTMLResponse, FileResponse from telegram_client import TelegramClient from config import get_settings +import mimetypes +import os +from pyrogram import errors logger = logging.getLogger(__name__) client = TelegramClient() @@ -68,4 +72,46 @@ async def health_check(): return { "status": "ok", "connected": client.client.is_connected - } \ No newline at end of file + } + +@app.get("/media/{file_id}") +async def get_media( + file_id: str, + background_tasks: BackgroundTasks, + response: Response +): + try: + file_path = await client.download_media_file(file_id) + + # Additional safety checks + if not os.path.exists(file_path): + raise HTTPException(status_code=404, detail="Temporary file not found") + + # Determine media type from file extension + media_type, _ = mimetypes.guess_type(file_path) + if not media_type: + media_type = "application/octet-stream" + + # Add cleanup task + background_tasks.add_task(lambda: os.remove(file_path) if os.path.exists(file_path) else None) + + return FileResponse( + path=file_path, + media_type=media_type, + headers={ + "Content-Disposition": f"inline; filename={os.path.basename(file_path)}" + } + ) + + except HTTPException: + # Пробрасываем уже сформированные HTTPException как есть + raise + except errors.RPCError as e: + logger.error(f"Media request RPC error {file_id}: {type(e).__name__} - {str(e)}") + raise HTTPException(status_code=404, detail="File not found in Telegram") + except Exception as e: + logger.error(f"Media request failed {file_id}: {type(e).__name__} - {str(e)}") + raise HTTPException( + status_code=500, + detail=f"Internal server error: {str(e)}" + ) \ No newline at end of file diff --git a/telegram_client.py b/telegram_client.py index 88dc95f..21b487c 100644 --- a/telegram_client.py +++ b/telegram_client.py @@ -2,7 +2,6 @@ import logging from pyrogram import Client, errors from pyrogram.types import Message from config import get_settings -import base64 import os import re @@ -69,42 +68,11 @@ class TelegramClient: # Extract HTML from entities raw_text = message.text.html if message.text else "" - # Process text entities for links - #text_parts = [] - #last_offset = 0 - #entities = getattr(message, "entities", None) - #if entities and isinstance(entities, list): - # # Sort entities by offset - # entities = sorted(entities, key=lambda x: x.offset) - # - # for entity in entities: - # if entity.offset > last_offset: - # text_parts.append(raw_text[last_offset:entity.offset]) - # - # entity_text = raw_text[entity.offset:entity.offset+entity.length] - # - # if entity.type == "text_link": - # text_parts.append(f"{entity_text}") - # elif entity.type == "url": - # text_parts.append(f"{entity_text}") - # else: - # text_parts.append(entity_text) - # - # last_offset = entity.offset + entity.length - # - # # Add remaining text - # if last_offset < len(raw_text): - # text_parts.append(raw_text[last_offset:]) - # - # processed_text = "".join(text_parts) - #else: - processed_text = raw_text - # Replace only URLs not already in tags processed_text = re.sub( r'(?)', r'\1', - processed_text + raw_text ) # Parse media and add video markers @@ -143,15 +111,29 @@ class TelegramClient: if media: previews = [] for m in media: - if m.get('thumbnail'): - media_type = m.get('type', 'media').upper() - overlay = f"
{media_type}
" + media_type = m.get('type', 'media') + file_id = m.get('url') + + if media_type in ['video', 'animation']: + # Direct video/animation embedding + previews.append( + f"
" + f"" + f"
" + ) + elif m.get('thumbnail_file_id'): + # Image preview for other types + overlay = f"
{media_type.upper()}
" previews.append( f"
" - f"" + f"" f"{overlay}" f"
" ) + if previews: previews_container = f"
{''.join(previews)}
" processed_text = previews_container + processed_text @@ -194,7 +176,9 @@ class TelegramClient: if hasattr(media_obj, "file_id"): file_id = media_obj.file_id - thumbnail_base64 = None + thumbnail_file_id = None + if media_obj.thumbs: + thumbnail_file_id = media_obj.thumbs[0].file_id # Special cases if media_type == "photofile": @@ -202,43 +186,20 @@ class TelegramClient: elif media_type == "webpage": return {"type": "webpage", "url": getattr(media_obj, "url", "")} elif media_type == "video": - # Download and encode thumbnail - if media_obj.thumbs: - try: - thumb_path = await self.client.download_media(media_obj.thumbs[0].file_id) - with open(thumb_path, "rb") as image_file: - thumbnail_base64 = base64.b64encode(image_file.read()).decode("utf-8") - os.remove(thumb_path) - except Exception as e: - logger.error(f"Thumbnail error: {e.__class__.__name__} - {str(e)}") - return { "type": "video", "url": file_id, "size": media_obj.file_size, "duration": media_obj.duration, - "thumbnail": thumbnail_base64 + "thumbnail_file_id": thumbnail_file_id } elif media_type == "animation": - # Process animation (GIF/Video) - logger.debug(f"Animation details: {media_obj}") - logger.debug(f"Animation file_id: {media_obj.file_id}") - logger.debug(f"Animation thumbs: {media_obj.thumbs}") - if media_obj.thumbs: - try: - thumb_path = await self.client.download_media(media_obj.thumbs[0].file_id) - with open(thumb_path, "rb") as image_file: - thumbnail_base64 = base64.b64encode(image_file.read()).decode("utf-8") - os.remove(thumb_path) - except Exception as e: - logger.error(f"Animation thumbnail error: {e.__class__.__name__} - {str(e)}") - return { "type": "animation", "url": media_obj.file_id, "size": media_obj.file_size, "duration": media_obj.duration, - "thumbnail": thumbnail_base64 + "thumbnail_file_id": thumbnail_file_id } elif media_type == "messagemediatype": # Handle Telegram's internal media type @@ -246,29 +207,18 @@ class TelegramClient: logger.debug(f"Resolved MessageMediaType: {actual_type}") return {"type": actual_type} elif media_type == "photo": - # Download and encode full photo - if hasattr(media_obj, "file_id"): - try: - # Download full size photo - photo_path = await self.client.download_media(media_obj.file_id) - with open(photo_path, "rb") as image_file: - thumbnail_base64 = base64.b64encode(image_file.read()).decode("utf-8") - os.remove(photo_path) - except Exception as e: - logger.error(f"Photo download error: {e.__class__.__name__} - {str(e)}") - return { "type": "photo", "url": file_id, "size": media_obj.file_size, - "thumbnail": thumbnail_base64 + "thumbnail_file_id": file_id } return { "type": media_type, "url": file_id, "size": getattr(media_obj, "file_size", None), - "thumbnail": None + "thumbnail_file_id": thumbnail_file_id } async def _parse_animation(self, animation_obj) -> dict: @@ -277,26 +227,16 @@ class TelegramClient: logger.error("Invalid animation object") return {"type": "animation", "error": "invalid_object"} - try: - # Check if thumbs exist and have items - if not animation_obj.thumbs or len(animation_obj.thumbs) == 0: - raise ValueError("No thumbnails available") - - thumb_file_id = animation_obj.thumbs[0].file_id - thumb_path = await self.client.download_media(thumb_file_id) - with open(thumb_path, "rb") as f: - thumbnail = base64.b64encode(f.read()).decode() - os.remove(thumb_path) - except Exception as e: - logger.error(f"Animation error: {str(e)}") - thumbnail = None + thumbnail_file_id = None + if animation_obj.thumbs and len(animation_obj.thumbs) > 0: + thumbnail_file_id = animation_obj.thumbs[0].file_id return { "type": "animation", "url": getattr(animation_obj, "file_id", ""), "size": animation_obj.file_size, "duration": animation_obj.duration, - "thumbnail": thumbnail + "thumbnail_file_id": thumbnail_file_id } def _generate_title(self, raw_text: str) -> str: @@ -491,4 +431,30 @@ class TelegramClient: "height": thumb.height, "file_size": thumb.file_size } for thumb in media_obj.thumbs - ] \ No newline at end of file + ] + + async def download_media_file(self, file_id: str) -> str: + """Download media file and return local path""" + try: + if not self.client.is_connected: + await self.start() + + # Validate file_id format before downloading + if len(file_id) < 20 or not re.match(r'^[a-zA-Z0-9_-]+$', file_id): + logger.error(f"Invalid file_id format: {file_id}") + raise ValueError("Invalid file identifier format") + + return await self.client.download_media( + file_id, + in_memory=False, + block=True + ) + except errors.RPCError as e: + logger.error(f"Telegram media download error {file_id}: {type(e).__name__} - {str(e)}") + raise + except ValueError as e: + logger.error(f"Invalid file_id {file_id}: {str(e)}") + raise + except Exception as e: + logger.error(f"Media download failed {file_id}: {type(e).__name__} - {str(e)}") + raise \ No newline at end of file