1446 lines
54 KiB
Python
1446 lines
54 KiB
Python
"""
|
||
媒体库管理服务
|
||
管理视频素材,包括上传、转场镜头分割、标签管理等
|
||
"""
|
||
|
||
import json
|
||
import uuid
|
||
import os
|
||
import hashlib
|
||
import shutil
|
||
from pathlib import Path
|
||
from typing import List, Dict, Optional, Tuple
|
||
from dataclasses import dataclass, asdict
|
||
from datetime import datetime
|
||
|
||
from python_core.config import settings
|
||
from python_core.utils.logger import logger
|
||
from python_core.utils.jsonrpc import create_response_handler
|
||
|
||
# 依赖管理器 - 遵循SOLID原则
|
||
class DependencyManager:
|
||
"""依赖管理器 - 单一职责原则(SRP): 只负责管理依赖"""
|
||
|
||
def __init__(self):
|
||
self._dependencies = {}
|
||
self._initialize_dependencies()
|
||
|
||
def _initialize_dependencies(self):
|
||
"""初始化所有依赖"""
|
||
self._check_opencv_dependency()
|
||
self._check_scenedetect_dependency()
|
||
|
||
def _check_opencv_dependency(self):
|
||
"""检查OpenCV依赖"""
|
||
try:
|
||
import cv2
|
||
import numpy as np
|
||
self._dependencies['opencv'] = {
|
||
'available': True,
|
||
'modules': {'cv2': cv2, 'numpy': np},
|
||
'version': cv2.__version__
|
||
}
|
||
logger.info(f"OpenCV {cv2.__version__} is available")
|
||
except ImportError as e:
|
||
self._dependencies['opencv'] = {
|
||
'available': False,
|
||
'error': str(e),
|
||
'modules': {}
|
||
}
|
||
logger.warning("OpenCV not available. Install opencv-python for full functionality.")
|
||
|
||
def _check_scenedetect_dependency(self):
|
||
"""检查PySceneDetect依赖"""
|
||
try:
|
||
from scenedetect import VideoManager, SceneManager
|
||
from scenedetect.detectors import ContentDetector
|
||
import scenedetect
|
||
|
||
self._dependencies['scenedetect'] = {
|
||
'available': True,
|
||
'modules': {
|
||
'VideoManager': VideoManager,
|
||
'SceneManager': SceneManager,
|
||
'ContentDetector': ContentDetector
|
||
},
|
||
'version': scenedetect.__version__
|
||
}
|
||
logger.info(f"PySceneDetect {scenedetect.__version__} is available for advanced scene detection")
|
||
except ImportError as e:
|
||
self._dependencies['scenedetect'] = {
|
||
'available': False,
|
||
'error': str(e),
|
||
'modules': {}
|
||
}
|
||
logger.warning("PySceneDetect not available. Install scenedetect for better scene detection.")
|
||
|
||
def is_available(self, dependency_name: str) -> bool:
|
||
"""检查依赖是否可用 - 开闭原则(OCP): 对扩展开放"""
|
||
return self._dependencies.get(dependency_name, {}).get('available', False)
|
||
|
||
def get_module(self, dependency_name: str, module_name: str):
|
||
"""获取依赖模块"""
|
||
if not self.is_available(dependency_name):
|
||
raise ImportError(f"Dependency '{dependency_name}' is not available")
|
||
|
||
modules = self._dependencies[dependency_name]['modules']
|
||
if module_name not in modules:
|
||
raise ImportError(f"Module '{module_name}' not found in dependency '{dependency_name}'")
|
||
|
||
return modules[module_name]
|
||
|
||
def get_dependency_info(self, dependency_name: str) -> dict:
|
||
"""获取依赖信息"""
|
||
return self._dependencies.get(dependency_name, {})
|
||
|
||
def get_all_dependencies(self) -> dict:
|
||
"""获取所有依赖信息"""
|
||
return self._dependencies.copy()
|
||
|
||
# 抽象接口 - 接口隔离原则(ISP): 定义专门的接口
|
||
from abc import ABC, abstractmethod
|
||
|
||
class VideoInfoExtractor(ABC):
|
||
"""视频信息提取器接口"""
|
||
|
||
@abstractmethod
|
||
def extract_video_info(self, file_path: str) -> Dict:
|
||
"""提取视频信息"""
|
||
pass
|
||
|
||
class SceneDetector(ABC):
|
||
"""场景检测器接口"""
|
||
|
||
@abstractmethod
|
||
def detect_scenes(self, file_path: str, threshold: float = 30.0) -> List[float]:
|
||
"""检测场景变化点"""
|
||
pass
|
||
|
||
class VideoProcessor(ABC):
|
||
"""视频处理器接口"""
|
||
|
||
@abstractmethod
|
||
def split_video(self, input_path: str, output_dir: str, scene_times: List[float]) -> List[str]:
|
||
"""分割视频"""
|
||
pass
|
||
|
||
class ThumbnailGenerator(ABC):
|
||
"""缩略图生成器接口"""
|
||
|
||
@abstractmethod
|
||
def generate_thumbnail(self, video_path: str, timestamp: float, output_path: str) -> bool:
|
||
"""生成视频缩略图"""
|
||
pass
|
||
|
||
class VideoSegmentCreator(ABC):
|
||
"""视频片段创建器接口"""
|
||
|
||
@abstractmethod
|
||
def create_segments_from_scenes(self, video_path: str, scene_changes: List[float],
|
||
original_video_id: str, tags: List[str] = None) -> List['VideoSegment']:
|
||
"""根据场景变化创建视频片段"""
|
||
pass
|
||
|
||
class FileHashCalculator(ABC):
|
||
"""文件哈希计算器接口"""
|
||
|
||
@abstractmethod
|
||
def calculate_hash(self, file_path: str) -> str:
|
||
"""计算文件哈希值"""
|
||
pass
|
||
|
||
# 具体实现类 - 单一职责原则(SRP): 每个类只负责一个功能
|
||
|
||
class FFProbeVideoInfoExtractor(VideoInfoExtractor):
|
||
"""使用FFProbe提取视频信息"""
|
||
|
||
def extract_video_info(self, file_path: str) -> Dict:
|
||
"""使用ffprobe获取准确的视频信息"""
|
||
file_size = os.path.getsize(file_path)
|
||
|
||
try:
|
||
import subprocess
|
||
import json
|
||
|
||
cmd = [
|
||
'ffprobe',
|
||
'-v', 'quiet',
|
||
'-print_format', 'json',
|
||
'-show_format',
|
||
'-show_streams',
|
||
file_path
|
||
]
|
||
|
||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||
|
||
if result.returncode == 0:
|
||
probe_data = json.loads(result.stdout)
|
||
|
||
video_stream = None
|
||
for stream in probe_data.get('streams', []):
|
||
if stream.get('codec_type') == 'video':
|
||
video_stream = stream
|
||
break
|
||
|
||
if video_stream:
|
||
duration = float(probe_data.get('format', {}).get('duration', 0))
|
||
width = int(video_stream.get('width', 0))
|
||
height = int(video_stream.get('height', 0))
|
||
|
||
fps_str = video_stream.get('r_frame_rate', '0/1')
|
||
if '/' in fps_str:
|
||
num, den = fps_str.split('/')
|
||
fps = float(num) / float(den) if float(den) != 0 else 0
|
||
else:
|
||
fps = float(fps_str)
|
||
|
||
frame_count = int(duration * fps) if fps > 0 else 0
|
||
|
||
logger.info(f"ffprobe video info: duration={duration:.2f}s, fps={fps:.2f}, resolution={width}x{height}")
|
||
|
||
return {
|
||
'duration': duration,
|
||
'width': width,
|
||
'height': height,
|
||
'fps': fps,
|
||
'frame_count': frame_count,
|
||
'file_size': file_size,
|
||
'codec': video_stream.get('codec_name', 'unknown')
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.warning(f"ffprobe failed: {e}")
|
||
raise
|
||
|
||
raise Exception("Failed to extract video info with ffprobe")
|
||
|
||
class OpenCVVideoInfoExtractor(VideoInfoExtractor):
|
||
"""使用OpenCV提取视频信息"""
|
||
|
||
def __init__(self, dependency_manager: DependencyManager):
|
||
self.dependency_manager = dependency_manager
|
||
|
||
def extract_video_info(self, file_path: str) -> Dict:
|
||
"""使用OpenCV获取视频信息"""
|
||
if not self.dependency_manager.is_available('opencv'):
|
||
raise Exception("OpenCV not available")
|
||
|
||
cv2 = self.dependency_manager.get_module('opencv', 'cv2')
|
||
file_size = os.path.getsize(file_path)
|
||
|
||
try:
|
||
cap = cv2.VideoCapture(file_path)
|
||
|
||
fps = cap.get(cv2.CAP_PROP_FPS)
|
||
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||
duration = frame_count / fps if fps > 0 else 0
|
||
|
||
cap.release()
|
||
|
||
logger.info(f"OpenCV video info: duration={duration:.2f}s, fps={fps:.2f}, resolution={width}x{height}")
|
||
|
||
return {
|
||
'duration': duration,
|
||
'width': width,
|
||
'height': height,
|
||
'fps': fps,
|
||
'frame_count': frame_count,
|
||
'file_size': file_size,
|
||
'codec': 'unknown'
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Failed to get video info with OpenCV: {e}")
|
||
raise
|
||
|
||
class PySceneDetectSceneDetector(SceneDetector):
|
||
"""使用PySceneDetect进行场景检测"""
|
||
|
||
def __init__(self, dependency_manager: DependencyManager):
|
||
self.dependency_manager = dependency_manager
|
||
|
||
def detect_scenes(self, file_path: str, threshold: float = 30.0) -> List[float]:
|
||
"""使用PySceneDetect检测场景变化点"""
|
||
if not self.dependency_manager.is_available('scenedetect'):
|
||
raise Exception("PySceneDetect not available")
|
||
|
||
VideoManager = self.dependency_manager.get_module('scenedetect', 'VideoManager')
|
||
SceneManager = self.dependency_manager.get_module('scenedetect', 'SceneManager')
|
||
ContentDetector = self.dependency_manager.get_module('scenedetect', 'ContentDetector')
|
||
|
||
try:
|
||
video_manager = VideoManager([file_path])
|
||
scene_manager = SceneManager()
|
||
scene_manager.add_detector(ContentDetector(threshold=threshold))
|
||
|
||
video_manager.start()
|
||
scene_manager.detect_scenes(frame_source=video_manager)
|
||
|
||
scene_list = scene_manager.get_scene_list()
|
||
|
||
scene_changes = [0.0]
|
||
for scene in scene_list:
|
||
start_time = scene[0].get_seconds()
|
||
end_time = scene[1].get_seconds()
|
||
|
||
if start_time > 0 and start_time not in scene_changes:
|
||
scene_changes.append(start_time)
|
||
|
||
if end_time not in scene_changes:
|
||
scene_changes.append(end_time)
|
||
|
||
scene_changes = sorted(list(set(scene_changes)))
|
||
video_manager.release()
|
||
|
||
logger.info(f"PySceneDetect found {len(scene_changes)-1} scene changes in video")
|
||
logger.debug(f"Scene change timestamps: {scene_changes}")
|
||
|
||
return scene_changes
|
||
|
||
except Exception as e:
|
||
logger.error(f"PySceneDetect failed: {e}")
|
||
raise
|
||
|
||
class OpenCVSceneDetector(SceneDetector):
|
||
"""使用OpenCV进行场景检测"""
|
||
|
||
def __init__(self, dependency_manager: DependencyManager):
|
||
self.dependency_manager = dependency_manager
|
||
|
||
def detect_scenes(self, file_path: str, threshold: float = 30.0) -> List[float]:
|
||
"""使用OpenCV检测场景变化点"""
|
||
if not self.dependency_manager.is_available('opencv'):
|
||
raise Exception("OpenCV not available")
|
||
|
||
cv2 = self.dependency_manager.get_module('opencv', 'cv2')
|
||
np = self.dependency_manager.get_module('opencv', 'numpy')
|
||
|
||
try:
|
||
cap = cv2.VideoCapture(file_path)
|
||
fps = cap.get(cv2.CAP_PROP_FPS)
|
||
|
||
if fps <= 0:
|
||
cap.release()
|
||
logger.warning(f"Invalid fps ({fps}) for video {file_path}")
|
||
return [0.0]
|
||
|
||
scene_changes = [0.0]
|
||
prev_frame = None
|
||
frame_count = 0
|
||
frame_skip = max(1, int(fps / 2))
|
||
|
||
while True:
|
||
ret, frame = cap.read()
|
||
if not ret:
|
||
break
|
||
|
||
if frame_count % frame_skip == 0:
|
||
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||
gray = cv2.resize(gray, (320, 240))
|
||
|
||
if prev_frame is not None:
|
||
diff = cv2.absdiff(prev_frame, gray)
|
||
mean_diff = np.mean(diff)
|
||
|
||
if mean_diff > threshold:
|
||
timestamp = frame_count / fps
|
||
if not scene_changes or timestamp - scene_changes[-1] > 1.0:
|
||
scene_changes.append(timestamp)
|
||
logger.debug(f"Scene change detected at {timestamp:.2f}s (diff: {mean_diff:.2f})")
|
||
|
||
prev_frame = gray
|
||
|
||
frame_count += 1
|
||
|
||
cap.release()
|
||
|
||
duration = frame_count / fps if fps > 0 else 0
|
||
if duration > 0 and (not scene_changes or duration - scene_changes[-1] > 0.5):
|
||
scene_changes.append(duration)
|
||
|
||
logger.info(f"OpenCV detected {len(scene_changes)-1} scene changes in video")
|
||
return scene_changes
|
||
|
||
except Exception as e:
|
||
logger.error(f"Failed to detect scene changes with OpenCV: {e}")
|
||
raise
|
||
|
||
# 工厂类 - 依赖倒置原则(DIP): 依赖抽象而不是具体实现
|
||
class VideoProcessorFactory:
|
||
"""视频处理器工厂 - 单一职责原则(SRP): 只负责创建对象"""
|
||
|
||
def __init__(self, dependency_manager: DependencyManager):
|
||
self.dependency_manager = dependency_manager
|
||
|
||
def create_video_info_extractor(self) -> VideoInfoExtractor:
|
||
"""创建视频信息提取器 - 开闭原则(OCP): 对扩展开放"""
|
||
# 优先使用FFProbe,回退到OpenCV
|
||
try:
|
||
return FFProbeVideoInfoExtractor()
|
||
except Exception:
|
||
if self.dependency_manager.is_available('opencv'):
|
||
return OpenCVVideoInfoExtractor(self.dependency_manager)
|
||
else:
|
||
raise Exception("No video info extractor available")
|
||
|
||
def create_scene_detector(self) -> SceneDetector:
|
||
"""创建场景检测器"""
|
||
# 优先使用PySceneDetect,回退到OpenCV
|
||
if self.dependency_manager.is_available('scenedetect'):
|
||
try:
|
||
return PySceneDetectSceneDetector(self.dependency_manager)
|
||
except Exception:
|
||
pass
|
||
|
||
if self.dependency_manager.is_available('opencv'):
|
||
return OpenCVSceneDetector(self.dependency_manager)
|
||
else:
|
||
raise Exception("No scene detector available")
|
||
|
||
def create_thumbnail_generator(self) -> ThumbnailGenerator:
|
||
"""创建缩略图生成器"""
|
||
if self.dependency_manager.is_available('opencv'):
|
||
return OpenCVThumbnailGenerator(self.dependency_manager)
|
||
else:
|
||
raise Exception("No thumbnail generator available")
|
||
|
||
def create_hash_calculator(self) -> FileHashCalculator:
|
||
"""创建哈希计算器"""
|
||
return MD5FileHashCalculator()
|
||
|
||
def create_video_segment_creator(self) -> VideoSegmentCreator:
|
||
"""创建视频片段创建器"""
|
||
hash_calculator = self.create_hash_calculator()
|
||
if self.dependency_manager.is_available('opencv'):
|
||
return OpenCVVideoSegmentCreator(self.dependency_manager, hash_calculator)
|
||
else:
|
||
raise Exception("No video segment creator available")
|
||
|
||
# 具体实现类继续
|
||
|
||
class OpenCVThumbnailGenerator(ThumbnailGenerator):
|
||
"""使用OpenCV生成缩略图"""
|
||
|
||
def __init__(self, dependency_manager: DependencyManager):
|
||
self.dependency_manager = dependency_manager
|
||
|
||
def generate_thumbnail(self, video_path: str, timestamp: float, output_path: str) -> bool:
|
||
"""生成视频缩略图"""
|
||
if not self.dependency_manager.is_available('opencv'):
|
||
return False
|
||
|
||
cv2 = self.dependency_manager.get_module('opencv', 'cv2')
|
||
|
||
try:
|
||
cap = cv2.VideoCapture(video_path)
|
||
fps = cap.get(cv2.CAP_PROP_FPS)
|
||
|
||
if fps <= 0:
|
||
cap.release()
|
||
return False
|
||
|
||
# 跳转到指定时间
|
||
frame_number = int(timestamp * fps)
|
||
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
|
||
|
||
ret, frame = cap.read()
|
||
cap.release()
|
||
|
||
if ret:
|
||
# 保存缩略图
|
||
success = cv2.imwrite(output_path, frame)
|
||
logger.info(f"Thumbnail generated: {output_path}")
|
||
return success
|
||
else:
|
||
logger.error(f"Failed to read frame at {timestamp}s")
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(f"Failed to generate thumbnail: {e}")
|
||
return False
|
||
|
||
class MD5FileHashCalculator(FileHashCalculator):
|
||
"""使用MD5计算文件哈希"""
|
||
|
||
def calculate_hash(self, file_path: str) -> str:
|
||
"""计算文件MD5哈希值"""
|
||
import hashlib
|
||
|
||
try:
|
||
hash_md5 = hashlib.md5()
|
||
with open(file_path, "rb") as f:
|
||
for chunk in iter(lambda: f.read(4096), b""):
|
||
hash_md5.update(chunk)
|
||
return hash_md5.hexdigest()
|
||
except Exception as e:
|
||
logger.error(f"Failed to calculate MD5 hash: {e}")
|
||
return ""
|
||
|
||
class OpenCVVideoSegmentCreator(VideoSegmentCreator):
|
||
"""使用OpenCV创建视频片段"""
|
||
|
||
def __init__(self, dependency_manager: DependencyManager, hash_calculator: FileHashCalculator):
|
||
self.dependency_manager = dependency_manager
|
||
self.hash_calculator = hash_calculator
|
||
self.segments_dir = settings.temp_dir / "video_segments"
|
||
self.segments_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
def create_segments_from_scenes(self, video_path: str, scene_changes: List[float],
|
||
original_video_id: str, tags: List[str] = None) -> List['VideoSegment']:
|
||
"""根据场景变化创建视频片段"""
|
||
if tags is None:
|
||
tags = []
|
||
|
||
# 为分镜片段处理标签:去掉"原始",添加"分镜"
|
||
segment_tags = [tag for tag in tags if tag != "原始"]
|
||
if "分镜" not in segment_tags:
|
||
segment_tags.append("分镜")
|
||
|
||
if not self.dependency_manager.is_available('opencv'):
|
||
logger.warning("OpenCV not available, creating single segment")
|
||
return self._create_single_segment(video_path, original_video_id, segment_tags)
|
||
|
||
return self._create_multiple_segments(video_path, scene_changes, original_video_id, segment_tags)
|
||
|
||
def _create_single_segment(self, video_path: str, original_video_id: str, tags: List[str]) -> List['VideoSegment']:
|
||
"""创建单个片段(当OpenCV不可用时)"""
|
||
from python_core.models.video_segment import VideoSegment
|
||
import shutil
|
||
import uuid
|
||
from datetime import datetime
|
||
|
||
# 获取视频信息(这里需要使用依赖注入的方式)
|
||
video_info = {'duration': 0.0, 'width': 0, 'height': 0, 'fps': 0.0, 'file_size': 0}
|
||
|
||
segment_id = str(uuid.uuid4())
|
||
segment_filename = f"{segment_id}.mp4"
|
||
segment_path = self.segments_dir / segment_filename
|
||
|
||
# 复制整个视频作为单个片段
|
||
shutil.copy2(video_path, segment_path)
|
||
|
||
now = datetime.now().isoformat()
|
||
segment = VideoSegment(
|
||
id=segment_id,
|
||
original_video_id=original_video_id,
|
||
segment_index=0,
|
||
filename=segment_filename,
|
||
file_path=str(segment_path),
|
||
md5_hash=self.hash_calculator.calculate_hash(str(segment_path)),
|
||
file_size=video_info['file_size'],
|
||
duration=video_info['duration'],
|
||
width=video_info['width'],
|
||
height=video_info['height'],
|
||
fps=video_info['fps'],
|
||
format='mp4',
|
||
start_time=0.0,
|
||
end_time=video_info['duration'],
|
||
tags=tags.copy(),
|
||
use_count=0,
|
||
created_at=now,
|
||
updated_at=now
|
||
)
|
||
|
||
return [segment]
|
||
|
||
def _create_multiple_segments(self, video_path: str, scene_changes: List[float],
|
||
original_video_id: str, tags: List[str]) -> List['VideoSegment']:
|
||
"""创建多个片段(使用OpenCV分割)"""
|
||
if not self.dependency_manager.is_available('opencv'):
|
||
logger.warning("OpenCV not available for video splitting")
|
||
return self._create_single_segment(video_path, original_video_id, tags)
|
||
|
||
cv2 = self.dependency_manager.get_module('opencv', 'cv2')
|
||
segments = []
|
||
|
||
try:
|
||
# 获取视频信息
|
||
cap = cv2.VideoCapture(video_path)
|
||
fps = cap.get(cv2.CAP_PROP_FPS)
|
||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||
|
||
if fps <= 0:
|
||
cap.release()
|
||
logger.warning("Invalid FPS, creating single segment")
|
||
return self._create_single_segment(video_path, original_video_id, tags)
|
||
|
||
# 设置视频编码器
|
||
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
||
|
||
for i in range(len(scene_changes) - 1):
|
||
start_time = scene_changes[i]
|
||
end_time = scene_changes[i + 1]
|
||
duration = end_time - start_time
|
||
|
||
# 跳过太短的片段(小于1秒)
|
||
if duration < 1.0:
|
||
logger.debug(f"Skipping short segment: {duration:.2f}s")
|
||
continue
|
||
|
||
segment_id = str(uuid.uuid4())
|
||
segment_filename = f"{segment_id}.mp4"
|
||
segment_path = self.segments_dir / segment_filename
|
||
|
||
# 创建视频写入器
|
||
out = cv2.VideoWriter(str(segment_path), fourcc, fps, (width, height))
|
||
|
||
# 跳转到开始帧
|
||
start_frame = int(start_time * fps)
|
||
end_frame = int(end_time * fps)
|
||
|
||
cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
|
||
|
||
frame_count = 0
|
||
target_frames = end_frame - start_frame
|
||
|
||
while frame_count < target_frames:
|
||
ret, frame = cap.read()
|
||
if not ret:
|
||
logger.warning(f"Failed to read frame {frame_count}/{target_frames}")
|
||
break
|
||
|
||
out.write(frame)
|
||
frame_count += 1
|
||
|
||
out.release()
|
||
|
||
# 检查文件是否创建成功
|
||
if not segment_path.exists() or segment_path.stat().st_size == 0:
|
||
logger.error(f"Failed to create segment: {segment_path}")
|
||
continue
|
||
|
||
# 创建片段记录
|
||
from datetime import datetime
|
||
now = datetime.now().isoformat()
|
||
|
||
# 这里需要导入VideoSegment,但为了避免循环导入,我们返回字典
|
||
segment_data = {
|
||
'id': segment_id,
|
||
'original_video_id': original_video_id,
|
||
'segment_index': i,
|
||
'filename': segment_filename,
|
||
'file_path': str(segment_path),
|
||
'md5_hash': self.hash_calculator.calculate_hash(str(segment_path)),
|
||
'file_size': segment_path.stat().st_size,
|
||
'duration': duration,
|
||
'width': width,
|
||
'height': height,
|
||
'fps': fps,
|
||
'format': 'mp4',
|
||
'start_time': start_time,
|
||
'end_time': end_time,
|
||
'tags': tags.copy(),
|
||
'use_count': 0,
|
||
'created_at': now,
|
||
'updated_at': now
|
||
}
|
||
|
||
segments.append(segment_data)
|
||
logger.info(f"Created segment {i}: {start_time:.2f}s - {end_time:.2f}s ({duration:.2f}s)")
|
||
|
||
cap.release()
|
||
logger.info(f"Successfully created {len(segments)} video segments")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Failed to create multiple segments: {e}")
|
||
return self._create_single_segment(video_path, original_video_id, tags)
|
||
|
||
return segments
|
||
|
||
# 全局依赖管理器实例
|
||
dependency_manager = DependencyManager()
|
||
video_processor_factory = VideoProcessorFactory(dependency_manager)
|
||
|
||
# 向后兼容的全局变量 - 里氏替换原则(LSP): 保持接口兼容
|
||
VIDEO_LIBS_AVAILABLE = dependency_manager.is_available('opencv')
|
||
SCENEDETECT_AVAILABLE = dependency_manager.is_available('scenedetect')
|
||
|
||
|
||
@dataclass
|
||
class VideoSegment:
|
||
"""视频片段数据结构"""
|
||
id: str
|
||
original_video_id: str
|
||
segment_index: int
|
||
filename: str
|
||
file_path: str
|
||
md5_hash: str
|
||
file_size: int
|
||
duration: float
|
||
width: int
|
||
height: int
|
||
fps: float
|
||
format: str
|
||
start_time: float
|
||
end_time: float
|
||
tags: List[str]
|
||
use_count: int
|
||
thumbnail_path: Optional[str] = None
|
||
scene_score: Optional[float] = None # 场景变化分数
|
||
motion_score: Optional[float] = None # 运动强度分数
|
||
brightness: Optional[float] = None
|
||
contrast: Optional[float] = None
|
||
created_at: str = ""
|
||
updated_at: str = ""
|
||
is_active: bool = True
|
||
|
||
|
||
@dataclass
|
||
class OriginalVideo:
|
||
"""原始视频数据结构"""
|
||
id: str
|
||
filename: str
|
||
file_path: str
|
||
md5_hash: str
|
||
file_size: int
|
||
duration: float
|
||
width: int
|
||
height: int
|
||
fps: float
|
||
format: str
|
||
segment_count: int
|
||
tags: List[str]
|
||
created_at: str = ""
|
||
updated_at: str = ""
|
||
is_active: bool = True
|
||
|
||
|
||
class MediaManager:
|
||
"""媒体库管理器
|
||
|
||
遵循SOLID原则:
|
||
- SRP: 只负责媒体文件的管理和协调
|
||
- OCP: 通过依赖注入支持扩展
|
||
- LSP: 可以替换不同的处理器实现
|
||
- ISP: 使用专门的接口
|
||
- DIP: 依赖抽象接口而不是具体实现
|
||
"""
|
||
|
||
def __init__(self,
|
||
dependency_manager: DependencyManager = None,
|
||
video_info_extractor: VideoInfoExtractor = None,
|
||
scene_detector: SceneDetector = None,
|
||
thumbnail_generator: ThumbnailGenerator = None,
|
||
hash_calculator: FileHashCalculator = None,
|
||
video_segment_creator: VideoSegmentCreator = None):
|
||
"""
|
||
初始化媒体管理器
|
||
|
||
Args:
|
||
dependency_manager: 依赖管理器
|
||
video_info_extractor: 视频信息提取器
|
||
scene_detector: 场景检测器
|
||
thumbnail_generator: 缩略图生成器
|
||
hash_calculator: 哈希计算器
|
||
video_segment_creator: 视频片段创建器
|
||
"""
|
||
self.cache_dir = settings.temp_dir / "cache"
|
||
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
# 数据文件
|
||
self.segments_file = self.cache_dir / "video_segments.json"
|
||
self.original_videos_file = self.cache_dir / "original_videos.json"
|
||
|
||
# 依赖注入 - 依赖倒置原则(DIP)
|
||
self.dependency_manager = dependency_manager or globals()['dependency_manager']
|
||
self.factory = VideoProcessorFactory(self.dependency_manager)
|
||
|
||
# 延迟初始化处理器 - 单一职责原则(SRP)
|
||
self._video_info_extractor = video_info_extractor
|
||
self._scene_detector = scene_detector
|
||
self._thumbnail_generator = thumbnail_generator
|
||
self._hash_calculator = hash_calculator
|
||
self._video_segment_creator = video_segment_creator
|
||
|
||
# 初始化目录
|
||
self._initialize_directories()
|
||
|
||
# 加载数据
|
||
self.video_segments = self._load_video_segments()
|
||
self.original_videos = self._load_original_videos()
|
||
|
||
def _initialize_directories(self):
|
||
"""初始化目录结构 - 单一职责原则(SRP)"""
|
||
self.video_storage_dir = settings.temp_dir / "video_storage"
|
||
self.video_storage_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
self.segments_dir = settings.temp_dir / "video_segments"
|
||
self.segments_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
self.thumbnails_dir = settings.temp_dir / "video_thumbnails"
|
||
self.thumbnails_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
@property
|
||
def video_info_extractor(self) -> VideoInfoExtractor:
|
||
"""获取视频信息提取器 - 懒加载"""
|
||
if self._video_info_extractor is None:
|
||
self._video_info_extractor = self.factory.create_video_info_extractor()
|
||
return self._video_info_extractor
|
||
|
||
@property
|
||
def scene_detector(self) -> SceneDetector:
|
||
"""获取场景检测器 - 懒加载"""
|
||
if self._scene_detector is None:
|
||
self._scene_detector = self.factory.create_scene_detector()
|
||
return self._scene_detector
|
||
|
||
@property
|
||
def thumbnail_generator(self) -> ThumbnailGenerator:
|
||
"""获取缩略图生成器 - 懒加载"""
|
||
if self._thumbnail_generator is None:
|
||
self._thumbnail_generator = self.factory.create_thumbnail_generator()
|
||
return self._thumbnail_generator
|
||
|
||
@property
|
||
def hash_calculator(self) -> FileHashCalculator:
|
||
"""获取哈希计算器 - 懒加载"""
|
||
if self._hash_calculator is None:
|
||
self._hash_calculator = self.factory.create_hash_calculator()
|
||
return self._hash_calculator
|
||
|
||
@property
|
||
def video_segment_creator(self) -> VideoSegmentCreator:
|
||
"""获取视频片段创建器 - 懒加载"""
|
||
if self._video_segment_creator is None:
|
||
self._video_segment_creator = self.factory.create_video_segment_creator()
|
||
return self._video_segment_creator
|
||
|
||
def _initialize_directories(self):
|
||
"""初始化目录结构 - 单一职责原则(SRP)"""
|
||
self.video_storage_dir = settings.temp_dir / "video_storage"
|
||
self.video_storage_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
self.segments_dir = settings.temp_dir / "video_segments"
|
||
self.segments_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
self.thumbnails_dir = settings.temp_dir / "video_thumbnails"
|
||
self.thumbnails_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
def _load_video_segments(self) -> List[VideoSegment]:
|
||
"""加载视频片段数据"""
|
||
if self.segments_file.exists():
|
||
try:
|
||
with open(self.segments_file, 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
return [VideoSegment(**item) for item in data]
|
||
except Exception as e:
|
||
logger.error(f"Failed to load video segments: {e}")
|
||
return []
|
||
else:
|
||
return []
|
||
|
||
def _load_original_videos(self) -> List[OriginalVideo]:
|
||
"""加载原始视频数据"""
|
||
if self.original_videos_file.exists():
|
||
try:
|
||
with open(self.original_videos_file, 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
return [OriginalVideo(**item) for item in data]
|
||
except Exception as e:
|
||
logger.error(f"Failed to load original videos: {e}")
|
||
return []
|
||
else:
|
||
return []
|
||
|
||
def _save_video_segments(self, segments: List[VideoSegment] = None):
|
||
"""保存视频片段数据"""
|
||
if segments is None:
|
||
segments = self.video_segments
|
||
|
||
try:
|
||
data = [asdict(segment) for segment in segments]
|
||
with open(self.segments_file, 'w', encoding='utf-8') as f:
|
||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||
logger.info(f"Video segments saved to {self.segments_file}")
|
||
except Exception as e:
|
||
logger.error(f"Failed to save video segments: {e}")
|
||
raise
|
||
|
||
def _save_original_videos(self, videos: List[OriginalVideo] = None):
|
||
"""保存原始视频数据"""
|
||
if videos is None:
|
||
videos = self.original_videos
|
||
|
||
try:
|
||
data = [asdict(video) for video in videos]
|
||
with open(self.original_videos_file, 'w', encoding='utf-8') as f:
|
||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||
logger.info(f"Original videos saved to {self.original_videos_file}")
|
||
except Exception as e:
|
||
logger.error(f"Failed to save original videos: {e}")
|
||
raise
|
||
|
||
def _calculate_md5(self, file_path: str) -> str:
|
||
"""计算文件MD5哈希值"""
|
||
hash_md5 = hashlib.md5()
|
||
with open(file_path, "rb") as f:
|
||
for chunk in iter(lambda: f.read(4096), b""):
|
||
hash_md5.update(chunk)
|
||
return hash_md5.hexdigest()
|
||
|
||
def _get_video_info(self, file_path: str) -> Dict:
|
||
"""获取视频基本信息 - 使用依赖注入的提取器"""
|
||
try:
|
||
return self.video_info_extractor.extract_video_info(file_path)
|
||
except Exception as e:
|
||
logger.error(f"Failed to get video info: {e}")
|
||
# 返回基本信息作为后备
|
||
file_size = os.path.getsize(file_path) if os.path.exists(file_path) else 0
|
||
return {
|
||
'duration': 0.0,
|
||
'width': 0,
|
||
'height': 0,
|
||
'fps': 0.0,
|
||
'frame_count': 0,
|
||
'file_size': file_size,
|
||
'codec': 'unknown'
|
||
}
|
||
|
||
def _detect_scene_changes(self, file_path: str, threshold: float = 30.0) -> List[float]:
|
||
"""检测场景变化点 - 使用依赖注入的检测器"""
|
||
try:
|
||
return self.scene_detector.detect_scenes(file_path, threshold)
|
||
except Exception as e:
|
||
logger.error(f"Scene detection failed: {e}")
|
||
# 返回基本的开始和结束时间作为后备
|
||
try:
|
||
video_info = self._get_video_info(file_path)
|
||
duration = video_info.get('duration', 0)
|
||
return [0.0, duration] if duration > 0 else [0.0]
|
||
except:
|
||
return [0.0]
|
||
|
||
|
||
|
||
def _generate_thumbnail(self, video_path: str, timestamp: float, output_path: str) -> bool:
|
||
"""生成视频缩略图 - 使用依赖注入的生成器"""
|
||
try:
|
||
return self.thumbnail_generator.generate_thumbnail(video_path, timestamp, output_path)
|
||
except Exception as e:
|
||
logger.error(f"Failed to generate thumbnail: {e}")
|
||
return False
|
||
|
||
def _calculate_md5(self, file_path: str) -> str:
|
||
"""计算文件MD5哈希值 - 使用依赖注入的计算器"""
|
||
try:
|
||
return self.hash_calculator.calculate_hash(file_path)
|
||
except Exception as e:
|
||
logger.error(f"Failed to calculate hash: {e}")
|
||
return ""
|
||
|
||
def _split_video_by_scenes(self, video_path: str, scene_changes: List[float],
|
||
original_video_id: str, tags: List[str] = None) -> List[VideoSegment]:
|
||
"""根据场景变化分割视频 - 使用依赖注入的创建器"""
|
||
try:
|
||
return self.video_segment_creator.create_segments_from_scenes(
|
||
video_path, scene_changes, original_video_id, tags
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Failed to split video using segment creator: {e}")
|
||
# 返回空列表作为后备
|
||
return []
|
||
|
||
|
||
|
||
def get_video_by_md5(self, md5_hash: str) -> Optional[Dict]:
|
||
"""根据MD5获取原始视频"""
|
||
for video in self.original_videos:
|
||
if video.md5_hash == md5_hash and video.is_active:
|
||
return asdict(video)
|
||
return None
|
||
|
||
def get_segment_by_md5(self, md5_hash: str) -> Optional[Dict]:
|
||
"""根据MD5获取视频片段"""
|
||
for segment in self.video_segments:
|
||
if segment.md5_hash == md5_hash and segment.is_active:
|
||
return asdict(segment)
|
||
return None
|
||
|
||
def get_all_segments(self) -> List[Dict]:
|
||
"""获取所有视频片段"""
|
||
return [asdict(segment) for segment in self.video_segments if segment.is_active]
|
||
|
||
def get_all_original_videos(self) -> List[Dict]:
|
||
"""获取所有原始视频"""
|
||
return [asdict(video) for video in self.original_videos if video.is_active]
|
||
|
||
def get_segments_by_video_id(self, video_id: str) -> List[Dict]:
|
||
"""获取指定原始视频的所有片段"""
|
||
segments = []
|
||
for segment in self.video_segments:
|
||
if segment.original_video_id == video_id and segment.is_active:
|
||
segments.append(asdict(segment))
|
||
return sorted(segments, key=lambda x: x['segment_index'])
|
||
|
||
def upload_video_file(self, source_path: str, filename: str = None, tags: List[str] = None) -> Dict:
|
||
"""上传单个视频文件并分割成片段"""
|
||
if not os.path.exists(source_path):
|
||
raise FileNotFoundError(f"Source file not found: {source_path}")
|
||
|
||
if tags is None:
|
||
tags = []
|
||
|
||
# 计算MD5
|
||
md5_hash = self._calculate_md5(source_path)
|
||
|
||
# 检查是否已存在相同MD5的视频
|
||
existing = self.get_video_by_md5(md5_hash)
|
||
if existing:
|
||
logger.info(f"Video with MD5 {md5_hash} already exists")
|
||
existing_segments = self.get_segments_by_video_id(existing['id'])
|
||
|
||
# 如果已存在的视频没有分镜头,重新生成分镜头
|
||
if not existing_segments:
|
||
logger.info(f"Existing video has no segments, regenerating segments for video {existing['id']}")
|
||
|
||
# 获取存储的视频文件路径
|
||
stored_video_path = existing['file_path']
|
||
|
||
# 检测场景变化
|
||
scene_changes = self._detect_scene_changes(stored_video_path)
|
||
logger.info(f"Detected {len(scene_changes)} scene changes for existing video")
|
||
|
||
# 为分镜片段处理标签:去掉"原始",添加"分镜"
|
||
segment_tags = [tag for tag in tags if tag != "原始"] if tags else []
|
||
if "分镜" not in segment_tags:
|
||
segment_tags.append("分镜")
|
||
|
||
# 分割视频成片段
|
||
segments = self._split_video_by_scenes(stored_video_path, scene_changes, existing['id'], segment_tags)
|
||
|
||
# 保存新生成的片段
|
||
self.video_segments.extend(segments)
|
||
self._save_video_segments()
|
||
|
||
# 更新原始视频的segment_count
|
||
for i, video in enumerate(self.original_videos):
|
||
if video.id == existing['id']:
|
||
video.segment_count = len(segments)
|
||
video.updated_at = datetime.now().isoformat()
|
||
self.original_videos[i] = video
|
||
break
|
||
|
||
self._save_original_videos()
|
||
|
||
logger.info(f"Regenerated {len(segments)} segments for existing video {existing['id']}")
|
||
|
||
return {
|
||
'original_video': existing,
|
||
'segments': [asdict(segment) if hasattr(segment, '__dict__') else segment for segment in segments],
|
||
'is_duplicate': True,
|
||
'segments_regenerated': True
|
||
}
|
||
else:
|
||
return {
|
||
'original_video': existing,
|
||
'segments': existing_segments,
|
||
'is_duplicate': True,
|
||
'segments_regenerated': False
|
||
}
|
||
|
||
# 生成新的视频ID和文件名
|
||
video_id = str(uuid.uuid4())
|
||
if filename is None:
|
||
filename = os.path.basename(source_path)
|
||
|
||
# 获取文件扩展名
|
||
file_ext = os.path.splitext(filename)[1].lower()
|
||
stored_filename = f"{video_id}{file_ext}"
|
||
stored_path = self.video_storage_dir / stored_filename
|
||
|
||
# 复制文件到存储目录
|
||
shutil.copy2(source_path, stored_path)
|
||
|
||
# 获取视频基本信息
|
||
video_info = self._get_video_info(str(stored_path))
|
||
|
||
# 检测场景变化
|
||
scene_changes = self._detect_scene_changes(str(stored_path))
|
||
|
||
# 创建原始视频记录
|
||
now = datetime.now().isoformat()
|
||
original_video = OriginalVideo(
|
||
id=video_id,
|
||
filename=filename,
|
||
file_path=str(stored_path),
|
||
md5_hash=md5_hash,
|
||
file_size=video_info['file_size'],
|
||
duration=video_info['duration'],
|
||
width=video_info['width'],
|
||
height=video_info['height'],
|
||
fps=video_info['fps'],
|
||
format=file_ext[1:] if file_ext else 'unknown',
|
||
segment_count=len(scene_changes) - 1,
|
||
tags=tags,
|
||
created_at=now,
|
||
updated_at=now
|
||
)
|
||
|
||
# 分割视频成片段,传递标签给片段(去掉"原始",添加"分镜")
|
||
segment_tags = [tag for tag in tags if tag != "原始"]
|
||
if "分镜" not in segment_tags:
|
||
segment_tags.append("分镜")
|
||
segments = self._split_video_by_scenes(str(stored_path), scene_changes, video_id, segment_tags)
|
||
|
||
# 保存数据
|
||
self.original_videos.append(original_video)
|
||
self.video_segments.extend(segments)
|
||
|
||
self._save_original_videos()
|
||
self._save_video_segments()
|
||
|
||
logger.info(f"Uploaded video: {filename} (MD5: {md5_hash}, {len(segments)} segments)")
|
||
|
||
return {
|
||
'original_video': asdict(original_video),
|
||
'segments': [asdict(segment) for segment in segments],
|
||
'is_duplicate': False
|
||
}
|
||
|
||
def batch_upload_video_files(self, source_directory: str, tags: List[str] = None) -> Dict:
|
||
"""批量上传视频文件"""
|
||
if not os.path.exists(source_directory):
|
||
raise FileNotFoundError(f"Source directory not found: {source_directory}")
|
||
|
||
if tags is None:
|
||
tags = []
|
||
|
||
# 支持的视频格式
|
||
video_extensions = {'.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.webm', '.m4v'}
|
||
|
||
results = {
|
||
'total_files': 0,
|
||
'uploaded_files': 0,
|
||
'skipped_files': 0,
|
||
'failed_files': 0,
|
||
'total_segments': 0,
|
||
'uploaded_list': [],
|
||
'skipped_list': [],
|
||
'failed_list': []
|
||
}
|
||
|
||
# 遍历目录中的所有文件
|
||
for root, dirs, files in os.walk(source_directory):
|
||
for file in files:
|
||
file_path = os.path.join(root, file)
|
||
file_ext = os.path.splitext(file)[1].lower()
|
||
|
||
# 检查是否为视频文件
|
||
if file_ext not in video_extensions:
|
||
continue
|
||
|
||
results['total_files'] += 1
|
||
|
||
try:
|
||
# 尝试上传文件
|
||
result = self.upload_video_file(file_path, file, tags)
|
||
|
||
if result['is_duplicate']:
|
||
results['skipped_files'] += 1
|
||
results['skipped_list'].append({
|
||
'filename': file,
|
||
'reason': 'Already exists (same MD5)'
|
||
})
|
||
else:
|
||
results['uploaded_files'] += 1
|
||
results['total_segments'] += len(result['segments'])
|
||
results['uploaded_list'].append(result)
|
||
|
||
except Exception as e:
|
||
results['failed_files'] += 1
|
||
results['failed_list'].append({
|
||
'filename': file,
|
||
'error': str(e)
|
||
})
|
||
logger.error(f"Failed to upload {file}: {e}")
|
||
|
||
logger.info(f"Batch upload completed: {results['uploaded_files']} uploaded, "
|
||
f"{results['skipped_files']} skipped, {results['failed_files']} failed, "
|
||
f"{results['total_segments']} total segments created")
|
||
|
||
return results
|
||
|
||
def add_segment_tags(self, segment_id: str, tags: List[str]) -> bool:
|
||
"""为视频片段添加标签"""
|
||
for i, segment in enumerate(self.video_segments):
|
||
if segment.id == segment_id and segment.is_active:
|
||
# 合并标签,去重
|
||
existing_tags = set(segment.tags)
|
||
new_tags = existing_tags.union(set(tags))
|
||
segment.tags = list(new_tags)
|
||
segment.updated_at = datetime.now().isoformat()
|
||
|
||
self.video_segments[i] = segment
|
||
self._save_video_segments()
|
||
|
||
logger.info(f"Added tags {tags} to segment {segment_id}")
|
||
return True
|
||
return False
|
||
|
||
def remove_segment_tags(self, segment_id: str, tags: List[str]) -> bool:
|
||
"""移除视频片段的标签"""
|
||
for i, segment in enumerate(self.video_segments):
|
||
if segment.id == segment_id and segment.is_active:
|
||
# 移除指定标签
|
||
existing_tags = set(segment.tags)
|
||
remaining_tags = existing_tags - set(tags)
|
||
segment.tags = list(remaining_tags)
|
||
segment.updated_at = datetime.now().isoformat()
|
||
|
||
self.video_segments[i] = segment
|
||
self._save_video_segments()
|
||
|
||
logger.info(f"Removed tags {tags} from segment {segment_id}")
|
||
return True
|
||
return False
|
||
|
||
def increment_segment_usage(self, segment_id: str) -> bool:
|
||
"""增加视频片段使用次数"""
|
||
for i, segment in enumerate(self.video_segments):
|
||
if segment.id == segment_id and segment.is_active:
|
||
segment.use_count += 1
|
||
segment.updated_at = datetime.now().isoformat()
|
||
|
||
self.video_segments[i] = segment
|
||
self._save_video_segments()
|
||
|
||
logger.info(f"Incremented usage count for segment {segment_id} to {segment.use_count}")
|
||
return True
|
||
return False
|
||
|
||
def get_segments_by_tags(self, tags: List[str], match_all: bool = False) -> List[Dict]:
|
||
"""根据标签搜索视频片段"""
|
||
results = []
|
||
tag_set = set(tags)
|
||
|
||
for segment in self.video_segments:
|
||
if not segment.is_active:
|
||
continue
|
||
|
||
segment_tags = set(segment.tags)
|
||
|
||
if match_all:
|
||
# 必须包含所有标签
|
||
if tag_set.issubset(segment_tags):
|
||
results.append(asdict(segment))
|
||
else:
|
||
# 包含任意一个标签
|
||
if tag_set.intersection(segment_tags):
|
||
results.append(asdict(segment))
|
||
|
||
return results
|
||
|
||
def get_popular_segments(self, limit: int = 10) -> List[Dict]:
|
||
"""获取最常用的视频片段"""
|
||
active_segments = [segment for segment in self.video_segments if segment.is_active]
|
||
sorted_segments = sorted(active_segments, key=lambda x: x.use_count, reverse=True)
|
||
|
||
return [asdict(segment) for segment in sorted_segments[:limit]]
|
||
|
||
def search_segments(self, keyword: str) -> List[Dict]:
|
||
"""搜索视频片段"""
|
||
keyword = keyword.lower()
|
||
results = []
|
||
|
||
for segment in self.video_segments:
|
||
if not segment.is_active:
|
||
continue
|
||
|
||
# 搜索文件名和标签
|
||
if (keyword in segment.filename.lower() or
|
||
any(keyword in tag.lower() for tag in segment.tags)):
|
||
results.append(asdict(segment))
|
||
|
||
return results
|
||
|
||
def delete_segment(self, segment_id: str) -> bool:
|
||
"""删除视频片段"""
|
||
for i, segment in enumerate(self.video_segments):
|
||
if segment.id == segment_id:
|
||
# 删除物理文件
|
||
try:
|
||
if os.path.exists(segment.file_path):
|
||
os.remove(segment.file_path)
|
||
|
||
# 删除缩略图
|
||
if segment.thumbnail_path and os.path.exists(segment.thumbnail_path):
|
||
os.remove(segment.thumbnail_path)
|
||
except Exception as e:
|
||
logger.error(f"Failed to delete physical files: {e}")
|
||
|
||
# 从列表中移除
|
||
deleted_segment = self.video_segments.pop(i)
|
||
self._save_video_segments()
|
||
|
||
logger.info(f"Deleted segment: {segment_id} - {deleted_segment.filename}")
|
||
return True
|
||
return False
|
||
|
||
def delete_original_video(self, video_id: str) -> bool:
|
||
"""删除原始视频及其所有片段"""
|
||
# 先删除所有相关片段
|
||
segments_to_delete = [s for s in self.video_segments if s.original_video_id == video_id]
|
||
for segment in segments_to_delete:
|
||
self.delete_segment(segment.id)
|
||
|
||
# 删除原始视频
|
||
for i, video in enumerate(self.original_videos):
|
||
if video.id == video_id:
|
||
# 删除物理文件
|
||
try:
|
||
if os.path.exists(video.file_path):
|
||
os.remove(video.file_path)
|
||
except Exception as e:
|
||
logger.error(f"Failed to delete video file: {e}")
|
||
|
||
# 从列表中移除
|
||
deleted_video = self.original_videos.pop(i)
|
||
self._save_original_videos()
|
||
|
||
logger.info(f"Deleted original video: {video_id} - {deleted_video.filename}")
|
||
return True
|
||
return False
|
||
|
||
|
||
# 全局实例 - 延迟初始化
|
||
_global_media_manager = None
|
||
|
||
def get_media_manager() -> MediaManager:
|
||
"""获取全局MediaManager实例 - 懒加载"""
|
||
global _global_media_manager
|
||
if _global_media_manager is None:
|
||
_global_media_manager = MediaManager()
|
||
return _global_media_manager
|
||
|
||
|
||
def main():
|
||
"""命令行接口 - 使用JSON-RPC协议"""
|
||
import sys
|
||
import json
|
||
from python_core.utils.jsonrpc import create_response_handler
|
||
|
||
# 创建响应处理器
|
||
rpc = create_response_handler()
|
||
|
||
if len(sys.argv) < 2:
|
||
rpc.error("INVALID_REQUEST", "No command specified")
|
||
return
|
||
|
||
command = sys.argv[1]
|
||
|
||
try:
|
||
# 获取全局MediaManager实例
|
||
media_manager = get_media_manager()
|
||
|
||
if command == "get_all_segments":
|
||
segments = media_manager.get_all_segments()
|
||
rpc.success(segments)
|
||
|
||
elif command == "get_all_original_videos":
|
||
videos = media_manager.get_all_original_videos()
|
||
rpc.success(videos)
|
||
|
||
elif command == "get_segments_by_video_id":
|
||
if len(sys.argv) < 3:
|
||
rpc.error("INVALID_REQUEST", "Video ID required")
|
||
return
|
||
video_id = sys.argv[2]
|
||
segments = media_manager.get_segments_by_video_id(video_id)
|
||
rpc.success(segments)
|
||
|
||
elif command == "upload_video_file":
|
||
if len(sys.argv) < 3:
|
||
rpc.error("INVALID_REQUEST", "Source path required")
|
||
return
|
||
source_path = sys.argv[2]
|
||
filename = sys.argv[3] if len(sys.argv) > 3 else None
|
||
tags = json.loads(sys.argv[4]) if len(sys.argv) > 4 else []
|
||
result = media_manager.upload_video_file(source_path, filename, tags)
|
||
rpc.success(result)
|
||
|
||
elif command == "batch_upload_video_files":
|
||
if len(sys.argv) < 3:
|
||
rpc.error("INVALID_REQUEST", "Source directory required")
|
||
return
|
||
source_directory = sys.argv[2]
|
||
tags = json.loads(sys.argv[3]) if len(sys.argv) > 3 else []
|
||
result = media_manager.batch_upload_video_files(source_directory, tags)
|
||
rpc.success(result)
|
||
|
||
elif command == "add_segment_tags":
|
||
if len(sys.argv) < 4:
|
||
rpc.error("INVALID_REQUEST", "Segment ID and tags required")
|
||
return
|
||
segment_id = sys.argv[2]
|
||
tags = json.loads(sys.argv[3])
|
||
success = media_manager.add_segment_tags(segment_id, tags)
|
||
rpc.success(success)
|
||
|
||
elif command == "remove_segment_tags":
|
||
if len(sys.argv) < 4:
|
||
rpc.error("INVALID_REQUEST", "Segment ID and tags required")
|
||
return
|
||
segment_id = sys.argv[2]
|
||
tags = json.loads(sys.argv[3])
|
||
success = media_manager.remove_segment_tags(segment_id, tags)
|
||
rpc.success(success)
|
||
|
||
elif command == "increment_segment_usage":
|
||
if len(sys.argv) < 3:
|
||
rpc.error("INVALID_REQUEST", "Segment ID required")
|
||
return
|
||
segment_id = sys.argv[2]
|
||
success = media_manager.increment_segment_usage(segment_id)
|
||
rpc.success(success)
|
||
|
||
elif command == "get_segments_by_tags":
|
||
if len(sys.argv) < 3:
|
||
rpc.error("INVALID_REQUEST", "Tags required")
|
||
return
|
||
tags = json.loads(sys.argv[2])
|
||
match_all = json.loads(sys.argv[3]) if len(sys.argv) > 3 else False
|
||
segments = media_manager.get_segments_by_tags(tags, match_all)
|
||
rpc.success(segments)
|
||
|
||
elif command == "get_popular_segments":
|
||
limit = int(sys.argv[2]) if len(sys.argv) > 2 else 10
|
||
segments = media_manager.get_popular_segments(limit)
|
||
rpc.success(segments)
|
||
|
||
elif command == "search_segments":
|
||
if len(sys.argv) < 3:
|
||
rpc.error("INVALID_REQUEST", "Search keyword required")
|
||
return
|
||
keyword = sys.argv[2]
|
||
results = media_manager.search_segments(keyword)
|
||
rpc.success(results)
|
||
|
||
elif command == "delete_segment":
|
||
if len(sys.argv) < 3:
|
||
rpc.error("INVALID_REQUEST", "Segment ID required")
|
||
return
|
||
segment_id = sys.argv[2]
|
||
success = media_manager.delete_segment(segment_id)
|
||
rpc.success(success)
|
||
|
||
elif command == "delete_original_video":
|
||
if len(sys.argv) < 3:
|
||
rpc.error("INVALID_REQUEST", "Video ID required")
|
||
return
|
||
video_id = sys.argv[2]
|
||
success = media_manager.delete_original_video(video_id)
|
||
rpc.success(success)
|
||
|
||
else:
|
||
rpc.error("INVALID_REQUEST", f"Unknown command: {command}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Command execution failed: {e}")
|
||
rpc.error("INTERNAL_ERROR", str(e))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|