import json import time import requests import re import shutil import subprocess import random from pathlib import Path from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler from logger import get_system_logger, log_exception # ================= 配置区域 ================= SESSION_DIR = Path("./session") COOKIE_FILE = Path("./cookies.json") CHECK_INTERVAL = 5 # 合集 ID 配置 SEASON_ID_A = 7196643 # 合集 A (同名视频) SEASON_ID_B = 7196624 # 合集 B (Upload切片) # 自动寻找 biliup BILIUP_PATH = shutil.which("biliup") or "biliup" # 初始化日志 logger = get_system_logger("add_to_collection.py") # =========================================== class BiliCollectionClient: def __init__(self): self.load_cookies() self.session = requests.Session() self.session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://member.bilibili.com/platform/upload-manager/distribution" }) def load_cookies(self): if not COOKIE_FILE.exists(): raise FileNotFoundError(f"Cookies 文件不存在: {COOKIE_FILE}") with open(COOKIE_FILE, "r", encoding="utf-8") as f: data = json.load(f) self.cookies = {c["name"]: c["value"] for c in data.get("cookie_info", {}).get("cookies", [])} if "cookie_info" in data else data self.csrf = self.cookies.get("bili_jct") def get_video_info(self, bvid): url = "https://api.bilibili.com/x/web-interface/view" try: self.session.cookies.update(self.cookies) res = self.session.get(url, params={"bvid": bvid}, timeout=10).json() if res["code"] == 0: d = res["data"] return {"aid": d["aid"], "cid": d["cid"], "title": d["title"], "charging_pay": 0} except Exception as e: logger.error(f"获取视频信息失败: {e}") return None def resolve_section_id(self, sid): url = "https://member.bilibili.com/x2/creative/web/seasons" try: self.session.cookies.update(self.cookies) res = self.session.get(url, params={"pn": 1, "ps": 50}).json() for s in res.get("data", {}).get("seasons", []): if s.get("season", {}).get("id") == sid: return s.get("sections", {}).get("sections", [])[0]["id"] except: pass return None def add_videos_batch(self, section_id, episodes): if not episodes: return True # 频率控制 wait = random.uniform(5.0, 10.0) logger.info(f"☕ 模拟人工操作,等待 {wait:.2f}s 后提交到合集...") time.sleep(wait) url = "https://member.bilibili.com/x2/creative/web/season/section/episodes/add" params = {"csrf": self.csrf} try: res = self.session.post(url, params=params, json={"sectionId": section_id, "episodes": episodes}).json() return res["code"] == 0 except Exception as e: log_exception(logger, e, "批量添加合集异常") return False class CollectionHandler(FileSystemEventHandler): def __init__(self, client, sid_a, sid_b): self.client = client self.sid_a = sid_a self.sid_b = sid_b self.ansi_escape = re.compile(r"\x1b\[[0-9;]*[A-Za-z]") def on_created(self, event): # 监听文件夹创建或 bvid.txt 创建 if event.is_directory or event.src_path.endswith("bvid.txt"): self.process_all() def process_all(self): recent = self.fetch_biliup_list() pending_a, pending_b = [], [] for folder in SESSION_DIR.iterdir(): if not folder.is_dir(): continue # 任务 A: 同名视频 -> 合集 A flag_a = folder / "collection_a_done.flag" if self.sid_a and not flag_a.exists(): bvid = self.match_bvid(folder.name, recent) if bvid: info = self.client.get_video_info(bvid) if info: pending_a.append((folder, info)) # 任务 B: 切片视频 -> 合集 B flag_b = folder / "collection_b_done.flag" txt = folder / "bvid.txt" if self.sid_b and not flag_b.exists() and txt.exists(): try: bvid = txt.read_text(encoding='utf-8').strip() if bvid.startswith("BV"): info = self.client.get_video_info(bvid) if info: pending_b.append((folder, info)) except: pass # 批量执行提交 if pending_a: if self.client.add_videos_batch(self.sid_a, [i[1] for i in pending_a]): for f, _ in pending_a: (f / "collection_a_done.flag").touch() logger.info(f"合集 A 更新完成: {len(pending_a)}个任务") if pending_b: if self.client.add_videos_batch(self.sid_b, [i[1] for i in pending_b]): for f, _ in pending_b: (f / "collection_b_done.flag").touch() logger.info(f"合集 B 更新完成: {len(pending_b)}个任务") def fetch_biliup_list(self): try: res = subprocess.run([BILIUP_PATH, "list"], capture_output=True, text=True, encoding='utf-8') clean_out = self.ansi_escape.sub("", res.stdout) return [{"bvid": l.split()[0], "title": "".join(l.split()[1:])} for l in clean_out.splitlines() if l.startswith("BV")] except: return [] def match_bvid(self, name, vlist): n = lambda x: re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9]', '', x).lower() target = n(name) for v in vlist: vn = n(v['title']) if target in vn or vn in target: return v['bvid'] return None def main(): logger.info("="*50) logger.info("合集监控模块启动") logger.info("="*50) client = BiliCollectionClient() sid_a = client.resolve_section_id(SEASON_ID_A) if SEASON_ID_A > 0 else None sid_b = client.resolve_section_id(SEASON_ID_B) if SEASON_ID_B > 0 else None handler = CollectionHandler(client, sid_a, sid_b) handler.process_all() # 初始扫描 observer = Observer() observer.schedule(handler, str(SESSION_DIR), recursive=False) observer.start() try: while True: time.sleep(CHECK_INTERVAL) except KeyboardInterrupt: observer.stop() observer.join() if __name__ == "__main__": main()