import os import time import subprocess import json import re import random import shutil from pathlib import Path from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler from logger import get_system_logger, log_exception # ========================================== # 接口配置 # ========================================== SESSION_DIR = r'./session' # 监控的工作区目录 CHECK_INTERVAL = 5 # 检查频率 BILIUP_PATH = "./biliup" # biliup 命令 CONFIG_FILE = "upload_config.json" # 配置文件路径 DONE_FLAG = "split_done.flag" # monitorSongs.py 生成的标记 UPLOAD_FLAG = "upload_done.flag" # 本脚本生成的完成标记 # 初始化日志 logger = get_system_logger('upload') # ========================================== class UploadConfig: """上传配置管理器""" def __init__(self, config_path): self.config_path = Path(config_path) self.config = self.load_config() def load_config(self): """加载配置文件""" try: if not self.config_path.exists(): logger.error(f"配置文件不存在: {self.config_path}") return self.get_default_config() with open(self.config_path, 'r', encoding='utf-8') as f: config = json.load(f) logger.info(f"成功加载配置文件: {self.config_path}") return config except Exception as e: log_exception(logger, e, "加载配置文件失败") return self.get_default_config() def get_default_config(self): """默认配置""" logger.warning("使用默认配置") return { "upload_settings": { "tid": 31, "copyright": 2, "source": "直播回放", "cover": "" }, "template": { "title": "{streamer}_{date}", "description": "录制剪辑\n\n{songs_list}", "tag": "翻唱,直播切片,唱歌,音乐", "dynamic": "" }, "streamers": {}, "quotes": [], "filename_patterns": { "patterns": [] } } def parse_filename(self, filename): """从文件名解析主播名和日期""" patterns = self.config.get("filename_patterns", {}).get("patterns", []) for pattern_config in patterns: regex = pattern_config.get("regex") if not regex: continue match = re.match(regex, filename) if match: data = match.groupdict() date_format = pattern_config.get("date_format", "{date}") # 格式化日期 try: formatted_date = date_format.format(**data) data['date'] = formatted_date except KeyError: pass logger.debug(f"文件名匹配成功: {pattern_config.get('name')} -> {data}") return data # 默认返回原始文件名 logger.warning(f"文件名未匹配任何模式: {filename}") return {"streamer": filename, "date": ""} def get_random_quote(self): """随机获取一句名言""" quotes = self.config.get("quotes", []) if not quotes: return {"text": "", "author": ""} return random.choice(quotes) class UploadHandler(FileSystemEventHandler): def __init__(self, config): self.processing_sets = set() self.config = config def on_created(self, event): # 兼容处理 watchdog 路径编码问题 src_path = event.src_path if isinstance(src_path, bytes): src_path = src_path.decode('utf-8') # 监听 split_done.flag 文件的生成 if not event.is_directory and src_path.lower().endswith(DONE_FLAG): logger.debug(f"检测到切割完成标记: {src_path}") self.handle_upload(Path(src_path)) def on_moved(self, event): dest_path = event.dest_path if isinstance(dest_path, bytes): dest_path = dest_path.decode('utf-8') if not event.is_directory and dest_path.lower().endswith(DONE_FLAG): logger.debug(f"检测到切割完成标记移动: {dest_path}") self.handle_upload(Path(dest_path)) def handle_upload(self, flag_path): work_dir = flag_path.parent video_stem = work_dir.name upload_done = work_dir / UPLOAD_FLAG split_dir = work_dir / "split_video" # 防重复检查 if upload_done.exists() or video_stem in self.processing_sets: logger.debug(f"上传已完成或正在处理,跳过: {video_stem}") return logger.info("="*50) logger.info(f"准备上传: {video_stem}") logger.info("="*50) self.processing_sets.add(video_stem) try: # 1. 解析文件名 parsed = self.config.parse_filename(video_stem) streamer = parsed.get('streamer', video_stem) date = parsed.get('date', '') logger.info(f"主播: {streamer}, 日期: {date}") # 2. 读取歌曲信息 songs_json = work_dir / "songs.json" songs_txt = work_dir / "songs.txt" songs = [] song_count = 0 songs_list = "" if songs_json.exists(): try: with open(songs_json, 'r', encoding='utf-8') as f: data = json.load(f) songs = data.get('songs', []) song_count = len(songs) logger.info(f"读取到 {song_count} 首歌曲") except Exception as e: log_exception(logger, e, "读取 songs.json 失败") if songs_txt.exists(): songs_list = songs_txt.read_text(encoding='utf-8').strip() logger.info("已读取歌单文本") # 3. 获取随机名言 quote = self.config.get_random_quote() daily_quote = quote.get('text', '') quote_author = quote.get('author', '') # 4. 构建模板变量 template_vars = { 'streamer': streamer, 'date': date, 'song_count': song_count, 'songs_list': songs_list, 'daily_quote': daily_quote, 'quote_author': quote_author } # 5. 渲染标题和简介 template = self.config.config.get('template', {}) title = template.get('title', '{streamer}_{date}').format(**template_vars) description = template.get('description', '{songs_list}').format(**template_vars) dynamic = template.get('dynamic', '').format(**template_vars) # 6. 获取标签(优先使用主播专属标签) streamers_config = self.config.config.get('streamers', {}) if streamer in streamers_config: tags = streamers_config[streamer].get('tags', template.get('tag', '')) logger.info(f"使用主播专属标签: {streamer}") else: tags = template.get('tag', '翻唱,唱歌,音乐').format(**template_vars) logger.info(f"标题: {title}") logger.info(f"标签: {tags}") logger.debug(f"简介预览: {description[:100]}...") # 7. 获取所有切片视频 video_files = sorted([str(v) for v in split_dir.glob("*") if v.suffix.lower() in {'.mp4', '.mkv', '.mov', '.flv'}]) if not video_files: logger.error(f"切片目录 {split_dir} 内没找到视频") return logger.info(f"找到 {len(video_files)} 个视频分片") # 8. 读取上传设置 upload_settings = self.config.config.get('upload_settings', {}) tid = upload_settings.get('tid', 31) copyright_val = upload_settings.get('copyright', 2) source = upload_settings.get('source', '直播回放') cover = upload_settings.get('cover', '') # 8. 刷新 biliup 登录信息 renew_cmd = [BILIUP_PATH, "renew"] logger.info("尝试刷新 biliup 登录信息") renew_result = subprocess.run(renew_cmd, shell=False, capture_output=True, text=True, encoding='utf-8') if renew_result.returncode != 0: logger.warning(f"biliup renew 返回非 0: {renew_result.returncode}") else: logger.info("biliup renew 成功") # 9. 执行分批上传 logger.info(f"启动分批投稿 (每批 5 个)...") # 第一批:使用 upload 创建稿件 first_batch = video_files[:5] remaining_batches = [video_files[i:i + 5] for i in range(5, len(video_files), 5)] # 构建初始上传命令 upload_cmd = [ BILIUP_PATH, "upload", *first_batch, "--title", title, "--tid", str(tid), "--tag", tags, "--copyright", str(copyright_val), "--source", source, "--desc", description ] if dynamic: upload_cmd.extend(["--dynamic", dynamic]) if cover and Path(cover).exists(): upload_cmd.extend(["--cover", cover]) # 执行初始上传 logger.info(f"正在上传第一批 ({len(first_batch)} 个文件)...") result = subprocess.run(upload_cmd, shell=False, capture_output=True, text=True, encoding='utf-8') if result.returncode == 0: # 从 stdout 提取 BV 号 bv_match = re.search(r'"bvid":"(BV[A-Za-z0-9]+)"', result.stdout) if not bv_match: bv_match = re.search(r'(BV[A-Za-z0-9]+)', result.stdout) if bv_match: bvid = bv_match.group(1) logger.info(f"第一批投稿成功,获得 BV 号: {bvid}") # 追加后续批次 for idx, batch in enumerate(remaining_batches, 2): logger.info(f"正在追加第 {idx} 批 ({len(batch)} 个文件) 到 {bvid}...") time.sleep(15) # 适当等待 append_cmd = [ BILIUP_PATH, "append", "--vid", bvid, *batch ] append_res = subprocess.run(append_cmd, shell=False, capture_output=True, text=True, encoding='utf-8') if append_res.returncode != 0: logger.error(f"第 {idx} 批追加失败: {append_res.stderr[:200]}") logger.info(f"所有批次处理完成: {video_stem}") upload_done.touch() # 上传成功后清理空间 try: if split_dir.exists(): shutil.rmtree(split_dir) logger.info(f"已删除切片目录: {split_dir}") for ext in ['.mp4', '.mkv', '.mov', '.flv', '.ts']: original_video = work_dir / f"{video_stem}{ext}" if original_video.exists(): original_video.unlink() logger.info(f"已删除原视频: {original_video}") except Exception as cleanup_err: logger.error(f"清理空间失败: {cleanup_err}") else: logger.error("第一批上传成功但未能在输出中识别到 BV 号,无法追加后续分片") else: logger.error(f"第一批投稿失败,错误码: {result.returncode}") logger.error(f"错误信息: {result.stderr[:500]}") except Exception as e: log_exception(logger, e, "上传处理异常") finally: self.processing_sets.discard(video_stem) logger.info("="*50) def main(): path = Path(SESSION_DIR) path.mkdir(parents=True, exist_ok=True) logger.info("="*50) logger.info("上传模块启动 (Biliup 自动分批投稿)") logger.info("="*50) # 加载配置 config = UploadConfig(CONFIG_FILE) event_handler = UploadHandler(config) observer = Observer() observer.schedule(event_handler, str(path), recursive=True) # 启动时扫描已有目录 logger.info("扫描待上传任务...") scan_count = 0 for sub_dir in path.iterdir(): if sub_dir.is_dir(): split_flag = sub_dir / DONE_FLAG upload_flag = sub_dir / UPLOAD_FLAG if split_flag.exists() and not upload_flag.exists(): logger.info(f"发现待上传任务: {sub_dir.name}") event_handler.handle_upload(split_flag) scan_count += 1 logger.info(f"扫描完成,处理 {scan_count} 个待上传任务") observer.start() try: while True: time.sleep(CHECK_INTERVAL) except KeyboardInterrupt: observer.stop() observer.join() if __name__ == "__main__": main()