# Thunder/utils/custom_dl.py import asyncio from typing import Any, AsyncGenerator, Awaitable, Callable, Dict, Optional from pyrogram import Client from pyrogram.errors import FloodWait from pyrogram.types import Message from Thunder.server.exceptions import FileNotFound from Thunder.utils.file_properties import get_media from Thunder.utils.logger import logger from Thunder.vars import Var class ByteStreamer: __slots__ = ('client', 'chat_id') def __init__(self, client: Client) -> None: self.client = client self.chat_id = int(Var.BIN_CHANNEL) async def get_message(self, message_id: int) -> Message: while True: try: message = await self.client.get_messages(self.chat_id, message_id) break except FloodWait as e: logger.debug(f"FloodWait: get_message, sleep {e.value}s") await asyncio.sleep(e.value) except Exception as e: logger.debug(f"Error fetching message {message_id}: {e}", exc_info=True) raise FileNotFound(f"Message {message_id} not found") from e if not message or not message.media: raise FileNotFound(f"Message {message_id} not found") return message async def stream_file( self, media_ref: int | Message, offset: int = 0, limit: int = 0, fallback_message_id: int | None = None, on_fallback_message: Optional[Callable[[Message], Awaitable[None]]] = None ) -> AsyncGenerator[bytes, None]: chunk_offset = offset // (1024 * 1024) chunk_limit = 0 if limit > 0: chunk_limit = ((limit + (1024 * 1024) - 1) // (1024 * 1024)) + 1 refs: list[int | Message] = [media_ref] media_id = media_ref if isinstance(media_ref, int) else None if isinstance(media_ref, Message): media_id = getattr(media_ref, "id", getattr(media_ref, "message_id", None)) if fallback_message_id is not None and (media_id is None or fallback_message_id != media_id): refs.append(fallback_message_id) last_error: Exception | None = None for ref in refs: started_stream = False while True: try: target = await self.get_message(ref) if isinstance(ref, int) else ref if ( on_fallback_message is not None and fallback_message_id is not None and ref == fallback_message_id and isinstance(target, Message) ): await on_fallback_message(target) async for chunk in self.client.stream_media( target, offset=chunk_offset, limit=chunk_limit ): started_stream = True yield chunk return except FloodWait as e: logger.debug(f"FloodWait: stream_file, sleep {e.value}s") await asyncio.sleep(e.value) except Exception as e: last_error = e logger.debug(f"Error streaming media ref {ref}: {e}", exc_info=True) if started_stream: raise break raise FileNotFound(f"Unable to stream file: {last_error}") def get_file_info_sync(self, message: Message) -> Dict[str, Any]: media = get_media(message) if not media: return {"message_id": message.id, "error": "No media"} media_type = type(media).__name__.lower() file_name = getattr(media, 'file_name', None) mime_type = getattr(media, 'mime_type', None) if not file_name: ext_map = { "photo": "jpg", "audio": "mp3", "voice": "ogg", "video": "mp4", "animation": "mp4", "videonote": "mp4", "sticker": "webp", } ext = ext_map.get(media_type, "bin") file_name = f"Thunder_{message.id}.{ext}" if not mime_type: mime_map = { "photo": "image/jpeg", "voice": "audio/ogg", "videonote": "video/mp4", } mime_type = mime_map.get(media_type) return { "message_id": message.id, "file_size": getattr(media, 'file_size', 0) or 0, "file_name": file_name, "mime_type": mime_type, "unique_id": getattr(media, 'file_unique_id', None), "media_type": media_type } async def get_file_info(self, message_id: int) -> Dict[str, Any]: try: message = await self.get_message(message_id) return self.get_file_info_sync(message) except Exception as e: logger.debug(f"Error getting file info for {message_id}: {e}", exc_info=True) return {"message_id": message_id, "error": str(e)}