重构
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ""
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user