Initial commit: sanitize repository for remote push

This commit is contained in:
theshy
2026-03-21 01:36:28 +08:00
commit 3925cb508f
21 changed files with 3357 additions and 0 deletions

165
session_top_comment.py Normal file
View File

@ -0,0 +1,165 @@
import json, re, subprocess, time, requests
from pathlib import Path
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
from logger import get_system_logger, log_exception
import shutil
# --- 配置 ---
SESSION_DIR = Path("./session")
COOKIE_FILE = Path("./cookies.json")
# BILIUP_PATH = Path("./biliup")
BILIUP_PATH = shutil.which("biliup") or "./biliup"
MAX_RETRIES, BASE_DELAY, POLL_INTERVAL = 5, 180, 10
ANSI_ESCAPE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
# 初始化系统日志
logger = get_system_logger("session_top_comment")
def strip_ansi(text: str) -> str:
return ANSI_ESCAPE.sub("", text or "")
class TopCommentClient:
def __init__(self):
with open(COOKIE_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
ck = {c["name"]: c["value"] for c in data.get("cookie_info", {}).get("cookies", [])}
self.csrf = ck.get("bili_jct")
if not self.csrf: raise ValueError("Cookie 中缺少 bili_jct")
self.session = requests.Session()
self.session.cookies.update(ck)
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Referer": "https://www.bilibili.com/",
"Origin": "https://www.bilibili.com"
})
def get_aid(self, bvid):
res = self.session.get("https://api.bilibili.com/x/web-interface/view", params={"bvid": bvid}).json()
if res.get('code') != 0: raise RuntimeError(f"View API: {res.get('message')}")
return res['data']['aid']
def post_and_top(self, aid, msg):
# 1. 发表评论
r = self.session.post("https://api.bilibili.com/x/v2/reply/add",
data={"type": 1, "oid": aid, "message": msg, "plat": 1, "csrf": self.csrf}).json()
if r.get('code') != 0: raise RuntimeError(f"Post API: {r.get('message')}")
rpid = r['data']['rpid']
# 2. 等待 3s 数据库同步
logger.info(f"评论已发布(rpid={rpid}),等待 3s 置顶...")
time.sleep(3)
# 3. 置顶
r = self.session.post("https://api.bilibili.com/x/v2/reply/top",
data={"type": 1, "oid": aid, "rpid": rpid, "action": 1, "csrf": self.csrf}).json()
if r.get('code') != 0: raise RuntimeError(f"Top API: {r.get('message')}")
class CommentManager:
def __init__(self):
self.client = TopCommentClient()
self.pending = {} # {folder_path: {'attempts': 0, 'next_run': 0}}
self._cache = {"time": 0, "videos": []}
def fetch_videos(self):
"""调用 biliup list 获取最近上传视频"""
if time.time() - self._cache["time"] < 60: return self._cache["videos"]
try:
res = subprocess.run([str(BILIUP_PATH), "list", "--max-pages", "1"], capture_output=True, text=True, encoding='utf-8')
videos = []
for line in strip_ansi(res.stdout).splitlines():
if line.startswith("BV"):
parts = line.split("\t")
if len(parts) >= 2: videos.append({"bvid": parts[0].strip(), "title": parts[1].strip()})
self._cache = {"time": time.time(), "videos": videos}
return videos
except Exception as e:
logger.error(f"biliup list 失败: {e}")
return []
def scan_and_add(self, folder: Path):
if not folder.is_dir() or (folder / "comment_done.flag").exists(): return
if (folder / "songs.txt").exists() and folder not in self.pending:
logger.info(f"发现待处理任务: {folder.name}")
self.pending[folder] = {'attempts': 0, 'next_run': 0}
def process_queue(self):
now = time.time()
for folder in list(self.pending.keys()):
task = self.pending[folder]
if task['next_run'] > now: continue
try:
# 1. 查找匹配视频
videos = self.fetch_videos()
# 模糊匹配:文件夹名包含在视频标题中,或视频标题包含在文件夹名中
matched_bvid = next((v['bvid'] for v in videos if folder.name in v['title'] or v['title'] in folder.name), None)
# 如果没找到,也尝试从文件夹名提取 [BV...]
if not matched_bvid:
bv_match = re.search(r"\[(BV[0-9A-Za-z]+)\]", folder.name)
if bv_match: matched_bvid = bv_match.group(1)
if not matched_bvid:
raise RuntimeError("未在最近上传列表中找到匹配视频")
# 2. 读取内容
content = (folder / "songs.txt").read_text(encoding="utf-8").strip()
if not content:
logger.warning(f"songs.txt 内容为空,取消任务: {folder.name}")
self.pending.pop(folder); continue
# 3. 执行发布和置顶
aid = self.client.get_aid(matched_bvid)
self.client.post_and_top(aid, content)
# 4. 成功标记
(folder / "comment_done.flag").touch()
logger.info(f"任务完成: {folder.name} -> {matched_bvid}")
self.pending.pop(folder)
except Exception as e:
task['attempts'] += 1
if task['attempts'] >= MAX_RETRIES:
logger.error(f"任务最终失败: {folder.name} - {e}")
self.pending.pop(folder)
else:
delay = BASE_DELAY * (2 ** (task['attempts']-1))
task['next_run'] = now + delay
logger.warning(f"任务推迟({task['attempts']}/{MAX_RETRIES}): {folder.name} - {e}. {delay}s 后重试")
def main():
logger.info("="*50)
logger.info("置顶评论模块启动")
logger.info("="*50)
try:
mgr = CommentManager()
except Exception as e:
logger.error(f"初始化失败: {e}")
return
# 1. 初始扫描
for f in SESSION_DIR.iterdir(): mgr.scan_and_add(f)
# 2. 启动 Watchdog
class Handler(FileSystemEventHandler):
def on_created(self, event):
p = Path(event.src_path)
if p.name == "songs.txt": mgr.scan_and_add(p.parent)
observer = Observer()
observer.schedule(Handler(), str(SESSION_DIR), recursive=True)
observer.start()
logger.info(f"开始监控目录: {SESSION_DIR}")
try:
while True:
mgr.process_queue()
time.sleep(POLL_INTERVAL)
except KeyboardInterrupt:
observer.stop()
observer.join()
logger.info("置顶评论模块已停止")
if __name__ == "__main__":
main()