Initial commit: sanitize repository for remote push
This commit is contained in:
165
session_top_comment.py
Normal file
165
session_top_comment.py
Normal 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()
|
||||
Reference in New Issue
Block a user