mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-07 16:46:48 +08:00
feat: add download locking and flood wait handling in TelegramAdapter
This commit is contained in:
@@ -6,7 +6,7 @@ import os
|
|||||||
import struct
|
import struct
|
||||||
import time
|
import time
|
||||||
from models import StorageAdapter
|
from models import StorageAdapter
|
||||||
from telethon import TelegramClient, utils
|
from telethon import TelegramClient, errors, utils
|
||||||
from telethon.crypto import AuthKey
|
from telethon.crypto import AuthKey
|
||||||
from telethon.sessions import StringSession
|
from telethon.sessions import StringSession
|
||||||
from telethon.tl import types
|
from telethon.tl import types
|
||||||
@@ -90,6 +90,7 @@ class TelegramAdapter:
|
|||||||
|
|
||||||
self._client: TelegramClient | None = None
|
self._client: TelegramClient | None = None
|
||||||
self._client_lock = asyncio.Lock()
|
self._client_lock = asyncio.Lock()
|
||||||
|
self._download_lock = asyncio.Lock()
|
||||||
self._message_cache: Dict[int, Tuple[float, object]] = {}
|
self._message_cache: Dict[int, Tuple[float, object]] = {}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -229,6 +230,19 @@ class TelegramAdapter:
|
|||||||
def _get_message_media(message):
|
def _get_message_media(message):
|
||||||
return message.document or message.video or message.photo
|
return message.document or message.video or message.photo
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _flood_wait_http_exception(exc: errors.FloodWaitError):
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
seconds = int(getattr(exc, "seconds", 0) or 0)
|
||||||
|
if seconds > 0:
|
||||||
|
return HTTPException(
|
||||||
|
status_code=429,
|
||||||
|
detail=f"Telegram 请求过于频繁,请等待 {seconds} 秒后重试",
|
||||||
|
headers={"Retry-After": str(seconds)},
|
||||||
|
)
|
||||||
|
return HTTPException(status_code=429, detail="Telegram 请求过于频繁,请稍后重试")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_message_file_size(message, media) -> int:
|
def _get_message_file_size(message, media) -> int:
|
||||||
file_meta = message.file
|
file_meta = message.file
|
||||||
@@ -310,7 +324,7 @@ class TelegramAdapter:
|
|||||||
"size": size,
|
"size": size,
|
||||||
"mtime": int(message.date.timestamp()),
|
"mtime": int(message.date.timestamp()),
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"has_thumbnail": self._message_has_thumbnail(message),
|
"has_thumbnail": False,
|
||||||
})
|
})
|
||||||
finally:
|
finally:
|
||||||
if client.is_connected():
|
if client.is_connected():
|
||||||
@@ -349,8 +363,13 @@ class TelegramAdapter:
|
|||||||
if not message or not self._get_message_media(message):
|
if not message or not self._get_message_media(message):
|
||||||
raise FileNotFoundError(f"在频道 {self.chat_id} 中未找到消息ID为 {message_id} 的文件")
|
raise FileNotFoundError(f"在频道 {self.chat_id} 中未找到消息ID为 {message_id} 的文件")
|
||||||
|
|
||||||
file_bytes = await client.download_media(message, file=bytes)
|
try:
|
||||||
return file_bytes
|
async with self._download_lock:
|
||||||
|
file_bytes = await client.download_media(message, file=bytes)
|
||||||
|
return file_bytes
|
||||||
|
except errors.FloodWaitError as exc:
|
||||||
|
await self._disconnect_shared_client()
|
||||||
|
raise self._flood_wait_http_exception(exc)
|
||||||
|
|
||||||
async def read_file_range(self, root: str, rel: str, start: int, end: Optional[int] = None) -> bytes:
|
async def read_file_range(self, root: str, rel: str, start: int, end: Optional[int] = None) -> bytes:
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
@@ -379,22 +398,27 @@ class TelegramAdapter:
|
|||||||
|
|
||||||
limit = end - start + 1
|
limit = end - start + 1
|
||||||
data = bytearray()
|
data = bytearray()
|
||||||
async for chunk in client.iter_download(
|
try:
|
||||||
media,
|
async with self._download_lock:
|
||||||
offset=start,
|
async for chunk in client.iter_download(
|
||||||
request_size=self._download_chunk_size,
|
media,
|
||||||
chunk_size=self._download_chunk_size,
|
offset=start,
|
||||||
file_size=file_size or None,
|
request_size=self._download_chunk_size,
|
||||||
):
|
chunk_size=self._download_chunk_size,
|
||||||
if not chunk:
|
file_size=file_size or None,
|
||||||
continue
|
):
|
||||||
need = limit - len(data)
|
if not chunk:
|
||||||
if need <= 0:
|
continue
|
||||||
break
|
need = limit - len(data)
|
||||||
data.extend(chunk[:need])
|
if need <= 0:
|
||||||
if len(data) >= limit:
|
break
|
||||||
break
|
data.extend(chunk[:need])
|
||||||
return bytes(data)
|
if len(data) >= limit:
|
||||||
|
break
|
||||||
|
return bytes(data)
|
||||||
|
except errors.FloodWaitError as exc:
|
||||||
|
await self._disconnect_shared_client()
|
||||||
|
raise self._flood_wait_http_exception(exc)
|
||||||
|
|
||||||
async def write_file(self, root: str, rel: str, data: bytes):
|
async def write_file(self, root: str, rel: str, data: bytes):
|
||||||
"""将字节数据作为文件上传"""
|
"""将字节数据作为文件上传"""
|
||||||
@@ -515,7 +539,8 @@ class TelegramAdapter:
|
|||||||
if embedded and isinstance(thumb, types.PhotoStrippedSize):
|
if embedded and isinstance(thumb, types.PhotoStrippedSize):
|
||||||
return utils.stripped_photo_to_jpg(bytes(embedded))
|
return utils.stripped_photo_to_jpg(bytes(embedded))
|
||||||
|
|
||||||
result = await client.download_media(message, bytes, thumb=thumb)
|
async with self._download_lock:
|
||||||
|
result = await client.download_media(message, bytes, thumb=thumb)
|
||||||
if isinstance(result, (bytes, bytearray)):
|
if isinstance(result, (bytes, bytearray)):
|
||||||
return bytes(result)
|
return bytes(result)
|
||||||
return None
|
return None
|
||||||
@@ -602,29 +627,56 @@ class TelegramAdapter:
|
|||||||
headers["Content-Length"] = str(end - start + 1)
|
headers["Content-Length"] = str(end - start + 1)
|
||||||
|
|
||||||
async def iterator():
|
async def iterator():
|
||||||
|
downloaded = 0
|
||||||
try:
|
try:
|
||||||
limit = end - start + 1
|
limit = end - start + 1
|
||||||
downloaded = 0
|
async with self._download_lock:
|
||||||
|
async for chunk in client.iter_download(
|
||||||
async for chunk in client.iter_download(
|
media,
|
||||||
media,
|
offset=start,
|
||||||
offset=start,
|
request_size=self._download_chunk_size,
|
||||||
request_size=self._download_chunk_size,
|
chunk_size=self._download_chunk_size,
|
||||||
chunk_size=self._download_chunk_size,
|
file_size=file_size,
|
||||||
file_size=file_size,
|
):
|
||||||
):
|
if not chunk:
|
||||||
if downloaded + len(chunk) > limit:
|
continue
|
||||||
yield chunk[:limit - downloaded]
|
remaining = limit - downloaded
|
||||||
break
|
if remaining <= 0:
|
||||||
yield chunk
|
break
|
||||||
downloaded += len(chunk)
|
data = chunk[:remaining]
|
||||||
if downloaded >= limit:
|
downloaded += len(data)
|
||||||
break
|
yield data
|
||||||
|
if downloaded >= limit:
|
||||||
|
break
|
||||||
|
except errors.FloodWaitError as exc:
|
||||||
|
await self._disconnect_shared_client()
|
||||||
|
if downloaded == 0:
|
||||||
|
raise self._flood_wait_http_exception(exc)
|
||||||
|
seconds = int(getattr(exc, "seconds", 0) or 0)
|
||||||
|
print(f"Telegram streaming stopped by FloodWait after partial response, wait={seconds}s")
|
||||||
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
await self._disconnect_shared_client()
|
await self._disconnect_shared_client()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
return StreamingResponse(iterator(), status_code=status, headers=headers)
|
agen = iterator()
|
||||||
|
try:
|
||||||
|
first_chunk = await agen.__anext__()
|
||||||
|
except StopAsyncIteration:
|
||||||
|
first_chunk = b""
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def response_iterator():
|
||||||
|
try:
|
||||||
|
if first_chunk:
|
||||||
|
yield first_chunk
|
||||||
|
async for chunk in agen:
|
||||||
|
yield chunk
|
||||||
|
finally:
|
||||||
|
await agen.aclose()
|
||||||
|
|
||||||
|
return StreamingResponse(response_iterator(), status_code=status, headers=headers)
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
@@ -654,7 +706,7 @@ class TelegramAdapter:
|
|||||||
"size": size,
|
"size": size,
|
||||||
"mtime": int(message.date.timestamp()),
|
"mtime": int(message.date.timestamp()),
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"has_thumbnail": self._message_has_thumbnail(message),
|
"has_thumbnail": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
def ADAPTER_FACTORY(rec: StorageAdapter) -> TelegramAdapter:
|
def ADAPTER_FACTORY(rec: StorageAdapter) -> TelegramAdapter:
|
||||||
|
|||||||
Reference in New Issue
Block a user