mxivideo/python_core/services/project_material_service.py

299 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
项目素材管理服务
"""
import json
import hashlib
from pathlib import Path
from typing import List, Dict, Any, Optional
from datetime import datetime
from python_core.utils.logger import logger
class ProjectMaterialService:
"""项目素材管理服务"""
def __init__(self):
logger.info("ProjectMaterialService 初始化完成")
def import_video_materials(
self,
video_segments: List[Any],
project_id: str,
project_directory: Path,
source_video_path: str,
material_tags: Optional[List[str]] = None
) -> Dict[str, Any]:
"""
导入视频素材到项目
Args:
video_segments: 视频片段列表
project_id: 项目ID
project_directory: 项目目录
source_video_path: 源视频路径
material_tags: 素材标签
Returns:
Dict: 导入结果
"""
try:
# 创建项目素材目录
uncategorized_dir = project_directory / "未分类文件夹"
uncategorized_dir.mkdir(parents=True, exist_ok=True)
# 加载现有的项目素材列表
material_list_file = project_directory / "project_material.json"
existing_materials = self._load_material_list(material_list_file)
# 处理视频片段
new_materials = []
imported_count = 0
skipped_count = 0
for segment in video_segments:
if not segment.success:
continue
try:
result = self._import_single_segment(
segment=segment,
uncategorized_dir=uncategorized_dir,
existing_materials=existing_materials,
project_id=project_id,
source_video_path=source_video_path,
material_tags=material_tags or []
)
if result["imported"]:
new_materials.append(result["material_info"])
imported_count += 1
logger.info(f"✅ 素材已导入: {result['original_filename']} -> {result['target_filename']}")
else:
skipped_count += 1
logger.info(f"📋 素材已存在,跳过: {result['original_filename']}")
except Exception as e:
logger.error(f"❌ 导入素材失败 {segment.output_path.name}: {e}")
skipped_count += 1
# 更新项目素材列表
if new_materials:
existing_materials.extend(new_materials)
self._save_material_list(material_list_file, existing_materials)
logger.info(f"📝 项目素材列表已更新: 新增 {len(new_materials)} 个素材")
return {
"success": True,
"imported_count": imported_count,
"skipped_count": skipped_count,
"total_count": len(video_segments),
"new_materials": new_materials
}
except Exception as e:
logger.error(f"❌ 项目素材导入失败: {e}")
return {
"success": False,
"error": str(e),
"imported_count": 0,
"skipped_count": 0,
"total_count": len(video_segments)
}
def _import_single_segment(
self,
segment: Any,
uncategorized_dir: Path,
existing_materials: List[Dict],
project_id: str,
source_video_path: str,
material_tags: List[str]
) -> Dict[str, Any]:
"""导入单个视频片段"""
# 复制文件到项目目录
import shutil
temp_target_path = uncategorized_dir / f"temp_{segment.output_path.name}"
shutil.copy2(segment.output_path, temp_target_path)
# 计算复制后文件的MD5
file_md5 = self._calculate_file_md5(temp_target_path)
# 检查是否已存在相同MD5的素材
if any(material.get('md5') == file_md5 for material in existing_materials):
# 删除临时文件
temp_target_path.unlink()
return {
"imported": False,
"original_filename": segment.output_path.name,
"reason": "duplicate_md5"
}
# 重命名为最终文件名
target_filename = f"{file_md5}.mp4"
target_path = uncategorized_dir / target_filename
temp_target_path.rename(target_path)
# 创建素材信息
material_info = {
"id": file_md5,
"md5": file_md5,
"original_filename": segment.output_path.name,
"filename": target_filename,
"file_path": str(target_path),
"relative_path": f"未分类文件夹/{target_filename}",
"file_size": segment.file_size,
"duration": segment.duration,
"start_time": segment.start_time,
"end_time": segment.end_time,
"scene_index": segment.scene_index,
"tags": material_tags.copy(),
"created_at": datetime.now().isoformat(),
"use_count": 0,
"source_video": source_video_path,
"project_id": project_id
}
return {
"imported": True,
"material_info": material_info,
"original_filename": segment.output_path.name,
"target_filename": target_filename
}
def _load_material_list(self, material_list_file: Path) -> List[Dict]:
"""加载项目素材列表"""
if not material_list_file.exists():
return []
try:
with open(material_list_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.warning(f"⚠️ 读取现有素材列表失败: {e}")
return []
def _save_material_list(self, material_list_file: Path, materials: List[Dict]):
"""保存项目素材列表"""
try:
with open(material_list_file, 'w', encoding='utf-8') as f:
json.dump(materials, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"❌ 保存项目素材列表失败: {e}")
raise
def _calculate_file_md5(self, file_path: Path) -> str:
"""计算文件MD5值"""
hash_md5 = hashlib.md5()
try:
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"❌ 计算MD5失败 {file_path}: {e}")
# 如果计算MD5失败使用文件名和大小作为备用标识
return hashlib.md5(f"{file_path.name}_{file_path.stat().st_size}".encode()).hexdigest()
def get_project_materials(self, project_directory: Path) -> List[Dict]:
"""获取项目素材列表"""
material_list_file = project_directory / "project_material.json"
return self._load_material_list(material_list_file)
def add_material_tags(self, project_directory: Path, material_id: str, tags: List[str]) -> bool:
"""为素材添加标签"""
try:
material_list_file = project_directory / "project_material.json"
materials = self._load_material_list(material_list_file)
for material in materials:
if material.get('id') == material_id:
existing_tags = set(material.get('tags', []))
new_tags = existing_tags.union(set(tags))
material['tags'] = list(new_tags)
self._save_material_list(material_list_file, materials)
logger.info(f"✅ 素材标签已更新: {material_id}")
return True
logger.warning(f"⚠️ 未找到素材: {material_id}")
return False
except Exception as e:
logger.error(f"❌ 添加素材标签失败: {e}")
return False
def remove_material(self, project_directory: Path, material_id: str) -> bool:
"""删除项目素材"""
try:
material_list_file = project_directory / "project_material.json"
materials = self._load_material_list(material_list_file)
# 查找要删除的素材
material_to_remove = None
for i, material in enumerate(materials):
if material.get('id') == material_id:
material_to_remove = materials.pop(i)
break
if material_to_remove:
# 删除文件
file_path = Path(material_to_remove['file_path'])
if file_path.exists():
file_path.unlink()
logger.info(f"🗑️ 已删除素材文件: {file_path.name}")
# 更新素材列表
self._save_material_list(material_list_file, materials)
logger.info(f"✅ 素材已删除: {material_id}")
return True
else:
logger.warning(f"⚠️ 未找到素材: {material_id}")
return False
except Exception as e:
logger.error(f"❌ 删除素材失败: {e}")
return False
def get_material_stats(self, project_directory: Path) -> Dict[str, Any]:
"""获取项目素材统计信息"""
try:
materials = self.get_project_materials(project_directory)
total_count = len(materials)
total_size = sum(material.get('file_size', 0) for material in materials)
total_duration = sum(material.get('duration', 0) for material in materials)
# 按标签统计
tag_stats = {}
for material in materials:
for tag in material.get('tags', []):
tag_stats[tag] = tag_stats.get(tag, 0) + 1
# 按使用次数统计
used_count = sum(1 for material in materials if material.get('use_count', 0) > 0)
unused_count = total_count - used_count
return {
"total_count": total_count,
"total_size": total_size,
"total_duration": total_duration,
"used_count": used_count,
"unused_count": unused_count,
"tag_stats": tag_stats
}
except Exception as e:
logger.error(f"❌ 获取素材统计失败: {e}")
return {
"total_count": 0,
"total_size": 0,
"total_duration": 0,
"used_count": 0,
"unused_count": 0,
"tag_stats": {}
}