Commit d20fe93
Changed files (12)
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 },
]