| | import asyncio |
| | import logging |
| | from typing import Dict, Union |
| | from FileStream.bot import work_loads |
| | from pyrogram import Client, utils, raw |
| | from .file_properties import get_file_ids |
| | from pyrogram.session import Session, Auth |
| | from pyrogram.errors import AuthBytesInvalid |
| | from pyrogram.file_id import FileId, FileType, ThumbnailSource |
| | from pyrogram.types import Message |
| |
|
| | class ByteStreamer: |
| | def __init__(self, client: Client): |
| | self.clean_timer = 30 * 60 |
| | self.client: Client = client |
| | self.cached_file_ids: Dict[str, FileId] = {} |
| | asyncio.create_task(self.clean_cache()) |
| |
|
| | async def get_file_properties(self, db_id: str, multi_clients) -> FileId: |
| | """ |
| | Returns the properties of a media of a specific message in a FIleId class. |
| | if the properties are cached, then it'll return the cached results. |
| | or it'll generate the properties from the Message ID and cache them. |
| | """ |
| | if not db_id in self.cached_file_ids: |
| | logging.debug("Before Calling generate_file_properties") |
| | await self.generate_file_properties(db_id, multi_clients) |
| | logging.debug(f"Cached file properties for file with ID {db_id}") |
| | return self.cached_file_ids[db_id] |
| | |
| | async def generate_file_properties(self, db_id: str, multi_clients) -> FileId: |
| | """ |
| | Generates the properties of a media file on a specific message. |
| | returns ths properties in a FIleId class. |
| | """ |
| | logging.debug("Before calling get_file_ids") |
| | file_id = await get_file_ids(self.client, db_id, multi_clients, Message) |
| | logging.debug(f"Generated file ID and Unique ID for file with ID {db_id}") |
| | self.cached_file_ids[db_id] = file_id |
| | logging.debug(f"Cached media file with ID {db_id}") |
| | return self.cached_file_ids[db_id] |
| |
|
| | async def generate_media_session(self, client: Client, file_id: FileId) -> Session: |
| | """ |
| | Generates the media session for the DC that contains the media file. |
| | This is required for getting the bytes from Telegram servers. |
| | """ |
| |
|
| | media_session = client.media_sessions.get(file_id.dc_id, None) |
| |
|
| | if media_session is None: |
| | if file_id.dc_id != await client.storage.dc_id(): |
| | media_session = Session( |
| | client, |
| | file_id.dc_id, |
| | await Auth( |
| | client, file_id.dc_id, await client.storage.test_mode() |
| | ).create(), |
| | await client.storage.test_mode(), |
| | is_media=True, |
| | ) |
| | await media_session.start() |
| |
|
| | for _ in range(6): |
| | exported_auth = await client.invoke( |
| | raw.functions.auth.ExportAuthorization(dc_id=file_id.dc_id) |
| | ) |
| |
|
| | try: |
| | await media_session.invoke( |
| | raw.functions.auth.ImportAuthorization( |
| | id=exported_auth.id, bytes=exported_auth.bytes |
| | ) |
| | ) |
| | break |
| | except AuthBytesInvalid: |
| | logging.debug( |
| | f"Invalid authorization bytes for DC {file_id.dc_id}" |
| | ) |
| | continue |
| | else: |
| | await media_session.stop() |
| | raise AuthBytesInvalid |
| | else: |
| | media_session = Session( |
| | client, |
| | file_id.dc_id, |
| | await client.storage.auth_key(), |
| | await client.storage.test_mode(), |
| | is_media=True, |
| | ) |
| | await media_session.start() |
| | logging.debug(f"Created media session for DC {file_id.dc_id}") |
| | client.media_sessions[file_id.dc_id] = media_session |
| | else: |
| | logging.debug(f"Using cached media session for DC {file_id.dc_id}") |
| | return media_session |
| |
|
| |
|
| | @staticmethod |
| | async def get_location(file_id: FileId) -> Union[raw.types.InputPhotoFileLocation, |
| | raw.types.InputDocumentFileLocation, |
| | raw.types.InputPeerPhotoFileLocation,]: |
| | """ |
| | Returns the file location for the media file. |
| | """ |
| | file_type = file_id.file_type |
| |
|
| | if file_type == FileType.CHAT_PHOTO: |
| | if file_id.chat_id > 0: |
| | peer = raw.types.InputPeerUser( |
| | user_id=file_id.chat_id, access_hash=file_id.chat_access_hash |
| | ) |
| | else: |
| | if file_id.chat_access_hash == 0: |
| | peer = raw.types.InputPeerChat(chat_id=-file_id.chat_id) |
| | else: |
| | peer = raw.types.InputPeerChannel( |
| | channel_id=utils.get_channel_id(file_id.chat_id), |
| | access_hash=file_id.chat_access_hash, |
| | ) |
| |
|
| | location = raw.types.InputPeerPhotoFileLocation( |
| | peer=peer, |
| | volume_id=file_id.volume_id, |
| | local_id=file_id.local_id, |
| | big=file_id.thumbnail_source == ThumbnailSource.CHAT_PHOTO_BIG, |
| | ) |
| | elif file_type == FileType.PHOTO: |
| | location = raw.types.InputPhotoFileLocation( |
| | id=file_id.media_id, |
| | access_hash=file_id.access_hash, |
| | file_reference=file_id.file_reference, |
| | thumb_size=file_id.thumbnail_size, |
| | ) |
| | else: |
| | location = raw.types.InputDocumentFileLocation( |
| | id=file_id.media_id, |
| | access_hash=file_id.access_hash, |
| | file_reference=file_id.file_reference, |
| | thumb_size=file_id.thumbnail_size, |
| | ) |
| | return location |
| |
|
| | async def yield_file( |
| | self, |
| | file_id: FileId, |
| | index: int, |
| | offset: int, |
| | first_part_cut: int, |
| | last_part_cut: int, |
| | part_count: int, |
| | chunk_size: int, |
| | ) -> Union[str, None]: |
| | """ |
| | Custom generator that yields the bytes of the media file. |
| | Modded from <https://github.com/eyaadh/megadlbot_oss/blob/master/mega/telegram/utils/custom_download.py#L20> |
| | Thanks to Eyaadh <https://github.com/eyaadh> |
| | """ |
| | client = self.client |
| | work_loads[index] += 1 |
| | logging.debug(f"Starting to yielding file with client {index}.") |
| | media_session = await self.generate_media_session(client, file_id) |
| |
|
| | current_part = 1 |
| |
|
| | location = await self.get_location(file_id) |
| |
|
| | try: |
| | r = await media_session.invoke( |
| | raw.functions.upload.GetFile( |
| | location=location, offset=offset, limit=chunk_size |
| | ), |
| | ) |
| | if isinstance(r, raw.types.upload.File): |
| | while True: |
| | chunk = r.bytes |
| | if not chunk: |
| | break |
| | elif part_count == 1: |
| | yield chunk[first_part_cut:last_part_cut] |
| | elif current_part == 1: |
| | yield chunk[first_part_cut:] |
| | elif current_part == part_count: |
| | yield chunk[:last_part_cut] |
| | else: |
| | yield chunk |
| |
|
| | current_part += 1 |
| | offset += chunk_size |
| |
|
| | if current_part > part_count: |
| | break |
| |
|
| | r = await media_session.invoke( |
| | raw.functions.upload.GetFile( |
| | location=location, offset=offset, limit=chunk_size |
| | ), |
| | ) |
| | except (TimeoutError, AttributeError): |
| | pass |
| | finally: |
| | logging.debug(f"Finished yielding file with {current_part} parts.") |
| | work_loads[index] -= 1 |
| |
|
| | |
| | async def clean_cache(self) -> None: |
| | """ |
| | function to clean the cache to reduce memory usage |
| | """ |
| | while True: |
| | await asyncio.sleep(self.clean_timer) |
| | self.cached_file_ids.clear() |
| | logging.debug("Cleaned the cache") |
| |
|