Initial commit: sanitize repository for remote push

This commit is contained in:
theshy
2026-03-21 01:36:28 +08:00
commit 3925cb508f
21 changed files with 3357 additions and 0 deletions

316
upload.py Normal file
View File

@ -0,0 +1,316 @@
import os
import time
import subprocess
import json
import re
import random
import shutil
import sys
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 = shutil.which("biliup") or "./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):
src_path = event.src_path
if isinstance(src_path, bytes): src_path = src_path.decode('utf-8')
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 _wait_exponential(self, retry_count, base_wait=300, max_wait=3600):
"""指数退避等待计算"""
# 计算等待时间60, 120, 240... 最大 600秒
wait_time = min(base_wait * (2 ** retry_count), max_wait)
return wait_time
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:
parsed = self.config.parse_filename(video_stem)
streamer = parsed.get('streamer', video_stem)
date = parsed.get('date', '')
songs_json = work_dir / "songs.json"
songs_txt = work_dir / "songs.txt"
songs_list = ""
song_count = 0
if songs_json.exists():
try:
with open(songs_json, 'r', encoding='utf-8') as f:
data = json.load(f)
song_count = len(data.get('songs', []))
except Exception: pass
if songs_txt.exists():
songs_list = songs_txt.read_text(encoding='utf-8').strip()
quote = self.config.get_random_quote()
template_vars = {
'streamer': streamer, 'date': date, 'song_count': song_count,
'songs_list': songs_list, 'daily_quote': quote.get('text', ''),
'quote_author': quote.get('author', '')
}
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)
streamers_config = self.config.config.get('streamers', {})
if streamer in streamers_config:
tags = streamers_config[streamer].get('tags', template.get('tag', ''))
else:
tags = template.get('tag', '翻唱,唱歌,音乐').format(**template_vars)
video_files = sorted([str(v) for v in split_dir.glob("*") if v.suffix.lower() in {'.mp4', '.mkv', '.mov', '.flv', '.ts'}])
if not video_files:
logger.error(f"切片目录 {split_dir} 内没找到视频")
return
upload_settings = self.config.config.get('upload_settings', {})
tid = upload_settings.get('tid', 31)
# 1. 刷新登录
subprocess.run([BILIUP_PATH, "renew"], shell=False, capture_output=True)
# 2. 准备分批
BATCH_SIZE = 5
logger.info(f"启动分批投稿 (总计 {len(video_files)} 个分片)...")
first_batch = video_files[:BATCH_SIZE]
remaining_batches = [video_files[i:i + BATCH_SIZE] for i in range(BATCH_SIZE, len(video_files), BATCH_SIZE)]
upload_cmd = [
BILIUP_PATH, "upload",
*first_batch,
"--title", title,
"--tid", str(tid),
"--tag", tags,
"--copyright", str(upload_settings.get('copyright', 2)),
"--source", upload_settings.get('source', '直播回放'),
"--desc", description
]
if dynamic: upload_cmd.extend(["--dynamic", dynamic])
cover = upload_settings.get('cover', '')
if cover and Path(cover).exists(): upload_cmd.extend(["--cover", cover])
bvid = None
MAX_ATTEMPTS = 5 # 定义最大尝试次数
# ==========================
# 阶段一:首批上传 (最多5次)
# ==========================
logger.info(f"正在上传第一批 ({len(first_batch)}个文件)...")
for attempt in range(1, MAX_ATTEMPTS + 1):
logger.info(f"首批上传尝试 [{attempt}/{MAX_ATTEMPTS}]...")
result = subprocess.run(upload_cmd, shell=False, capture_output=True, text=True, encoding='utf-8')
if result.returncode == 0:
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}")
(work_dir / "bvid.txt").write_text(bvid, encoding='utf-8')
break # 成功退出循环
else:
logger.warning(f"上传命令返回成功但未找到BVID (尝试 {attempt}/{MAX_ATTEMPTS})")
# 如果没有成功 (没有break)
if attempt < MAX_ATTEMPTS:
err_msg = result.stderr.strip()[-100:] if result.stderr else "无标准错误输出"
wait_time = self._wait_exponential(attempt - 1)
logger.error(f"第一批上传失败或未获取BVID等待 {wait_time}秒后重试。错误片段: {err_msg}")
time.sleep(wait_time)
else:
logger.error("第一批上传已达到最大重试次数 (5次),中止本次任务。")
return # 彻底结束函数,不进行后续操作
# ==========================
# 阶段二:追加上传 (每批最多5次)
# ==========================
if bvid:
for idx, batch in enumerate(remaining_batches, 2):
logger.info(f"等待 45 秒冷却时间,准备上传第 {idx} 批...")
time.sleep(45)
batch_success = False
for attempt in range(1, MAX_ATTEMPTS + 1):
logger.info(f"正在追加第 {idx} 批 ({len(batch)}个) - 尝试 [{attempt}/{MAX_ATTEMPTS}]...")
append_cmd = [BILIUP_PATH, "append", "--vid", bvid, *batch]
res = subprocess.run(append_cmd, shell=False, capture_output=True, text=True, encoding='utf-8')
if res.returncode == 0:
logger.info(f"{idx} 批追加成功")
batch_success = True
break # 成功退出内层循环,进入下一批
# 如果失败
if attempt < MAX_ATTEMPTS:
err_msg = res.stderr.strip()[-100:] if res.stderr else "无标准错误输出"
wait_time = self._wait_exponential(attempt - 1)
logger.error(f"{idx} 批追加失败,等待 {wait_time}秒后重试。错误片段: {err_msg}")
time.sleep(wait_time)
if not batch_success:
logger.error(f"{idx} 批追加已达到最大重试次数 (5次)。为防止顺序错乱,中止后续上传。")
return # 某一批次彻底失败,停止整个流程
# 只有当所有循环都正常走完没有 return才会执行到这里
logger.info(f"所有分片上传完成: {bvid}")
upload_done.touch()
# 清理
try:
if split_dir.exists(): shutil.rmtree(split_dir)
for ext in ['.mp4', '.mkv', '.mov', '.flv', '.ts']:
orig = work_dir / f"{video_stem}{ext}"
if orig.exists(): orig.unlink()
except Exception as e:
logger.error(f"清理空间失败: {e}")
else:
# 逻辑上如果第一阶段return了这里不会执行
# 但如果第一阶段break了但没bvid(理论上不可能,除非正则漏了),做个保险
logger.error("逻辑错误流程继续但无BVID上传中止")
except Exception as e:
log_exception(logger, e, "上传异常")
finally:
self.processing_sets.discard(video_stem)
def main():
path = Path(SESSION_DIR)
path.mkdir(parents=True, exist_ok=True)
logger.info("上传模块启动 (MaxRetry=5)")
config = UploadConfig(CONFIG_FILE)
handler = UploadHandler(config)
for sub_dir in path.iterdir():
if sub_dir.is_dir():
if (sub_dir / DONE_FLAG).exists() and not (sub_dir / UPLOAD_FLAG).exists():
handler.handle_upload(sub_dir / DONE_FLAG)
observer = Observer()
observer.schedule(handler, str(path), recursive=True)
observer.start()
try:
while True: time.sleep(5)
except KeyboardInterrupt:
observer.stop()
observer.join()
if __name__ == "__main__":
main()