import json import time import requests import re import shutil import subprocess from pathlib import Path from logger import get_system_logger, log_exception # ================= 配置区域 ================= SESSION_DIR = Path("./session") COOKIE_FILE = Path("./cookies.json") # 【这里填你 B 站网页上看到的合集 ID】 # 脚本会自动根据这两个 ID 去查找对应的 Section ID (小节ID) SEASON_ID_A = 7196643 # 合集 A (同名视频) SEASON_ID_B = 7196624 # 合集 B (Upload切片) # 自动寻找 biliup BILIUP_PATH = shutil.which("biliup") or "biliup" # =========================================== logger = get_system_logger("collection_manager") class BiliCollectionClient: def __init__(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) # 兼容处理 cookie 格式 if "cookie_info" in data: self.cookies = {c["name"]: c["value"] for c in data.get("cookie_info", {}).get("cookies", [])} else: self.cookies = data self.csrf = self.cookies.get("bili_jct") if not self.csrf: raise ValueError("Cookie 中缺少 bili_jct (CSRF Token)") self.session = requests.Session() self.session.cookies.update(self.cookies) # 使用你测试成功的 Headers 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://member.bilibili.com/platform/upload-manager/distribution" }) def get_video_info(self, bvid): """通过 BVID 获取 AID, CID 和 Title""" url = "https://api.bilibili.com/x/web-interface/view" try: res = self.session.get(url, params={"bvid": bvid}, timeout=10).json() if res["code"] != 0: logger.error(f"查询视频信息失败 [{bvid}]: {res['message']}") return None data = res["data"] return { "aid": data["aid"], "cid": data["cid"], "title": data["title"] } except Exception as e: logger.error(f"获取视频信息异常: {e}") return None def resolve_section_id(self, target_season_id): """ 【关键逻辑】通过 Season ID (合集ID) 查找 Section ID (小节ID) """ url = "https://member.bilibili.com/x2/creative/web/seasons" params = {"pn": 1, "ps": 50} # 获取前50个合集 try: res = self.session.get(url, params=params, timeout=10).json() if res.get("code") != 0: logger.error(f"获取合集列表失败: {res.get('message')}") return None seasons = res.get("data", {}).get("seasons", []) for s in seasons: current_sid = s.get("season", {}).get("id") # 找到目标合集 if current_sid == target_season_id: title = s.get("season", {}).get("title", "未知标题") sections = s.get("sections", {}).get("sections", []) if sections: # 默认取第一个小节 first_section_id = sections[0]["id"] logger.info(f"✅ ID解析成功: 合集[{title}]({target_season_id}) -> 小节ID: {first_section_id}") return first_section_id else: logger.error(f"❌ 合集[{title}]({target_season_id}) 存在,但没有创建任何小节!") return None logger.error(f"❌ 未找到 Season ID 为 {target_season_id} 的合集,请检查 ID 是否正确。") return None except Exception as e: logger.error(f"解析 Section ID 异常: {e}") return None def add_video_to_section(self, section_id, video_info): """正式添加视频到合集""" url = "https://member.bilibili.com/x2/creative/web/season/section/episodes/add" # 参数必须包含 csrf params = {"csrf": self.csrf} payload = { "sectionId": section_id, "episodes": [{ "aid": video_info["aid"], "cid": video_info["cid"], "title": video_info["title"], "charging_pay": 0 }] } try: res = self.session.post(url, params=params, json=payload, timeout=15).json() if res["code"] == 0: logger.info(f"🎉 成功添加: {video_info['title']}") return True else: logger.error(f"添加失败: {res['message']} (Code: {res['code']})") return False except Exception as e: logger.error(f"添加请求异常: {e}") return False class CollectionWorker: def __init__(self, client, section_id_a, section_id_b): self.client = client self.section_id_a = section_id_a self.section_id_b = section_id_b self.ansi_escape = re.compile(r"\x1b\[[0-9;]*[A-Za-z]") def fetch_recent_videos(self): """获取最近投稿""" try: cmd = [str(BILIUP_PATH), "list", "--max-pages", "2"] res = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8') videos = [] for line in self.ansi_escape.sub("", res.stdout).splitlines(): parts = line.split() if len(parts) >= 2 and parts[0].startswith("BV"): raw_title = " ".join(parts[1:]) title = re.sub(r"(开放浏览|直播回放|审核中|-)$", "", raw_title).strip() videos.append({"bvid": parts[0], "title": title}) return videos except Exception: logger.warning("biliup list 执行失败,跳过同名视频匹配。") return [] def normalize(self, text): return re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9]', '', text).replace('点', '时').lower() def find_bvid_by_title(self, target_title, video_list): target_norm = self.normalize(target_title) for v in video_list: v_norm = self.normalize(v['title']) if target_norm in v_norm or v_norm in target_norm: return v['bvid'] return None def process_folder(self, folder: Path, video_list): flag_a = folder / "collection_a_done.flag" flag_b = folder / "collection_b_done.flag" # 任务 A: 同名视频 -> 合集 A if self.section_id_a and not flag_a.exists(): matched_bvid = self.find_bvid_by_title(folder.name, video_list) if matched_bvid: logger.info(f"任务A (同名): 匹配到 {matched_bvid},尝试添加...") info = self.client.get_video_info(matched_bvid) if info and self.client.add_video_to_section(self.section_id_a, info): flag_a.touch() # 任务 B: Upload切片 -> 合集 B if self.section_id_b and not flag_b.exists(): bvid_file = folder / "bvid.txt" if bvid_file.exists(): bvid = bvid_file.read_text(encoding='utf-8').strip() logger.info(f"任务B (切片): 读取到 {bvid},尝试添加...") info = self.client.get_video_info(bvid) if info and self.client.add_video_to_section(self.section_id_b, info): flag_b.touch() def main(): logger.info("启动合集管理模块 (基于成功测试版)...") try: client = BiliCollectionClient() except Exception as e: logger.error(f"客户端初始化失败: {e}") return # 1. 解析 ID (这是最关键的一步) logger.info("正在解析合集 ID...") real_section_a = None real_section_b = None if SEASON_ID_A > 0: real_section_a = client.resolve_section_id(SEASON_ID_A) if SEASON_ID_B > 0: real_section_b = client.resolve_section_id(SEASON_ID_B) if not real_section_a and not real_section_b: logger.error("没有解析到任何有效的 Section ID,脚本停止。") return # 2. 初始化 Worker worker = CollectionWorker(client, real_section_a, real_section_b) # 3. 扫描逻辑 logger.info("开始扫描目录...") recent_videos = worker.fetch_recent_videos() if SESSION_DIR.exists(): for folder in SESSION_DIR.iterdir(): if folder.is_dir(): worker.process_folder(folder, recent_videos) logger.info("扫描完成。") if __name__ == "__main__": main()