This commit is contained in:
sansenhoshi
2026-01-04 17:15:40 +08:00
commit 275f05ee4a
207 changed files with 12305 additions and 0 deletions
+462
View File
@@ -0,0 +1,462 @@
import time
import shutil
import requests
import json
import re
import sys
import os
import html
import tempfile
import asyncio
from pathlib import Path
from typing import Optional
import hashlib
from nonebot import on_message, logger
from nonebot.rule import to_me
from nonebot.adapters import Event
from yt_dlp import YoutubeDL
from httpx import AsyncClient
import msgspec
from .minio import upload_to_s3
# 匹配链接的正则
URL_PATTERN = re.compile(r"(https?://[^\s]+)")
pattern = r"QQ小程序(?:]|]|\])"
# 保留原有平台识别,同时加入抖音相关域名
VALID_HOSTS = [
"b23.tv",
"bilibili.com",
"youtube.com",
"youtu.be",
# douyin
"douyin.com",
"v.douyin.com",
"iesdouyin.com",
"m.douyin.com",
"jingxuan.douyin.com",
]
# 抖音短链识别
SHORT_LINK_PATTERN = re.compile(r"(v\.douyin\.com/[A-Za-z0-9_\-]+)")
# 从 HTML 提取路由数据
DOUYIN_ROUTER_PATTERN = re.compile(r"window\._ROUTER_DATA\s*=\s*(.*?)</script>", re.DOTALL)
video_handler = on_message(priority=10, block=False, rule=to_me())
@video_handler.handle()
async def handle_video_download(event: Event):
msg = str(event.get_message()).strip()
logger.info(f"获取到的消息:{msg}")
target_url: Optional[str] = None
# 提取 URL
if not re.search(pattern, msg):
urls = URL_PATTERN.findall(msg)
for u in urls:
logger.info(f"链接:{u}")
# 优先处理抖音(单独解析以获取直链)
if "douyin.com" in u or "v.douyin.com" in u or "iesdouyin.com" in u:
await video_handler.send("检测到链接,正在尝试下载视频,请稍候...")
try:
parsed_path = await parse_douyin(u)
if parsed_path:
# 抖音直接获取直链并且合并文件后返回文件地址后直接上传s3
logger.info(f"文件路径:{parsed_path}")
# 构建path对象
await video_handler.send(f"视频:{parsed_path.name} 已缓存\n正在生成下载链接,请耐心等待!")
s3_url = upload_to_s3(parsed_path)
await video_handler.send(f"{s3_url}")
break
else:
await video_handler.send(f"抖音解析失败或未找到直链:{u}")
logger.warning(f"抖音解析失败或未找到直链:{u}")
# 如果解析失败,继续尝试其他链接
continue
except Exception as e:
await video_handler.send(f"解析抖音链接时出错:{e}")
logger.exception(f"解析抖音链接时出错:{e}")
continue
# 非抖音走原逻辑
if any(domain in u for domain in VALID_HOSTS):
target_url = u
break
if not target_url:
return # 不处理无效链接
await video_handler.send("检测到链接,正在尝试下载视频,请稍候...")
else:
target_url = None
logger.info("检测到小程序")
url = await proc_xcx(msg)
logger.info(f"链接:{url}")
if url and any(domain in url for domain in VALID_HOSTS):
target_url = url
if not target_url:
return # 不处理无效链接
await video_handler.send("检测到视频,正在尝试下载视频,请稍候...")
try:
video_file = await download_video(target_url)
if not video_file:
await video_handler.send("视频下载失败。")
return
await video_handler.send(f"视频已缓存:{video_file.name}\n正在生成下载链接,请耐心等待!")
logger.info(f"文件路径:{video_file}")
s3_url = upload_to_s3(video_file)
logger.info(f"文件上传完成,返回链接:{s3_url}")
await video_handler.send(f"{s3_url}")
except Exception as e:
logger.exception(e)
await video_handler.send("下载过程中出现错误。")
async def download_video(url: str) -> Optional[Path]:
"""
如果 url 看起来是直接的媒体直链(如 mp4),则使用 httpx 直接下载;
否则回退到 yt-dlp 下载(兼容多平台),并支持:
1) Firefox cookies
2) cookies.txt 兜底
"""
# ---------- 1. 直链探测 ----------
direct_media_ext = re.search(r"\.(mp4|m3u8|ts|webm|mov|flv)(?:$|\?)", url, re.IGNORECASE)
is_direct = bool(direct_media_ext)
if not is_direct:
try:
async with AsyncClient(follow_redirects=True, timeout=30) as client:
head = await client.head(url, follow_redirects=True)
ctype = head.headers.get("content-type", "")
if ctype.startswith("video/") or "application/octet-stream" in ctype:
is_direct = True
except Exception:
is_direct = False
if is_direct:
temp_dir = tempfile.mkdtemp(prefix="direct_ytcache_")
ext = "mp4"
m = re.search(r"\.([a-zA-Z0-9]{2,5})(?:$|\?)", url)
if m and len(m.group(1)) <= 5:
ext = m.group(1)
filename = os.path.join(temp_dir, f"downloaded_video.{ext}")
try:
async with AsyncClient(follow_redirects=True, timeout=120) as client:
async with client.stream("GET", url) as resp:
resp.raise_for_status()
with open(filename, "wb") as fh:
async for chunk in resp.aiter_bytes(chunk_size=8192):
fh.write(chunk)
p = Path(filename)
new_path = p.with_name(clean_filename(p.name))
p.rename(new_path)
logger.info(f"直接下载完成: {new_path}")
return new_path
except Exception:
logger.exception("直接下载失败,回退 yt-dlp")
try:
if os.path.exists(filename):
os.remove(filename)
except Exception:
pass
# ---------- 2. yt-dlp 下载 ----------
temp_dir = tempfile.mkdtemp(prefix="ytcache_")
output_path = os.path.join(temp_dir, "%(title).80s.%(ext)s")
base_opts = {
"outtmpl": output_path,
"format": "bestvideo+bestaudio/best",
"noplaylist": True,
"quiet": True,
"ffmpeg_location": get_ffmpeg_path(),
"extractor_args": {
"youtube": {
"player_client": ["android"]
}
},
}
loop = asyncio.get_event_loop()
def _run_yt(opts: dict):
with YoutubeDL(opts) as ydl:
ydl.download([url])
# ---------- 2.1 优先:Firefox cookies ----------
try:
opts = dict(base_opts)
opts["cookiesfrombrowser"] = ("firefox",)
logger.info("尝试使用 Firefox cookies 下载")
await loop.run_in_executor(None, _run_yt, opts)
except Exception as e:
logger.warning(f"Firefox cookies 下载失败,尝试 cookies.txt{e}")
# ---------- 2.2 回退:cookies.txt ----------
cookie_path = Path(__file__).resolve().parent / "cookies.txt"
if cookie_path.exists():
opts = dict(base_opts)
opts["cookiefile"] = str(cookie_path)
logger.info(f"使用 cookies 文件: {cookie_path}")
await loop.run_in_executor(None, _run_yt, opts)
else:
logger.error("未找到 cookies.txt,无法通过登录验证")
raise
# ---------- 3. 结果处理 ----------
files = list(Path(temp_dir).glob("*.*"))
if not files:
return None
original_file = files[0]
new_path = original_file.with_name(clean_filename(original_file.name))
original_file.rename(new_path)
logger.info(f"下载完成并重命名: {new_path}")
return new_path
def get_ffmpeg_path() -> str:
scripts_dir = os.path.dirname(sys.executable)
ffmpeg_path = os.path.join(scripts_dir, "ffmpeg.exe")
if os.path.exists(ffmpeg_path):
return ffmpeg_path
return "ffmpeg"
def clean_filename(filename: str) -> str:
return (
filename.replace(" ", "_")
.replace("&", "_")
.replace("#", "_")
.replace("'", "")
.replace('"', "")
.replace("?", "")
.replace(":", "_")
.replace("|", "_")
.replace("/", "_")
.replace("\\", "_")
.replace("*", "_")
.replace("<", "_")
.replace(">", "_")
.replace("", "")
.replace("", "")
.replace("", "")
.replace("", "")
.replace("", "_")
.replace("", "")
.replace("", "")
.replace("", "_")
.replace("", "")
)
async def proc_xcx(msg):
p_pattern = r'"qqdocurl":"(.*?)"'
match = re.search(p_pattern, msg)
if match:
raw_url = match.group(1)
unescaped = html.unescape(raw_url)
cleaned_url = unescaped.replace(r"\\/", "/")
base_url = cleaned_url.split("?")[0]
print("最终提取链接:", base_url)
return base_url
else:
print("未找到 qqdocurl 字段")
return None
# ----------------- Douyin parsing helpers -----------------
async def parse_douyin(url: str) -> Optional[str]:
"""
解析抖音链接,返回可直接下载的媒体直链(优先)或 None。
支持短链(v.douyin.com)、长链、iesdouyin、m.douyin 等。
"""
# 处理短链
short = SHORT_LINK_PATTERN.search(url)
if short:
short_url = "https://" + short.group(1)
try:
cookies_path = os.path.dirname(__file__).replace("\\", "/") + "/cookies.txt"
file_path = await fetch_douyin_mp4(short_url, cookies_path, 10)
return file_path
except Exception:
logger.exception("获取直链失败")
# 仍然尝试原始 url
# 现在尝试从页面抓取 router data
try:
logger.info(f"获取到的信息:{url}")
except Exception:
logger.exception("抖音页面解析失败")
return None
import asyncio
import tempfile
import aiohttp
import os
from typing import List, Dict, Optional
from playwright.async_api import async_playwright
import ffmpeg
class DouyinFetchError(Exception):
pass
def parse_netscape_cookies(cookies_txt_path: str) -> List[Dict]:
"""
解析 Netscape HTTP Cookie File -> Playwright cookies
"""
cookies: List[Dict] = []
with open(cookies_txt_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split("\t")
if len(parts) != 7:
continue
domain, include_sub, path, secure, expiry, name, value = parts
cookie = {
"name": name,
"value": value,
"domain": domain,
"path": path,
"secure": secure.upper() == "TRUE",
"httpOnly": False,
}
if expiry.isdigit() and int(expiry) > 0:
cookie["expires"] = int(expiry)
cookies.append(cookie)
return cookies
async def download_url(url: str, filepath: str, headers: Optional[Dict] = None):
headers = headers or {}
timeout = aiohttp.ClientTimeout(total=300)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url, headers=headers) as resp:
if resp.status != 200:
raise DouyinFetchError(f"下载失败 {resp.status}: {url}")
with open(filepath, "wb") as f:
async for chunk in resp.content.iter_chunked(1024 * 64):
f.write(chunk)
async def fetch_douyin_mp4(
douyin_url: str,
cookies_txt: str,
wait_seconds: int = 15,
) -> str:
"""
输入抖音 URL
输出:合并后的 MP4 临时文件路径
"""
cookies = parse_netscape_cookies(cookies_txt)
if not cookies:
raise DouyinFetchError("cookies.txt 解析失败或为空")
video_url: Optional[str] = None
audio_url: Optional[str] = None
async with async_playwright() as p:
browser = await p.chromium.launch(
executable_path="C:/Program Files/Google/Chrome/Application/chrome.exe",
headless=False,
args=[
"--autoplay-policy=no-user-gesture-required",
"--disable-features=AutoplayDisableSuppression",
"--use-fake-ui-for-media-stream",
]
)
context = await browser.new_context(
viewport={"width": 640, "height": 320},
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/122.0.0.0 Safari/537.36"
),
)
await context.add_cookies(cookies)
page = await context.new_page()
def handle_response(response):
nonlocal video_url, audio_url
url = response.url
# 分轨视频
if video_url is None and ("video" in url and "mime_type=video_mp4" in url):
video_url = url
elif audio_url is None and ("audio" in url and "mime_type=audio" in url):
audio_url = url
page.on("response", handle_response)
await page.goto(douyin_url, wait_until="domcontentloaded")
await page.wait_for_timeout(wait_seconds * 1000)
await browser.close()
if not video_url:
raise DouyinFetchError("未捕获视频流 URL")
# -----------------------------
# 下载到临时文件
# -----------------------------
tmp_dir = Path(tempfile.gettempdir())
timestamp = int(time.time() * 1000)
tmp_video = tmp_dir / f"douyin/{timestamp}_video.mp4"
tmp_out = tmp_dir / f"douyin/{timestamp}_out.mp4"
# 下载视频
await download_url(video_url, tmp_video)
if audio_url:
tmp_audio = tmp_dir / f"{timestamp}_audio.m4a"
try:
await download_url(audio_url, tmp_audio)
(
ffmpeg
.input(tmp_video)
.output(tmp_audio, tmp_out, vcodec="copy", acodec="aac", movflags="faststart")
.overwrite_output()
.run(quiet=True)
)
finally:
tmp_video.unlink(missing_ok=True)
tmp_audio.unlink(missing_ok=True)
else:
# 视频自带音频,直接拷贝到输出
shutil.copy(tmp_video, tmp_out)
tmp_video.unlink(missing_ok=True)
return tmp_out
+61
View File
@@ -0,0 +1,61 @@
# Netscape HTTP Cookie File
# This file is generated by yt-dlp. Do not edit.
.bilibili.com TRUE / FALSE 1797867119 b_nut 1766331119
.bilibili.com TRUE / FALSE 1852731119 buvid3 DD8D9A3E-E377-7185-8B36-69849D5FB89419813infoc
.bilibili.com TRUE / FALSE 0 sid nu48lk72
.douyin.com TRUE / TRUE 1789188229 SEARCH_RESULT_LIST_TYPE %22single%22
.douyin.com TRUE / FALSE 1797188918 SelfTabRedDotControl %5B%5D
.douyin.com TRUE / TRUE 1788959886 UIFID_TEMP 1b474bc7e0db9591e645dd8feb8c65aae4845018effd0c2743039a380ee64740f7a504ffa95976eb1ff6c460678462bf4c11608eea38012581a3420231dc8cc18e9985cc85771821f5b805864df920d969836148d4de574213c64a065f29b8542b9886775a8ef257ab52165271883e0b
.douyin.com TRUE / FALSE 1797108073 __druidClientInfo JTdCJTIyY2xpZW50V2lkdGglMjIlM0E1ODUlMkMlMjJjbGllbnRIZWlnaHQlMjIlM0E5MzElMkMlMjJ3aWR0aCUyMiUzQTU4NSUyQyUyMmhlaWdodCUyMiUzQTkzMSUyQyUyMmRldmljZVBpeGVsUmF0aW8lMjIlM0ExLjI1JTJDJTIydXNlckFnZW50JTIyJTNBJTIyTW96aWxsYSUyRjUuMCUyMChXaW5kb3dzJTIwTlQlMjAxMC4wJTNCJTIwV2luNjQlM0IlMjB4NjQpJTIwQXBwbGVXZWJLaXQlMkY1MzcuMzYlMjAoS0hUTUwlMkMlMjBsaWtlJTIwR2Vja28pJTIwQ2hyb21lJTJGMTQzLjAuMC4wJTIwU2FmYXJpJTJGNTM3LjM2JTIyJTdE
.douyin.com TRUE / FALSE 1791425613 __live_version__ %221.1.3.9068%22
.douyin.com TRUE / FALSE 1770842600 __security_mc_1_s_sdk_cert_key 1ca1c5bd-4fad-83db
.douyin.com TRUE / FALSE 1770842600 __security_mc_1_s_sdk_crypt_sdk 047179a2-412d-984e
.douyin.com TRUE / FALSE 1770842600 __security_mc_1_s_sdk_sign_data_key_web_protect 84cb6190-4b2f-883c
.douyin.com TRUE / FALSE 1770842600 _bd_ticket_crypt_cookie 966e8a4a1243419806f690bb52d8f687
.douyin.com TRUE / FALSE 1770836920 bd_ticket_guard_client_data eyJiZC10aWNrZXQtZ3VhcmQtdmVyc2lvbiI6MiwiYmQtdGlja2V0LWd1YXJkLWl0ZXJhdGlvbi12ZXJzaW9uIjoxLCJiZC10aWNrZXQtZ3VhcmQtcmVlLXB1YmxpYy1rZXkiOiJCSDljaENXbS9RanFWaTFEK2d5bm9QbTFodnpMTkJCWVFwcGVNSXAyYUlSWEFqbThTdDJ0bG91TW1Na09OZk9rR2VZbXZ0Skp5aU11di9tY2VsZXpRZUU9IiwiYmQtdGlja2V0LWd1YXJkLXdlYi12ZXJzaW9uIjoyfQ%3D%3D
.douyin.com TRUE / FALSE 1770842600 bd_ticket_guard_client_data_v2 eyJyZWVfcHVibGljX2tleSI6IkJIOWNoQ1dtL1FqcVZpMUQrZ3lub1BtMWh2ekxOQkJZUXBwZU1JcDJhSVJYQWptOFN0MnRsb3VNbU1rT05mT2tHZVltdnRKSnlpTXV2L21jZWxlelFlRT0iLCJ0c19zaWduIjoidHMuMi4xY2QxNjhkOGI2NjJjMjQ0MGYwOWMwMmNhMDcyYWM1NDVmZDNmZDUwN2RjYjgwZDE2ZjA2YTNkZjZiYTU1ZDQzYzRmYmU4N2QyMzE5Y2YwNTMxODYyNGNlZGExNDkxMWNhNDA2ZGVkYmViZWRkYjJlMzBmY2U4ZDRmYTAyNTc1ZCIsInJlcV9jb250ZW50Ijoic2VjX3RzIiwicmVxX3NpZ24iOiJKMENGMEc3dHZSdGpEL1VzWGF1WjcrdkVhYjlVZmJYZzRvSkV3WFNEVXdBPSIsInNlY190cyI6IiNXQzM2bW1WbTZvZUU4Y0V3eTdpdERuZU83bkdpRjFaSzFZU0JzdXZ6MUUvK1ZPeTZwOEd5S2t0VTVqOFQifQ%3D%3D
.douyin.com TRUE / FALSE 1770836920 bd_ticket_guard_client_web_domain 2
.douyin.com TRUE / FALSE 0 biz_trace_id d7cbac16
.douyin.com TRUE / FALSE 1785935956 d_ticket 01ddc437be4e3cbfd1cb3bb56dd327e876498
.douyin.com TRUE / TRUE 1800212916 enter_pc_once 1
.douyin.com TRUE / FALSE 1800212918 hevc_supported true
.douyin.com TRUE / TRUE 1769842917 is_staff_user false
.douyin.com TRUE / FALSE 1794260703 live_use_vvc %22false%22
.douyin.com TRUE / FALSE 1788959956 login_time 1754399956210
.douyin.com TRUE / FALSE 1788959962 my_rd 2
.douyin.com TRUE / FALSE 1797188988 odin_tt 2683055763a782b94a74634bf8fcd7e90b0dcca44630c771f1ba99436b48bfc79d2b234a29a0e299299379659da80a11daf739a2814a10f66d2d1a5684811053a30fa9b3a9b401ccb901146e065f0283
.douyin.com TRUE / TRUE 1788959956 passport_assist_user CkF0l51t7CgABiF67MBw3A7AHKIkbitVe_56ICZPweOyywpFk54SNFyio_swIE8DJbzDO9aMykPJaxHUbT2QG6KVGBpKCjwAAAAAAAAAAAAAT1HMv2v-0GEWxxM2txtqIiClBsZvR3gtWGiNkYTJcF4qMsFK3Kg20fVfR9_rVAm0s9QQ3tL4DRiJr9ZUIAEiAQPfMXD5
.douyin.com TRUE / TRUE 1770222444 passport_csrf_token e38b20b0c35196051cf9c83b8a835368
.douyin.com TRUE / FALSE 1770222444 passport_csrf_token_default e38b20b0c35196051cf9c83b8a835368
.douyin.com TRUE / TRUE 1769842917 session_tlb_tag sttt%7C13%7CqMU0JZcc7nWXE6DVNPekaP_________HGhsfOzUszhPpsRcrIANMaX3bvgd_H6JnB7oTHAd_DbQ%3D
.douyin.com TRUE / TRUE 1769842917 session_tlb_tag_bk sttt%7C13%7CqMU0JZcc7nWXE6DVNPekaP_________HGhsfOzUszhPpsRcrIANMaX3bvgd_H6JnB7oTHAd_DbQ%3D
.douyin.com TRUE / TRUE 1769842917 sessionid a8c53425971cee759713a0d534f7a468
.douyin.com TRUE / TRUE 1769842917 sessionid_ss a8c53425971cee759713a0d534f7a468
.douyin.com TRUE / TRUE 1795762917 sid_guard a8c53425971cee759713a0d534f7a468%7C1764658889%7C5184000%7CSat%2C+31-Jan-2026+07%3A01%3A29+GMT
.douyin.com TRUE / TRUE 1769842917 sid_tt a8c53425971cee759713a0d534f7a468
.douyin.com TRUE / TRUE 1769842917 sid_ucp_v1 1.0.0-KDgzOWJmYzI1NDM4ZmUyMzY3NTQ0ZDc0OTVmNjZkZWY5Y2VhZDEwYzEKIQi-yZDPrfS7BRDJnbrJBhjvMSAMMPO-tvkFOAdA9AdIBBoCaGwiIGE4YzUzNDI1OTcxY2VlNzU5NzEzYTBkNTM0ZjdhNDY4
.douyin.com TRUE / TRUE 1769842917 ssid_ucp_v1 1.0.0-KDgzOWJmYzI1NDM4ZmUyMzY3NTQ0ZDc0OTVmNjZkZWY5Y2VhZDEwYzEKIQi-yZDPrfS7BRDJnbrJBhjvMSAMMPO-tvkFOAdA9AdIBBoCaGwiIGE4YzUzNDI1OTcxY2VlNzU5NzEzYTBkNTM0ZjdhNDY4
.douyin.com TRUE / TRUE 1797188919 ttwid 1%7C3OTaRIjzlagT1R3CIKzm-lrgR4QZHSSf4C_JY28hZBE%7C1765652883%7Cdd5944a2b71c814a19310cc28ec65bb57c41a5924b98889c818c647c1a9cd789
.douyin.com TRUE / TRUE 1769842917 uid_tt 2d2292491b47cba2bca8ad469044313c
.douyin.com TRUE / TRUE 1769842917 uid_tt_ss 2d2292491b47cba2bca8ad469044313c
.douyin.com TRUE / FALSE 1800218614 volume_info %7B%22volume%22%3A0.058%2C%22isUserMute%22%3Afalse%2C%22isMute%22%3Afalse%7D
.youtube.com TRUE / FALSE 0 PREF hl=en&tz=UTC
.youtube.com TRUE / TRUE 0 SOCS CAI
.youtube.com TRUE / TRUE 1781404953 VISITOR_INFO1_LIVE eizmje-lCRg
.youtube.com TRUE / TRUE 1781404953 VISITOR_PRIVACY_METADATA CgJISxIEGgAgJQ%3D%3D
.youtube.com TRUE / TRUE 0 YSC u0QAormHZmk
.youtube.com TRUE / TRUE 1781404174 __Secure-ROLLOUT_TOKEN CIz99prG2qWIMRCI96v8h8GRAxiw4c_8h8GRAw%3D%3D
.youtube.com TRUE / TRUE 1781404173 __Secure-YNID 14.YT=WCdFIxVbI-_UdJV0Oq9PEe7pwy0VrgqhG0x3t_NACLFpSj-vXlBCkFfQoogFvF-r43NnykP1s0yUXwaTsCdeUupHubi3bzIujZfb0KwEs-iZWAmO6GJC8lnFoYmqkgrI6im2jbS6m1ww1glISZwm5ixzM0sXlM3h730ILh2cD2B15t--SmtNKSTfUWADmMeKItgtDIT8rEUvY9ZKROzqBwY3oBakKlWuRn0mvJIc0XgL14-ZKWclidfpcEF5bTh3RSE8FrL-3zPS1rXeDwuzzmxWHnBHrF0PTf0NjRj4iurKCHQ5eG9mbSWqWkBlSUKYHiqqhr7hvqMbl51qMstINQ
www.douyin.com FALSE / TRUE 1788959894 UIFID 1b474bc7e0db9591e645dd8feb8c65aae4845018effd0c2743039a380ee64740f7a504ffa95976eb1ff6c460678462bf4c11608eea38012581a3420231dc8cc1df6a3994b9d2ea2231c9e9a71ad7e62bb51997b911f38a7ed79c52934af24edfceccc758104a7f2cbe7bf3efa405b4d4ae93e7e6b75ced4d09a9b03ede6adba13fdca01fdad4f53e16ea91ce3994615029e37b399746db3de5ed17bd2ec1cb1fc70373395d6531975ace4e6cbabc5f96fd8dd422cf953d54e4f2e3be4bf0bae4
www.douyin.com FALSE / TRUE 1797112873 __ac_signature _02B4Z6wo00f01FLc0ygAAIDBvfwhoiQ43JhS.NeAAH2T2b
www.douyin.com FALSE / FALSE 0 architecture amd64
www.douyin.com FALSE / FALSE 0 device_web_cpu_core 20
www.douyin.com FALSE / FALSE 0 device_web_memory_size 8
www.douyin.com FALSE / FALSE 0 douyin.com
www.douyin.com FALSE / TRUE 1788959892 fpk1 U2FsdGVkX1+uAYVnpjAJyYq7U+tilyeRF9JR93oAt/PgAjmKOUTwF/jaDe6Ly0Iac8u96PCa2wekKxFu6pI23w==
www.douyin.com FALSE / TRUE 1788959892 fpk2 7ddeda88d0c599cc494da0dece6554d5
www.douyin.com FALSE / FALSE 1770222441 s_v_web_id verify_miuibdp5_o5ZsovxD_B7YC_4FM4_9wgs_j56nppke8yve
www.douyin.com FALSE / FALSE 0 xg_device_score 8.097240372472122
www.douyin.com FALSE / FALSE 1785936062 xgplayer_device_id 96210314128
www.douyin.com FALSE / FALSE 1785936062 xgplayer_user_id 148294698804
+40
View File
@@ -0,0 +1,40 @@
import boto3
from botocore.client import Config
# minio config
# MinIO配置
MINIO_ENDPOINT = "s3.sansenhoshi.top" # 不含 http/https
MINIO_ACCESS_KEY = "JDMynACSjPaN8JRwriwS"
MINIO_SECRET_KEY = "JRl4bIGxeiqwqvfeBTlAWQbUKMpNNHSZPm6ne93j"
MINIO_BUCKET = "s-file-trans"
MINIO_REGION = "bot"
MINIO_SECURE = True # False 则用 HTTP
# 可访问的公网地址前缀
MINIO_PUBLIC_DOMAIN = f"https://{MINIO_ENDPOINT}/{MINIO_BUCKET}"
s3_client = boto3.client(
"s3",
endpoint_url=f"{'https' if MINIO_SECURE else 'http'}://{MINIO_ENDPOINT}",
aws_access_key_id=MINIO_ACCESS_KEY,
aws_secret_access_key=MINIO_SECRET_KEY,
region_name=MINIO_REGION,
config=Config(signature_version="s3v4"),
)
def upload_to_s3(file_path) -> str:
file_key = f"cache/{file_path.name}" # 你可以换成别的目录
try:
s3_client.upload_file(
str(file_path),
MINIO_BUCKET,
file_key,
ExtraArgs={"ACL": "public-read"} # 如果你设置桶为私有,可以移除这行
)
return f"{MINIO_PUBLIC_DOMAIN}/{file_key}"
except Exception as e:
logger.exception(f"上传到 MinIO 失败: {e}")
return ""
+114
View File
@@ -0,0 +1,114 @@
import hmac
import hashlib
import time
import urllib.parse
from functools import reduce
from hashlib import md5
from aiohttp import ClientSession
# doc: https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
}
# fmt: off
mixinKeyEncTab = [
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
36, 20, 34, 44, 52
]
# fmt: on
def getMixinKey(orig: str):
"对 imgKey 和 subKey 进行字符顺序打乱编码"
return reduce(lambda s, i: s + orig[i], mixinKeyEncTab, "")[:32]
def encWbi(params: dict, img_key: str, sub_key: str):
"为请求参数进行 wbi 签名"
mixin_key = getMixinKey(img_key + sub_key)
curr_time = round(time.time())
params["wts"] = curr_time # 添加 wts 字段
params = dict(sorted(params.items())) # 按照 key 重排参数
# 过滤 value 中的 "!'()*" 字符
params = {
k: "".join(filter(lambda chr: chr not in "!'()*", str(v)))
for k, v in params.items()
}
query = urllib.parse.urlencode(params) # 序列化参数
wbi_sign = md5((query + mixin_key).encode()).hexdigest() # 计算 w_rid
params["w_rid"] = wbi_sign
return params
async def getWbiKeys():
"获取最新的 img_key 和 sub_key"
async with ClientSession(headers=headers) as session:
async with session.get("https://api.bilibili.com/x/web-interface/nav") as resp:
json_content = await resp.json()
img_url: str = json_content["data"]["wbi_img"]["img_url"]
sub_url: str = json_content["data"]["wbi_img"]["sub_url"]
img_key = img_url.rsplit("/", 1)[1].split(".")[0]
sub_key = sub_url.rsplit("/", 1)[1].split(".")[0]
return img_key, sub_key
async def get_query(params: dict):
"""
获取签名后的查询参数
"""
img_key, sub_key = await getWbiKeys()
signed_params = encWbi(params=params, img_key=img_key, sub_key=sub_key)
query = urllib.parse.urlencode(signed_params)
return query
def hmac_sha256(key, message):
"""
使用HMAC-SHA256算法对给定的消息进行加密
:param key: 密钥
:param message: 要加密的消息
:return: 加密后的哈希值
"""
# 将密钥和消息转换为字节串
key = key.encode("utf-8")
message = message.encode("utf-8")
# 创建HMAC对象,使用SHA256哈希算法
hmac_obj = hmac.new(key, message, hashlib.sha256)
# 计算哈希值
hash_value = hmac_obj.digest()
# 将哈希值转换为十六进制字符串
hash_hex = hash_value.hex()
return hash_hex
async def get_ticket():
"""
获取ticket
"""
o = hmac_sha256("XgwSnGZ1p", f"ts{int(time.time())}")
url = "https://api.bilibili.com/bapis/bilibili.api.ticket.v1.Ticket/GenWebTicket"
params = {
"key_id": "ec02",
"hexsign": o,
"context[ts]": f"{int(time.time())}",
"csrf": "",
}
async with ClientSession(headers=headers) as session:
async with session.post(url, params=params) as resp:
json_content = await resp.json()
return json_content["data"]["ticket"]
if __name__ == "__main__":
import asyncio
loop = asyncio.get_event_loop()
loop.run_until_complete(get_ticket())