Files
2026-03-21_why-manifest/add_to_collection.py

172 lines
6.4 KiB
Python
Executable File

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()