11import asyncio
22import os
3+ from collections .abc import Awaitable , Callable
4+ from itertools import batched
35from typing import Literal
46
57from parsehub .types import (
1113 VideoFile ,
1214)
1315from pyrogram import Client , enums , filters
16+ from pyrogram .errors import FloodWait
1417from pyrogram .types import (
1518 InputMediaAnimation ,
1619 InputMediaDocument ,
3740
3841logger = logger .bind (name = "Parse" )
3942SKIP_DOWNLOAD_THRESHOLD = 0
43+ MAX_RETRIES = 5
44+
45+
46+ async def _send_with_rate_limit [T ](
47+ send_coro_fn : Callable [[], Awaitable [T ]],
48+ ) -> T :
49+ """带自动重试的发送包装器。
50+
51+ Args:
52+ send_coro_fn: 返回协程的可调用对象(lambda 或函数),每次重试会重新调用
53+ """
54+ for attempt in range (MAX_RETRIES ):
55+ try :
56+ return await send_coro_fn ()
57+ except FloodWait as e :
58+ if attempt < MAX_RETRIES - 1 :
59+ logger .warning (f"FloodWait 重试 ({ attempt + 1 } /{ MAX_RETRIES } ),等待 { e .value } s" )
60+ await asyncio .sleep (e .value )
61+ else :
62+ raise e from e
63+ return None
4064
4165
4266class MessageStatusReporter (StatusReporter ):
@@ -67,12 +91,15 @@ async def dismiss(self) -> None:
6791 await self ._msg .delete ()
6892
6993 async def _edit_text (self , text : str , ** kwargs ):
70- if self ._msg is None :
71- self ._msg = await self ._user_msg .reply_text (text , ** kwargs )
72- else :
73- if self ._msg .text != text :
74- await self ._msg .edit_text (text , ** kwargs )
75- self ._msg .text = text
94+ try :
95+ if self ._msg is None :
96+ self ._msg = await self ._user_msg .reply_text (text , ** kwargs )
97+ else :
98+ if self ._msg .text != text :
99+ await self ._msg .edit_text (text , ** kwargs )
100+ self ._msg .text = text
101+ except FloodWait :
102+ pass
76103
77104
78105# ── Handler ──────────────────────────────────────────────────────────
@@ -221,7 +248,6 @@ async def handle_parse(
221248 logger .debug (f"开始上传媒体: media_count={ len (result .processed_list )} " )
222249 await reporter .report ("上 传 中..." )
223250 try :
224- await msg .reply_chat_action (enums .ChatAction .UPLOAD_PHOTO )
225251 cache_entry = await _send_media (msg , parse_result , result .processed_list , caption )
226252 if cache_entry :
227253 await persistent_cache .set (raw_url , cache_entry )
@@ -327,35 +353,42 @@ async def _send_raw(
327353 logger .debug ("Raw 模式, 直接上传文件" )
328354 await reporter .report ("上 传 中..." )
329355 try :
330- await msg .reply_chat_action (enums .ChatAction .UPLOAD_DOCUMENT )
331356 caption = build_caption (result .parse_result )
332-
333357 all_docs : list [InputMediaDocument ] = []
334358 livephoto_videos : dict [int , InputMediaDocument ] = {}
359+
335360 for idx , processed in enumerate (result .processed_list ):
336361 # raw 模式下 processed.output_paths 只有一个文件
337362 file_path = processed .output_paths [0 ]
338363 all_docs .append (InputMediaDocument (media = str (file_path )))
339364 if isinstance (processed .source , LivePhotoFile ):
340365 livephoto_videos [idx ] = InputMediaDocument (media = str (processed .source .video_path ))
366+
341367 if len (all_docs ) == 1 :
342- m = await msg .reply_document (all_docs [0 ].media , caption = caption , force_document = True )
368+ await msg .reply_chat_action (enums .ChatAction .UPLOAD_DOCUMENT )
369+ m = await _send_with_rate_limit (
370+ lambda : msg .reply_document (all_docs [0 ].media , caption = caption , force_document = True )
371+ )
343372 if livephoto_videos :
344- await m .reply_document (livephoto_videos [0 ].media , force_document = True )
373+ await _send_with_rate_limit ( lambda : m .reply_document (livephoto_videos [0 ].media , force_document = True ) )
345374 else :
346375 msgs : list [Message ] = []
347- for i in range (0 , len (all_docs ), 10 ):
348- batch = all_docs [i : i + 10 ]
349- mg = await msg .reply_media_group (batch ) # type: ignore
376+ for batch in batched (all_docs , 10 ):
377+ await msg .reply_chat_action (enums .ChatAction .UPLOAD_DOCUMENT )
378+ # noinspection PyDefaultArgument
379+ mg = await _send_with_rate_limit (lambda b = list (batch ): msg .reply_media_group (b )) # type: ignore
350380 msgs .extend (mg )
351- await asyncio .sleep (0.5 )
352381 if livephoto_videos :
353382 for idx , m in livephoto_videos .items ():
354- await msgs [idx ].reply_document (m .media , force_document = True )
355- await asyncio .sleep (0.5 )
356- await msg .reply_text (
357- caption ,
358- link_preview_options = LinkPreviewOptions (is_disabled = True ),
383+ await msg .reply_chat_action (enums .ChatAction .UPLOAD_DOCUMENT )
384+ await _send_with_rate_limit (
385+ lambda m_ = m , idx_ = idx : msgs [idx_ ].reply_document (m_ .media , force_document = True )
386+ )
387+ await _send_with_rate_limit (
388+ lambda : msg .reply_text (
389+ caption ,
390+ link_preview_options = LinkPreviewOptions (is_disabled = True ),
391+ )
359392 )
360393
361394 except Exception as e :
@@ -390,7 +423,7 @@ async def _send_zip(
390423 await reporter .report ("上 传 中..." )
391424 try :
392425 await msg .reply_chat_action (enums .ChatAction .UPLOAD_DOCUMENT )
393- await msg .reply_document (str (pack_path ), caption = caption )
426+ await _send_with_rate_limit ( lambda : msg .reply_document (str (pack_path ), caption = caption ) )
394427 except Exception as e :
395428 logger .opt (exception = e ).debug ("详细堆栈" )
396429 logger .error (f"上传失败: { e } " )
@@ -421,28 +454,36 @@ async def _send_single(
421454
422455 try :
423456 if animations :
424- sent = await msg .reply_animation (animations [0 ].media , caption = caption )
457+ await msg .reply_chat_action (enums .ChatAction .UPLOAD_PHOTO )
458+ sent = await _send_with_rate_limit (lambda : msg .reply_animation (animations [0 ].media , caption = caption ))
425459 else :
426460 single = photos_videos [0 ]
427461 match single :
428462 case InputMediaPhoto ():
429- sent = await msg .reply_photo (single .media , caption = caption )
463+ await msg .reply_chat_action (enums .ChatAction .UPLOAD_PHOTO )
464+ sent = await _send_with_rate_limit (lambda : msg .reply_photo (single .media , caption = caption ))
430465 case InputMediaVideo ():
431- sent = await msg .reply_video (
432- single .media ,
433- caption = caption ,
434- video_cover = single .video_cover ,
435- duration = single .duration ,
436- width = single .width ,
437- height = single .height ,
438- supports_streaming = True ,
466+ await msg .reply_chat_action (enums .ChatAction .UPLOAD_VIDEO )
467+ sent = await _send_with_rate_limit (
468+ lambda : msg .reply_video (
469+ single .media ,
470+ caption = caption ,
471+ video_cover = single .video_cover ,
472+ duration = single .duration ,
473+ width = single .width ,
474+ height = single .height ,
475+ supports_streaming = True ,
476+ )
439477 )
440478
441479 if sent and (cm := _cache_media_from_message (sent )):
442480 media_list .append (cm )
443481 except Exception as e :
444482 logger .warning (f"上传失败 { e } , 使用兼容模式上传" )
445- await msg .reply_document (all_media [0 ].media , caption = caption , force_document = True )
483+ await msg .reply_chat_action (enums .ChatAction .UPLOAD_DOCUMENT )
484+ await _send_with_rate_limit (
485+ lambda : msg .reply_document (all_media [0 ].media , caption = caption , force_document = True )
486+ )
446487 return None
447488
448489 return media_list
@@ -461,40 +502,52 @@ async def _send_multi(
461502 not_cache = False
462503
463504 for ani in animations :
505+ await msg .reply_chat_action (enums .ChatAction .UPLOAD_PHOTO )
506+ caption_ = caption if ani == animations [- 1 ] and not photos_videos else ""
464507 try :
465- sent = await msg .reply_animation (ani .media )
508+ sent = await _send_with_rate_limit (
509+ lambda a = ani , c = caption_ : msg .reply_animation (
510+ a .media ,
511+ caption = c ,
512+ )
513+ )
466514 except Exception as e :
467515 logger .warning (f"上传失败 { e } , 使用兼容模式上传" )
468516 not_cache = True
469- await msg .reply_document (ani .media , force_document = True )
517+ await msg .reply_chat_action (enums .ChatAction .UPLOAD_DOCUMENT )
518+ await _send_with_rate_limit (
519+ lambda a = ani , c = caption_ : msg .reply_document (a .media , caption = c , force_document = True )
520+ )
470521 else :
471522 # 过大的 GIF 会返回 document
472523 if sent .document :
473524 media_list .append (CacheMedia (type = CacheMediaType .DOCUMENT , file_id = sent .document .file_id ))
474525 else :
475526 media_list .append (CacheMedia (type = CacheMediaType .ANIMATION , file_id = sent .animation .file_id ))
476- await asyncio .sleep (0.5 )
477527
478528 try :
479- for i in range (0 , len (photos_videos ), 10 ):
480- batch = photos_videos [i : i + 10 ]
481- sent_msgs = await msg .reply_media_group (batch )
529+ for batch in batched (photos_videos , 10 ):
530+ if batch [- 1 ] == photos_videos [- 1 ]:
531+ batch [0 ].caption = caption
532+
533+ await msg .reply_chat_action (enums .ChatAction .UPLOAD_PHOTO )
534+ # noinspection PyDefaultArgument
535+ sent_msgs = await _send_with_rate_limit (lambda b = list (batch ): msg .reply_media_group (media = b ))
482536 for m in sent_msgs :
483537 if cm := _cache_media_from_message (m ):
484538 media_list .append (cm )
485539 except Exception as e :
486540 logger .warning (f"上传失败 { e } , 使用兼容模式上传" )
487541 input_documents = [InputMediaDocument (media = item .media ) for item in photos_videos ]
488- for i in range (0 , len (input_documents ), 10 ):
489- batch = input_documents [i : i + 10 ]
490- await msg .reply_media_group (batch ) # type: ignore
491- await asyncio .sleep (0.5 )
542+ for batch in batched (input_documents , 10 ):
543+ if batch [- 1 ] == input_documents [- 1 ]:
544+ batch [0 ].caption = caption
545+
546+ await msg .reply_chat_action (enums .ChatAction .UPLOAD_DOCUMENT )
547+ # noinspection PyDefaultArgument
548+ await _send_with_rate_limit (lambda b = list (batch ): msg .reply_media_group (media = b )) # type: ignore
492549 return None
493550
494- await msg .reply_text (
495- caption ,
496- link_preview_options = LinkPreviewOptions (is_disabled = True ),
497- )
498551 return None if not_cache else media_list
499552
500553
@@ -554,34 +607,45 @@ async def _send_cached_single(msg: Message, m: CacheMedia, caption: str) -> None
554607 """从缓存发送单个媒体。"""
555608 match m .type :
556609 case CacheMediaType .PHOTO :
557- await msg .reply_photo (m .file_id , caption = caption )
610+ await msg .reply_chat_action (enums .ChatAction .UPLOAD_PHOTO )
611+ await _send_with_rate_limit (lambda : msg .reply_photo (m .file_id , caption = caption ))
558612 case CacheMediaType .VIDEO :
559- await msg .reply_video (m .file_id , caption = caption , supports_streaming = True , video_cover = m .cover_file_id )
613+ await msg .reply_chat_action (enums .ChatAction .UPLOAD_VIDEO )
614+ await _send_with_rate_limit (
615+ lambda : msg .reply_video (
616+ m .file_id , caption = caption , supports_streaming = True , video_cover = m .cover_file_id
617+ )
618+ )
560619 case CacheMediaType .ANIMATION :
561- await msg .reply_animation (m .file_id , caption = caption )
620+ await msg .reply_chat_action (enums .ChatAction .UPLOAD_PHOTO )
621+ await _send_with_rate_limit (lambda : msg .reply_animation (m .file_id , caption = caption ))
562622 case CacheMediaType .DOCUMENT :
563- await msg .reply_document (m .file_id , caption = caption , force_document = True )
623+ await msg .reply_chat_action (enums .ChatAction .UPLOAD_DOCUMENT )
624+ await _send_with_rate_limit (lambda : msg .reply_document (m .file_id , caption = caption , force_document = True ))
564625
565626
566627async def _send_cached_multi (msg : Message , media : list [CacheMedia ], caption : str ) -> None :
567628 """从缓存发送多个媒体。"""
568629 animations = [m for m in media if m .type == CacheMediaType .ANIMATION ]
569630 others = [m for m in media if m .type != CacheMediaType .ANIMATION ]
570631
571- for m in animations :
572- await msg .reply_animation (m .file_id )
573- await asyncio .sleep (0.5 )
632+ for ani in animations :
633+ await msg .reply_chat_action (enums .ChatAction .UPLOAD_PHOTO )
634+ await _send_with_rate_limit (
635+ lambda a = ani : msg .reply_animation (
636+ a .file_id ,
637+ caption = caption if a == animations [- 1 ] and not others else "" ,
638+ )
639+ )
574640
575- if others :
576- media_group = _build_cached_media_group (others )
577- for i in range (0 , len (media_group ), 10 ):
578- await msg .reply_media_group (media_group [i : i + 10 ])
579- await asyncio .sleep (0.5 )
641+ media_group = _build_cached_media_group (others )
642+ for batch in batched (media_group , 10 ):
643+ if batch [- 1 ] == media_group [- 1 ]:
644+ batch [0 ].caption = caption
580645
581- await msg .reply_text (
582- caption ,
583- link_preview_options = LinkPreviewOptions (is_disabled = True ),
584- )
646+ await msg .reply_chat_action (enums .ChatAction .UPLOAD_PHOTO )
647+ # noinspection PyDefaultArgument
648+ await _send_with_rate_limit (lambda m = list (batch ): msg .reply_media_group (m ))
585649
586650
587651def _build_cached_media_group (
0 commit comments