Commit d20fe93

benny-dou <60535774+benny-dou@users.noreply.github.com>
2025-03-08 05:51:58
feat: update blockquote style
1 parent c7886e5
src/llm/gpt.py
@@ -106,7 +106,7 @@ async def gpt_response(client: Client, message: Message, **kwargs):
         response = await send_to_gpt(config, **kwargs)
     if content := response.get("content"):
         if reasoning := response.get("reasoning"):
-            content = f"{reasoning}\n\n{content}"
+            content = f"{reasoning}\n{content}"
             texts = f"🤖**{response['model']}**: ({BOT_TIPS})\n{content}"
         else:
             texts = f"🤖**{response['model']}**: ({BOT_TIPS})\n\n{content}"
src/llm/response.py
@@ -7,6 +7,7 @@ import json
 from glom import Coalesce, glom
 from loguru import logger
 from openai import AsyncOpenAI
+from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM, BLOCKQUOTE_EXPANDABLE_END_DELIM
 
 from config import GPT
 from llm.models import openrouter_hook
@@ -132,8 +133,8 @@ async def parse_response(config: dict, response: dict) -> dict[str, str]:
         if not reasoning:
             reasoning = glom(choice, Coalesce("message.reasoning_content", "message.reasoning"), default="") or ""
         if reasoning and str(reasoning) != "None":  # add expandable block quotation mark for reasoning
-            reasoning = reasoning.strip().replace("\n", "\n> ")
-            reasoning = f"**> 🤔{reasoning}💡"  # if change this line, remember to remove the reasoning from contexts (`llm/contexts.py`)
+            # if change this line, remember to remove the reasoning from contexts (`llm/contexts.py`)
+            reasoning = f"{BLOCKQUOTE_EXPANDABLE_DELIM}🤔{reasoning.strip()}💡\n{BLOCKQUOTE_EXPANDABLE_END_DELIM}"
 
         primary_model = glom(config, "completions.model", default="") or ""
         used_model = glom(response, "model", default="") or ""
src/messages/sender.py
@@ -6,12 +6,13 @@ from pathlib import Path
 
 from loguru import logger
 from pyrogram.client import Client
+from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM, BLOCKQUOTE_EXPANDABLE_END_DELIM
 from pyrogram.types import Message, ReplyParameters
 
 from config import CAPTION_LENGTH
 from messages.preprocess import preprocess_media, warp_media_group
 from messages.progress import modify_progress, telegram_uploading
-from messages.utils import count_without_entities, get_reply_to, smart_split, summay_media
+from messages.utils import get_reply_to, smart_split, summay_media, warp_comments
 from utils import to_int
 
 
@@ -67,27 +68,14 @@ async def send2tg(
     reply_parameters = get_reply_to(message.id, reply_msg_id)
     media = media or []
     media = preprocess_media(media)
-    comments = comments or []
-
-    # no text, but has comments. treat comments as texts
-    texts = texts if texts else "".join(comments).strip()
+    texts = texts or ""
+    if comments:  # append comments to texts
+        texts = texts + "".join(comments) + BLOCKQUOTE_EXPANDABLE_END_DELIM
 
     if send_from_user:  # prefix send_from_user
         texts = f"{send_from_user}{texts.strip()}"
-
     if kwargs.get("progress") and len(media) > 0:
         await modify_progress(text=f"⏫正在上传:\n{summay_media(media)}", force_update=True, **kwargs)
-
-    # append comments to texts
-    # For texts length < 1024 , ensure the combined texts and comments remains below 1024 characters to avoid sending a subsequent message containing only the comments.
-    # For long texts, keep all comments
-    if await count_without_entities(texts) < CAPTION_LENGTH:
-        for comment in comments:
-            if await count_without_entities(f"{texts}{comment}") < CAPTION_LENGTH:
-                texts += comment
-    else:
-        texts = texts + "".join(comments)
-
     sent_messages: list[Message | None] = []  # return sent messages
     logger.trace(f"Sending {len(media)} media with {len(texts)} texts")
     if len(media) == 0:
@@ -96,6 +84,7 @@ async def send2tg(
         return await send_single_media(client, target_chat, reply_parameters, media=media[0], texts=texts, cooldown=cooldown, **kwargs)
 
     caption = (await smart_split(texts, CAPTION_LENGTH))[0]
+    caption = warp_comments(caption)
     remaining_texts = texts.removeprefix(caption)
     if 1 < len(media) <= 10:
         group = await warp_media_group(media, caption=caption)
@@ -138,12 +127,19 @@ async def send_texts(
     for idx, msg in enumerate(await smart_split(texts.strip())):
         if not msg:
             continue
+        texts = warp_comments(msg)
+        # we do not send comments only texts
+        if (
+            texts.startswith(BLOCKQUOTE_EXPANDABLE_DELIM)
+            and texts.endswith(BLOCKQUOTE_EXPANDABLE_END_DELIM)
+            and texts.count(BLOCKQUOTE_EXPANDABLE_DELIM) == 1
+            and texts.count(BLOCKQUOTE_EXPANDABLE_END_DELIM) == 1
+        ):
+            continue
         if idx == 0:
-            if msg.startswith("> "):  # this is the remaining part of comments
-                msg = f"**{msg}"  # noqa: PLW2901 use block quote
-            sent_messages.append(await client.send_message(target_chat, msg, reply_parameters=reply_parameters))
+            sent_messages.append(await client.send_message(target_chat, texts, reply_parameters=reply_parameters))
         else:  # disbale reply
-            sent_messages.append(await client.send_message(target_chat, msg, reply_parameters=ReplyParameters()))
+            sent_messages.append(await client.send_message(target_chat, texts, reply_parameters=ReplyParameters()))
             await asyncio.sleep(cooldown)
     return sent_messages
 
@@ -162,6 +158,7 @@ async def send_single_media(
     logger.trace(f"Sending single media with {len(texts)} texts")
     caption = (await smart_split(texts, CAPTION_LENGTH))[0]
     remaining_texts = texts.removeprefix(caption)
+    caption = warp_comments(caption)
     if photo := media.get("photo"):
         sent_messages.append(await client.send_photo(chat_id=target_chat, photo=photo, caption=caption, reply_parameters=reply_parameters))
     elif video := media.get("video"):
src/messages/utils.py
@@ -4,6 +4,7 @@
 import re
 
 from pyrogram.enums import ParseMode
+from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM, BLOCKQUOTE_EXPANDABLE_END_DELIM
 from pyrogram.parser.parser import Parser
 from pyrogram.types import ReplyParameters
 
@@ -108,6 +109,8 @@ async def smart_split(text: str, chars_per_string: int = TEXT_LENGTH, mode: Pars
             return strings.split("?")[0] + "?"
         return strings
 
+    # for some reason, we may need to prepend `BLOCKQUOTE_EXPANDABLE_DELIM` or append `BLOCKQUOTE_EXPANDABLE_END_DELIM`
+    chars_per_string = chars_per_string - len(BLOCKQUOTE_EXPANDABLE_DELIM) - len(BLOCKQUOTE_EXPANDABLE_END_DELIM)
     parts = []
     while True:
         if await count_without_entities(text, mode) < chars_per_string:
@@ -124,3 +127,29 @@ async def smart_split(text: str, chars_per_string: int = TEXT_LENGTH, mode: Pars
         parts.append(part)
         text = left
     return parts
+
+
+def warp_comments(texts: str) -> str:
+    texts = texts or ""
+
+    start_idx = texts.find(BLOCKQUOTE_EXPANDABLE_DELIM)
+    end_idx = texts.find(BLOCKQUOTE_EXPANDABLE_END_DELIM)
+    if start_idx == -1 and end_idx == -1:  # no comments
+        return texts
+
+    # <comments><end_delim> ... <content> ... <start_delim><comments>
+    if start_idx > -1 and end_idx > -1 and end_idx < start_idx:
+        texts = texts.removeprefix(BLOCKQUOTE_EXPANDABLE_DELIM).removesuffix(BLOCKQUOTE_EXPANDABLE_END_DELIM)
+        return BLOCKQUOTE_EXPANDABLE_DELIM + texts + BLOCKQUOTE_EXPANDABLE_END_DELIM
+
+    # <content> ... <start_delim><comments>
+    if start_idx > -1 and end_idx == -1:
+        texts = texts.removesuffix(BLOCKQUOTE_EXPANDABLE_END_DELIM)
+        return texts + BLOCKQUOTE_EXPANDABLE_END_DELIM
+
+    # <comments><end_delim> ... <content>
+    if start_idx == -1 and end_idx > -1:
+        texts = texts.removeprefix(BLOCKQUOTE_EXPANDABLE_DELIM)
+        return BLOCKQUOTE_EXPANDABLE_DELIM + texts
+
+    return texts
src/preview/douyin.py
@@ -7,6 +7,7 @@ from zoneinfo import ZoneInfo
 from glom import glom
 from loguru import logger
 from pyrogram.client import Client
+from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM
 from pyrogram.types import Message
 
 from bridge.social import send_to_social_media_bridge
@@ -102,10 +103,8 @@ async def preview_douyin(
 
     comments = []
     if comments_list := await get_comments(aweme_id, platform, douyin_comments_provider):
-        comments.append("\n**> 💬**点此展开评论区**:")
-        for cmt in comments_list:
-            cmt_text = cmt["text"].replace("\n", "\n> ")
-            comments.append(f"\n> 💬**{cmt['name']}**{cmt['region']}: {cmt_text}")
+        comments.append(f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**点此展开评论区**:")
+        comments.extend(f"\n💬**{cmt['name']}**{cmt['region']}: {cmt['text']}" for cmt in comments_list)
 
     sent_messages = await send2tg(client, message, texts=emojify(texts), media=media, comments=comments, **kwargs)
     await modify_progress(del_status=True, **kwargs)
src/preview/instagram.py
@@ -8,6 +8,7 @@ from bs4 import BeautifulSoup
 from glom import glom
 from loguru import logger
 from pyrogram.client import Client
+from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM
 from pyrogram.types import Message
 
 from bridge.social import send_to_social_media_bridge
@@ -93,12 +94,9 @@ async def preview_instagram(
         comment_nodes = glom(data, "edge_media_to_parent_comment.edges", default=[])
         comment_nodes = sorted(comment_nodes, key=lambda x: glom(x, "node.created_at", default=0))
         comment_list = [{"author": glom(node, "node.owner.username", default="user"), "text": glom(node, "node.text", default="")} for node in comment_nodes]
-        comment_list = [x for x in comment_list if x["text"]]
-        for idx, cmt in enumerate(comment_list):
-            cmt_text = cmt["text"].replace("\n", "\n> ")
-            if idx == 0:
-                comments.append("\n**> 💬**点此展开评论区**:")
-            comments.append(f"\n> 💬**[{cmt['author']}](https://www.instagram.com/{cmt['author']})**: {cmt_text}")
+        if comment_list := [x for x in comment_list if x["text"]]:
+            comments.append(f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**点此展开评论区**:")
+            comments.extend(f"\n💬**[{cmt['author']}](https://www.instagram.com/{cmt['author']})**: {cmt['text']}" for cmt in comment_list)
 
     await modify_progress(text=f"⏬正在下载:\n{summay_media(media)}", force_update=True, **kwargs)
     media = await download_media(media, **kwargs)
src/preview/twitter.py
@@ -8,6 +8,7 @@ from zoneinfo import ZoneInfo
 from glom import glom
 from loguru import logger
 from pyrogram.client import Client
+from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM, BLOCKQUOTE_EXPANDABLE_END_DELIM
 from pyrogram.types import Message
 
 from bridge.social import send_to_social_media_bridge
@@ -149,13 +150,12 @@ async def preview_twitter(
         if texts := master_info.get("texts"):
             msg += f"\n{texts}"
         if true(twitter_comments_provider) and (comments := master_info.get("comments")):
-            msg += "\n**> 💬**点此展开评论区**:"
+            msg += f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**点此展开评论区**:"
             for cmt in comments:
                 if str(cmt["post_id"]) == str(this_info["post_id"]):
                     continue
-                cmt_texts = cmt["text"].replace("\n", "\n> ")
-                msg += f"\n> 💬**{cmt['author']}**: {cmt_texts}"
-
+                msg += f"\n💬**{cmt['author']}**: {cmt['text']}"
+            msg += BLOCKQUOTE_EXPANDABLE_END_DELIM
         media.extend(master_media)
 
     # 本条推文
@@ -179,10 +179,11 @@ async def preview_twitter(
         msg += f"\n{texts}"
 
     if true(twitter_comments_provider) and (comments := this_info.get("comments")):
-        msg += "\n**> 💬**点此展开评论区**:"
+        msg += f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**点此展开评论区**:"
         for cmt in comments:
-            cmt_texts = cmt["text"].strip().removeprefix(f"@{master_handle}").strip().replace("\n", "\n> ")  # 有时回推的comment前会附带被回推的handle, 这里去掉
-            msg += f"\n> 💬**{cmt['author']}**: {cmt_texts}"
+            cmt_texts = cmt["text"].strip().removeprefix(f"@{master_handle}").strip()  # 有时回推的comment前会附带被回推的handle, 这里去掉
+            msg += f"\n💬**{cmt['author']}**: {cmt_texts}"
+        msg += BLOCKQUOTE_EXPANDABLE_END_DELIM
 
     # 引用推文
     if quote_info:
@@ -282,7 +283,7 @@ async def get_tweet_info_via_tikhub(url: str = "", post_id: str = "", quote_info
         comment_text = await flatten_rediercts(comment_text)
         comment_text = comment_text.strip()
         if comment_handle and comment_text:
-            comments.append({"author": comment_author, "text": comment_text})
+            comments.append({"author": comment_author, "text": comment_text, "post_id": comment_post_id})
 
     info["comments"] = comments
     info["quote_info"] = data.get("quoted", {})
src/preview/weibo.py
@@ -11,6 +11,7 @@ from bs4 import BeautifulSoup
 from glom import glom
 from loguru import logger
 from pyrogram.client import Client
+from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM
 from pyrogram.types import Message
 
 from bridge.social import send_to_social_media_bridge
@@ -282,7 +283,7 @@ async def parse_weibo_comments(post_id: str) -> list[str]:
         logger.error(f"Weibo Comments API failed: {resp}")
         return []
 
-    comments = ["\n**> 💬**点此展开评论区**:"]
+    comments = [f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**点此展开评论区**:"]
     for info in resp.get("data", []):
         if not info.get("text"):
             continue
@@ -299,7 +300,7 @@ async def parse_weibo_comments(post_id: str) -> list[str]:
         if text := info.get("text"):
             cmt += f" {soup_to_text(BeautifulSoup(text, 'html.parser'))}"
         cmt = emojify(cmt)
-        comments.append(f"\n> {cmt}")
+        comments.append(f"\n{cmt}")
     if len(comments) == 1:
         return []
     return comments
src/preview/ytdlp.py
@@ -13,6 +13,7 @@ from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning
 from glom import glom
 from loguru import logger
 from pyrogram.client import Client
+from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM
 from pyrogram.types import Message
 from yt_dlp import YoutubeDL
 from yt_dlp.utils import DownloadError, ExtractorError, YoutubeDLError
@@ -24,7 +25,7 @@ from messages.database import copy_messages_from_db, save_messages
 from messages.preprocess import preprocess_media
 from messages.progress import modify_progress, telegram_uploading
 from messages.sender import send2tg
-from messages.utils import count_without_entities, get_reply_to, smart_split
+from messages.utils import count_without_entities, get_reply_to, smart_split, warp_comments
 from multimedia import convert_to_h264, generate_cover
 from networking import hx_req
 from others.emoji import emojify
@@ -184,11 +185,12 @@ async def preview_ytdlp(
         for idx, video in enumerate(videos):
             video["thumb"] = thumb
             caption = texts.replace("📝[", f"📝[P{idx + 1}-") if len(videos) > 1 else texts
+            caption = (await smart_split(caption, CAPTION_LENGTH))[0]
             await modify_progress(text=f"🎬视频上传中-P{idx + 1}: {readable_size(path=video['video'])}", force_update=True, **kwargs)
             sent_messages.append(
                 await client.send_video(
                     chat_id=to_int(target_chat),
-                    caption=(await smart_split(caption, CAPTION_LENGTH))[0],
+                    caption=warp_comments(caption),
                     reply_parameters=reply_parameters,
                     progress=telegram_uploading,
                     progress_args=(kwargs.get("progress", False), video["video"], true(kwargs.get("detail_progress"))),  # message, path, detail_progress
@@ -199,11 +201,12 @@ async def preview_ytdlp(
         target_chat = target_chat if ytdlp_send_audio else TID.CHANNEL_YTDLP_BACKUP  # backup to channel if not send audio, so we can save it to db
         await modify_progress(text=f"🎧音频上传中: {readable_size(path=audio_path)}", force_update=True, **kwargs)
         target_chat = to_int(target_chat)
+        caption = (await smart_split(texts, CAPTION_LENGTH))[0]
         sent_messages.append(
             await client.send_audio(
                 chat_id=target_chat,
                 audio=audio_path.as_posix(),
-                caption=(await smart_split(texts, CAPTION_LENGTH))[0],
+                caption=warp_comments(caption),
                 performer=info["author"],
                 title=info["title"],
                 duration=duration,
@@ -505,10 +508,9 @@ async def get_bilibili_comments(bvid: str | None, provider: str = PROVIDER.BILIB
             location = glom(x, "reply_control.location", default="").removeprefix("IP属地:")  # noqa: RUF001
             location = f"({location})" if location else ""
             if cmt := glom(x, "content.message", default=""):
-                cmt = cmt.replace("\n", "\n> ")
                 if idx == 0:
-                    comments.append("\n**> 💬**点此展开评论区**:")
-                comments.append(f"\n> 💬**{name}**{location}: {emojify(cmt)}")
+                    comments.append(f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**点此展开评论区**:")
+                comments.append(f"\n💬**{name}**{location}: {emojify(cmt)}")
             # replies of comments, free api only got 3 comments, so we add replies here
             if provider == "free" and (replies := x.get("replies")):
                 for r in replies:
@@ -518,8 +520,7 @@ async def get_bilibili_comments(bvid: str | None, provider: str = PROVIDER.BILIB
                     location = glom(r, "reply_control.location", default="").removeprefix("IP属地:")  # noqa: RUF001
                     location = f"({location})" if location else ""
                     if cmt := glom(r, "content.message", default=""):
-                        cmt = cmt.replace("\n", "\n> ")
-                        comments.append(f"\n> ↪️**{name}**{location}: {emojify(cmt)}")
+                        comments.append(f"\n↪️**{name}**{location}: {emojify(cmt)}")
     except Exception as e:
         logger.error(f"Failed to get Bilibili comments: {e}")
         return []
@@ -545,10 +546,9 @@ async def get_youtube_comments(vid: str | None, provider: str = PROVIDER.YOUTUBE
             if author_url := glom(x, "snippet.topLevelComment.snippet.authorChannelUrl", default=""):
                 name = f"[{name}]({author_url})"
             if cmt := glom(x, "snippet.topLevelComment.snippet.textDisplay", default=""):
-                cmt = cmt.replace("\n", "\n> ")
                 if idx == 0:
-                    comments.append("\n**> 💬**点此展开评论区**:")
-                comments.append(f"\n> 💬**{name}**: {cmt}")
+                    comments.append(f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**点此展开评论区**:")
+                comments.append(f"\n💬**{name}**: {cmt}")
     except Exception as e:
         logger.error(f"Failed to get YouTube comments: {e}")
         return []
src/database.py
@@ -121,7 +121,7 @@ async def get_cf_r2(key: str) -> dict:
     return {}
 
 
-async def set_db(key: str, data: dict | list, ttl: int = 86400, metadata: dict | None = None) -> bool:
+async def set_db(key: str, data: dict | list, ttl: int = 600, metadata: dict | None = None) -> bool:
     """Set KV."""
     key = quote_plus(key)
     success = False
@@ -134,7 +134,7 @@ async def set_db(key: str, data: dict | list, ttl: int = 86400, metadata: dict |
     return success
 
 
-def set_memory_kv(key: str, data: dict | list | str, ttl: int = 86400) -> None:
+def set_memory_kv(key: str, data: dict | list | str, ttl: int = 600) -> None:
     """Set to memory cache."""
     cache.set(key, data, ttl=ttl)
     logger.trace(f"SET KV to memory cache for {key}: {data}")
pyproject.toml
@@ -12,7 +12,7 @@ dependencies = [
   "pillow-heif>=0.18.0",
   "pillow>=10.4.0",
   "puremagic>=1.28",
-  "pyrotgfork==2.2.5",             # Pin 2.2.5. The blockquote style is changed in 2.2.6
+  "pyrotgfork>=2.2.8",
   "pysocks>=1.7.1",
   "pytgcrypto>=1.2.9.2",
   "python-ffmpeg>=2.0.12",
uv.lock
@@ -260,7 +260,7 @@ requires-dist = [
     { name = "pillow", specifier = ">=10.4.0" },
     { name = "pillow-heif", specifier = ">=0.18.0" },
     { name = "puremagic", specifier = ">=1.28" },
-    { name = "pyrotgfork", specifier = "==2.2.5" },
+    { name = "pyrotgfork", specifier = ">=2.2.8" },
     { name = "pysocks", specifier = ">=1.7.1" },
     { name = "pytgcrypto", specifier = ">=1.2.9.2" },
     { name = "python-ffmpeg", specifier = ">=2.0.12" },
@@ -1133,15 +1133,15 @@ wheels = [
 
 [[package]]
 name = "pyrotgfork"
-version = "2.2.5"
+version = "2.2.9"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "pyaes", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
     { name = "pysocks", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/4c/cb/ceb3e611a865ee014d5655f63f77f0e43365c75dcca858e2ee3514af559f/pyrotgfork-2.2.5.tar.gz", hash = "sha256:1a356e017abfd7fee9206ed344d9de6a81e75d7923f6845a72bec0d3f0d834a0", size = 477393 }
+sdist = { url = "https://files.pythonhosted.org/packages/97/91/1af8ac8edf28a72c08496d7bc87ece4d1c7fb537c63d9831b2ee05931ac4/pyrotgfork-2.2.9.tar.gz", hash = "sha256:6c44c694e5f425f5ad617d6b7f62d37b462b6194113b1380dd6866d8668f8863", size = 486317 }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/6f/7f/577bb66259c6810a5e365622ee43121a8d71fb40a992fa7a355be853890f/pyrotgfork-2.2.5-py3-none-any.whl", hash = "sha256:4698d1b5c7f2fbdf67f594f4cdfaab422f1af42bad7bb02982babe751e6dab34", size = 4941190 },
+    { url = "https://files.pythonhosted.org/packages/8b/7d/2f37ca4087b151e509dc945109f4d51f9aaf21186f1c97ab220c8c43ad62/pyrotgfork-2.2.9-py3-none-any.whl", hash = "sha256:cae5393df5d0c4a125b584444e1098918794326a65bf6bac6dbbc97c50c984ab", size = 4974727 },
 ]
 
 [[package]]
@@ -1599,9 +1599,9 @@ wheels = [
 
 [[package]]
 name = "yt-dlp"
-version = "2025.3.5.232947.dev0"
+version = "2025.3.7.232704.dev0"
 source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/97/7a/a28cf16295015640bd645481e06c5807dc7ae580a8acb497f65ad02f200b/yt_dlp-2025.3.5.232947.dev0.tar.gz", hash = "sha256:15514c9c9fd370dc33b51be32e6e6d13e46dea36046d6419103a90f355d2da2b", size = 2951898 }
+sdist = { url = "https://files.pythonhosted.org/packages/48/00/7c61171263da5f2bdeb71ec9278685053603366ec1ec0fe6474930109d7e/yt_dlp-2025.3.7.232704.dev0.tar.gz", hash = "sha256:59374f9fa5c8c0036fe3a8f20e48a9aba17b7ba4e923cb4b17aafcfd27750450", size = 2952283 }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/c7/2a/497482e9878ec44d4b7b8b0d1b5265f18e480a01240e9a83e4d1039bea8d/yt_dlp-2025.3.5.232947.dev0-py3-none-any.whl", hash = "sha256:16ee8a17b25451cce0484e68bfa1218708edf08421a119493281ae1bb9f25e16", size = 3207575 },
+    { url = "https://files.pythonhosted.org/packages/cb/08/8412bdfae1c36d4472191e0d9d02cd5add0bada930a701dd405633f9f0e8/yt_dlp-2025.3.7.232704.dev0-py3-none-any.whl", hash = "sha256:048ed49e232bbb44e46d5eabca3d32832c41db1079958e92051828a9c276c92a", size = 3207955 },
 ]