fix
This commit is contained in:
298
python_core/services/project_material_service.py
Normal file
298
python_core/services/project_material_service.py
Normal file
@@ -0,0 +1,298 @@
|
||||
"""
|
||||
项目素材管理服务
|
||||
"""
|
||||
|
||||
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": {}
|
||||
}
|
||||
Reference in New Issue
Block a user