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): # 监听 split_done.flag 文件的生成 if not event.is_directory and event.src_path.lower().endswith(DONE_FLAG): logger.debug(f"检测到切割完成标记: {event.src_path}") self.handle_upload(Path(event.src_path)) def on_moved(self, event): if not event.is_directory and event.dest_path.lower().endswith(DONE_FLAG): logger.debug(f"检测到切割完成标记移动: {event.dest_path}") self.handle_upload(Path(event.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}") logger.debug(f"renew stderr: {renew_result.stderr.strip()}") else: logger.info("biliup renew 成功") # 9. 执行上传 logger.info(f"启动 biliup 投稿...") cmd = [ BILIUP_PATH, "upload", *video_files, "--title", title, "--tid", str(tid), "--tag", tags, "--copyright", str(copyright_val), "--source", source, "--desc", description ] if dynamic: cmd.extend(["--dynamic", dynamic]) if cover and Path(cover).exists(): cmd.extend(["--cover", cover]) logger.debug(f"biliup 命令: {' '.join(cmd[:5])}... (共 {len(video_files)} 个文件)") # shell=True 确保在 Windows 下调用正常 result = subprocess.run(cmd, shell=False, capture_output=True, text=True, encoding='utf-8') if result.returncode == 0: logger.info(f"投稿成功: {video_stem}") logger.info(f"标题: {title}") upload_done.touch() # 盖上"上传完成"戳 logger.info("生成上传完成标记") # 上传成功后清理空间 try: # 1. 删除 split_video 目录 if split_dir.exists(): shutil.rmtree(split_dir) logger.info(f"已删除切片目录: {split_dir}") # 2. 删除原视频文件 (匹配常见视频后缀) 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(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) logger.info(f"监控目录: {SESSION_DIR}") logger.info(f"Biliup 路径: {BILIUP_PATH}") logger.info(f"配置文件: {CONFIG_FILE}") # 加载配置 config = UploadConfig(CONFIG_FILE) event_handler = UploadHandler(config) observer = Observer() observer.schedule(event_handler, str(path), recursive=True) # 启动时扫描已有目录:如果有 split_done.flag 但没 upload_done.flag,补投 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() logger.info("文件监控已启动") try: while True: time.sleep(CHECK_INTERVAL) except KeyboardInterrupt: logger.info("接收到停止信号,正在关闭...") observer.stop() observer.join() logger.info("上传模块已停止") if __name__ == "__main__": main()